学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

若川大约 22 分钟

学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

1. 前言

你好,我是若川open in new window,微信搜索「若川视野」open in new window关注我,专注前端技术分享。欢迎加我微信ruochuan12,加群交流学习。

这是学习源码整体架构系列第七篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。

本文仓库地址open in new windowgit clone https://github.com/lxchuan12/koa-analysis.git

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库open in new window
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库open in new window
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库open in new window
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDKopen in new window
5.学习 vuex 源码整体架构,打造属于自己的状态管理库open in new window
6.学习 axios 源码整体架构,打造属于自己的请求库open in new window
7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理open in new window
8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理open in new window

感兴趣的读者可以点击阅读。
其他源码计划中的有:expressopen in new windowvue-rotueropen in new windowreact-reduxopen in new window 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

如果你简历上一不小心写了熟悉koa,面试官大概率会问:

1、koa洋葱模型怎么实现的。
2、如果中间件中的next()方法报错了怎么办。
3、co的原理是怎样的。
等等问题

导读
文章通过例子调试koa,梳理koa的主流程,来理解koa-compose洋葱模型原理和co库的原理,相信看完一定会有所收获。

本文目录
本文目录

本文学习的koa版本是v2.11.0。克隆的官方仓库的master分支。 截至目前(2020年3月11日),最新一次commit2020-01-04 07:41 Olle Jonsson eda27608build: Drop unused Travis sudo: false directive (#1416)

本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysisopen in new window。求个star呀。

2. 本文阅读最佳方式

star一下我的仓库,再把它git clone https://github.com/lxchuan12/koa-analysis.git克隆下来。不用管你是否用过nodejs。会一点点promise、generator、async、await等知识即可看懂。如果一点点也不会,可以边看阮一峰老师的《ES6标准入门》open in new window相关章节。跟着文章节奏调试和示例代码调试,动手调试(用vscode或者chrome)印象更加深刻。文章长段代码不用细看,可以调试时再细看。看这类源码文章百遍,可能不如自己多调试几遍。也欢迎加我微信交流ruochuan12

# 克隆我的这个仓库
git clone https://github.com/lxchuan12/koa-analysis.git
# chrome 调试:
# 全局安装 http-server
npm i -g http-server
hs koa/examples/
# 可以指定端口 -p 3001
# hs -p 3001 koa/examples/
# 浏览器中打开
# 然后在浏览器中打开localhost:8080,开心的把代码调试起来

这里把这个examples文件夹做个简单介绍。

  • middleware文件夹是用来vscode调试整体流程的。
  • simpleKoa 文件夹是koa简化版,为了调试koa-compose洋葱模型如何串联起来各个中间件的。
  • koa-convert文件夹是用来调试koa-convertco源码的。
  • co-generator文件夹是模拟实现co的示例代码。

3. vscode 调试 koa 源码方法

之前,我在知乎回答了一个问题一年内的前端看不懂前端框架源码怎么办?open in new window 推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:

1.借助调试
2.搜索查阅相关高赞文章
3.把不懂的地方记录下来,查阅相关文档
4.总结

看源码,调试很重要,所以我详细写下 koa 源码调试方法,帮助一些可能不知道如何调试的读者。

# 我已经克隆到我的koa-analysis仓库了
git clone https://github.com/koajs/koa.git
// package.json
{
  "name": "koa",
  "version": "2.11.0",
  "description": "Koa web app framework",
  "main": "lib/application.js",
}

克隆源码后,看package.json找到main,就知道入口文件是lib/application.js了。

大概看完项目结构后发现没有examples文件夹(一般项目都会有这个文件夹,告知用户如何使用该项目),这时仔细看README.md。 如果看英文README.md有些吃力,会发现在Community标题下有一个中文文档 v2.xopen in new window。同时也有一个examples仓库open in new window

# 我已经克隆下来到我的仓库了
git clone https://github.com/koajs/examples.git

这时再开心的把examples克隆到自己电脑。可以安装好依赖,逐个研究学习下这里的例子,然后可能就一不小心掌握了koa的基本用法。当然,我这里不详细写这一块了,我是自己手写一些例子来调试。

继续看文档会发现使用指南讲述编写中间件

3.1 使用文档中的中间件koa-compose例子来调试

学习 koa-compose 前,先看两张图。

洋葱模型示意图
洋葱模型示意图
洋葱模型中间件示意图
洋葱模型中间件示意图

koa中,请求响应都放在中间件的第一个参数context对象中了。

再引用Koa中文文档open in new window中的一段:

如果您是前端开发人员,您可以将 next(); 之前的任意代码视为“捕获”阶段,这个简易的 gif 说明了 async 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:

中间件gif图
中间件gif图
  1. 创建一个跟踪响应时间的日期
  2. 等待下一个中间件的控制
  3. 创建另一个日期跟踪持续时间
  4. 等待下一个中间件的控制
  5. 将响应主体设置为“Hello World”
  6. 计算持续时间
  7. 输出日志行
  8. 计算响应时间
  9. 设置 X-Response-Time 头字段
  10. 交给 Koa 处理响应

读者们看完这个gif图,也可以思考下如何实现的。根据表现,可以猜测是next是一个函数,而且返回的可能是一个promise,被await调用。

看到这个gif图,我把之前写的examples/koa-compose的调试方法含泪删除了。默默写上gif图上的这些代码,想着这个读者们更容易读懂。 我把这段代码写在这里 koa/examples/middleware/app.jsopen in new window便于调试。

在项目路径下配置新建.vscode/launch.jsonopen in new window文件,program配置为自己写的koa/examples/middleware/app.js文件。

.vscode/launch.json 代码,点击这里展开/收缩,可以复制
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/koa/examples/middleware/app.js"
        }
    ]
}

