{"id":13798678,"url":"https://github.com/ruochuan12/koa-analysis","last_synced_at":"2025-09-05T21:38:47.414Z","repository":{"id":53930353,"uuid":"229003250","full_name":"ruochuan12/koa-analysis","owner":"ruochuan12","description":"学习源码整体架构系列多篇之koa源码，前端面试高频源码，微信搜索「若川视野」关注我，长期交流学习~","archived":false,"fork":false,"pushed_at":"2021-04-03T09:53:14.000Z","size":2604,"stargazers_count":77,"open_issues_count":1,"forks_count":8,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-05-19T04:47:10.041Z","etag":null,"topics":["koa","koa-analysis","koa-express","nodejs","nodejs-server"],"latest_commit_sha":null,"homepage":"https://juejin.im/post/5e69925cf265da571e262fe6","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ruochuan12.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-19T07:51:04.000Z","updated_at":"2023-12-30T02:37:28.000Z","dependencies_parsed_at":"2022-08-13T04:40:47.564Z","dependency_job_id":null,"html_url":"https://github.com/ruochuan12/koa-analysis","commit_stats":null,"previous_names":["ruochuan12/koa-analysis"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fkoa-analysis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fkoa-analysis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fkoa-analysis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fkoa-analysis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruochuan12","download_url":"https://codeload.github.com/ruochuan12/koa-analysis/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244822684,"owners_count":20516152,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["koa","koa-analysis","koa-express","nodejs","nodejs-server"],"created_at":"2024-08-04T00:00:49.574Z","updated_at":"2025-03-21T15:31:35.118Z","avatar_url":"https://github.com/ruochuan12.png","language":"JavaScript","funding_links":[],"categories":["资源"],"sub_categories":["教程"],"readme":"# 学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理\n\n## 1. 前言\n\n\u003e你好，我是[若川](https://lxchuan12.gitee.io)，微信搜索[「若川视野」](https://mp.weixin.qq.com/s/c3hFML3XN9KCUetDOZd-DQ)关注我，专注前端技术分享。欢迎加我微信`ruochuan12`，加群交流学习。\n\n\u003e这是`学习源码整体架构系列`第七篇。整体架构这词语好像有点大，姑且就算是源码整体结构吧，主要就是学习是代码整体结构，不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。\n\n\u003e[本文仓库地址](https://github.com/lxchuan12/koa-analysis.git)：`git clone https://github.com/lxchuan12/koa-analysis.git`\n\n\u003e**要是有人说到怎么读源码，正在读文章的你能推荐我的源码系列文章，那真是太好了**。\n\n`学习源码整体架构系列`文章如下：\n\u003e1.[学习 jQuery 源码整体架构，打造属于自己的 js 类库](https://juejin.im/post/5d39d2cbf265da1bc23fbd42)\u003cbr\u003e\n\u003e2.[学习 underscore 源码整体架构，打造属于自己的函数式编程类库](https://juejin.im/post/5d4bf94de51d453bb13b65dc)\u003cbr\u003e\n\u003e3.[学习 lodash 源码整体架构，打造属于自己的函数式编程类库](https://juejin.im/post/5d767e1d6fb9a06b032025ea)\u003cbr\u003e\n\u003e4.[学习 sentry 源码整体架构，打造属于自己的前端异常监控SDK](https://juejin.im/post/5dba5a39e51d452a2378348a)\u003cbr\u003e\n\u003e5.[学习 vuex 源码整体架构，打造属于自己的状态管理库](https://juejin.im/post/5dd4e61a6fb9a05a5c010af0)\u003cbr\u003e\n\u003e6.[学习 axios 源码整体架构，打造属于自己的请求库](https://juejin.im/post/5df349b5518825123751ba66)\u003cbr\u003e\n\u003e7.[学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理](https://juejin.im/post/5e69925cf265da571e262fe6)\u003cbr\u003e\n\u003e8.[学习 redux 源码整体架构，深入理解 redux 及其中间件原理](https://juejin.im/post/5ee63b7d51882542fc6265ad)\n\n感兴趣的读者可以点击阅读。\u003cbr\u003e\n其他源码计划中的有：[`express`](https://github.com/lxchuan12/express-analysis)、[`vue-rotuer`](https://github.com/lxchuan12/vue-router-analysis)、[`react-redux`](https://github.com/lxchuan12/react-redux-analysis) 等源码，不知何时能写完（哭泣），欢迎持续关注我（若川）。\n\n源码类文章，一般阅读量不高。已经有能力看懂的，自己就看了。不想看，不敢看的就不会去看源码。\u003cbr\u003e\n所以我的文章，尽量写得让想看源码又不知道怎么看的读者能看懂。\n\n如果你简历上一不小心写了熟悉`koa`，面试官大概率会问：\n\u003e1、`koa`洋葱模型怎么实现的。\u003cbr\u003e\n\u003e2、如果中间件中的`next()`方法报错了怎么办。\u003cbr\u003e\n\u003e3、`co`的原理是怎样的。\u003cbr\u003e\n\u003e等等问题\u003cbr\u003e\n\n**导读**\u003cbr\u003e\n文章通过例子调试`koa`，梳理`koa`的主流程，来理解`koa-compose`洋葱模型原理和`co`库的原理，相信看完一定会有所收获。\n\n![本文目录](./images/contents.png)\n\n本文学习的`koa`版本是`v2.11.0`。克隆的官方仓库的`master`分支。\n截至目前（2020年3月11日），最新一次`commit`是`2020-01-04 07:41 Olle Jonsson` `eda27608`，`build: Drop unused Travis sudo: false directive (#1416)`。\n\n本文仓库在这里[若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis](https://github.com/lxchuan12/koa-analysis)。求个`star`呀。\n\n## 2. 本文阅读最佳方式\n\n先`star`一下我的仓库，再把它`git clone https://github.com/lxchuan12/koa-analysis.git`克隆下来。不用管你是否用过`nodejs`。会一点点`promise、generator、async、await`等知识即可看懂。如果一点点也不会，可以边看阮一峰老师的[《ES6标准入门》](https://es6.ruanyifeng.com/#docs/generator)相关章节。**跟着文章节奏调试和示例代码调试，动手调试（用`vscode`或者`chrome`）印象更加深刻**。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流`ruochuan12`。\n\n```bash\n# 克隆我的这个仓库\ngit clone https://github.com/lxchuan12/koa-analysis.git\n# chrome 调试：\n# 全局安装 http-server\nnpm i -g http-server\nhs koa/examples/\n# 可以指定端口 -p 3001\n# hs -p 3001 koa/examples/\n# 浏览器中打开\n# 然后在浏览器中打开localhost:8080，开心的把代码调试起来\n```\n\n这里把这个`examples`文件夹做个简单介绍。\u003cbr\u003e\n- `middleware`文件夹是用来`vscode`调试整体流程的。\u003cbr\u003e\n- `simpleKoa` 文件夹是`koa`简化版，为了调试`koa-compose`洋葱模型如何串联起来各个中间件的。\u003cbr\u003e\n- `koa-convert`文件夹是用来调试`koa-convert`和`co`源码的。\u003cbr\u003e\n- `co-generator`文件夹是模拟实现`co`的示例代码。\u003cbr\u003e\n\n## 3. vscode 调试 koa 源码方法\n\n之前，我在知乎回答了一个问题[一年内的前端看不懂前端框架源码怎么办？](https://www.zhihu.com/question/350289336/answer/910970733)\n推荐了一些资料，阅读量还不错，大家有兴趣可以看看。主要有四点：\u003cbr\u003e\n\u003e1.借助调试\u003cbr\u003e\n\u003e2.搜索查阅相关高赞文章\u003cbr\u003e\n\u003e3.把不懂的地方记录下来，查阅相关文档\u003cbr\u003e\n\u003e4.总结\u003cbr\u003e\n\n看源码，调试很重要，所以我详细写下 `koa` 源码调试方法，帮助一些可能不知道如何调试的读者。\n\n```bash\n# 我已经克隆到我的koa-analysis仓库了\ngit clone https://github.com/koajs/koa.git\n```\n\n```json\n// package.json\n{\n  \"name\": \"koa\",\n  \"version\": \"2.11.0\",\n  \"description\": \"Koa web app framework\",\n  \"main\": \"lib/application.js\",\n}\n```\n\n克隆源码后，看`package.json`找到`main`，就知道入口文件是`lib/application.js`了。\n\n大概看完项目结构后发现没有`examples`文件夹（一般项目都会有这个文件夹，告知用户如何使用该项目），这时仔细看`README.md`。\n如果看英文`README.md`有些吃力，会发现在`Community`标题下有一个[中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN)。同时也有一个[`examples`仓库](https://github.com/koajs/examples)。\n\n```bash\n# 我已经克隆下来到我的仓库了\ngit clone https://github.com/koajs/examples.git\n```\n\n这时再开心的把`examples`克隆到自己电脑。可以安装好依赖，逐个研究学习下这里的例子，然后可能就一不小心掌握了`koa`的基本用法。当然，我这里不详细写这一块了，我是自己手写一些例子来调试。\n\n继续看文档会发现**使用指南**讲述`编写中间件`。\n\n### 3.1 使用文档中的中间件`koa-compose`例子来调试\n\n学习 `koa-compose` 前，先看两张图。\n\n![洋葱模型示意图](./images/models.png)\n\n![洋葱模型中间件示意图](./images/middleware.png)\n\n在`koa`中，请求响应都放在中间件的第一个参数`context`对象中了。\n\n再引用[Koa中文文档](https://github.com/demopark/koa-docs-Zh-CN/blob/master/guide.md#debugging-koa)中的一段：\n\n如果您是前端开发人员，您可以将 `next()`; 之前的任意代码视为“捕获”阶段，这个简易的 `gif` 说明了 `async` 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流：\n\n![中间件gif图](./images/middleware.gif)\n\n\u003e\n\u003e   1. 创建一个跟踪响应时间的日期\n\u003e   2. 等待下一个中间件的控制\n\u003e   3. 创建另一个日期跟踪持续时间\n\u003e   4. 等待下一个中间件的控制\n\u003e   5. 将响应主体设置为“Hello World”\n\u003e   6. 计算持续时间\n\u003e   7. 输出日志行\n\u003e   8. 计算响应时间\n\u003e   9. 设置 `X-Response-Time` 头字段\n\u003e   10. 交给 Koa 处理响应\n\n读者们看完这个gif图，也可以思考下如何实现的。根据表现，可以猜测是`next`是一个函数，而且返回的可能是一个`promise`，被`await`调用。\n\n看到这个`gif`图，我把之前写的`examples/koa-compose`的调试方法**含泪删除**了。默默写上`gif`图上的这些代码，想着这个读者们更容易读懂。\n我把这段代码写在这里 [`koa/examples/middleware/app.js`](https://github.com/lxchuan12/koa-analysis/blob/master/koa/examples/middleware/app.js)便于调试。\n\n在项目路径下配置新建[.vscode/launch.json](https://github.com/lxchuan12/koa-analysis/blob/master/.vscode/launch.json)文件，`program`配置为自己写的`koa/examples/middleware/app.js`文件。\n\n\u003cdetails\u003e\n\u003csummary\u003e.vscode/launch.json 代码，点击这里展开/收缩，可以复制\u003c/summary\u003e\n\n```json\n{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"启动程序\",\n            \"skipFiles\": [\n                \"\u003cnode_internals\u003e/**\"\n            ],\n            \"program\": \"${workspaceFolder}/koa/examples/middleware/app.js\"\n        }\n    ]\n}\n```\n\n\u003c/details\u003e\n\n按`F5键`开始调试，调试时先走主流程，必要的地方打上断点，不用一开始就关心细枝末节。\n\n\u003e**断点调试要领：**\u003cbr\u003e\n**赋值语句可以一步跳过，看返回值即可，后续详细再看。**\u003cbr\u003e\n**函数执行需要断点跟着看，也可以结合注释和上下文倒推这个函数做了什么。**\u003cbr\u003e\n\n上述比较啰嗦的写了一堆调试方法。主要是想着`授人予鱼不如授人予渔`，这样换成其他源码也会调试了。\n\n简单说下`chrome`调试`nodejs`，`chrome`浏览器打开`chrome://inspect`，点击配置**configure...**配置`127.0.0.1:端口号`(端口号在`Vscode` 调试控制台显示了)。\u003cbr\u003e\n更多可以查看[English Debugging Guide](https://nodejs.org/en/docs/inspector)\u003cbr\u003e\n[中文调试指南](https://nodejs.org/zh-cn/docs/guides/debugging-getting-started/)\u003cbr\u003e\n喜欢看视频的读者也可以看慕课网这个视频[node.js调试入门](https://www.imooc.com/learn/1093)，讲得还是比较详细的。\u003cbr\u003e\n不过我感觉在`chrome`调试`nodejs`项目体验不是很好（可能是我方式不对），所以我大部分具体的代码时都放在`html`文件`script`形式，在`chrome`调试了。\n\n## 4. 先看看 `new Koa()` 结果`app`是什么\n\n看源码我习惯性看**它的实例对象结构**，一般所有属性和方法都放在实例对象上了，而且会通过原型链查找形式查找最顶端的属性和方法。\n\n用`koa/examples/middleware/app.js`文件调试时，先看下执行`new Koa()`之后，`app`是什么，有个初步印象。\n\n```js\n// 文件 koa/examples/middleware/app.js\nconst Koa = require('../../lib/application');\n\n// const Koa = require('koa');\n// 这里打个断点\nconst app = new Koa();\n// x-response-time\n\n// 这里打个断点\napp.use(async (ctx, next) =\u003e {\n\n});\n```\n\n在调试控制台`ctrl + 反引号键（一般在`Tab`上方的按键）唤起`，输入`app`，按`enter`键打印`app`。会有一张这样的图。\n\n![koa 实例对象调试图](./images/koa-instance.jpeg)\n\n`VScode`也有一个代码调试神器插件[`Debug Visualizer`](https://marketplace.visualstudio.com/items?itemName=hediet.debug-visualizer)。\n\n安装好后插件后，按`ctrl + shift + p`，输入`Open a new Debug Visualizer View`，来使用，输入`app`，显示是这样的。\n\n![koa 实例对象可视化简版](./images/koa-instance-debug.jpeg)\n\n不过目前体验来看，相对还比较鸡肋，只能显示一级，而且只能显示对象，相信以后会更好。更多玩法可以查看它的文档。\n\n我把koa实例对象比较完整的用`xmind`画出来了，大概看看就好，有个初步印象。\n\n![koa 实例对象](./images/koa-instance-xmind.png)\n\n接着，我们可以看下`app 实例、context、request、request`的官方文档。\n\n### 4.1 app 实例、context、request、request 官方API文档\n\n- [index API](https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/index.md) | [context API](https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/context.md) | [request API](https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/request.md) | [response API](https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/response.md)\n\n可以真正使用的时候再去仔细看文档。\n\n## 5. koa 主流程梳理简化\n\n通过`F5启动调试（直接跳到下一个断点处）`、`F10单步跳过`、`F11单步调试`等，配合重要的地方断点，调试完整体代码，其实比较容易整理出如下主流程的代码。\n\n```js\nclass Emitter{\n  // node 内置模块\n  constructor(){\n  }\n}\nclass Koa extends Emitter{\n  constructor(options){\n    super();\n    options = options || {};\n    this.middleware = [];\n    this.context = {\n      method: 'GET',\n      url: '/url',\n      body: undefined,\n      set: function(key, val){\n        console.log('context.set', key, val);\n      },\n    };\n  }\n  use(fn){\n    this.middleware.push(fn);\n    return this;\n  }\n  listen(){\n    const  fnMiddleware = compose(this.middleware);\n    const ctx = this.context;\n    const handleResponse = () =\u003e respond(ctx);\n    const onerror = function(){\n      console.log('onerror');\n    };\n    fnMiddleware(ctx).then(handleResponse).catch(onerror);\n  }\n}\nfunction respond(ctx){\n  console.log('handleResponse');\n  console.log('response.end', ctx.body);\n}\n```\n\n重点就在`listen`函数里的`compose`这个函数，接下来我们就详细来**欣赏**下这个函数。\n\n## 6. koa-compose 源码（洋葱模型实现）\n\n通过`app.use()` 添加了若干函数，但是要把它们串起来执行呀。像上文的`gif`图一样。\n\n`compose`函数，传入一个数组，返回一个函数。对入参是不是数组和校验数组每一项是不是函数。\n\n```js\nfunction compose (middleware) {\n  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')\n  for (const fn of middleware) {\n    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')\n  }\n\n //  传入对象 context 返回Promise\n  return function (context, next) {\n    // last called middleware #\n    let index = -1\n    return dispatch(0)\n    function dispatch (i) {\n      if (i \u003c= index) return Promise.reject(new Error('next() called multiple times'))\n      index = i\n      let fn = middleware[i]\n      if (i === middleware.length) fn = next\n      if (!fn) return Promise.resolve()\n      try {\n        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));\n      } catch (err) {\n        return Promise.reject(err)\n      }\n    }\n  }\n}\n```\n\n把简化的代码和`koa-compose`代码写在了一个文件中。[koa/examples/simpleKoa/koa-compose.js](https://github.com/lxchuan12/koa-analysis/blob/master/koa/examples/simpleKoa/koa-compose.js)\n\n```bash\nhs koa/examples/\n# 然后可以打开localhost:8080/simpleKoa，开心的把代码调试起来\n```\n\n不过这样好像还是有点麻烦，我还把这些代码放在[`codepen` https://codepen.io/lxchuan12/pen/wvarPEb](https://codepen.io/lxchuan12/pen/wvarPEb)中，**直接可以在线调试啦**。是不是觉得很贴心^_^，自己多调试几遍便于消化理解。\n\n你会发现`compose`就是类似这样的结构（移除一些判断）。\n\n```js\n// 这样就可能更好理解了。\n// simpleKoaCompose\nconst [fn1, fn2, fn3] = this.middleware;\nconst fnMiddleware = function(context){\n    return Promise.resolve(\n      fn1(context, function next(){\n        return Promise.resolve(\n          fn2(context, function next(){\n              return Promise.resolve(\n                  fn3(context, function next(){\n                    return Promise.resolve();\n                  })\n              )\n          })\n        )\n    })\n  );\n};\nfnMiddleware(ctx).then(handleResponse).catch(onerror);\n```\n\n\u003e也就是说`koa-compose`返回的是一个`Promise`，`Promise`中取出第一个函数（`app.use`添加的中间件），传入`context`和第一个`next`函数来执行。\u003cbr\u003e\n第一个`next`函数里也是返回的是一个`Promise`，`Promise`中取出第二个函数（`app.use`添加的中间件），传入`context`和第二个`next`函数来执行。\u003cbr\u003e\n第二个`next`函数里也是返回的是一个`Promise`，`Promise`中取出第三个函数（`app.use`添加的中间件），传入`context`和第三个`next`函数来执行。\u003cbr\u003e\n第三个...\u003cbr\u003e\n以此类推。最后一个中间件中有调用`next`函数，则返回`Promise.resolve`。如果没有，则不执行`next`函数。\n这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。\u003cbr\u003e\n\n**不得不说非常惊艳，“玩还是大神会玩”**。\n\n这种把函数存储下来的方式，在很多源码中都有看到。比如`lodash`源码的惰性求值，`vuex`也是把`action`等函数存储下，最后才去调用。\n\n搞懂了`koa-compose` 洋葱模型实现的代码，其他代码就不在话下了。\n\n## 7. 错误处理\n\n[中文文档 错误处理](https://github.com/demopark/koa-docs-Zh-CN/blob/master/error-handling.md)\n\n仔细看文档，文档中写了三种捕获错误的方式。\n\n- `ctx.onerror` 中间件中的错误捕获\n- `app.on('error', (err) =\u003e {})` 最外层实例事件监听形式\n  也可以看看例子[koajs/examples/errors/app.js 文件](https://github.com/koajs/examples/blob/master/errors/app.js)\n- `app.onerror = (err) =\u003e {}` 重写`onerror`自定义形式\n  也可以看[测试用例 onerror](https://github.com/lxchuan12/koa-analysis/blob/master/koa/test/context/onerror.js)\n\n```js\n// application.js 文件\nclass Application extends Emitter {\n  // 代码有简化组合\n  listen(){\n    const  fnMiddleware = compose(this.middleware);\n    if (!this.listenerCount('error')) this.on('error', this.onerror);\n    const onerror = err =\u003e ctx.onerror(err);\n    fnMiddleware(ctx).then(handleResponse).catch(onerror);\n  }\n  onerror(err) {\n    // 代码省略\n    // ...\n  }\n}\n```\n\n**ctx.onerror**\n\n`lib/context.js`文件中，有一个函数`onerror`，而且有这么一行代码`this.app.emit('error', err, this)`。\n\n```js\nmodule.exports = {\n  onerror(){\n    // delegate\n    // app 是在new Koa() 实例\n    this.app.emit('error', err, this);\n  }\n}\n```\n\n```js\napp.use(async (ctx, next) =\u003e {\n  try {\n    await next();\n  } catch (err) {\n    err.status = err.statusCode || err.status || 500;\n    throw err;\n  }\n});\n```\n\n`try catch` 错误或被`fnMiddleware(ctx).then(handleResponse).catch(onerror);`，这里的`onerror`是`ctx.onerror`\u003cbr\u003e\n而`ctx.onerror`函数中又调用了`this.app.emit('error', err, this)`，所以在最外围`app.on('error'，err =\u003e {})`可以捕获中间件链中的错误。\n因为`koa`继承自`events模块`，所以有'emit'和`on`等方法）\n\n## 8. koa2 和 koa1 的简单对比\n\n[中文文档中描述了 koa2 和 koa1 的区别](https://github.com/demopark/koa-docs-Zh-CN/blob/master/migration.md)\n\n`koa1`中主要是`generator`函数。`koa2`中会自动转换`generator`函数。\n\n```js\n// Koa 将转换\napp.use(function *(next) {\n  const start = Date.now();\n  yield next;\n  const ms = Date.now() - start;\n  console.log(`${this.method} ${this.url} - ${ms}ms`);\n});\n```\n\n### 8.1 koa-convert 源码\n\n在`vscode/launch.json`文件，找到这个`program`字段，修改为`\"program\": \"${workspaceFolder}/koa/examples/koa-convert/app.js\"`。\n\n通过`F5启动调试（直接跳到下一个断点处）`、`F10单步跳过`、`F11单步调试`调试走一遍流程。重要地方断点调试。\n\n`app.use`时有一层判断，是否是`generator`函数，如果是则用`koa-convert`暴露的方法`convert`来转换重新赋值，再存入`middleware`，后续再使用。\n\n```js\nclass Koa extends Emitter{\n  use(fn) {\n    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');\n    if (isGeneratorFunction(fn)) {\n      deprecate('Support for generators will be removed in v3. ' +\n                'See the documentation for examples of how to convert old middleware ' +\n                'https://github.com/koajs/koa/blob/master/docs/migration.md');\n      fn = convert(fn);\n    }\n    debug('use %s', fn._name || fn.name || '-');\n    this.middleware.push(fn);\n    return this;\n  }\n}\n```\n\n`koa-convert`源码挺多，核心代码其实是这样的。\n\n```js\nfunction convert(){\n return function (ctx, next) {\n    return co.call(ctx, mw.call(ctx, createGenerator(next)))\n  }\n  function * createGenerator (next) {\n    return yield next()\n  }\n}\n```\n\n最后还是通过`co`来转换的。所以接下来看`co`的源码。\n\n### 8.2 co 源码\n\n[tj大神写的co 仓库](https://github.com/tj/co)\n\n本小节的示例代码都在这个文件夹[`koa/examples/co-generator`](https://github.com/lxchuan12/koa-analysis/tree/master/koa/examples/co-generator)中，`hs koa/example`，可以自行打开`https://localhost:8080/co-generator`调试查看。\n\n看`co`源码前，先看几段简单代码。\n\n```js\n// 写一个请求简版请求\nfunction request(ms= 1000) {\n  return new Promise((resolve) =\u003e {\n    setTimeout(() =\u003e {\n      resolve({name: '若川'});\n    }, ms);\n  });\n}\n```\n\n```js\n// 获取generator的值\nfunction* generatorFunc(){\n  const res = yield request();\n  console.log(res, 'generatorFunc-res');\n}\ngeneratorFunc(); // 报告，我不会输出你想要的结果的\n```\n\n简单来说`co`，就是把`generator`自动执行，再返回一个`promise`。\n**`generator`函数这玩意它不自动执行呀，还要一步步调用`next()`，也就是叫它走一步才走一步**。\n\n所以有了`async、await`函数。\n\n```js\n// await 函数 自动执行\nasync function asyncFunc(){\n    const res = await request();\n    console.log(res, 'asyncFunc-res await 函数 自动执行');\n}\nasyncFunc(); // 输出结果\n```\n\n也就是说`co`需要做的事情，是让`generator`向`async、await`函数一样自动执行。\n\n### 8.3 模拟实现简版 co（第一版）\n\n这时，我们来模拟实现第一版的`co`。根据`generator`的特性，其实容易写出如下代码。\n\n```js\n// 获取generator的值\nfunction* generatorFunc(){\n  const res = yield request();\n  console.log(res, 'generatorFunc-res');\n}\n\nfunction coSimple(gen){\n  gen = gen();\n  console.log(gen, 'gen');\n\n  const ret = gen.next();\n  const promise = ret.value;\n  promise.then(res =\u003e {\n    gen.next(res);\n  });\n}\ncoSimple(generatorFunc);\n// 输出了想要的结果\n// {name: \"若川\"}\"generatorFunc-res\"\n```\n\n### 8.4 模拟实现简版 co（第二版）\n\n但是实际上，不会上面那么简单的。有可能是多个`yield`和传参数的情况。\n传参可以通过这如下两行代码来解决。\n\n```js\nconst args = Array.prototype.slice.call(arguments, 1);\ngen = gen.apply(ctx, args);\n```\n\n两个`yield`，我大不了重新调用一下`promise.then`，搞定。\n\n```js\n// 多个yeild，传参情况\nfunction* generatorFunc(suffix = ''){\n  const res = yield request();\n  console.log(res, 'generatorFunc-res' + suffix);\n\n  const res2 = yield request();\n  console.log(res2, 'generatorFunc-res-2' + suffix);\n}\n\nfunction coSimple(gen){\n  const ctx = this;\n  const args = Array.prototype.slice.call(arguments, 1);\n  gen = gen.apply(ctx, args);\n  console.log(gen, 'gen');\n\n  const ret = gen.next();\n  const promise = ret.value;\n  promise.then(res =\u003e {\n    const ret = gen.next(res);\n    const promise = ret.value;\n      promise.then(res =\u003e {\n        gen.next(res);\n      });\n  });\n}\n\ncoSimple(generatorFunc, ' 哎呀，我真的是后缀');\n```\n\n### 8.5 模拟实现简版 co（第三版）\n\n问题是肯定不止两次，无限次的`yield`的呢，这时肯定要把重复的封装起来。而且返回是`promise`，这就实现了如下版本的代码。\n\n```js\nfunction* generatorFunc(suffix = ''){\n  const res = yield request();\n  console.log(res, 'generatorFunc-res' + suffix);\n\n  const res2 = yield request();\n  console.log(res2, 'generatorFunc-res-2' + suffix);\n\n  const res3 = yield request();\n  console.log(res3, 'generatorFunc-res-3' + suffix);\n\n  const res4 = yield request();\n  console.log(res4, 'generatorFunc-res-4' + suffix);\n}\n\nfunction coSimple(gen){\n  const ctx = this;\n  const args = Array.prototype.slice.call(arguments, 1);\n  gen = gen.apply(ctx, args);\n  console.log(gen, 'gen');\n\n  return new Promise((resolve, reject) =\u003e {\n\n    onFulfilled();\n\n    function onFulfilled(res){\n      const ret = gen.next(res);\n      next(ret);\n    }\n\n    function next(ret) {\n      const promise = ret.value;\n      promise \u0026\u0026 promise.then(onFulfilled);\n    }\n\n  });\n}\n\ncoSimple(generatorFunc, ' 哎呀，我真的是后缀');\n```\n\n但第三版的模拟实现简版`co`中，还没有考虑报错和一些参数合法的情况。\n\n### 8.6 最终来看下`co`源码\n\n这时来看看`co`的源码，报错和错误的情况，错误时调用`reject`，是不是就好理解了一些呢。\n\n```js\nfunction co(gen) {\n  var ctx = this;\n  var args = slice.call(arguments, 1)\n\n  // we wrap everything in a promise to avoid promise chaining,\n  // which leads to memory leak errors.\n  // see https://github.com/tj/co/issues/180\n  return new Promise(function(resolve, reject) {\n    // 把参数传递给gen函数并执行\n    if (typeof gen === 'function') gen = gen.apply(ctx, args);\n    // 如果不是函数 直接返回\n    if (!gen || typeof gen.next !== 'function') return resolve(gen);\n\n    onFulfilled();\n\n    /**\n     * @param {Mixed} res\n     * @return {Promise}\n     * @api private\n     */\n\n    function onFulfilled(res) {\n      var ret;\n      try {\n        ret = gen.next(res);\n      } catch (e) {\n        return reject(e);\n      }\n      next(ret);\n    }\n\n    /**\n     * @param {Error} err\n     * @return {Promise}\n     * @api private\n     */\n\n    function onRejected(err) {\n      var ret;\n      try {\n        ret = gen.throw(err);\n      } catch (e) {\n        return reject(e);\n      }\n      next(ret);\n    }\n\n    /**\n     * Get the next value in the generator,\n     * return a promise.\n     *\n     * @param {Object} ret\n     * @return {Promise}\n     * @api private\n     */\n\n    // 反复执行调用自己\n    function next(ret) {\n      // 检查当前是否为 Generator 函数的最后一步，如果是就返回\n      if (ret.done) return resolve(ret.value);\n      // 确保返回值是promise对象。\n      var value = toPromise.call(ctx, ret.value);\n      // 使用 then 方法，为返回值加上回调函数，然后通过 onFulfilled 函数再次调用 next 函数。\n      if (value \u0026\u0026 isPromise(value)) return value.then(onFulfilled, onRejected);\n      // 在参数不符合要求的情况下（参数非 Thunk 函数和 Promise 对象），将 Promise 对象的状态改为 rejected，从而终止执行。\n      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '\n        + 'but the following object was passed: \"' + String(ret.value) + '\"'));\n    }\n  });\n}\n```\n\n## 9. koa 和 express 简单对比\n\n[中文文档 koa 和 express 对比](https://github.com/demopark/koa-docs-Zh-CN/blob/master/koa-vs-express.md)\n\n文档里写的挺全面的。简单来说`koa2`语法更先进，更容易深度定制（`egg.js`、`think.js`、底层框架都是`koa`）。\n\n## 10. 总结\n\n文章通过`授人予鱼不如授人予鱼`的方式，告知如何调试源码，看完了`koa-compose`洋葱模型实现，`koa-convert`和`co`等源码。\n\n`koa-compose`是将`app.use`添加到`middleware`数组中的中间件（函数），通过使用`Promise`串联起来，`next()`返回的是一个`promise`。\n\n`koa-convert` 判断`app.use`传入的函数是否是`generator`函数，如果是则用`koa-convert`来转换，最终还是调用的`co`来转换。\n\n`co`源码实现原理：其实就是通过不断的调用`generator`函数的`next()`函数，来达到自动执行`generator`函数的效果（类似`async、await函数的自动自行`）。\n\n`koa`框架总结：主要就是四个核心概念，洋葱模型（把中间件串联起来），`http`请求上下文（`context`）、`http`请求对象、`http`响应对象。\n\n本文仓库在这里[若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis](https://github.com/lxchuan12/koa-analysis)。求个`star`呀。\n\n```bash\ngit clone https://github.com/lxchuan12/koa-analysis.git\n```\n\n再强烈建议下按照**本文阅读最佳方式**，克隆代码下来，**动手调试代码学习更加深刻**。\n\n\u003e如果读者发现有不妥或可改善之处，再或者哪里没写明白的地方，欢迎评论指出，也欢迎加我微信交流`ruochuan12`。另外觉得写得不错，对您有些许帮助，可以点赞、评论、转发分享，也是对笔者的一种支持，万分感谢。\n\n### 10.1 解答下开头的提问\n\n仅供参考\n\n\u003e1、`koa`洋葱模型怎么实现的。\u003cbr\u003e\n\n可以参考上文整理的简版`koa-compose`作答。\n\n```js\n// 这样就可能更好理解了。\n// simpleKoaCompose\nconst [fn1, fn2, fn3] = this.middleware;\nconst fnMiddleware = function(context){\n    return Promise.resolve(\n      fn1(context, function next(){\n        return Promise.resolve(\n          fn2(context, function next(){\n              return Promise.resolve(\n                  fn3(context, function next(){\n                    return Promise.resolve();\n                  })\n              )\n          })\n        )\n    })\n  );\n};\nfnMiddleware(ctx).then(handleResponse).catch(onerror);\n```\n\n答：app.use() 把中间件函数存储在`middleware`数组中，最终会调用`koa-compose`导出的函数`compose`返回一个`promise`，中间函数的第一个参数`ctx`是包含响应和请求的一个对象，会不断传递给下一个中间件。`next`是一个函数，返回的是一个`promise`。\n\n\u003e2、如果中间件中的`next()`方法报错了怎么办。\u003cbr\u003e\n\n可参考上文整理的错误处理作答。\n\n```js\nctx.onerror = function {\n  this.app.emit('error', err, this);\n};\n  listen(){\n    const  fnMiddleware = compose(this.middleware);\n    if (!this.listenerCount('error')) this.on('error', this.onerror);\n    const onerror = err =\u003e ctx.onerror(err);\n    fnMiddleware(ctx).then(handleResponse).catch(onerror);\n  }\n  onerror(err) {\n    // 代码省略\n    // ...\n  }\n```\n\n答：中间件链错误会由`ctx.onerror`捕获，该函数中会调用`this.app.emit('error', err, this)`（因为`koa`继承自`events模块`，所以有'emit'和`on`等方法），可以使用`app.on('error', (err) =\u003e {})`，或者`app.onerror = (err) =\u003e {}`进行捕获。\n\n\u003e3、`co`的原理是怎样的。\u003cbr\u003e\n答：`co`的原理是通过不断调用`generator`函数的`next`方法来达到自动执行`generator`函数的，类似`async、await`函数自动执行。\n\n答完，面试官可能觉得小伙子还是蛮懂`koa`的啊。当然也可能继续追问，直到答不出...\n\n### 10.2 还能做些什么 ？\n\n学完了整体流程，`koa-compose`、`koa-convert`和`co`的源码。\n\n还能仔细看看看`http`请求上下文（`context`）、`http`请求对象、`http`响应对象的具体实现。\n\n还能根据我文章说的调试方式调试[koa 组织](https://github.com/koajs)中的各种中间件，比如`koa-bodyparser`, `koa-router`，`koa-jwt`，`koa-session`、`koa-cors`等等。\n\n还能把[`examples`仓库](https://github.com/koajs/examples)克隆下来，我的这个仓库已经克隆了，挨个调试学习下源码。\n\n`web`框架有很多，比如`Express.js`，`Koa.js`、`Egg.js`、`Nest.js`、`Next.js`、`Fastify.js`、`Hapi.js`、`Restify.js`、`Loopback.io`、`Sails.js`、`Midway.js`等等。\n\n还能把这些框架的优势劣势、设计思想等学习下。\n\n还能继续学习`HTTP`协议、`TCP/IP`协议网络相关，虽然不属于`koa`的知识，但需深入学习掌握。\n\n学无止境~~~\n\n## 11. 推荐阅读\n\n[koa 官网](https://koajs.com/) | [koa 仓库](https://github.com/koajs/koa) | [koa 组织](https://github.com/koajs) | [koa2 中文文档](https://github.com/demopark/koa-docs-Zh-CN) | [co 仓库](https://github.com/tj/co)\u003cbr\u003e\n[知乎@姚大帅：可能是目前市面上比较有诚意的Koa2源码解读](https://zhuanlan.zhihu.com/p/34797505)\u003cbr\u003e\n[知乎@零小白：十分钟带你看完 KOA 源码](https://zhuanlan.zhihu.com/p/24559011)\u003cbr\u003e\n[微信开放社区@小丹の：可能是目前最全的koa源码解析指南](https://developers.weixin.qq.com/community/develop/article/doc/0000e4c9290bc069f3380e7645b813)\u003cbr\u003e\n[IVWEB官方账号: KOA2框架原理解析和实现](https://ivweb.io/article.html?_id=100334)\u003cbr\u003e\n[深入浅出vue.js 作者 berwin: 深入浅出 Koa2 原理](https://github.com/berwin/Blog/issues/9)\u003cbr\u003e\n[阮一峰老师：co 函数库的含义和用法](http://www.ruanyifeng.com/blog/2015/05/co.html)\u003cbr\u003e\n\n## 另一个系列\n\n[面试官问：JS的继承](https://juejin.im/post/5c433e216fb9a049c15f841b)\u003cbr\u003e\n[面试官问：JS的this指向](https://juejin.im/post/5c0c87b35188252e8966c78a)\u003cbr\u003e\n[面试官问：能否模拟实现JS的call和apply方法](https://juejin.im/post/5bf6c79bf265da6142738b29)\u003cbr\u003e\n[面试官问：能否模拟实现JS的bind方法](https://juejin.im/post/5bec4183f265da616b1044d7)\u003cbr\u003e\n[面试官问：能否模拟实现JS的new操作符](https://juejin.im/post/5bde7c926fb9a049f66b8b52)\u003cbr\u003e\n\n## 关于\n\n作者：常以**若川**为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少，唯善学。\u003cbr\u003e\n[若川的博客](https://lxchuan12.gitee.io)，使用`vuepress`重构了，阅读体验可能更好些\u003cbr\u003e\n[掘金专栏](https://juejin.cn/user/1415826704971918/posts)，欢迎关注~\u003cbr\u003e\n[`segmentfault`前端视野专栏](https://segmentfault.com/blog/lxchuan12)，欢迎关注~\u003cbr\u003e\n[知乎前端视野专栏](https://zhuanlan.zhihu.com/lxchuan12)，欢迎关注~\u003cbr\u003e\n[语雀前端视野专栏](https://www.yuque.com/lxchuan12/blog)，新增语雀专栏，欢迎关注~\u003cbr\u003e\n[github blog](https://github.com/lxchuan12/blog)，相关源码和资源都放在这里，求个`star`^_^~\n\n## 欢迎加微信交流 微信公众号\n\n可能比较有趣的微信公众号，长按扫码关注（**回复pdf获取前端优质书籍pdf**）。欢迎加我微信`ruochuan12`（注明来源，基本来者不拒），拉您进【前端视野交流群】，长期交流学习~\n\n![若川视野](https://github.com/lxchuan12/blog/raw/master/docs/about/wechat-official-accounts-mini.jpg)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruochuan12%2Fkoa-analysis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruochuan12%2Fkoa-analysis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruochuan12%2Fkoa-analysis/lists"}