Koa源码分析

Posted 元程序

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Koa源码分析相关的知识,希望对你有一定的参考价值。

上篇文章写了如何阅读Koa的源码, 粗略的过了一下Koa的源码, 但是作为一个没有得出一个具体的结论, 中间件的运行原理也不清楚, 这里我们再仔细的过一遍Koa的源码.

跟着例子过一遍

首先还是先过一遍例子

 
   
   
 
  1. const Koa = require('koa');

  2. const app = new Koa();

  3. app.use(async ctx => {

  4.  ctx.body = 'Hello World';

  5. });

  6. app.listen(3000);

起一个web服务, 来一个Hello World, 作为http模块的再封装, 我们还是慢慢来挖掘它是如何封装的吧(无关的代码我都会删掉).

首先是 listen:

 
   
   
 
  1.  listen(...args) {

  2.    const server = http.createServer(this.callback());

  3.    return server.listen(...args);

  4.  }

http模块我们都知道 无非是 http.createServer(fn).listen(port), 其中fn带着req, res. 根据上面的封装我们可以肯定 this.callback肯定是带着请求以及进行响应了. 那么再来看看 this.callback吧.

 
   
   
 
  1.  callback() {

  2.    const fn = compose(this.middleware);

  3.    const handleRequest = (req, res) => {

  4.      const ctx = this.createContext(req, res);

  5.      return this.handleRequest(ctx, fn);

  6.    };

  7.    return handleRequest;

  8.  }

果然 callback返回的函数是带着req, res的, 那我继续往下走看 handleRequest究竟经历了什么, ctx大佬出现了, 我们在用koa的时候所有请求响应都是挂在ctx上的, 看起来ctx是通过 createContext创建的, 那就继续看 createContext吧:

 
   
   
 
  1.  createContext(req, res) {

  2.    const context = Object.create(this.context);

  3.    const request = context.request = Object.create(this.request);

  4.    const response = context.response = Object.create(this.response);

  5.    context.app = request.app = response.app = this;

  6.    context.req = request.req = response.req = req;

  7.    context.res = request.res = response.res = res;

  8.    request.ctx = response.ctx = context;

  9.    request.response = response;

  10.    response.request = request;

  11.    context.originalUrl = request.originalUrl = req.url;

  12.    context.cookies = new Cookies(req, res, {

  13.      keys: this.keys,

  14.      secure: request.secure

  15.    });

  16.    request.ip = request.ips[0] || req.socket.remoteAddress || '';

  17.    context.accept = request.accept = accepts(req);

  18.    context.state = {};

  19.    return context;

  20.  }

createContext比较简单, 就是把各种有用的没用的变量挂到context上, 代码也很简单, 但是因为涉及到request和response我们需要简单看一下request.js和response.js:

 
   
   
 
  1. module.exports = {

  2.  get header() {

  3.    return this.req.headers;

  4.  },

  5. //..more items

  6. }

都是很简单获取变量没啥好说的, 那么回到前面callback部分, ctx创建好了然后调用并返回了 this.handleReques, 没啥好说的, 继续看呗:

 
   
   
 
  1.  handleRequest(ctx, fnMiddleware) {

  2.    const res = ctx.res;

  3.    res.statusCode = 404;

  4.    const onerror = err => ctx.onerror(err);

  5.    const handleResponse = () => respond(ctx);

  6.    onFinished(res, onerror);

  7.    return fnMiddleware(ctx).then(handleResponse).catch(onerror);

  8.  }

这一部分略微复杂一点, 由上面看出来 fnMiddleware 是我们取出来的中间件, 然后我们把ctx传到中间件里执行, 跟我们的通常用法有点像了. 到这一步重点来了: 中间件

中间件

在探究中间件的原理之前, 不妨先来看看中间件是怎么用的, 来个简单的例子:

 
   
   
 
  1. const Koa = require('koa')

  2. const app = new Koa()

  3. app.use(async function m1 (ctx, nex) {

  4.   console.log('m1')

  5.   await next()

  6.   console.log('m2 end')

  7. })

  8. app.use(async function m2 (ctx, nex) {

  9.  console.log('m2')

  10.  await next()

  11.  console.log('m2 end')

  12. })

  13. app.use(async function m3 (ctx, nex) {

  14.  console.log('m3')

  15.  ctx.body = 'Hello World'

  16. })

上面的结果很明确了, 但是我们不妨来可视化一下: m1: 输出m1 await1: m1你先暂停一下让m2先走 m1: ... m2: 输出m2 await2: m2你也停一下让m3先走 m2: ...(委屈) m3: 输出m3, 上面的注意啦要远路返回了 m2: 输出m2 end m1注意了我要返回啦 m1: 输出m1 end

respond: ctx.body是Hello world呢 就糊弄一下用户返回吧

看到没, ctx.body不代表立即响应, 仅仅是一个我们后面会用到的变量, 也就是说我们的ctx过了一遍所有的中间件然后才会做出响应. 这里不提await神奇的暂停效果, 我们就需要可以这么用就行了. 那么我们这个中间件是怎么实现的呢, 来看compose.js:

 
   
   
 
  1. function compose (middleware) {

  2.  /**

  3.   * @param {Object} context

  4.   * @return {Promise}

  5.   * @api public

  6.   */

  7.  return function (context, next) {

  8.    let index = -1

  9.    return dispatch(0)

  10.    function dispatch (i) {

  11.      index = i

  12.      let fn = middleware[i]

  13.      if (!fn) return Promise.resolve()

  14.      return Promise.resolve(fn(context, function next () {

  15.          return dispatch(i + 1)

  16.        }))

  17.    }

  18.  }

  19. }

看过我前一篇的可以知道这里其实就是一个递归. 但是跟connect的递归不一样这里是Promise, 我们都知道await 跟Promise搭配味道更佳嘛. 重点是这个next, 我们调用了await next之后, 程序非得等这个Promise执行完不可, 我们来简化一下中间件的模型:

 
   
   
 
  1. Promise.resolve(async m1 () {

  2.  console.log(m1)

  3.  await Promise.resolve(async m2 () {

  4.    console.log(m2)

  5.    await Promise.resolve(async m3 () {

  6.      console.log(m3)

  7.      ctx.body = 'xxx'

  8.     })

  9.     console.log(m2 end)

  10.  })

  11.  console.log(m1 end)

  12. })

是不是这样一下就清楚了, 作为应用层的东西, 我们不需要去考虑async/await究竟是怎么实现的, 只需要了解它实现了什么样的效果.

还是得佩服tj大神. 有问题可以互相交流哈.


以上是关于Koa源码分析的主要内容,如果未能解决你的问题,请参考以下文章

koa2中间件koa和koa-compose源码分析原理

Koa源码分析

十分钟带你看完 KOA 源码

koa源码阅读[1]-koa与koa-compose

Koa入门和源码分析

koa-session 源码分析和理解