F5键开始调试,调试时先走主流程,必要的地方打上断点,不用一开始就关心细枝末节。

断点调试要领:
赋值语句可以一步跳过,看返回值即可,后续详细再看。
函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。

上述比较啰嗦的写了一堆调试方法。主要是想着授人予鱼不如授人予渔,这样换成其他源码也会调试了。

简单说下chrome调试nodejschrome浏览器打开chrome://inspect,点击配置**configure...**配置127.0.0.1:端口号(端口号在Vscode 调试控制台显示了)。
更多可以查看English Debugging Guideopen in new window
中文调试指南open in new window
喜欢看视频的读者也可以看慕课网这个视频node.js调试入门open in new window,讲得还是比较详细的。
不过我感觉在chrome调试nodejs项目体验不是很好(可能是我方式不对),所以我大部分具体的代码时都放在html文件script形式,在chrome调试了。

4. 先看看 new Koa() 结果app是什么

看源码我习惯性看它的实例对象结构,一般所有属性和方法都放在实例对象上了,而且会通过原型链查找形式查找最顶端的属性和方法。

koa/examples/middleware/app.js文件调试时,先看下执行new Koa()之后,app是什么,有个初步印象。

// 文件 koa/examples/middleware/app.js
const Koa = require('../../lib/application');

// const Koa = require('koa');
// 这里打个断点
const app = new Koa();
// x-response-time

// 这里打个断点
app.use(async (ctx, next) => {

});

在调试控制台ctrl + 反引号键(一般在Tab上方的按键)唤起,输入app,按enter键打印app。会有一张这样的图。

koa 实例对象调试图
koa 实例对象调试图

VScode也有一个代码调试神器插件Debug Visualizeropen in new window

安装好后插件后,按ctrl + shift + p,输入Open a new Debug Visualizer View,来使用,输入app,显示是这样的。

koa 实例对象可视化简版
koa 实例对象可视化简版

不过目前体验来看,相对还比较鸡肋,只能显示一级,而且只能显示对象,相信以后会更好。更多玩法可以查看它的文档。

我把koa实例对象比较完整的用xmind画出来了,大概看看就好,有个初步印象。

koa 实例对象
koa 实例对象

接着,我们可以看下app 实例、context、request、request的官方文档。

4.1 app 实例、context、request、request 官方API文档

可以真正使用的时候再去仔细看文档。

5. koa 主流程梳理简化

通过F5启动调试(直接跳到下一个断点处)F10单步跳过F11单步调试等,配合重要的地方断点,调试完整体代码,其实比较容易整理出如下主流程的代码。

