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

最近使用koa2搭建博客,async/await异步流程控制确实比较优雅,可是在使用koa2过程当中也遇到很多的问题,如何编写中间件,如何替换express中间件为koa中间件,还有在实现服务端渲染的时候因为koa对于response有本身的封装,当时也花了不少时间去调bug。以为以上问题不少也是归根于本身对框架的不熟悉引发的,因此花了点时间阅读了下koa源码,分享下阅读时收获的东西以及koa2框架相关的分析。
javascript

koa2划分浅析

下图是我从node_modules目录下截的,koa核心就在这两部分,一个koa自己,一个中间件的合成流程控制koa-composevue

koa关键目录

这里先简略介绍koa2的各部分吧,后面再讲流程java

application.js

这个就是koa的入口主要文件,暴露应用的class, 这个class继承自node自带的events,这里就能够看出跟koa1.x很大的不一样,koa2大量使用es6的语法,这里就是一个例子,调用的时候就跟koa1.x有区别node

var koa = require('koa');
// koa 1.x
var app = koa();
// koa 2.x
// 使用class必须使用new来调用
var app = new koa();复制代码

application就是应用,暴露了一些公用的api,好比两个常见的,一个是listen,一个是use, listen就是调用http.createServer,传入callback,固然这个callback就是核心,它里面包含了中间件的合并,上下文的处理,对res的特殊处理(后面说流程会细说,这里先粗略讲讲),use的话用得就更多了,中间件每每是web框架的主要部分,可是use其实就是很简单起到收集中间件的做用而已,重点在于如何组合它们,如何设计请求到来时如何调用中间件,这些东西其实都在koa-composegit

context.js

这部分就是koa的应用上下文ctx,其实就一个简单的对象暴露,里面的重点在delegate,这个就是代理,这个就是为了开发者方便而设计的,好比咱们要访问ctx.repsponse.status可是咱们经过delegate,能够直接访问ctx.status访问到它(这个实现也不是很复杂,后面再讲)es6

request.js、response.js

这两部分就是对原生的res、req的一些操做了,大量使用es6的getset的一些语法,去取headers或者设置headers、还有设置body等等,这些就不详细介绍了,有兴趣的读者能够自行看源码github

koa2流程控制/中间件

一个简单的koa应用

咱们就先从简单的koa应用开始web

// 这里就先不用async/await
// 它们并非必须的
var koa = require('koa');
var app = new koa();

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

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

app.use((ctx, next) => {
  console.log(3)
  ctx.body = 'Hello World';
});

app.listen(3000);
// 访问http://localhost:3000
// 打印出一、二、三、四、5复制代码

上述简单的应用打印出一、二、三、四、5,这个其实就是koa中间件控制的核心,一个洋葱结构,从上往下一层一层进来,再从下往上一层一层回去,乍一看很复杂,为何不直接一层一层下来就结束呢,就像express/connect同样,咱们就只要next就去下一个中间件,干吗还要回来?express

其实这就是为了解决复杂应用中频繁的回调而设计的级联代码,并不直接把控制权彻底交给下一个中间件,而是碰到next去下一个中间件,等下面都执行完了,还会执行next如下的内容api

解决频繁的回调,这又有什么依据呢?举个简单的例子,假如咱们须要知道穿过中间件的时间,咱们使用koa能够轻松地写出来,可是使用express呢,能够去看下express reponse-time的源码,它就只能经过监听header被write out的时候而后触发回调函数计算时间,可是koa彻底不用写callback,咱们只须要在next后面加几行代码就解决了(直接使用.then()均可以)

// koa-guide v1的示例代码就是计算中间件穿越时间

var koa = require('koa');
var app = koa();

// x-response-time
app.use(function *(next){
  // (1) 进入路由
  var start = new Date;
  yield next;
  // (5) 再次进入 x-response-time 中间件,记录2次经过此中间件「穿越」的时间
  var ms = new Date - start;
  this.set('X-Response-Time', ms + 'ms');
  // (6) 返回 this.body
});

