An overview of the
Compose is a utility function composed by koa.js middleware and executed synchronously in app.use() order, thus forming onion ring calls.
The source code for this function is not long, less than 50 lines, the code address github.com/koajs/compo…
The chain execution of Promise is realized by recursion, and whether it is synchronous or asynchronous in middleware, it is transformed into asynchronous chain execution through Promise.
The source code interpretation
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! ')}... }Copy the code
The function starts with a type check to ensure the input is correct. Middleware must be an array, and its elements must be functions.
function compose (middleware) {
/ /...
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, function next () {
return dispatch(i + 1)}}))catch (err) {
return Promise.reject(err)
}
}
}
}Copy the code
Next, it returns a function that takes two arguments, context and next. Context is THE CTX in KOA, and next is the callback function that the framework consumer finally handles the request and return after all middleware execution is complete. The function is also a closure function that stores all the middleware and runs the middleware recursively.
As you can see from the code, as middleware, you must also accept two parameters, context and next. If one middleware does not call next(), subsequent middleware will not execute. This is a very common way to synchronize multiple asynchronous functions.
How to write the Middleware function
Look directly at the code:
const compose = require('./compose')
function mw1 (context, next) {
console.log('===== middleware 1 =====')
console.log(context)
setTimeout((a)= > {
console.log(`inner: ${context}`)
next()
}, 1000)}function mw2 (context, next) {
console.log('===== middleware 2 =====')
console.log(context)
next()
}
function mw3 (context, next) {
console.log('===== middleware 3 =====')
console.log(context)
setTimeout((a)= > {
console.log(`inner: ${context}`)},1000)
next()
}
const run = compose([mw1, mw2, mw3])
run('context'.function () {
console.log('all middleware done! ')})Copy the code
The output is:
===== middleware 1 =====
context
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
all middleware done!
inner: contextCopy the code
In the third middleware, deliberately writing next() outside of async causes the middleware to proceed to the next middleware run while it is still running (here is the callback function after all middleware runs). Compose () generates the thenable function, so let’s change the last part of the run.
run('context').then((a)= > {
console.log('all middleware done! ')})Copy the code
The result is:
===== middleware 1 =====
context
all middleware done!
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
inner: contextCopy the code
The result does not seem to be what we expected, because in the Compose source, the middleware returns a Promise object after execution, and it would be unreasonable to use asynchronous functions in the Promise and not use THEN for asynchronous processes. We can change the middleware code above.
function mw1 (context, next) {
console.log('===== middleware 1 =====')
console.log(context)
return new Promise(resolve= > {
setTimeout((a)= > {
console.log(`inner: ${context}`)
resolve()
}, 1000)
}).then((a)= > {
return next ()
})
}
function mw2 (context, next) {
console.log('===== middleware 2 =====')
console.log(context)
return next()
}
function mw3 (context, next) {
console.log('===== middleware 3 =====')
console.log(context)
return new Promise(resolve= > {
setTimeout((a)= > {
console.log(`inner: ${context}`)
resolve()
}, 1000)
}).then((a)= > {
return next ()
})
}Copy the code
Output:
===== middleware 1 =====
context
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
inner: context
all middleware done!Copy the code
That’s fine. Each middleware will return a ThEnable Promise object.
Since we are working on koa.js we will change the above code and rewrite it with async/await and change the asynchronous function to a thenable function.
async function sleep (context) {
return new Promise(resolve= > {
setTimeout((a)= > {
console.log(`inner: ${context}`)
resolve()
}, 1000)})}async function mw1 (context, next) {
console.log('===== middleware 1 =====')
console.log(context)
await sleep(context)
await next()
}
async function mw2 (context, next) {
console.log('===== middleware 2 =====')
console.log(context)
return next()
}
async function mw3 (context, next) {
console.log('===== middleware 3 =====')
console.log(context)
await sleep(context)
await next ()
}Copy the code
Application scenarios
In daily development, the Node backend is usually used as a terminal-facing API Gateway in the microservices architecture. Here is a scenario where we take data from three other microservices and aggregate it into an HTTP API. If the services provided by the three services have no dependencies, this situation is relatively simple and can be implemented with promise.all (), as follows:
function service1 () {
return new Promise((resolve, reject) = > {
resolve(1)})}function service2 () {
return new Promise((resolve, reject) = > {
resolve(2)})}function service3 () {
return new Promise((resolve, reject) = > {
resolve(3)})}Promise.all([service1(), service2(), service3()])
.then(res= > {
console.log(res)
})Copy the code
For example, if service2’s request parameters depend on the result returned by Service1, and service3’s request parameters depend on the result returned by Service2, for example, compose should be composed by converting a series of asynchronous requests into synchronous ones. , of course, also can be achieved with the Promise of chain calls, but the code coupling is high, is not conducive to the late maintenance and code changes, if the order of 1, 2, 3, change, code change is big, in addition to the code for unit testing coupling is too high, there is an article is decoupled by means of dependency injection module, Keep modules independent to facilitate module unit testing.
conclusion
Compose is a promise-based process control approach that synchronizes asynchronous processes, addressing previously nested callbacks and Promise chain coupling.
There are many different types of Promise flow control, and the next article will write about the methods used in different application scenarios.