class Emitter{
  // node 内置模块
  constructor(){
  }
}
class Koa extends Emitter{
  constructor(options){
    super();
    options = options || {};
    this.middleware = [];
    this.context = {
      method: 'GET',
      url: '/url',
      body: undefined,
      set: function(key, val){
        console.log('context.set', key, val);
      },
    };
  }
  use(fn){
    this.middleware.push(fn);
    return this;
  }
  listen(){
    const  fnMiddleware = compose(this.middleware);
    const ctx = this.context;
    const handleResponse = () => respond(ctx);
    const onerror = function(){
      console.log('onerror');
    };
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}
function respond(ctx){
  console.log('handleResponse');
  console.log('response.end', ctx.body);
}

重点就在listen函数里的compose这个函数,接下来我们就详细来欣赏下这个函数。

6. koa-compose 源码(洋葱模型实现)

通过app.use() 添加了若干函数,但是要把它们串起来执行呀。像上文的gif图一样。

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

 //  传入对象 context 返回Promise
  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'))
      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)
      }
    }
  }
}

把简化的代码和koa-compose代码写在了一个文件中。koa/examples/simpleKoa/koa-compose.jsopen in new window

hs koa/examples/
# 然后可以打开localhost:8080/simpleKoa,开心的把代码调试起来

不过这样好像还是有点麻烦,我还把这些代码放在codepen https://codepen.io/lxchuan12/pen/wvarPEbopen in new window中,直接可以在线调试啦。是不是觉得很贴心_,自己多调试几遍便于消化理解。

你会发现compose就是类似这样的结构(移除一些判断)。

// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);

也就是说koa-compose返回的是一个PromisePromise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。
第一个next函数里也是返回的是一个PromisePromise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。
第二个next函数里也是返回的是一个PromisePromise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。
第三个...
以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。 这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。

不得不说非常惊艳,“玩还是大神会玩”

这种把函数存储下来的方式,在很多源码中都有看到。比如lodash源码的惰性求值,vuex也是把action等函数存储下,最后才去调用。

搞懂了koa-compose 洋葱模型实现的代码,其他代码就不在话下了。

7. 错误处理

中文文档 错误处理open in new window

仔细看文档,文档中写了三种捕获错误的方式。

// application.js 文件
class Application extends Emitter {
  // 代码有简化组合
  listen(){
    const  fnMiddleware = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const onerror = err => ctx.onerror(err);
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
  onerror(err) {
    // 代码省略
    // ...
  }
}

ctx.onerror

lib/context.js文件中,有一个函数onerror,而且有这么一行代码this.app.emit('error', err, this)

module.exports = {
  onerror(){
    // delegate
    // app 是在new Koa() 实例
    this.app.emit('error', err, this);
  }
}
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    err.status = err.statusCode || err.status || 500;
    throw err;
  }
});

try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);,这里的onerrorctx.onerror
ctx.onerror函数中又调用了this.app.emit('error', err, this),所以在最外围app.on('error',err => {})可以捕获中间件链中的错误。 因为koa继承自events模块,所以有'emit'和on等方法)

8. koa2 和 koa1 的简单对比

中文文档中描述了 koa2 和 koa1 的区别open in new window

koa1中主要是generator函数。koa2中会自动转换generator函数。

// Koa 将转换
app.use(function *(next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
});

8.1 koa-convert 源码

vscode/launch.json文件,找到这个program字段,修改为"program": "${workspaceFolder}/koa/examples/koa-convert/app.js"

通过F5启动调试(直接跳到下一个断点处)F10单步跳过F11单步调试调试走一遍流程。重要地方断点调试。

app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

class Koa extends Emitter{
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
}

koa-convert源码挺多,核心代码其实是这样的。

function convert(){
 return function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  function * createGenerator (next) {
    return yield next()
  }
}

最后还是通过co来转换的。所以接下来看co的源码。

8.2 co 源码

tj大神写的co 仓库open in new window

本小节的示例代码都在这个文件夹koa/examples/co-generatoropen in new window中,hs koa/example,可以自行打开https://localhost:8080/co-generator调试查看。

co源码前,先看几段简单代码。

