{"action":"opened","issue":{"url":"https://api.github.com/repos/LouisWT/Blog/issues/1","repository_url":"https://api.github.com/repos/LouisWT/Blog","labels_url":"https://api.github.com/repos/LouisWT/Blog/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/LouisWT/Blog/issues/1/comments","events_url":"https://api.github.com/repos/LouisWT/Blog/issues/1/events","html_url":"https://github.com/LouisWT/Blog/issues/1","id":341041561,"node_id":"MDU6SXNzdWUzNDEwNDE1NjE=","number":1,"title":"[Node.js] 十步完成Koa2框架源码阅读","user":{"login":"LouisWT","id":22902565,"node_id":"MDQ6VXNlcjIyOTAyNTY1","avatar_url":"https://avatars3.githubusercontent.com/u/22902565?v=4","gravatar_id":"","url":"https://api.github.com/users/LouisWT","html_url":"https://github.com/LouisWT","followers_url":"https://api.github.com/users/LouisWT/followers","following_url":"https://api.github.com/users/LouisWT/following{/other_user}","gists_url":"https://api.github.com/users/LouisWT/gists{/gist_id}","starred_url":"https://api.github.com/users/LouisWT/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/LouisWT/subscriptions","organizations_url":"https://api.github.com/users/LouisWT/orgs","repos_url":"https://api.github.com/users/LouisWT/repos","events_url":"https://api.github.com/users/LouisWT/events{/privacy}","received_events_url":"https://api.github.com/users/LouisWT/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2018-07-13T14:31:21Z","updated_at":"2018-07-13T14:31:21Z","closed_at":null,"author_association":"OWNER","body":"### 前记\r\n本人华科小硕一枚，非正式后台工程师一个，用Node已经做了两个项目，最近终于老板肯让我歇歇了XD。于是写几篇博客，主要为了沉淀下知识。\r\n\r\n这篇博客主要写的是 Koa （写这篇博客时，**版本为 v2.5.2**） 框架的原理，我当初用Koa时，上了官网，就看到下面这张经典的洋葱图。\r\n\r\n![onion](https://qingmooc-v1.oss-cn-qingdao.aliyuncs.com/other/onion.png)\r\n\r\n说真的，这个图真的是非常非常生动，完美地说明了Koa框架的运行机制。\r\n\r\n具体为何，且听我细细道来。\r\n\r\n### 1. Koa基本用法\r\n\r\n首先咱们先看下Koa框架的基本用法\r\n\r\n```\r\nconst Koa = require('koa');\r\n\r\nconst app = new Koa();\r\napp.use(async (ctx, next) => {\r\n  const oldTime = +new Date();\r\n  await next();\r\n  const newTime = +new Date();\r\n  const interval =  newTime - oldTime;\r\n  console.log(interval);\r\n});\r\napp.use(async (ctx, next) => {\r\n  ctx.body = \"hello\";\r\n  await next();\r\n});\r\napp.listen(3000);\r\n```\r\n\r\n`ctx` ，顾名思义，是app运行上下文的意思，里面绑定了request和response的信息，具备可以描述一次请求的所有信息；`next`就很神奇了，**每调用一次next，就会进入下一个app.use的函数，直到调用到执行完最后一个函数，又会返回之前上一层调用next函数的地方，继续执行上一层函数接下来的部分，这样以此类推，直到运行完所有的app.use中的函数**。如果不太明白，可以看下洋葱图，洋葱图的每一层就是一个app.use中的函数，这个函数我们给它个名称，叫做 **`middleware(中间件)`**。\r\n\r\n至于怎样达到上面的效果，让我们从Koa源码来看看。\r\n\r\n### 2. Koa源码 \r\n\r\n#### 1. 首先看`package.json`\r\n\r\n我们只需要找到入口文件就好了。\r\n```\r\n...\r\n\"main\": \"lib/application.js\",\r\n...\r\n```\r\n入口文件确定了 `lib/application.js`\r\n\r\n#### 2. 接着翻到`lib/application.js`\r\n\r\n```\r\n...\r\nmodule.exports = class Application extends Emitter {\r\n\r\n  constructor() {\r\n    super();\r\n\r\n    this.proxy = false;\r\n    this.middleware = [];\r\n    this.subdomainOffset = 2;\r\n    this.env = process.env.NODE_ENV || 'development';\r\n    this.context = Object.create(context);\r\n    this.request = Object.create(request);\r\n    this.response = Object.create(response);\r\n    if (util.inspect.custom) {\r\n      this[util.inspect.custom] = this.inspect;\r\n    }\r\n  }\r\n...\r\n}\r\n```\r\n在我们执行 `const app = new Koa();`时，调用的就是以上的constructor()。\r\n\r\n可见，Application继承自Emitter，所以它也有 Emitter 的方法，比如 emit, once 等等\r\n\r\n==构造函数中构建了`context request response`三个对象，还有一个middleware数组==，这四个变量非常重要，可以说整个Koa框架就是围绕着这四个变量。\r\n\r\n#### 3. `lib/application.js`中可以看到`const context = require('./context');`，所以我们翻到`lib/context.js`看看\r\n```\r\nconst delegate = require('delegates');\r\n...\r\nconst proto = module.exports = {\r\n\r\n  ...\r\n  get cookies() {\r\n    if (!this[COOKIES]) {\r\n      this[COOKIES] = new Cookies(this.req, this.res, {\r\n        keys: this.app.keys,\r\n        secure: this.request.secure\r\n      });\r\n    }\r\n    return this[COOKIES];\r\n  },\r\n\r\n  set cookies(_cookies) {\r\n    this[COOKIES] = _cookies;\r\n  }\r\n};\r\n...\r\n// 将response方法委托到 context\r\ndelegate(proto, 'response')\r\n  .method('attachment')\r\n  .method('redirect')\r\n  .method('remove')\r\n  .method('vary')\r\n  .method('set')\r\n  .method('append')\r\n  .method('flushHeaders')\r\n  .access('status')\r\n  .access('message')\r\n  .access('body')\r\n  .access('length')\r\n  .access('type')\r\n  .access('lastModified')\r\n  .access('etag')\r\n  .getter('headerSent')\r\n  .getter('writable');\r\n\r\n// 将request方法委托到 context\r\ndelegate(proto, 'request')\r\n  .method('acceptsLanguages')\r\n  .method('acceptsEncodings')\r\n  .method('acceptsCharsets')\r\n  .method('accepts')\r\n  .method('get')\r\n  .method('is')\r\n  .access('querystring')\r\n  .access('idempotent')\r\n  .access('socket')\r\n  .access('search')\r\n  .access('method')\r\n  .access('query')\r\n  .access('path')\r\n  .access('url')\r\n  .access('accept')\r\n  .getter('origin')\r\n  .getter('href')\r\n  .getter('subdomains')\r\n  .getter('protocol')\r\n  .getter('host')\r\n  .getter('hostname')\r\n  .getter('URL')\r\n  .getter('header')\r\n  .getter('headers')\r\n  .getter('secure')\r\n  .getter('stale')\r\n  .getter('fresh')\r\n  .getter('ips')\r\n  .getter('ip');\r\n```\r\n> 对cookies的操作使用了 getter setter的写法 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/get\r\n\r\n这里，他用了一个叫做`delegates`的库，将proto和request还有response做了一个委托的操作。具体我们来看看`delegate`的代码\r\n\r\n#### 4. `delegate`库\r\n```\r\nfunction Delegator(proto, target) {\r\n  if (!(this instanceof Delegator)) return new Delegator(proto, target);\r\n  this.proto = proto;\r\n  this.target = target;\r\n  this.methods = [];\r\n  this.getters = [];\r\n  this.setters = [];\r\n  this.fluents = [];\r\n}\r\n```\r\n`delegate(proto, 'request')`调用的就是上面这个函数，这里 target 就是 'request'。\r\n```\r\nDelegator.prototype.method = function(name){\r\n  var proto = this.proto;\r\n  var target = this.target;\r\n  this.methods.push(name);\r\n\r\n  proto[name] = function(){\r\n    return this[target][name].apply(this[target], arguments);\r\n  };\r\n\r\n  return this;\r\n};\r\n```\r\n\r\n`.method('acceptsLanguages')`调用的就是上面的函数，这里 name 就是 'acceptsLanguages'\r\n\r\n这个函数主要做的事就是当调用 proto 对象的名为 name 的方法时，将去 target 中寻找这个方法，然后调用。\r\n\r\n> JS apply 函数 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply\r\n\r\n```\r\nDelegator.prototype.getter = function(name){\r\n  var proto = this.proto;\r\n  var target = this.target;\r\n  this.getters.push(name);\r\n\r\n  proto.__defineGetter__(name, function(){\r\n    return this[target][name];\r\n  });\r\n\r\n  return this;\r\n};\r\n\r\nDelegator.prototype.setter = function(name){\r\n  var proto = this.proto;\r\n  var target = this.target;\r\n  this.setters.push(name);\r\n\r\n  proto.__defineSetter__(name, function(val){\r\n    return this[target][name] = val;\r\n  });\r\n\r\n  return this;\r\n};\r\n\r\nDelegator.prototype.access = function(name){\r\n  return this.getter(name).setter(name);\r\n};\r\n```\r\n`.getter('origin')` 和 `.access('querystring')`调用的是上面的函数，与`.method`类似，也是调用proto对象的 getter 或者 accessor 时，在 target 中寻找对应的 getter 或者 accessor \r\n> JS __defineGetter__方法 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__\r\n\r\n至此，**就明白`lib/context.js`中那两大串函数是干嘛的了，就是将 request 和 response 的方法或者getter/setter委托到 context 中，起到的作用就是可以用多个方法访问一个属性或者方法**，比如 `ctx.acceptsEncodings()` 和 `ctx.request.acceptsEncodings()`，的效果是完全一模一样的。\r\n\r\n#### 5. 再看`lib/request.js` `lib/response.js`\r\n\r\n```\r\n// lib/request.js\r\nmodule.exports = {\r\n\r\n  get header() {\r\n    return this.req.headers;\r\n  },\r\n\r\n  set header(val) {\r\n    this.req.headers = val;\r\n  },\r\n  ...\r\n}\r\n\r\n// lib/response.js\r\nmodule.exports = {\r\n\r\n  get socket() {\r\n    return this.res.socket;\r\n  },\r\n  ...\r\n}\r\n```\r\n这两个文件还是比较长的，但是里面全是对 request 和 response 的 getter, setter 还有 method 的定义，还是很容易懂。\r\n\r\n值得注意的是，从上面代码可以看到，属性都是大部分都是从`this.req`或者`this.res`中取来的，而在这两个文件中并不能找到`this.res`或者`this.req`的声明，说明`res`和`req`都是之后才绑定到 request 或者 response上的。\r\n\r\n#### 6. 知道了context request response 之后，再看 application的use, listen方法\r\n```\r\n  ...\r\n  use(fn) {\r\n    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');\r\n    if (isGeneratorFunction(fn)) {\r\n      deprecate('Support for generators will be removed in v3. ' +\r\n                'See the documentation for examples of how to convert old middleware ' +\r\n                'https://github.com/koajs/koa/blob/master/docs/migration.md');\r\n      fn = convert(fn);\r\n    }\r\n    debug('use %s', fn._name || fn.name || '-');\r\n    this.middleware.push(fn);\r\n    return this;\r\n  }\r\n  \r\n  listen(...args) {\r\n    debug('listen');\r\n    const server = http.createServer(this.callback());\r\n    return server.listen(...args);\r\n  }\r\n  ...\r\n```\r\n可见，`use`就是将 fn 加入到 middleware 数组中，并且确认 fn 一定为函数。这里还判断了下 fn 是否为 generator，如果是的话，将 fn 利用 koa-convert 来转换下，这个涉及到异步编程的问题，这里先不详细讲，以后的博文会提到。\r\n\r\n`listen中`将`this.callback()`作为参数传入，根据下面node源码片段(来自`lib/_http_server.js`)可以知道，当Server收到一个请求，就会触发 'request' 事件，也就会调用 requestListener ，这里就是 `this.callback()`\r\n\r\n```\r\nfunction Server(requestListener) {\r\n  if (!(this instanceof Server)) return new Server(requestListener);\r\n  net.Server.call(this, { allowHalfOpen: true });\r\n\r\n  if (requestListener) {\r\n    this.addListener('request', requestListener);\r\n  }\r\n  ...\r\n}\r\n```\r\n\r\n#### 7. 再看application中的callback()\r\n```\r\n  ...\r\n  callback() {\r\n    const fn = compose(this.middleware);\r\n\r\n    if (!this.listenerCount('error')) this.on('error', this.onerror);\r\n    \r\n    // req res是在触发'request'事件后，传递的参数\r\n    const handleRequest = (req, res) => {\r\n      const ctx = this.createContext(req, res);\r\n      return this.handleRequest(ctx, fn);\r\n    };\r\n\r\n    return handleRequest;\r\n  }\r\n  ...\r\n```\r\n\r\n1. 首先执行的是`this.createContext(req, res)`，那就先看`createContext函数`，==注意这里的req，res参数里面已经有具体的请求参数了==\r\n\r\n```\r\n  ...\r\n  createContext(req, res) {\r\n    const context = Object.create(this.context);\r\n    const request = context.request = Object.create(this.request);\r\n    const response = context.response = Object.create(this.response);\r\n    context.app = request.app = response.app = this;\r\n    context.req = request.req = response.req = req;\r\n    context.res = request.res = response.res = res;\r\n    request.ctx = response.ctx = context;\r\n    request.response = response;\r\n    response.request = request;\r\n    context.originalUrl = request.originalUrl = req.url;\r\n    context.state = {};\r\n    return context;\r\n  }\r\n  ...\r\n```\r\n到这里就能看出来了，就是在这里将`req res app`绑在 request response 和 context上，request和response 还有context 也互相绑定。\r\n\r\n\r\n2. 绑定后，调用了`this.handleRequest(ctx, fn);`，fn是由`const fn = compose(this.middleware);`生成的，所以这里看下`koa-compose`干了什么\r\n```\r\nfunction compose (middleware) {\r\n  // 参数检查\r\n  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')\r\n  for (const fn of middleware) {\r\n    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')\r\n  }\r\n  return function (context, next) {\r\n    // last called middleware #\r\n    let index = -1\r\n    return dispatch(0)\r\n    \r\n    function dispatch (i) {\r\n      if (i <= index) return Promise.reject(new Error('next() called multiple times'))\r\n      index = i\r\n      let fn = middleware[i]\r\n      if (i === middleware.length) fn = next\r\n      if (!fn) return Promise.resolve()\r\n      try {\r\n        // 返回一个Promise.resolve \r\n        // fn 指的是middleware中一个一个的函数\r\n        // fn 的第一个参数是context\r\n        // fn的第二个参数是middleware数组中它之后的函数\r\n        // 这下知道为啥 koa的中间件，参数都是 (ctx, next) 了吧\r\n        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));\r\n      } catch (err) {\r\n        return Promise.reject(err)\r\n      }\r\n    }\r\n  }\r\n}\r\n```\r\n关键就是`return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));`，这是一个递归。\r\n\r\nmiddleware数组中是app.use(fn)中的fn组成的数组。dispatch(0) 会取出middleware数组的第一个fn，然后将 context 作为第一个参数，将 `dispatch.bind(null, 1)`作为第二个参数传入 fn。`dispatch.bind(null, 1)`会取出middleware数组中的第二个fn，然后将`dispatch.bind(null, 2)`做为第二个参数，传给fn，以此类推。直到将 middleware 数组的函数都取一遍，也就是说 `i === middleware.length`，这时会返回一个 `Promise.resolve()`\r\n\r\n**fn 的第一个参数是context，fn的第二个参数是middleware数组中它之后的函数\r\n，这下知道为啥 koa的中间件，参数都是 (ctx, next) 了吧**。\r\n\r\n在fn中，如果有`await next()`，这样的语句，那么就会直接调用下一层函数，下一层函数再执行`await next()`,就会继续下一层，直到最后一层，最后的是fn是`Promise.resolve()`，它没有返回值，然后返回倒数第二层，继续执行没执行完的部分，以次类推\r\n\r\n返回的像是一个函数洋葱，第一层是middleware中第一个函数，第一层调用next进入第二层函数，第二层调用next进入第三个...以此类推\r\n\r\n**有以下几点需要注意**：\r\n\r\n* `return function (context, next) {...}` 中的next参数根本没有用，因为在之后的调用中可以看到，这个参数根本没传，所以是undifined，但是还好有\r\n```\r\nif (i === middleware.length) fn = next; // fn = undifined\r\nif (!fn) return Promise.resolve() \r\n```\r\n这样就会在函数洋葱的最后补上一个Promise.resolve();\r\n\r\n* 一个fn内只能有一个`await next()`，也就是说只能调用下一个fn一次，如果调用两次，会导致报出`Error: next() called multiple times`错误\r\n\r\n> 在同一个中间件中多次调用 next() （执行next就是执行dispatch）的时候，index 会大于 i，从代码上看，每个中间件都有一个自己的作用域，也就是说同一个中间件，i 是不变的，在 i 不变的情况下，多次调用 next的情况下，第一次调用，index 小于 i，第二次调用，index就等于 i了，然后就会报错了\r\n\r\n* 之所以返回Primose.resolve()而不是Promise，是因为中间件函数不止支持 async 函数，还支持普通函数。\r\n\r\n#### 8. 再看 `this.handleRequest(ctx, fn)`, fn也就是上面`compose`的函数了\r\n```\r\n  handleRequest(ctx, fnMiddleware) {\r\n    const res = ctx.res;\r\n    res.statusCode = 404;\r\n    const onerror = err => ctx.onerror(err);\r\n    const handleResponse = () => respond(ctx);\r\n    onFinished(res, onerror);\r\n    return fnMiddleware(ctx).then(handleResponse).catch(onerror);\r\n  }\r\n```\r\n\r\n`fnMiddleware(ctx).then(handleResponse).catch(onerror)`这句话是核心，fnMiddleware就是之前返回的函数的火车；先执行这一系列的函数，这里面可能有解析参数啊，处理请求啊，等等操作，然后再执行 `handleResponse`。handleResponse就是调用了respond函数\r\n\r\n#### 9. 再看respond函数\r\n```\r\nfunction respond(ctx) {\r\n  // allow bypassing koa\r\n  if (false === ctx.respond) return;\r\n\r\n  const res = ctx.res;\r\n  if (!ctx.writable) return;\r\n\r\n  let body = ctx.body;\r\n  const code = ctx.status;\r\n\r\n  // ignore body\r\n  if (statuses.empty[code]) {\r\n    // strip headers\r\n    ctx.body = null;\r\n    return res.end();\r\n  }\r\n\r\n  if ('HEAD' == ctx.method) {\r\n    if (!res.headersSent && isJSON(body)) {\r\n      ctx.length = Buffer.byteLength(JSON.stringify(body));\r\n    }\r\n    return res.end();\r\n  }\r\n\r\n  // status body\r\n  if (null == body) {\r\n    body = ctx.message || String(code);\r\n    if (!res.headersSent) {\r\n      ctx.type = 'text';\r\n      ctx.length = Buffer.byteLength(body);\r\n    }\r\n    return res.end(body);\r\n  }\r\n\r\n  // responses\r\n  if (Buffer.isBuffer(body)) return res.end(body);\r\n  if ('string' == typeof body) return res.end(body);\r\n  if (body instanceof Stream) return body.pipe(res);\r\n\r\n  // body: json\r\n  body = JSON.stringify(body);\r\n  if (!res.headersSent) {\r\n    ctx.length = Buffer.byteLength(body);\r\n  }\r\n  res.end(body);\r\n}\r\n```\r\n**可以看出，就是根据操作改变后的ctx，来规整出一个response**\r\n\r\n#### 10. 最后是错误处理的onerror函数\r\n```\r\n  onerror(err) {\r\n    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));\r\n\r\n    if (404 == err.status || err.expose) return;\r\n    if (this.silent) return;\r\n\r\n    const msg = err.stack || err.toString();\r\n    console.error();\r\n    console.error(msg.replace(/^/gm, '  '));\r\n    console.error();\r\n  }\r\n```\r\n\r\n### 3. 总结\r\nKoa 官网对 Koa 有如下描述：\r\n> koa 是由 Express 原班人马打造的，致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用，通过组合不同的 generator，可以免除重复繁琐的回调函数嵌套， 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件， 它仅仅提供了一个轻量优雅的函数库，使得编写 Web 应用变得得心应手。\r\n\r\n可以说是非常贴切了，Koa的实现非常简洁，源码加起来不过两千行，但是借助中间件模型保持了极大的可拓展性，只要用各种不同的 middleware 一层一层的搭建就能实现多种多样的功能，感叹一句，TJ大神实在是强。"}}
{"actor":{"display_login":"LouisWT"}}