// logger
app.use(function *(next){
  // (2) 进入 logger 中间件
  var start = new Date;
  yield next;
  // (4) 再次进入 logger 中间件,记录2次经过此中间件「穿越」的时间
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

// response
app.use(function *(){
  // (3) 进入 response 中间件,没有捕获到下一个符合条件的中间件,传递到 upstream
  this.body = 'Hello World';
});

app.listen(3000);复制代码

koa-compose源码分析

这里你应该就对如何实现感兴趣了,这里目光就得转到koa-compose,
其实代码就这么点

const Promise = require('any-promise')
// 这里使用any-promise是为了兼容低版本node
module.exports = compose
function compose (middleware) {
    // 传入的middleware必须是一个数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    // 传入的middleware的每个元素都必须是函数
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  return function (context, next) {
    let index = -1 // 这里维护一个index的闭包
    return dispatch(0) // 从数组的第一个元素开始`dispatch`
    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()  //这两行就是来处理最后一个中间件还有next的状况的,实际上是能够直接resolve出来的
      try {
            // 这里就是传入next执行中间件代码了
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}复制代码

我稍微注释了一下代码

其实这部分要跟application.js中的callback 结合起来看

/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */

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

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

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

    return handleRequest;
  }复制代码

而callback的做用就是http.createServer(app.callback()).listen(...)

这里开始讲重点,koa-compose从语义上看就是组合,其实就是对koa中间件的组合,它返回了一个promise,执行完成后就执行koa2对res的特殊处理,最后res.end()

固然咱们关心的是如何对中间件组合,其实就是传入一个middleware数组
而后第一次取出数组的第一个元素,传入context和next代码,执行当前这个元素(这个中间件)

// 这里就是传入next执行中间件代码了
return Promise.resolve(fn(context, function next () {
    return dispatch(i + 1)
}))复制代码

其实后面根本没用到resolve的内容,这部分代码等价于

fn(context, function next () {
    return dispatch(i + 1)
})
return Promise.resolve()复制代码

核心就在于dispatch(i + 1),不过也很好理解嘛,就是将数组指针移向下一个,执行下一个中间件的代码
而后一直这样到最后一个中间件,假如最后一个中间件还有next那么下面这两段代码就起做用了

if (i === middleware.length) fn = next 
 if (!fn) return Promise.resolve()复制代码

由于middleware没有下一个了,而且其实外面那个next是空的,因此其实就能够return结束了

这里其实直接return就好了,这里的return Promise.resolve()实际上是没有用的,真正return出外面的是调用第一个中间件的resolve

嗯嗯,这样其实就结束了,一整套的中间件调用
读者可能还想问,不是洋葱结构吗?那怎么回去的呢?其实回去的代码其实就是函数压栈和出栈

看完如下代码你就懂了(其实这就是koa的中间件原理)

function a() {
    console.log(1)
    b();
    console.log(5)
    return Promise.resolve();
}
function b() {
    console.log(2)
    c();
    console.log(4)
}

function c() {
    console.log(3)
    return;
}
a();
// 输出一、二、三、四、5复制代码

koa2处理流程

一图胜千言

koa2核心设计/流程

我就粗浅划分为几部分吧

  • 初始化应用
  • 请求到来-建立上下文部分
  • 请求到来-中间件执行部分
  • 返回res特殊处理部分

初始化应用部分

首先就是咱们的app.js代码,初始的时候就是咱们new了个koa实例,而后开始写各类use,写个app.listen(3000);
use其实就是把你写的函数一个一个收集到一个middleware数组,listen的话就是http.createServer(app.callback()).listen(...)

请求到来-建立上下文部分

当一个请求过来的时候,由http.createServer的callback知道,它是能够传入req、res的,因此其实从这个入口能够拿到req、res,koa拿到后就createContext建立应用上下文,根据context.js、request.js、response.js建立,而且进行属性代理delegate

请求到来-中间件执行部分

请求到来执行了中间件的一系列流程,使用koa-compose将传入的middleware组合起来,而后返回了一个promise, 其实真正传入http.createServer callback的就下面(我简写了)

http.createServer((req, res) => {
 // ... 经过req,res建立上下文
 // fn是`koa-compose`返回的promise
 return fn(ctx).then(handleResponse).catch(onerror);
})复制代码

返回res特殊处理部分

咱们上一部分能够看到一个handleResponse,它是什么?其实咱们到这里尚未res.end(), koa前面其实都是使用ctx.body = xxx,那它是怎么write回res的呢,这部分逻辑就在function respond(){},handleResponse就如下一句

const handleResponse = () => respond(ctx);复制代码

respond到底作了什么呢,其实它就是判断你以前中间件写的body的类型,作一些处理,而后使用res.end(body)
到这里就结束了,返回了页面

读者到这里能够再看看那张图就比较清晰了

小插曲

Object.create(X.prototype) VS new X

在源码阅读时大量看到Object.create()的用法,这个又跟new X()有什么区别呢

引用下stackoverflow答案
stackoverflow.com/questions/4…

new Test():

1.create new Object() obj
2.set obj.__proto__ to Test.prototype
3.return Test.call(obj) || obj;
// normally obj is returned but constructors in JS can return a value

Object.create( Test.prototype )

1.create new Object() obj
2.set obj.__proto__ to Test.prototype
3.return obj;

能够看到其实new和Object.create前两个步骤都是同样,区别在第三步,其实就是Object.create不会执行构造函数,咱们来两段更直接的代码

var a = new A();
var b = Object.create(B.prototype)
function A() {
    console.log('a')
}
function B() {
    console.log('b')
}
// 咱们能够看到只输出了a,可是b并无输出复制代码
var a = new A();
var b = Object.create(B.prototype)
function A() {
  return {}
}
function B() {
  return {}
}
console.log(a)
console.log(b)
// 能够看到a就是{}
// b是一个对象,它的__proto__指向B.prototype复制代码

这样应该就很清晰了,Object.create()特殊还在于它的第二个参数,就像Object.defineProperties()能够定义新的属性或修改现有属性

delegates

koa2里有一堆属性代理,为了方便开发者更容易访问到一些属性,koa设计了一些属性代理,能够用ctx.body之类的去访问ctx.response.body,调用也很简单,诸如如下

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove'
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');复制代码

其实这个实现很简单
咱们简单来讲下method代理,其余同理
这里咱们用obj.foo来替代obj.request.foo

function method(proto, target, name) {
    proto[name] = function() {
        return proto[target][name].apply(proto[target], arguments)
    }
}
var obj = {};
obj.request = {
    foo: function(bar) {
       console.log(bar)
       console.log(this)
       return bar;
    }
}
method(obj, 'request', 'foo')
obj.foo('123')
// 输出123
// this输出obj.request复制代码

最后

谢谢阅读~
欢迎follow我哈哈github.com/BUPT-HJM
欢迎继续观光个人新博客~(老博客近期可能迁移)
个人博客也是关于koa2的一个实践,欢迎star😸
github.com/BUPT-HJM/vu…

欢迎关注

相关文章
相关标签/搜索