// 写一个请求简版请求
function request(ms= 1000) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({name: '若川'});
    }, ms);
  });
}
// 获取generator的值
function* generatorFunc(){
  const res = yield request();
  console.log(res, 'generatorFunc-res');
}
generatorFunc(); // 报告,我不会输出你想要的结果的

简单来说co,就是把generator自动执行,再返回一个promisegenerator函数这玩意它不自动执行呀,还要一步步调用next(),也就是叫它走一步才走一步

所以有了async、await函数。

// await 函数 自动执行
async function asyncFunc(){
    const res = await request();
    console.log(res, 'asyncFunc-res await 函数 自动执行');
}
asyncFunc(); // 输出结果

也就是说co需要做的事情,是让generatorasync、await函数一样自动执行。

8.3 模拟实现简版 co(第一版)

这时,我们来模拟实现第一版的co。根据generator的特性,其实容易写出如下代码。

// 获取generator的值
function* generatorFunc(){
  const res = yield request();
  console.log(res, 'generatorFunc-res');
}

function coSimple(gen){
  gen = gen();
  console.log(gen, 'gen');

  const ret = gen.next();
  const promise = ret.value;
  promise.then(res => {
    gen.next(res);
  });
}
coSimple(generatorFunc);
// 输出了想要的结果
// {name: "若川"}"generatorFunc-res"

8.4 模拟实现简版 co(第二版)

但是实际上,不会上面那么简单的。有可能是多个yield和传参数的情况。 传参可以通过这如下两行代码来解决。

const args = Array.prototype.slice.call(arguments, 1);
gen = gen.apply(ctx, args);

两个yield,我大不了重新调用一下promise.then,搞定。

// 多个yeild,传参情况
function* generatorFunc(suffix = ''){
  const res = yield request();
  console.log(res, 'generatorFunc-res' + suffix);

  const res2 = yield request();
  console.log(res2, 'generatorFunc-res-2' + suffix);
}

function coSimple(gen){
  const ctx = this;
  const args = Array.prototype.slice.call(arguments, 1);
  gen = gen.apply(ctx, args);
  console.log(gen, 'gen');

  const ret = gen.next();
  const promise = ret.value;
  promise.then(res => {
    const ret = gen.next(res);
    const promise = ret.value;
      promise.then(res => {
        gen.next(res);
      });
  });
}

coSimple(generatorFunc, ' 哎呀,我真的是后缀');

8.5 模拟实现简版 co(第三版)

问题是肯定不止两次,无限次的yield的呢,这时肯定要把重复的封装起来。而且返回是promise,这就实现了如下版本的代码。

function* generatorFunc(suffix = ''){
  const res = yield request();
  console.log(res, 'generatorFunc-res' + suffix);

  const res2 = yield request();
  console.log(res2, 'generatorFunc-res-2' + suffix);

  const res3 = yield request();
  console.log(res3, 'generatorFunc-res-3' + suffix);

  const res4 = yield request();
  console.log(res4, 'generatorFunc-res-4' + suffix);
}

function coSimple(gen){
  const ctx = this;
  const args = Array.prototype.slice.call(arguments, 1);
  gen = gen.apply(ctx, args);
  console.log(gen, 'gen');

  return new Promise((resolve, reject) => {

    onFulfilled();

    function onFulfilled(res){
      const ret = gen.next(res);
      next(ret);
    }

    function next(ret) {
      const promise = ret.value;
      promise && promise.then(onFulfilled);
    }

  });
}

coSimple(generatorFunc, ' 哎呀,我真的是后缀');

但第三版的模拟实现简版co中,还没有考虑报错和一些参数合法的情况。

8.6 最终来看下co源码

