1

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

briefly review

what is koa2

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

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

koa2源码

It can be seen that there are only four files: application.js , context.js , request.js , response.js

application

As the entry file, it inherits the Emitter module. The Emitter module is a native module of NodeJS. In short, the Emitter module can implement event monitoring and event triggering capabilities.

application1

Delete the comments, from the point of view of finishing Application constructor

Application构造函数

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

  • listen: Provide HTTP service
  • use: middleware mount
  • callback: Get the callback function required by the http server
  • handleRequest: handle the request body
  • createContext: Construct ctx, merge node's req, res, and construct Koa's parameters - ctx
  • onerror: error handling

Don't care about the rest, let's take a look at the constructor constructor

Application的构造器

Halo, this is all and what, let's start the simplest service and see the example

 const Koa = require('Koa')

const app = new Koa()

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

app.listen(3000, () => {
  console.log('3000请求成功')
})

console.dir(app)

实例

It can be seen that our instances correspond to the constructors one by one,

Break point to see prototype

断点

Oh, remove the non-key fields, we only focus on the key points

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

The prototypes are: listen, use, callback, handleRequest, createContext, onerror

Note: The following code is to delete exceptions and non-critical code

watch 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 go to 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 handling of context, and the special handling of res

Consolidation of middleware

Used koa-compose to merge middleware, which is also the key to the onion model, the source address of koa-compose: https://github.com/koajs/compose . This code has not been moved for three years, and it is stable.

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

It's hard to understand in a blink of an eye. We need to understand what middleware is, that is, an array of middleware. So how did it come from? There is this.middleware in the constructor, who used it -- use method

Let's jump out and look at the use method first

use

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

Except for exception handling, the key is these two steps, this.middleware is an array, the first step is to push the middleware in this.middleware ; the second step returns this so that it can be called in a chain. I was interviewed on how to make a chain call of promises, and I was confused. I didn't expect to see it here.

Looking back at the koa-compose source code, 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 it's running 123456

The composition of its this.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, and you can pass values if you are an object.

const fn = compose(this.middleware)

We JavaScriptize it, nothing else needs to be changed, just change the last function to

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

测试compose

测试compose2

Parse koa-compose line by line

This paragraph is very important. It is often tested during the interview. It asks you to write a compose by hand. Look at it.

 //1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中间件
//2. const fn = compose(this.middleware) 合并中间件
//3. fn() 执行中间件

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) , that is, 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() , that is, 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 为第一个中间件
      if (i === middleware.length) fn = next // 当弄到最后一个中间件时,最后一个中间件赋值为 fn
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
          // 返回一个 Promise 实例,执行 递归执行 dispatch(1)
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

That is, the first middleware must wait for the second middleware to finish executing before returning, and the second must wait for the third one to finish executing before returning until the middleware is finished executing.

Promise.resolve is a Promise instance, the reason why Promise.resolve is used is to solve asynchrony, and the reason why Promise.resolve is used to solve asynchronous

Throw away Promise.resolve , let's look at the use of recursion first, 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

Going back and looking at compose again, the code is something like

 // 假设 this.middleware = [fn1, fn2, fn3]
function fn(context, next) {
    if (i === middleware.length) fn = next // fn3 没有 next
    if (!fn) return Promise.resolve() // 因为 fn 为空,执行这一行
    function dispatch (0) {
        return Promise.resolve(fn(context, function dispatch(1) {
            return Promise.resolve(fn(context, function dispatch(2) {
                return Promise.resolve()
            }))
        }))
    }
  }
}

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

执行栈

Think more about the use of recursion, don't care too much about Promise.resolve

handling of context

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, return a context - context, the code is very clear, not explained

Special handling of res

In the callback, this.createContext is executed first. After getting the context, handleRequest is executed. 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请求成功');
});

After such a piece of code is instantiated, the four generals of this.middleware, this.context, this.request, and this.response are obtained. When you use app.use(), push the functions in it to this.middleware. When using app.listen() again, it is equivalent to starting an HTTP service, which combines middleware, obtains context, and handles res specially

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 caught my eye

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

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

The second can be seen this way, delegate is a proxy, which is designed for the convenience of developers

 // 将内部对象 response 的属性,委托至暴露在外的 proto 上
delegate(proto, 'response')
  .method('redirect')
  .method('vary')
  .access('status')
  .access('body')
  .getter('headerSent')
  .getter('writable');
  ...

And use delegate(proto, 'response').access('status')... , which is the file exported in context.js, to proxy all parameters on proto.response to proto, what is proto.response? It is context.response, where does context.response come from?

To recap, 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))
    ...
}

With context.response, it is clear, context.response = this.response, because of the delegate, the parameters on context.response are delegated to the context, for example

  • ctx.header is proxied on ctx.request.header
  • ctx.body is proxied on ctx.response.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

This is probably the case. After knowing so much, how to write a Koa2 by hand? Please see the next article - handwriting Koa2

References


山头人汉波
394 声望555 粉丝