From shallow to deep understanding of Koa2 source code

In the previous article, we introduced what is Basis of Koa2

Briefly review

What is koa2

  1. web development framework of NodeJS
  2. Koa can be seen as an abstraction of nodejs' HTTP module

Source code focus

Middleware mechanism

Onion model

compose

Source code structure

Source address of Koa2: https://github.com/koajs/koa

Among them, lib is its source code

As you can see, there are only four files: application js,context.js,request.js,response.js

application

As an entry file, it inherits the Emitter module, which is the native module of NodeJS. In short, the Emitter module can realize the ability of event listening and event triggering

Delete the comments and look at the Application constructor from the perspective of sorting

Application provides eight methods on its prototype, including listen, toJSON, inspect, use, callback, handleRequest, createContext and oneror

  • listen: provides HTTP services
  • use: middleware mount
  • Callback: get the callback function required by http server
  • handleRequest: handle the request body
  • createContext: construct ctx, combine req and res of node, and construct the parameter of Koa - ctx
  • onerror: error handling

Don't care about the others. Let's take a look at the constructor

Halo, what and what are these? Let's start one of the simplest services and take a look at the examples

const Koa = require('Koa')

const app = new Koa()

app.use((ctx) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('3000 Request succeeded')
})

console.dir(app)

It can be seen that our instance corresponds to the constructor one by one,

Interrupt and look at the prototype

Oh, except for non critical fields, we only focus on the key points

This on Koa's constructor middleware, this.context, this.request,this.response

The prototype includes: listen, use, callback, handleRequest, createContext and oneror

Note: the following codes delete exceptions and non critical codes

listen first

...
  listen(...args) {
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }
...

It can be seen that listen encapsulates an http service with the http module, focusing on the incoming this callback(). OK, let's look at the callback method now

callback

  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }
    return handleRequest
  }

It includes the merging of middleware, the processing of context, and the special processing of res

Merging of Middleware

Koa compose is used to merge middleware, which is also the key of onion model. The source address of KOA compose is: https://github.com/koajs/compose . The code hasn't moved for three years. It's steady

function compose(middleware) {
  return function (context, next) {
    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)
      }
    }
  }
}

We don't know what the constructor of this.middleware needs first Middleware, who used the use method

Let's jump out and look at the method first

use

use(fn) {
    this.middleware.push(fn)
    return this
}

The key to removing exception handling is these two steps, this Middleware is an array. The first step is this push Middleware in middleware; The second step is to return to this so that it can be called in a chain. At the beginning, I was interviewed how to do the chain call of promise. I was stunned. I didn't expect to see it here

Looking back at the source code of KOA compose, imagine this scenario

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

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});
...

We know that its operation is 123456

It's this The composition of middleware is

this.middleware = [
  async (ctx, next) => {
    console.log(1)
    await next()
    console.log(6)
  },
  async (ctx, next) => {
    console.log(2)
    await next()
    console.log(5)
  },
  async (ctx, next) => {
    console.log(3)
    ctx.body = 'hello world'
    console.log(4)
  },
]

Don't be surprised. Functions are also one of the objects. If they are objects, they can pass values

const fn = compose(this.middleware)

We will JavaScript it. We don't need to change anything else. We just need to change the last function to

async (ctx, next) => {
  console.log(3);
  -ctx.body = 'hello world';
  +console.log('hello world');
  console.log(4);
}

Parsing koa compose line by line

This paragraph is very important. You often take it in the interview. Let you write a composition and write it

//1. async (ctx, next) => { console.log(1); await next(); console.log(6); }  middleware 
//2. const fn = compose(this.middleware) merge Middleware
//3. fn() execution Middleware

function compose(middleware) {
    return function (context, next) {
        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);
            }
        }
    };
}

Execute const fn = compose(this.middleware), i.e. the following code

const fn = function (context, next) {
    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)
      }
    }
  }
}

