preface

KoaNodeJS is the current mainstream NodeJS framework, known for its lightweight, and its middleware mechanism is relatively traditional
ExpressAsynchrony is supported, so it is often used when coding
async/await, improves readability and makes code more elegant
NodeJS Advanced – Koa source code analysis, also on the “Onion model” and the implementation of it
composeCarry out the analysis as personally feel
composeThe idea of programming is more important, widely used, so this article through the topic of “Onion model”, intends to use four ways to achieve
compose.

The Onion Model case

If you already use Koa, the term “Onion model” is familiar. It is a serial mechanism of Koa middleware that supports asynchrony. Here is a classic example of how to express the “Onion model”.

const Koa = require("koa"); const app = new Koa(); app.use(async (ctx, next) => { console.log(1); await next(); console.log(2); }); app.use(async (ctx, next) => { console.log(3); await next(); console.log(4); }); app.use(async (ctx, next) => { console.log(5); await next(); console.log(6); }); app.listen(3000); // 1/3/5/6/4/2Copy the code

The above written according to the official recommendation, we use the async/await, but if it doesn’t matter that is synchronized code does not use, this simple analysis the execution mechanism, the first middleware function if you execute the next, and the next middleware can be performed, so on, have we the result of the above, and in Koa source code, This function is realized by a compose method. In this paper, we implement synchronous and asynchronous methods in four ways of implementing compose, and provide corresponding cases to verify.

The preparatory work

Before actually creating the compose method, you should do some preparatory work, such as creating an App object to replace the instance object created by Koa, and adding the Use method and middlewares, an array for managing the middleware.

Const app = {middlewares: []}; App.use = function(fn) {app.middlewares.push(fn); }; // app.compose..... module.exports = app;Copy the code

The above module exports the app object and creates middlewares to store middleware functions and a use method to add middleware, because these are required no matter which way compose is implemented, just the compose logic is different. So the next block of code will just write the compose method.

The implementation of compose in Koa

First of all, I will introduce the implementation method in Koa source code. In Koa source code, it is actually realized through the KOA-compose middleware. Here, we extract the core logic of this module and implement it in our own way. So the CTX parameter is removed, because we won’t use it, and the focus is on the next parameter.

Implementation of synchronization

