Koa – The next generation Web development framework based on the Node.js platform
Koa2 was built by the original Express team to be a smaller, more expressive, and more robust Web framework. Using KOA to write Web applications eliminates repetitive nested callback functions and greatly improves error handling efficiency. Koa doesn’t bundle any middleware into kernel methods, it just provides a lightweight and elegant library of functions that makes writing Web applications a breeze. The development idea is similar to Express, the biggest feature is to avoid asynchronous nesting.
Koa2 uses ES7’s async/await feature to solve the problem of asynchro in nodeJS development, so this is one of koA2’s core concepts – the Onion model.
The onion model
The Onion model adopts the advanced out stack structure, plus the async and await features, so as to solve the hell callback and make the middleware combination more flexible by the developer, so as to extend the request context better.
Let’s start with an example:
const Koa = require('koa2');
const app = new Koa();
app.use(async (cxt, next) => {
console.log('middleware_1 start'.'test: ', cxt.test)
cxt.test = 'middleware_1'
await next()
console.log('middleware_1 end'.'test: ', cxt.test)
})
app.use(async (cxt, next) => {
console.log('middleware_2 start'.'test: ', cxt.test)
cxt.test = 'middleware_2'
await next()
console.log('middleware_2 end'.'test: ', cxt.test)
})
app.use(async (cxt, next) => {
console.log('middleware_3 start'.'test: ', cxt.test)
cxt.test = 'middleware_3'
console.log('middleware_3 end'.'test: ', cxt.test)
})
app.listen(3000)
Copy the code
Output result:
middleware_1 start test: undefined
middleware_2 start test: middleware_1
middleware_3 start test: middleware_2
middleware_3 end test: middleware_3
middleware_2 end test: middleware_3
middleware_1 end test: middleware_3
Copy the code
In the above code, there are three functions in use, and two parameters in the function are CXT and NEXT. Different values are assigned to test in the three functions, and the printing is performed respectively above and below the next function. According to the results, the test value of each middleware is passed to the next middleware. Follow the example above to see how the source code is implemented:
1. First locate the koa2 folder in the node_modules folder and find the main field in package.json (which is the entry file path for the entire component package).
Then open it according to the path shown in the main field (lib/application.js).
According to the example above, koA2 requires the new operator to return an instance, so application.js must return a constructor or class. Then you need to look at the use and listen methods in detail.
// XXX // Other components
const Emitter = require('events'); // This is a Node publish-subscribe module
const http = require('http'); // HTTP request module
const compose = require('koa-compose');
module.exports = class Application extends Emitter {
constructor(server) {
super(a);this.server=server
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
listen() {
debug('listen');
const server =this.server || http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
use(fn) {
if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will been removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x---deprecated');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || The '-');
this.middleware.push(fn);
return this;
}
// xx other methods
}
Copy the code
Looking at the use method first, the entire use method does just that: it uses a middlewre variable declared within constructor to collect all middleware functions passed in by use, excluding if bounds.
The Listen method, which starts a port service using HTTP, takes precedence if constructor passes a server object when instantiating before starting, otherwise it creates a new HTTP server using HTTP. In the example above, Koa is instantiated without passing any arguments, so you need to look at the create logic. In createServer, a new callback method is executed, and then look at the callback method.
Callback method
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error'.this.onerror);
return (req, res) = > {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err= > ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() = > respond(ctx)).catch(onerror);
};
}
Copy the code
The compose method is the core of the onion model. The callback returns a function that is also the callback to the createServer above. The request and response headers used to receive HTTP requests are first processed using the internal createContext method, and then CTX is passed to the fn function returned by compose.
The createContext method creates a context, processes the request header and response header object with the context, and returns the first argument to the use callback:
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || ' ';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
Copy the code
There is also a respond function that returns what needs to be returned to the client when all is done:
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
const res = ctx.res;
if(! ctx.writable)return;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' == ctx.method) {
if(! res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
body = ctx.message || String(code);
if(! res.headersSent) { ctx.type ='text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string'= =typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if(! res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }Copy the code
4. Compose method
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}/ * * *@param {Object} context
* @return {Promise}
* @api public* /
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0) // Start with the first middleware
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if(! fn)return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1) // Execute the next intermediate price}}))catch (err) {
return Promise.reject(err)
}
}
}
}
Copy the code
The compose method takes arguments from all the middleware collections collected by the Use method and returns a function whose internal dispatch method is the core implementation of the entire Onion model.
All middleware are executed in turn, starting with subscript 0. When the middleware is executed, the context parameter CTX is passed in along with the next method with internal execution of the next middleware. All middleware is executed in turn until fn is empty and an empty promise.resolve is returned.
Finally, convert the initial example to a normal nested function execution:
async function fn1(cxt) {
console.log('middleware_1 start'.'test: ', cxt.test)
cxt.test = 'middleware_1'
await fn2(cxt)
console.log('middleware_1 end'.'test: ', cxt.test)
}
async function fn2(cxt) {
console.log('middleware_2 start'.'test: ', cxt.test)
cxt.test = 'middleware_2'
await fn3(cxt)
console.log('middleware_2 end'.'test: ', cxt.test)
}
async function fn3(cxt) {
console.log('middleware_3 start'.'test: ', cxt.test)
cxt.test = 'middleware_3'
console.log('middleware_3 end'.'test: ', cxt.test)
}
fn1({})
/ / output
// middleware_1 start test: undefined
// middleware_2 start test: middleware_1
// middleware_3 start test: middleware_2
// middleware_3 end test: middleware_3
// middleware_2 end test: middleware_3
// middleware_1 end test: middleware_3
Copy the code