Detailed explanation of the principle of onion model of node framework koa2

Detailed explanation of the principle of onion model of node framework koa2

Koa - Next-generation web development framework based on Node.js platform

koa2 was created by the original team of Express, and is committed to becoming a smaller, more expressive, and more robust Web framework. Use koa writing web applications, eliminating duplication of tedious callback function nesting, and greatly enhance the efficiency of error handling. koa kernel method is not bound to any middleware , it only provides a lightweight and elegant library, makes writing Web applications become handy. The development idea is similar to express, the biggest feature is that it can avoid asynchronous nesting.

Koa2 takes advantage of ES7's async/await features, which greatly solves the annoyance that asynchrony brings to us when we are doing nodejs development, so this is also a core concept of koa2---the onion model .

Onion model

The onion model uses a first-in-last-out stack structure, plus the characteristics of async and await, which solves the problem of hell callbacks and allows developers to more flexibly combine middleware and better extend the request context.

Let's look at 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 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 code

In the above code, three functions are placed in the use, and there are two parameters in the function, namely cxt and next. In the three functions, different values are assigned to test, and printing is performed respectively above and below the next function. According to the results, you can see that the test value of each middleware is passed to the next middleware. According to the above example, see how the source code is implemented:

1. First find the koa2 folder in the node_modules folder, and find the main field in package.json (this field is the entry file path of the entire component package).

Then open it according to the path displayed in the main field (lib/application.js).

2. According to the above example, koa2 needs the new operator to return an instance, so application.js must return a constructor or class, and then you need to look at the use method and the listen method in detail .

//xxx//other components const Emitter = require ( 'events' ); //this is a node publish and subscribe module const http = require ( 'http' ); //http request module const compose = require ( 'koa -compose' ); module .exports = class Application extends Emitter { constructor ( server ) { super (); 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 ( typeof fn !== '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' + ' -middleware-v1x---deprecated' ); fn = convert(fn); } debug( 'use %s' , fn._name || || '-' ); this .middleware.push(fn); return this ; } //xx other methods } Copy code

Look at the use method first , except for the boundary judgment of if, the whole use method actually does one thing, which is to use a middlewre variable declared in the constructor to collect all the middleware functions passed in by use.

Then look at the listen method. What it does is use http to start a port service. Before starting, if the constructor passes a server object when instantiating, it will be used first, otherwise, use http to create a new http server. . In the example above, no parameters are passed when instantiating Koa, so you need to look at the create logic. There is a new callback method to execute in createServer , and then look at the callback method.

3. 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 code

Inside the callback, the first is to call a compose method to get an fn, wait to see the compose method (the core of the entire onion model), callback returns a function, which is also the callback function of createServer above, used to receive the request header of the http request And response headers, first use the internal createContext method to process the request headers and response headers, and then pass ctx to the function fn returned by compose execution.

The createContext method actually creates a context, then combines the request header and response header object with the context and returns it, which is the first parameter of the callback function of use:

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) ; = = = 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.ip = request.ips[ 0 ] || req.socket.remoteAddress || '' ; context.accept = request.accept = accepts(req); context.state = {}; return context; } Copy code

There is also a respond function, which returns the content that needs to be returned to the client after all the intermediate executions are completed:

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 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 ( typeof fn !== '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 from 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 middle price })) } catch (err) { return Promise .reject(err) } } } } Copy code

The parameter of the compose method is the collection of all middleware collected by the use method and returns a function. The dispatch method inside the function is the core implementation of the entire onion model.

Starting from the subscript 0, execute all middleware in turn. When executing the middleware, pass in the context parameter ctx and the next method with the next middleware executed internally, and execute all middleware in turn until fn is empty , Return an empty Promise.resolve to end.

Finally, convert the initial example into a normal function nested 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 //Start Test middleware_1: undefined //Start Test middleware_2: middleware_1 //Start Test middleware_3: middleware_2 //End Test middleware_3: middleware_3 //End Test middleware_2: middleware_3 //End Test middleware_1: middleware_3 copy the code