This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

In the last big front, you said you couldn’t Koa? Come in with your hand stroked source code, read not, although spray! , introduces the implementation of Koa context, and Koa’s encapsulation of Node’s native HTTP modules Request and Response.

In order for new readers to read this article directly, let’s comb through it again.

Export Koa class, and the implementation of listen and use functions.

let http = require('http') class Application { use(fn) { this.fn=fn } callback = (req, res) => { this.fn(req, res) } listen() { const server = http.createServer(this.callback); server.listen(... arguments) } } module.exports = ApplicationCopy the code

To encapsulate CTX, we added a function called in callback. Let’s pass it to the function of use.

CreateContext = (req, res) => {// This is mainly to bind native REq and RES to CTX. } callback = (req, res) => { let ctx = this.createContext(req, res) this.fn(ctx) xCopy the code

Returns content in response to a user request

callback = (req, res) => {
  let ctx = this.createContext(req, res)
  this.fn(ctx)
  let body = ctx.body;
  if (body) {
      res.end(body);
  } else {
      res.end('Not Found')
  }
}
Copy the code

So if you look at that, Koa is pretty simple. Of course not, this is just the most basic source code, this article, we are going to hand tear Koa middleware system implementation.

The onion model

This concept is introduced in almost every article on Koa. And this is the picture below.

This is the Onion model. Without looking at the code, what is the simplest way to understand it?

Koa loads external functions via app.use. After creating the context, we do what we normally do above. We respond directly to the user request and return the content, but what does the Onion model do? We will not respond directly at this time, but to finish the external function loaded, and then respond to the user request, return the content.

Callback = (req, res) => {let CTX = this.createcontext (req, res) use1; use2; use3; if (body) { res.end(body); } else { res.end('Not Found') } }Copy the code

The code above is just our simplest state; there is no asynchrony.

App.use and middleware

Koa middleware is essentially a function that is called through app.use. App.use () returns this, so it can be expressed chained.

The middleware can be normal functions or asynchronous functions defined by async.

app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(1)});Copy the code

Next is used to execute the next piece of middleware, meaning that console.log(1) below is not executed.

Let’s talk about the onion model. Above we briefly guessed the logic of the Onion model. Let’s use an example to illustrate.

app.use(async (ctx, next) => {
    console.log(1)
    await next();
    console.log(1)
});

app.use(async (ctx, next) => {
    console.log(2)
    next();
    console.log(2)
})

app.use(async (ctx, next) => {
    console.log(3)

})
Copy the code

The output for this example is 1, 2, 3, 2, 1. If you look at this picture again, you know what the onion looks like.

As stated above, the role of Next is to execute the next piece of middleware. In the above example, we made a change.

app.use(async (ctx, next) => { console.log(1) await next(); console.log(1) }); app.use(async (ctx, next) => { console.log(2) next(); console.log(2) }) app.use(async (ctx, Next) => {console.log(3)}) app.use((CTX) => {console.log('koa') ctx.body = "native koad"}) app.use(async (CTX, next) => { console.log(4) next(); console.log(4) })Copy the code

For this example, the last two middleware pieces are not executable.

Don’t say much; To summarize the Onion model, intermediate execution, rather than layer by layer, is bounded by Next, executing the code above Next until all the upper layers are finished, and then executing the code below Next.

An onion structure, coming in from the top and coming back from the bottom.

Step by step to upgrade

The most simple

We already know the rough implementation of the Onion model above, so let’s start with a simple implementation. First, the function of Next is like a placeholder, or executor. When next is encountered, it will execute the next function. Let’s simulate it briefly:

function fn1() {
  console.log(1)
  fn2();
  console.log(1)
}
function fn2() {
  console.log(2)
  fn3();
  console.log(2)
}

function fn3() {
  console.log(3)
  return;
}
fn1();

Copy the code

We can do this by placing the function in next’s position.

Upgrade – Function wrap

Above we put the function in the next location, so it can be implemented. So let’s put next back.

async function fn1(next) {
  console.log(1);
  await next();
  console.log(1);
}