这时来看看co的源码,报错和错误的情况,错误时调用reject,是不是就好理解了一些呢。

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    // 把参数传递给gen函数并执行
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    // 如果不是函数 直接返回
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    // 反复执行调用自己
    function next(ret) {
      // 检查当前是否为 Generator 函数的最后一步,如果是就返回
      if (ret.done) return resolve(ret.value);
      // 确保返回值是promise对象。
      var value = toPromise.call(ctx, ret.value);
      // 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      // 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

9. koa 和 express 简单对比

中文文档 koa 和 express 对比open in new window

文档里写的挺全面的。简单来说koa2语法更先进,更容易深度定制(egg.jsthink.js、底层框架都是koa)。

10. 总结

文章通过授人予鱼不如授人予鱼的方式,告知如何调试源码,看完了koa-compose洋葱模型实现,koa-convertco等源码。

koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise

koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。

co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。

koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。

本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysisopen in new window。求个star呀。

git clone https://github.com/lxchuan12/koa-analysis.git

再强烈建议下按照本文阅读最佳方式,克隆代码下来,动手调试代码学习更加深刻

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出,也欢迎加我微信交流ruochuan12。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。

10.1 解答下开头的提问

仅供参考

1、koa洋葱模型怎么实现的。

可以参考上文整理的简版koa-compose作答。

// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);

答:app.use() 把中间件函数存储在middleware数组中,最终会调用koa-compose导出的函数compose返回一个promise,中间函数的第一个参数ctx是包含响应和请求的一个对象,会不断传递给下一个中间件。next是一个函数,返回的是一个promise

2、如果中间件中的next()方法报错了怎么办。

可参考上文整理的错误处理作答。

ctx.onerror = function {
  this.app.emit('error', err, this);
};
  listen(){
    const  fnMiddleware = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const onerror = err => ctx.onerror(err);
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
  onerror(err) {
    // 代码省略
    // ...
  }

答:中间件链错误会由ctx.onerror捕获,该函数中会调用this.app.emit('error', err, this)(因为koa继承自events模块,所以有'emit'和on等方法),可以使用app.on('error', (err) => {}),或者app.onerror = (err) => {}进行捕获。

3、co的原理是怎样的。
答:co的原理是通过不断调用generator函数的next方法来达到自动执行generator函数的,类似async、await函数自动执行。

答完,面试官可能觉得小伙子还是蛮懂koa的啊。当然也可能继续追问,直到答不出...

10.2 还能做些什么 ?

学完了整体流程,koa-composekoa-convertco的源码。

还能仔细看看看http请求上下文(context)、http请求对象、http响应对象的具体实现。

还能根据我文章说的调试方式调试koa 组织open in new window中的各种中间件,比如koa-bodyparser, koa-routerkoa-jwtkoa-sessionkoa-cors等等。

还能把examples仓库open in new window克隆下来,我的这个仓库已经克隆了,挨个调试学习下源码。

web框架有很多,比如Express.jsKoa.jsEgg.jsNest.jsNext.jsFastify.jsHapi.jsRestify.jsLoopback.ioSails.jsMidway.js等等。

还能把这些框架的优势劣势、设计思想等学习下。

还能继续学习HTTP协议、TCP/IP协议网络相关,虽然不属于koa的知识,但需深入学习掌握。

学无止境~~~

11. 推荐阅读

koa 官网open in new window | koa 仓库open in new window | koa 组织open in new window | koa2 中文文档open in new window | co 仓库open in new window
知乎@姚大帅:可能是目前市面上比较有诚意的Koa2源码解读open in new window
知乎@零小白:十分钟带你看完 KOA 源码open in new window
微信开放社区@小丹の:可能是目前最全的koa源码解析指南open in new window
IVWEB官方账号: KOA2框架原理解析和实现open in new window
深入浅出vue.js 作者 berwin: 深入浅出 Koa2 原理open in new window
阮一峰老师:co 函数库的含义和用法open in new window

另一个系列

面试官问:JS的继承open in new window
面试官问:JS的this指向open in new window
面试官问:能否模拟实现JS的call和apply方法open in new window
面试官问:能否模拟实现JS的bind方法open in new window
面试官问:能否模拟实现JS的new操作符open in new window

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
若川的博客open in new window,使用vuepress重构了,阅读体验可能更好些
掘金专栏open in new window,欢迎关注~
segmentfault前端视野专栏open in new window,欢迎关注~
知乎前端视野专栏open in new window,欢迎关注~
语雀前端视野专栏open in new window,新增语雀专栏,欢迎关注~
github blogopen in new window,相关源码和资源都放在这里,求个star_~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注(回复pdf获取前端优质书籍pdf)。欢迎加我微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野
若川视野
欢迎扫码加我微信
拉你进源码共读群
一起学习源码