Execute fn(), i.e. the following code:

const fn = function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i    // index = 0
      let fn = middleware[i] // fn is the first middleware
      if (i === middleware.length) fn = next // When the last middleware is obtained, the value of the last middleware is fn
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
          // Return a Promise instance and execute recursive dispatch(1)
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

That is, the first middleware will not return until the second middleware is executed, and the second middleware will not return until the third middleware is executed

Promise.resolve is a promise instance, so promise is used Resolve is to solve asynchrony, so promise is used Resolve is to solve asynchronous

Throw promise Resolve, let's take a look at the use of recursion and execute the following code

const fn = function () {
    return dispatch(0);
    function dispatch(i) {
        if (i > 3) return;
        i++;
        console.log(i);
        return dispatch(i++);
    }
};
fn(); // 1,2,3,4

Looking back at compose again, the code is similar to

// Suppose this middleware = [fn1, fn2, fn3]
function fn(context, next) {
    if (i === middleware.length) fn = next // fn3 no next
    if (!fn) return Promise.resolve() // Execute this line because fn is empty
    function dispatch (0) {
        return Promise.resolve(fn(context, function dispatch(1) {
            return Promise.resolve(fn(context, function dispatch(2) {
                return Promise.resolve()
            }))
        }))
    }
  }
}

This recursive method is similar to the execution stack, first in first out

Here we need to think more about the use of recursion Resolve don't care too much

Context processing

The processing of the context calls createContext

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))
    context.app = request.app = response.app = 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.state = {}
    return context
}

Pass in the native request and response and return a context - context. The code is very clear and does not explain

Special treatment of res

In the callback, execute this first Createcontext. After getting the context, execute handleRequest. Look at the code first:

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = (err) => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}

Everything is clear

const Koa = require('Koa');
const app = new Koa();

console.log('app', app);
app.use((ctx, next) => {
    ctx.body = 'hello world';
});
app.listen(3000, () => {
    console.log('3000 Request succeeded');
});

After instantiating such a piece of code, you get this middleware,this.context,this.request,this.response four generals, you use app When using (), push the function to this middleware. Then use app Listen () is equivalent to an HTTP service. It combines middleware, obtains context, and performs special processing on res

error handling

onerror(err) {
    if (!(err instanceof Error))
        throw new TypeError(util.format('non-error thrown: %j', err))

    if (404 == err.status || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error()
    console.error(msg.replace(/^/gm, '  '))
    console.error()
}

context.js

Two things introduced me

// 1.
const proto = module.exports = {
    inspect(){...},
    toJSON(){...},
    ...
}
// 2.
delegate(proto, 'response')
  .method('attachment')
  .access('status')
  ...

The first can be understood as const proto = {inspect() {...}...}, And module Exports exports this object

Second, delegate is an agent, which is designed for the convenience of developers

// Delegate the attribute of the internal object response to the exposed proto
delegate(proto, 'response')
  .method('redirect')
  .method('vary')
  .access('status')
  .access('body')
  .getter('headerSent')
  .getter('writable');
  ...

Instead, use delegate (proto, 'response') access('status')..., It's in context JS exported file, put proto All parameters on the response are proxied to proto, which is proto What is response? It's context response,context. Where did the response come from?

To review, in createContext

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))
    ...
}

context. If you have a response, you will understand it. Context response = this. Response, because delegate, so context The parameters on the response are proxied to the context, for example

  • ctx. The header is CTX request. Proxy on header
  • ctx. Yes. CTX response. Agent on body

request.js and response js

One handles the request object and the other handles the return object, which is basically a simplified processing of the native req and res, using a lot of get and post syntax in ES6

That's about it. After knowing so much, how to write a Koa2? Please see the next article - handwritten Koa2

reference material

Tags: Javascript node.js Front-end source code analysis koa2

Posted by peeter_talvistu on Tue, 05 Apr 2022 10:02:24 +0300