Koa compose source analysis
Koa – The next generation Web development framework based on Node.js.
Its biggest characteristic is the unique middleware flow control, is a typical onion model. Koa and KOA2 middleware have the same idea, but their implementation is different. After Node7.6, KOA2 can directly use async/await instead of generator to use middleware. This paper takes the last case as an example.
This article focuses on the source code of the compose module
Preparation of source code interpretation
Understanding the Onion model
The following diagram, found on the Internet, clearly shows how a request passes through the middleware to generate a response. This pattern is very convenient to develop and use the middleware. Compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose compose This is combined with next() to form the onion model shown below
Koa sample tests to see the execution order and benefits of the Onion model
- To perform the sequential tests we create a KOA application
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('First middleware function')
await next();
console.log('After the first middleware function next! ');
})
app.use(async (ctx, next) => {
console.log('Second middleware function')
await next();
console.log('After the second middleware function next! ');
})
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Copy the code
Run node demo1.js
The following information is displayed:
Why is it the above results, we continue to look with these questions, to see the end will certainly understand.
Note: When app.use is used to add the given middleware to the application, middleWAR (which is really just a function) takes two arguments: CTX and next. Where next is also a function.
Compose source code
The compose code is composed as follows. If you remove the comments, the compose code is 25 lines long.
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
functionCompose (middleware) {// The middleware arguments passed in must be arraysif(! Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! '// The elements of the Middleware array must be functionsfor (const fn of middleware) {
if(typeof fn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')
}
/**
* @param {Object} context
* @return{Promise} returns a closure that returns a Promise object, keeping a reference to middleware. * @api public */return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
Copy the code
Let’s first get rid of the conditional and look at the innermost actual return
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
Copy the code
Fn = middleware[I] (middleware) : fn = middleware[I] (middleware)
fn(context, dispatch.bind(null, i + 1))
Copy the code
Here you can see two parameters passed to the middleware: context and the next function. As mentioned earlier, when certain middleware is added to an application using app.use, the middleware (which is really just a function) takes two parameters: CTX and next. Where next is also a function. Middwleare have two parameters when registering middwleare
Going back to the question of why our demo was executed as above, let’s look at the first middleware,
app.use(async (ctx, next) => {
console.log('First middleware function')
await next();
console.log('After the first middleware function next! ');
})
Copy the code
Return Dispatch (0) is executed for the first time, at which point the first middleware is called and the expansion continues
dispatch(0)
an
Promise.resolve((async (ctx, next) => {
console.log('First middleware function')
await next();
console.log('After the first middleware function next');
})(context, dispatch.bind(null, i + 1)));
Copy the code
There is nothing wrong with first executing console.log(‘ the first middleware function ‘), and then executing next(), it goes to the second middleware, so no second console.log() is executed.
app.use(async (ctx, next) => {
console.log('Second middleware function')
await next();
console.log('After the second middleware function next! ');
})
Copy the code
dispatch(1)
an
Promise.resolve(async (ctx, next) => Promise.resolve(async (ctx, next) => {
console.log('First middleware function')
Promise.resolve((async (ctx, next) => {
console.log('Second middleware function')
await next();
console.log('Second middleware function after Next');
})(context, dispatch.bind(null, i + 1)));
console.log('After the first middleware function next')}))Copy the code
So executing onsole.log(‘ the second middleware function ‘) should be clear.
As the second middleware executes to await next(), it will also rotate to a third middleware, then if there is a fourth middleware, then a fifth middleware, you will be smart enough to find out, and so on to the last middleware.
Compose is called by KoA in a later article
conclusion
That’s my interpretation of Koa Compose and the onion model. We hope it will be helpful. From the code, we can see that the Onion model is also flawed. Once there is too much middleware, performance will be affected to a certain extent, so we need to make the appropriate choice according to our own project scenario.
If you have any questions, please leave a message and discuss with us. Thank you! .
Refer to the link
- Understand the middleware mechanisms of Koa