async function fn2(next) {
  console.log(2);
  await next();
  console.log(2);
}

async function fn3() {
  console.log(3);
}

let next1 = async function () {
  await fn2(next2);
}
let next2 = async function() {
  await fn3();
}
fn1(next1);

Copy the code

Here we wrap the function and assign it to Next. And then pass it to the function. Call again.

Upgrade – Encapsulate common

In the example above we wrapped the function, which is essentially passing the function to another function, and if we had N functions, you could do that if you didn’t bother, but we could come up with a common function based on the idea above.

First of all:

  1. Middleware is either a normal function or an async function.
  2. The middleware accepts next to call the next middleware
  3. Execute the first middleware first

When we pass next above, we create the next1 and next2 secondary wrapper intermediate values, and finally execute the first function. Now we want to use a next wrapper, the first response must be in “for” or “while”, where there is “for”, since we have so many middleware, we should collect them and pass them through the loop.

Collect function

const middlewares = [fn1, fn2, fn3];
Copy the code

The next refs

And what this function does is it passes a parameter to each intermediate value.

function compose(middleware, next) { return async function() { await middleware(next); }}Copy the code

Loop transfer and

A next is defined to hold the last function that accepted Next.

let next;
for (let i = middlewares.length - 1; i >= 0; i--) {
   next = compose(middlewares[i], next);

}
Copy the code

Through this for loop, we have passed next in its entirety to each middleware. That’s something like this.

next = async function(){
  await fn1(async function() {
    await fn2(async function() {
      await fn3(async function(){
        return Promise.resolve();
      });
    });
  });
};

async function fn1(next) {
  console.log(1);
  await fn2();
  console.log(1);
}

async function fn2(next) {
  console.log(2);
  await fn3();
  console.log(2);
}

async function fn3() {
  console.log(3);
}

Copy the code

There’s a point here where we start for from the last one, in order to be able to execute the function, we pass the end argument, so we have to execute the function, next saves the first function last.

The complete code

async function fn1(next) {
  console.log(1);
  await fn2();
  console.log(1);
}

async function fn2(next) {
  console.log(2);
  await fn3();
  console.log(2);
}

async function fn3() {
  console.log(3);
}

function compose(middleware, oldNext) {
  return async function() {
    awaitmiddleware(oldNext); }}const middlewares = [fn1, fn2, fn3];

let next ;

for (let i = middlewares.length - 1; i >= 0; i--) {
  next = compose(middlewares[i], next);
}
next();
Copy the code

The realization of Koa

The above simple implementation of an onion model. The logic implemented in Koa is essentially the same.

this.middlewares

constructor() {
  this.middlewares = [];
}
Copy the code

Before we use is

use(fn) {
      this.fn=fn
  }

Copy the code

Now just:

use(fn) {
  this.middlewares.push(fn)
}
Copy the code

Gather the middleware together

compose

We did this in callback earlier

callback = (req, res) => {
  this.fn(ctx)
}
Copy the code

Now with the concept of middleware we certainly can’t write that anymore. Similarly, we package all the middleware into a function and execute it.

callback = (req, res) => {
    this.compose(ctx).then(() => {
            let body = ctx.body;
            if (body) {
                res.end(body);
            } else {
                res.end('Not Found')
            }
        }).catch((e)=>{
            
        })
}
  
Copy the code

Compose is composed inside the for /while loop above. The while loop is used in Koa. Let’s do that again with the while loop.

  1. While loop is back to the beginning, and there is only one next in one middleware.
  2. When the last middleware is reached, a promsie is returned
  3. Finally, the first middleware is executed
  compose(ctx){
        // index
        let index = -1
        const dispatch = (i) = >{
            if(i <= index){
                return Promise.reject('Next () is only allowed to be called once')
            }
            index = i;
            if(this.middlewares.length == i) return Promise.resolve();
            let middleware = this.middlewares[i];
            try{
                return Promise.resolve(middleware(ctx,() = > dispatch(i+1))); 
            }catch(e){
                return Promise.reject(e)
            }
        }
        return dispatch(0); 
    }

Copy the code

This completes the implementation of the onion model.