App.pose = function() {function dispatch(index) {if (index === app.middlewares.length) return; // Fetch the index middleware and execute const route = app.middlewares[index]; return route(() => dispatch(++index)); } // Fetch the first middleware function to execute dispatch(0); }Copy the code

Above is the implementation of synchronization. The first middleware function in the array is taken out and executed through the execution of the recursive function Dispatch, a function is passed in during execution, and the recursive execution of dispatch with the passed parameter +1 is carried out, so the next middleware function is executed, and so on, until all the middleware is executed. If the middleware execution condition is not met, it will jump out, so the execution is performed according to the above case 1 3 5 6 4 2, the test example is as follows (synchronous up, asynchronous down).

const app = require("./app"); app.use(next => { console.log(1); next(); console.log(2); }); app.use(next => { console.log(3); next(); console.log(4); }); app.use(next => { console.log(5); next(); console.log(6); }); app.compose(); // 1/3/5/6/4/2Copy the code
const app = require("./app"); Return new Promise((resolve, reject) => {setTimeout(() => {resolve(); console.log("hello"); }, 3000); }); } app.use(async next => { console.log(1); await next(); console.log(2); }); app.use(async next => { console.log(3); await fn(); // call the asynchronous function await next(); console.log(4); }); app.use(async next => { console.log(5); await next(); console.log(6); }); app.compose();Copy the code

We found that if the case is written according to Koa’s recommendation, that is, using async function, it will pass, but ordinary function or async function may be passed when passing the parameter to use. We need to package all the return values of middleware as Promise to accommodate the two cases. Compose returns a Promise in Koa for future logic writing, which is not currently supported.

Note: the backcomposeAll other implementations of thesync-test.jsasync-test.jsVerify, so I’m not going to repeat this.

Upgrade to support asynchrony

App.pose = function() {function dispatch(index) { And return a Promise if (index === app.middlewares.length) return promise.resolve (); // Fetch the index middleware and execute const route = app.middlewares[index]; // Return promise.resolve (route(() => dispatch(++index))); } // Fetch the first middleware function to execute dispatch(0); }Copy the code

As we know, asynchronous code executed after await in async function needs to wait, and then continue to execute after asynchronous execution, waiting for Promise. Therefore, we finally return a Promise of success state when calling each middleware function. Async-test. js is used to test, and the result is 1 3 hello(3s later) 5 6 4 2.

An implementation of the old version of Redux, Compose

Implementation of synchronization

app.compose = function() {
  return app.middlewares.reduceRight((a, b) => () => b(a), () => {})();
}
Copy the code

Middlewares stores three middleware functions fn1, Fn2 and FN3, and uses the reduceRight method, so it merges in reverse order. The first time a represents the initial value (an empty function), b represents Fn3, and the execution of fn3 returns a function that serves as a for the next merge, fn2 as B, and so on.

// The return value of the 1st reduceRight, the next time will be as a () => fn3(() => {}); / / the return value of a second reduceRight, next time will be a () = > fn2 (() = > fn3 (() = > {})); / / the third reduceRight return value, the next will be a () = > fn1 (() = > fn2 (() = > fn3 (() = > {})));Copy the code

As can be seen from the above debunking process, if we call this function, fn1 will be executed first, if we call next, fn2 will be executed, and if we call next, fn3 will be executed. Fn3 is the last middleware function, and next will execute the empty function we originally passed. This is why the initial value of reduceRight should be set to an empty function in case the last middleware call next gives an error.

The above code does not appear to be out of order after compose is executed, but we want to do some subsequent operations for compose, so we want to return a Promise, and we want the middleware function passed to use to be either a normal or async function. This requires that our compose be fully asynchronous.

Upgrade to support asynchrony

app.compose = function() {
  return Promise.resolve(
    app.middlewares.reduceRight(
      (a, b) => () => Promise.resolve(b(a)),
      () => Promise.resolve()
    )()
  );
}
Copy the code

Referring to the analysis process of synchronization, the empty function executed by the last middleware must have no logic in it, but in order to continue executing asynchronous code (such as executing next and calling THEN), promises were processed. It ensures that the function returned by reduceRight each time it merges returns a Promise, which is fully compatible with async and ordinary functions. When all middleware execution is completed, a Promise is also returned. Compose can then call the then method to perform the subsequent logic.

The implementation of Redux’s new version, Compose

Implementation of synchronization

app.compose = function() {
  return app.middlewares.reduce((a, b) => arg => a(() => b(arg)))(() => {});
}
Copy the code

In the new version of Redux, compose’s logic is changed by changing the original reduceRight into reduce, that is to say, the reverse order merges into the positive order. We are not necessarily the same as Redux source code, but we realize the requirements of serial middleware according to the same idea.

Personally, I think it is more difficult to understand after positive order merging, so I still split the above code in combination with the case. The middleware is still FN1, Fn2 and FN3. Since reduce has no initial value, a is FN1 and B is Fn2.

// The return value of the first reduce is a arg => fn1(() => fn2(arg)); / / the return value of a second reduce, the next will be used as a arg = > (arg = > fn1 (() = > fn2 (arg))) (() = > fn3 (arg)); // It is equivalent to... arg => fn1(() => fn2(() => fn3(arg))); // Execute the last returned function to connect to the middleware, the return value is equivalent to... fn1(() => fn2(() => fn3(() => {})));Copy the code

Therefore, when calling the last function returned by Reduce, an empty function is passed as a parameter, which is actually passed to FN3, the third middleware, ensuring that no error is reported when the last middleware calls Next.

Upgrade to support asynchrony

The harder task is to change the above code to support asynchrony, as follows.

app.compose = function() {
  return Promise.resolve(
    app.middlewares.reduce((a, b) => {
      return arg => a(() => Promise.resolve(b(arg)));
    })(() => Promise.resolve())
  );
}
Copy the code

Implementing async and reverse merges is a trick of making each middleware function return a Promise and making compose return a Promise.

Use async functions

This version is before I learn Koa source code accidentally in a big guy in an analysis of Koa principle of the article (turned over for a long time really did not find the link), here also take out and share with you, because it is the use of async function implementation, so the default is to support asynchronous, Because async returns a Promise.

App.pose = function() {// async function() returns Promise return (async function() {// Define default next, Next let next = async () => promise.resolve (); // Middleware returns an async for each middleware function, oldNext returns an async for the next function in each middleware function, and async returns a Promise, Function createNext(middleware, oldNext) {return async () => {await middleware(oldNext); // Call the next middleware function, // call the next middleware function, Pass the newly generated next to for (let I = app.middlewares. Length-1; i >= 0; i--) { next = createNext(app.middlewares[i], next); } await next(); }) (); }Copy the code

In the code above, next is a function that only returns a success state Promise, which can be understood as next called by the last middleware in the other implementation. The array middlewares is traversed backwards, and the first value fetched is the last middleware. CreateNext returns a new async function that executes the last middleware in the array, passes in the initial next, returns the async function as the new Next, and calls createNext, the penultimate middleware. An async function is returned, which is the execution of the penultimate middleware. The next passed in is the next generated last time, and so on to the first middleware.

So next, which is returned by the first middleware, will execute the last generated next function passed in, which will execute the second middleware, which will execute next in the second middleware, and so on until the execution of the original definition of Next is completed. Through the verification of the case, the execution result is exactly the same as the Onion model.

As for asynchracy, each time the next function is executed, it returns a Promise, and the outermost async function, which is composed, returns a Promise.

This way is put last, because I personally feel difficult to understand, I am in order of their own understanding of the difficulty of these ways from top to bottom.

conclusion

Maybe after you look at all of these, you’ll think, well
Koafor
composeIs the easiest way to understand, and you may feel the same way I do
ReduxThere are two ways to realize and
asyncThe function implementation is so clever that JavaScript is flexible and creative while being criticized for being “weakly typed” and “not rigorous.” It’s hard to judge whether this is a strength or weakness (depending on your opinion), but one thing is for sure. Learning JavaScript not to be constrained by the “conventions” of strongly typed languages (in my opinion, strongly typed language developers do not write) is to absorb such clever programming ideas, write
composeThis elegant and high standards of code, long road ahead, may you in the technical road “gone forever”.