{"id":20739253,"url":"https://github.com/chanwahfung/koa-mini","last_synced_at":"2025-07-28T06:04:26.537Z","repository":{"id":139117639,"uuid":"270739527","full_name":"ChanWahFung/koa-mini","owner":"ChanWahFung","description":"Koa源码解析，手写Koa源码","archived":false,"fork":false,"pushed_at":"2020-06-10T03:36:13.000Z","size":13,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-30T02:05:58.833Z","etag":null,"topics":["koa"],"latest_commit_sha":null,"homepage":"","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/ChanWahFung.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-08T16:22:32.000Z","updated_at":"2022-05-01T03:56:56.000Z","dependencies_parsed_at":null,"dependency_job_id":"6cc57d14-e952-4c83-9ff9-af9037a445cd","html_url":"https://github.com/ChanWahFung/koa-mini","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ChanWahFung/koa-mini","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChanWahFung%2Fkoa-mini","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChanWahFung%2Fkoa-mini/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChanWahFung%2Fkoa-mini/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChanWahFung%2Fkoa-mini/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ChanWahFung","download_url":"https://codeload.github.com/ChanWahFung/koa-mini/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChanWahFung%2Fkoa-mini/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267470062,"owners_count":24092352,"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","status":"online","status_checked_at":"2025-07-28T02:00:09.689Z","response_time":68,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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"],"created_at":"2024-11-17T06:24:13.227Z","updated_at":"2025-07-28T06:04:26.515Z","avatar_url":"https://github.com/ChanWahFung.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 前言\n\n本文是我在阅读 `Koa` 源码后，并实现迷你版 `Koa` 的过程。如果你使用过 `Koa` 但不知道内部的原理，我想这篇文章应该能够帮助到你，实现一个迷你版的 `Koa` 不会很难。\n\n本文会循序渐进的解析内部原理，包括：\n\n1. 基础版本的 koa\n2. context 的实现\n3. 中间件原理及实现\n\n## 文件结构\n\n* `application.js`: 入口文件，里面包括我们常用的 `use` 方法、`listen` 方法以及对 `ctx.body` 做输出处理\n* `context.js`: 主要是做属性和方法的代理，让用户能够更简便的访问到`request`和`response`的属性和方法\n* `request.js`: 对原生的 `req` 属性做处理，扩展更多可用的属性和方法，比如：`query` 属性、`get` 方法\n* `response.js`: 对原生的 `res` 属性做处理，扩展更多可用的属性和方法，比如：`status` 属性、`set` 方法\n\n## 基础版本\n\n用法：\n\n```js\nconst Coa = require('./coa/application')\nconst app = new Coa()\n\n// 应用中间件\napp.use((ctx) =\u003e {\n  ctx.body = '\u003ch1\u003eHello\u003c/h1\u003e'\n})\n\napp.listen(3000, '127.0.0.1')\n```\n\n`application.js`:\n\n```js\nconst http = require('http')\n\nmodule.exports = class Coa {\n  use(fn) {\n    this.fn = fn\n  }\n  // listen 只是语法糖  本身还是使用 http.createServer\n  listen(...args) {\n    const server = http.createServer(this.callback())\n    server.listen(...args)\n  }\n  callback() {\n    const handleRequest = (req, res) =\u003e {\n      // 创建上下文\n      const ctx = this.createContext(req, res)\n      // 调用中间件\n      this.fn(ctx)\n      // 输出内容\n      res.end(ctx.body)\n    }\n    return handleRequest\n  }\n  createContext(req, res) {\n    let ctx = {}\n    ctx.req = req\n    ctx.res = res\n    return ctx\n  }\n}\n```\n\n基础版本的实现很简单，调用 `use` 将函数存储起来，在启动服务器时再执行这个函数，并输出 ` ctx.body` 的内容。\n\n但是这样是没有灵魂的。接下来，实现 `context` 和中间件原理，`Koa` 才算完整。\n\n## Context\n\n`ctx` 为我们扩展了很多好用的属性和方法，比如 `ctx.query`、`ctx.set()`。但它们并不是 `context` 封装的，而是在访问 `ctx` 上的属性时，它内部通过属性劫持将 `request` 和 `response` 内封装的属性返回。就像你访问 `ctx.query`，实际上访问的是 `ctx.request.query`。\n\n说到劫持你可能会想到 `Object.defineProperty`，在 `Kao` 内部使用的是 `ES6` 提供的对象的 `setter` 和 `getter`，效果也是一样的。所以要实现 `ctx`，我们首先要实现 `request` 和 `response`。\n\n在此之前，需要修改下 `createContext` 方法：\n\n```js\n// 这三个都是对象\nconst context = require('./context')\nconst request = require('./request')\nconst response = require('./response')\n\nmodule.exports = class Coa {\n  constructor() {\n    this.context = context\n    this.request = request\n    this.response = response\n  }\n  createContext(req, res) {\n    const ctx = Object.create(this.context)\n    // 将扩展的 request、response 挂载到 ctx 上\n    // 使用 Object.create 创建以传入参数为原型的对象，避免添加属性时因为冲突影响到原对象\n    const request = ctx.request = Object.create(this.request)\n    const response = ctx.response = Object.create(this.response)\n    \n    ctx.app = request.app = response.app = this;\n    // 挂载原生属性\n    ctx.req = request.req = response.req = req\n    ctx.res = request.res = response.res = res\n    \n    request.ctx = response.ctx = ctx;\n    request.response = response;\n    response.request = request;\n    \n    return ctx\n  }\n}\n```\n\n上面一堆花里胡哨的赋值，是为了能通过多种途径获取属性。比如获取 `query` 属性，可以有 `ctx.query`、`ctx.request.query`、`ctx.app.query` 等等的方式。\n\n如果你觉得看起来有点冗余，也可以主要理解这几行，因为我们实现源码时也就用到下面这些：\n\n```js\nconst request = ctx.request = Object.create(this.request)\nconst response = ctx.response = Object.create(this.response)\n\nctx.req = request.req = response.req = req\nctx.res = request.res = response.res = res\n```\n\n### request\n\n`request.js`：\n\n```js\nconst url = require('url')\n\nmodule.exports = {\n /* 查看这两步操作\n  * const request = ctx.request = Object.create(this.request)\n  * ctx.req = request.req = response.req = req \n  * \n  * 此时的 this 是指向 ctx，所以这里的 this.req 访问的是原生属性 req\n  * 同样，也可以通过 this.request.req 来访问\n  */\n  // 请求的 query 参数\n  get query() {\n    return url.parse(this.req.url).query\n  },\n  // 请求的路径\n  get path() {\n    return url.parse(this.req.url).pathname\n  },\n  // 请求的方法\n  get method() {\n    return this.req.method.toLowerCase()\n  }\n}\n```\n\n### response\n\n`response.js`：\n\n```js\nmodule.exports = {\n  // 这里的 this.res 也和上面同理 \n  // 返回的状态码\n  get status() {\n    return this.res.statusCode\n  },\n  set status(val) {\n    return this.res.statusCode = val\n  },\n  // 返回的输出内容\n  get body() {\n    return this._body\n  },\n  set body(val) {\n    return this._body = val\n  },\n  // 设置头部\n  set(filed, val) {\n    if (typeof filed === 'string') {\n      this.res.setHeader(filed, val)\n    }\n    if (toString.call(filed) === '[object Object]') {\n      for (const key in filed) {\n        this.set(key, filed[key])\n      }\n    }\n  }\n}\n```\n\n### 属性代理\n\n通过上面的实现，我们可以使用 `ctx.request.query` 来访问到扩展的属性。但是在实际应用中，更常用的是 `ctx.query`。不过 `query` 是在 `request` 的属性，通过 `ctx.query` 是无法访问的。\n\n这时只需稍微做个代理，在访问 `ctx.query`  时，将 `ctx.request.query` 返回就可以实现上面的效果。\n\n`context.js`:\n\n```js\nmodule.exports = {\n    get query() {\n        return this.request.query\n    }\n}\n```\n\n实际的代码中会有很多扩展的属性，总不可能一个一个去写吧。为了优雅的代理属性，`Koa` 使用 `delegates` 包实现。这里我就直接简单封装下代理函数，代理函数主要用到`__defineGetter__` 和 `__defineSetter__` 两个方法。\n\n在对象上都会带有 `__defineGetter__` 和 `__defineSetter__`，它们可以将一个函数绑定在当前对象的指定属性上，当属性被获取或赋值时，绑定的函数就会被调用。就像这样：\n\n```js\nlet obj = {}\nlet obj1 = {\n    name: 'JoJo'\n}\nobj.__defineGetter__('name', function(){\n    return obj1.name\n})\n```\n\n此时访问 `obj.name`，获取到的是 `obj1.name` 的值。\n\n了解这个两个方法的用处后，接下来开始修改 `context.js`：\n\n```js\nconst proto = module.exports = {\n}\n\n// getter代理\nfunction delegateGetter(prop, name){\n  proto.__defineGetter__(name, function(){\n    return this[prop][name]\n  })\n}\n// setter代理\nfunction delegateSetter(prop, name){\n  proto.__defineSetter__(name, function(val){\n    return this[prop][name] = val\n  })\n}\n// 方法代理\nfunction delegateMethod(prop, name){\n  proto[name] = function() {\n    return this[prop][name].apply(this[prop], arguments)\n  }\n}\n\ndelegateGetter('request', 'query')\ndelegateGetter('request', 'path')\ndelegateGetter('request', 'method')\n\ndelegateGetter('response', 'status')\ndelegateSetter('response', 'status')\ndelegateGetter('response', 'body')\ndelegateSetter('response', 'body')\ndelegateMethod('response', 'set')\n```\n\n## 中间件原理\n\n中间件思想是 `Koa` 最精髓的地方，为扩展功能提供很大的帮助。这也是它虽然小，却很强大的原因。还有一个优点，中间件使功能模块的职责更加分明，一个功能就是一个中间件，多个中间件组合起来成为一个完整的应用。\n\n下面是著名的“洋葱模型”。这幅图很形象的表达了中间件思想的作用，它就像一个流水线一样，上游加工后的东西传递给下游，下游可以继续接着加工，最终输出加工结果。\n\n![](https://s1.ax1x.com/2020/06/08/tWMiTJ.jpg)\n\n### 原理分析\n\n在调用 `use` 注册中间件的时候，内部会将每个中间件存储到数组中，执行中间件时，为其提供 `next` 参数。调用 `next` 即执行下一个中间件，以此类推。当数组中的中间件执行完毕后，再原路返回。就像这样:\n\n```js\napp.use((ctx, next) =\u003e {\n  console.log('1 start')\n  next()\n  console.log('1 end')\n})\n\napp.use((ctx, next) =\u003e {\n  console.log('2 start')\n  next()\n  console.log('2 end')\n})\n\napp.use((ctx, next) =\u003e {\n  console.log('3 start')\n  next()\n  console.log('3 end')\n})\n```\n\n输出结果如下：\n\n``` txt\n1 start\n2 start\n3 start\n3 end\n2 end\n1 end\n```\n\n有点数据结构知识的同学，很快就想到这是一个“栈”结构，执行的顺序符合“先入后出”。\n\n下面我将内部中间件实现原理进行简化，模拟中间件执行：\n\n```js\nfunction next1() {\n  console.log('1 start')\n  next2()\n  console.log('1 end')\n}\nfunction next2() {\n  console.log('2 start')\n  next3()\n  console.log('2 end')\n}\nfunction next3() {\n  console.log('3 start')\n  console.log('3 end')\n}\nnext1()\n```\n\n**执行过程：**\n\n1. 调用 `next1`，将其入栈执行，输出 `1 start`\n2. 遇到 `next2` 函数，将其入栈执行，输出 `2 start`\n3. 遇到 `next3` 函数，将其入栈执行，输出 `3 start`\n4. 输出 `3 end`，函数执行完毕，`next3` 弹出栈\n5. 输出 `2 end`，函数执行完毕，`next2` 弹出栈\n6. 输出 `1 end`，函数执行完毕，`next1` 弹出栈\n7. 栈空，全部执行完毕\n\n相信通过这个简单的例子，都大概明白中间件的执行过程了吧。\n\n### 原理实现\n\n中间件原理实现的关键点主要是 `ctx` 和 `next` 的传递。\n\n```js\nfunction compose(middleware) {\n  return function(ctx) {\n    return dispatch(0)\n    function dispatch(i){\n      // 取出中间件\n      let fn = middleware[i]\n      if (!fn) {\n        return\n      }\n      // dispatch.bind(null, i + 1) 为应用中间件接受到的 next\n      // next 即下一个应用中间件\n      fn(ctx, dispatch.bind(null, i + 1))\n    }\n  }\n}\n```\n\n可以看到，实现过程本质是函数的递归调用。在内部实现时，其实 `next` 没有做什么神奇的操作，它就是下一个中间件调用的函数，作为参数传入供使用者调用。\n\n下面我们来单独测试 `compose`，你可以将它粘贴到控制台上运行：\n\n```js\nfunction next1(ctx, next) {\n  console.log('1 start')\n  next()\n  console.log('1 end')\n}\nfunction next2(ctx, next) {\n  console.log('2 start')\n  next()\n  console.log('2 end')\n}\nfunction next3(ctx, next) {\n  console.log('3 start')\n  next()\n  console.log('3 end')\n}\n\nlet ctx = {}\nlet fn = compose([next1, next2, next3])\nfn(ctx)\n```\n\n最后，因为 `Koa` 中间件是可以使用 `async/await` 异步执行的，所以还需要修改下 `compose` 返回 `Promise`：\n\n```js\nfunction compose(middleware) {\n  return function(ctx) {\n    return dispatch(0)\n    function dispatch(i){\n      // 取出中间件\n      let fn = middleware[i]\n      if (!fn) {\n        return Promise.resolve()\n      }\n      // dispatch.bind(null, i + 1) 为应用中间件接受到的 next\n      // next 即下一个应用中间件\n      try {\n        return Promise.resolve( fn(ctx, dispatch.bind(null, i + 1)) )\n      } catch (error) {\n        return Promise.reject(error)\n      }\n    }\n  }\n}\n```\n\n### 应用\n\n实现完成中间件的逻辑后，将它应用到迷你版`Koa`中，原来的代码逻辑要做一些修改(部分代码忽略)\n\n`application.js`:\n\n```js\nmodule.exports = class Coa {\n  constructor() {\n    // 存储中间件的数组 \n    this.middleware = []\n  }\n\n  use(fn) {\n    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');\n    // 将中间件加入数组\n    this.middleware.push(fn)\n    return this\n  }\n  \n  listen(...args) {\n    const server = http.createServer(this.callback())\n    server.listen(...args)\n  }\n\n  callback() {\n    const handleRequest = (req, res) =\u003e {\n      // 创建上下文\n      const ctx = this.createContext(req, res)\n      // fn 为第一个应用中间件\n      const fn = this.compose(this.middleware)\n      // 在所有中间件执行完毕后 respond 函数用于处理 ctx.body 输出\n      return fn(ctx).then(() =\u003e respond(ctx)).catch(console.error)\n    }\n    return handleRequest\n  }\n  \n  compose(middleware) {\n    return function(ctx) {\n      return dispatch(0)\n      function dispatch(i){\n        let fn = middleware[i]\n        if (!fn) {\n          return Promise.resolve()\n        }\n        // dispatch.bind(null, i + 1) 为应用中间件接受到的 next\n        try {\n          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))\n        } catch (error) {\n          return Promise.reject(error)\n        }\n      }\n    }\n  }\n}\n\nfunction respond(ctx) {\n  let res = ctx.res\n  let body = ctx.body\n  if (typeof body === 'string') {\n    return res.end(body)\n  }\n  if (typeof body === 'object') {\n    return res.end(JSON.stringify(body))\n  }\n}\n```\n\n## 完整实现\n\n`application.js`:\n\n```js\nconst http = require('http')\nconst context = require('./context')\nconst request = require('./request')\nconst response = require('./response')\n\nmodule.exports = class Coa {\n  constructor() {\n    this.middleware = []\n    this.context = context\n    this.request = request\n    this.response = response\n  }\n\n  use(fn) {\n    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');\n    this.middleware.push(fn)\n    return this\n  }\n\n  listen(...args) {\n    const server = http.createServer(this.callback())\n    server.listen(...args)\n  }\n\n  callback() {\n    const handleRequest = (req, res) =\u003e {\n      // 创建上下文\n      const ctx = this.createContext(req, res)\n      // fn 为第一个应用中间件\n      const fn = this.compose(this.middleware)\n      return fn(ctx).then(() =\u003e respond(ctx)).catch(console.error)\n    }\n    return handleRequest\n  }\n\n  // 创建上下文\n  createContext(req, res) {\n    const ctx = Object.create(this.context)\n    // 处理过的属性\n    const request = ctx.request = Object.create(this.request)\n    const response = ctx.response = Object.create(this.response)\n    // 原生属性\n    ctx.app = request.app = response.app = this;\n    ctx.req = request.req = response.req = req\n    ctx.res = request.res = response.res = res\n\n    request.ctx = response.ctx = ctx;\n    request.response = response;\n    response.request = request;\n\n    return ctx\n  }\n\n  // 中间件处理逻辑实现\n  compose(middleware) {\n    return function(ctx) {\n      return dispatch(0)\n      function dispatch(i){\n        let fn = middleware[i]\n        if (!fn) {\n          return Promise.resolve()\n        }\n        // dispatch.bind(null, i + 1) 为应用中间件接受到的 next\n        // next 即下一个应用中间件\n        try {\n          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))\n        } catch (error) {\n          return Promise.reject(error)\n        }\n      }\n    }\n  }\n}\n\n// 处理 body 不同类型输出\nfunction respond(ctx) {\n  let res = ctx.res\n  let body = ctx.body\n  if (typeof body === 'string') {\n    return res.end(body)\n  }\n  if (typeof body === 'object') {\n    return res.end(JSON.stringify(body))\n  }\n}\n```\n\n## 写在最后\n\n本文的简单实现了 `Koa` 主要的功能。有兴趣最好还是自己去看源码，实现自己的迷你版 `Koa`。其实 `Koa` 的源码不算多，总共4个文件，全部代码包括注释也就 1800 行左右。而且逻辑不会很难，很推荐阅读，尤其适合源码入门级别的同学观看。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchanwahfung%2Fkoa-mini","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchanwahfung%2Fkoa-mini","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchanwahfung%2Fkoa-mini/lists"}