Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Koa2原理详解 #18

Open
pfan123 opened this issue Aug 3, 2017 · 0 comments
Open

Koa2原理详解 #18

pfan123 opened this issue Aug 3, 2017 · 0 comments

Comments

@pfan123
Copy link
Owner

pfan123 commented Aug 3, 2017

Koa vs Express

Koa是继Express之后,Node的又一主流Web开发框架。相比于Express,Koa只保留了核心的中间件处理逻辑,去掉了路由,模板,以及其他一些功能。详细的比较可以参考Koa vs Express

另一方面,在中间件的处理过程中,Koa和Express也有着一定区别,看下面例子:

// http style
http.createServer((req, res) => {
  // ...
})

// express style
app.use((req, res, next) => {
  // ...
})

// koa style
app.use( async (ctx, next) => {
  // ...
})

Node自带的http模块处理请求的时候,参数是一个req和res,分别为http.IncomingMessage和http.ServerResponse的实例。

Express对请求参数req和res的原型链进行了扩展,增强了req和res的行为。

而Koa并没有改变req和res,而是通过req和res封装了一个ctx (context)对象,进行后面的逻辑处理。

Koa基本组成

Koa源码非常精简,只有四个文件:

  • application.js:Application(或Koa)负责管理中间件,以及处理请求
  • context.js:Context维护了一个请求的上下文环境
  • request.js:Request对req做了抽象和封装
  • response.js:Response对res做了抽象和封装

Application

Application主要维护了中间件以及其它一些环境:

// application.js
module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  // ...
}
通过app.use(fn)可以将fn添加到中间件列表this.middleware中。

app.listen方法源码如下:

// application.js
listen() {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
}

首先会通过this.callback方法来返回一个函数作为http.createServer的回调函数,然后进行监听。我们已经知道,http.createServer的回调函数接收两个参数:req和res,下面来看this.callback的实现:

// application.js
callback() {
  const fn = compose(this.middleware);

  if (!this.listeners('error').length) this.on('error', this.onerror);

  return (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
  };
}

首先是将所有的中间件通过 compose 组合成一个函数fn,然后返回http.createServer所需要的回调函数。于是我们可以看到,当服务器收到一个请求的时候,会使用req和res通过this.createContext方法来创建一个上下文环境ctx,然后使用fn来进行中间件的逻辑处理。

Context

通过上面的分析,我们已经可以大概得知Koa处理请求的过程:当请求到来的时候,会通过req和res来创建一个context (ctx),然后执行中间件。

事实上,在创建context的时候,还会同时创建request和response,通过下图可以比较直观地看到所有这些对象之间的关系。

context

图中:

  • 最左边一列表示每个文件的导出对象
  • 中间一列表示每个Koa应用及其维护的属性
  • 右边两列表示对应每个请求所维护的一些对象
  • 黑色的线表示实例化
  • 红色的线表示原型链
  • 蓝色的线表示属性

实际上,ctx主要的功能是代理request和response的功能,提供了对request和response对象的便捷访问能力。在源码中,我们可以看到:

// context.js
delegate(proto, 'response')
  .method('attachment')
  // ...
  .access('status')
  // ...
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  // ...
  .access('querystring')
  // ...
  .getter('ip');

这里使用了 delegates 模块来实现属性访问的代理。简单来说,通过delegate(proto, 'response'),当访问proto的代理属性的时候,实际上是在访问proto.response的对应属性。

Request & Response

Request对req进行了抽象和封装,其中对于请求的url相关的处理如图:

┌────────────────────────────────────────────────────────┐
│ href │
├────────────────────────────┬───────────────────────────┤
│ origin │ url / originalurl │
├──────────┬─────────────────┼──────────┬────────────────┤
│ protocol │ host │ path │ search │
├──────────├──────────┬──────┼──────────┼─┬──────────────┤
│ │ hostname │ port │ │?│ querystring │
│ ├──────────┼──────┤ ├─┼──────────────┤
│ │ │ │ │ │ │
" http: │ host.com : 8080 /p/a/t/h ? query=string │
│ │ │ │ │ │ │
└──────────┴──────────┴──────┴──────────┴─┴──────────────┘

Response对res进行了封装和抽象,这里不做赘述。

中间件的执行

在上面已经提到,所有的中间件会经过 compose 处理,返回一个新的函数。该模块源码如下:

function compose(middleware) {
  // 错误处理
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function(context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)

    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 当前执行第 i 个中间件
      index = i
      let fn = middleware[i]
      // 所有的中间件执行完毕
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()

      try {
        // 执行当前的中间件
        // 这里的fn也就是app.use(fn)中的fn
        return Promise.resolve(fn(context, function next() {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa的中间件支持普通函数,返回一个Promise的函数,以及async函数。由于generator函数中间件在新的版本中将不再支持,因此不建议使用。

参考资料:

koa2-note

koa-guide

koa-0.0.2

带你走进koa2的世界(koa2源码浅谈)

深入浅出 Koa

Koa2源码初读

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant