{"action":"created","issue":{"url":"https://api.github.com/repos/loveky/Blog/issues/3","labels_url":"https://api.github.com/repos/loveky/Blog/issues/3/labels{/name}","comments_url":"https://api.github.com/repos/loveky/Blog/issues/3/comments","events_url":"https://api.github.com/repos/loveky/Blog/issues/3/events","html_url":"https://github.com/loveky/Blog/issues/3","id":124045065,"number":3,"title":"koa框架源码阅读笔记 之 lib/application.js","user":{"login":"loveky","id":1198651,"avatar_url":"https://avatars.githubusercontent.com/u/1198651?v=3","gravatar_id":"","url":"https://api.github.com/users/loveky","html_url":"https://github.com/loveky","followers_url":"https://api.github.com/users/loveky/followers","following_url":"https://api.github.com/users/loveky/following{/other_user}","gists_url":"https://api.github.com/users/loveky/gists{/gist_id}","starred_url":"https://api.github.com/users/loveky/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/loveky/subscriptions","organizations_url":"https://api.github.com/users/loveky/orgs","repos_url":"https://api.github.com/users/loveky/repos","events_url":"https://api.github.com/users/loveky/events{/privacy}","received_events_url":"https://api.github.com/users/loveky/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/loveky/Blog/labels/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB","name":"源码阅读","color":"e6e6e6"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":2,"created_at":"2015-12-28T09:57:47Z","updated_at":"2016-01-11T07:10:27Z","closed_at":null,"body":"最近在学习ES6 Generator特性时发现了koa这个基于Generator的Web框架，它可以让开发者以一种“同步的方式”编写包含各种异步请求的Web应用。下面是关于它的一段中文介绍：\r\n\r\n> 由 Express 原班人马打造的 koa，致力于成为一个更小、更健壮、更富有表现力的 Web 框架。使用 koa 编写 web 应用，通过组合不同的 generator，可以免除重复繁琐的回调函数嵌套，并极大地提升常用错误处理效率。Koa 不在内核方法中绑定任何中间件，它仅仅提供了一个轻量优雅的函数库，使得编写 Web 应用变得得心应手。\r\n\r\n从介绍中可以看出这又是一个小而美的框架。到[GitHub项目页面](https://github.com/koajs/koa)上看了一下发现源码里只有[4个JS文件](https://github.com/koajs/koa/tree/1.1.2/lib)，总代码量只有1571行(以2015年12月26日最新稳定版1.1.2为准)。于是决定把代码clone下来学习一下，本篇会先从整个框架的入口文件`lib/application.js`说起。\r\n\r\n### 总体说明\r\n`lib/application.js`文件export出的是一个构造函数，用来创建一个koa应用。一个koa应用最常用的方法有2个：\r\n\r\n- `listen(port)` 执行listen后会通过`http.createServer`启动一个服务器并监听指定端口\r\n- `use(middleware)` 注册一个中间件，一个koa应用可以注册多个中间件， 处理请求时会按照中间件注册的顺序执行这些中间件。\r\n\r\n更多详细信息可以参考[GitHub文档页面](https://github.com/koajs/koa/blob/master/docs/api/index.md)。\r\n\r\n### 代码注释\r\n\r\n```javascript\r\nfunction Application() {\r\n  if (!(this instanceof Application)) return new Application;\r\n  this.env = process.env.NODE_ENV || 'development';\r\n  this.subdomainOffset = 2;\r\n  this.middleware = [];\r\n  this.proxy = false;\r\n  this.context = Object.create(context);\r\n  this.request = Object.create(request);\r\n  this.response = Object.create(response);\r\n}\r\n```\r\n以上就是`Application`构造函数的定义，很简洁，主要做了以下几件事情：   \r\n\r\n- 通过`instanceof`判断来支持不带`new`关键字的调用。\r\n- 设置应用运行环境，会从环境变量`NODE_ENV`读取，默认值为`development`\r\n- 声明`this.middleware`属性，默认为空数组，用来保存所有注册的中间件\r\n- 创建了`this.context`, `this.request`, `this.response` 对象，分别继承自koa项目里`lib`目录下对应文件export出来的对象。需要注意的是，这3个在`this`上的属性仍然只是`prototype`。每当一个请求到来时，koa会以这些对象为原型创建出与每个请求对应的`context`, `request`以及`response`对象。这样处理的好处是`lib/request.js`文件中定义的是koa框架级别的方法。`this.request`对象上我们可以定义项目级别里每个request需要用到的方法，属性。\r\n\r\n```javascript\r\nObject.setPrototypeOf(Application.prototype, Emitter.prototype);\r\n```\r\n在koa应用的原型链上追加一级`Emitter.prototype`，方便事件处理。\r\n\r\n```javascript\r\napp.listen = function(){\r\n  debug('listen');\r\n  var server = http.createServer(this.callback());\r\n  return server.listen.apply(server, arguments);\r\n};\r\n```\r\n`listen`方法会创建一个[http.Server](https://nodejs.org/dist/latest-v4.x/docs/api/http.html#http_class_http_server)实例，并将所有参数转给这个`http.Server `实例的listen方法。这里需要注意的是，koa应用的`listen`方法是可以调用多次的。可以通过多以调用`listen`方法来创建多个事例监听在不同端口上。\r\n\r\n```javascript\r\napp.inspect =\r\napp.toJSON = function(){\r\n  return only(this, [\r\n    'subdomainOffset',\r\n    'proxy',\r\n    'env'\r\n  ]);\r\n};\r\n```\r\n覆写koa应用的`toJSON`方法，并创建别名`inspect`。通过代码可以看出，在把koa应用以JSON格式输出时只会输出这个应用的`subdomainOffset`, `proxy`和`env`这三条信息。\r\n\r\n```javascript\r\napp.use = function(fn){\r\n  if (!this.experimental) {\r\n    // es7 async functions are allowed\r\n    assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');\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`app.use`方法用来注册一个中间件，默认传入的参数必须是一个Generator，否则报错。如果开启了`experimental`选项，则不再检查传入参数的类型，而是利用ES7中的`async/await`特性进行流程控制。\r\n\r\n```javascript\r\napp.callback = function(){\r\n  var fn = this.experimental\r\n    ? compose_es7(this.middleware)\r\n    : co.wrap(compose(this.middleware));\r\n  var self = this;\r\n\r\n  if (!this.listeners('error').length) this.on('error', this.onerror);\r\n\r\n  return function(req, res){\r\n    res.statusCode = 404;\r\n    var ctx = self.createContext(req, res);\r\n    onFinished(res, ctx.onerror);\r\n    fn.call(ctx).then(function () {\r\n      respond.call(ctx);\r\n    }).catch(ctx.onerror);\r\n  }\r\n};\r\n```\r\n`app.callback`用来根据当前的`middleware`队列生成一个callback，该callback会在`app.listen`操作中创建`http.Server`时作为参数传递给新创建的Server实例，用来处理客户端请求。\r\n\r\n第2至4行根据中间件队列生成一个普通函数`fn`，执行`fn`会返回一个Promise。如果中间件执行的过程中遇到错误，这个promise会被reject。在创建`fn`时依旧会判断`experimental`参数是否启用。如果该参数启用，则利用[composition](https://github.com/thenables/composition)模块进行转换。否则使用[co](https://github.com/tj/co) + [koa-compose](https://github.com/koajs/compose) 转换。\r\n\r\n第7行判断当前应用是否注册了错误处理函数，如果没有则使用koa框架默认的默认值。\r\n\r\n第9-16行则是实际处理请求的方法，接受一个request和一个response对象。首先koa会将node默认的request和response对象封装成一个koa的`context`对象。这个`context`对象就是我们中间件函数中的this，这样就可以很方便的访问与请求相关的信息。接着通过[on-finished](https://github.com/jshttp/on-finished)模块给当前请求注册一个callback，当处理该请求的过程中遇到错误时，该callback会根据错误信息进行一些状态设置，比如404，500状态码。最后就是调用`fn`方法开始处理请求。如果顺利执行，则会调用`respond`方法对response做最后处理，如果执行遇到错误，则执行`context`的`onerror`方法处理。\r\n\r\n\r\n`app.createContext`方法用于创建与一个请求对应的`context`对象。内容基本是一些属性的设置，此处不再详述。代码参考[GitHub页面](https://github.com/koajs/koa/blob/1.1.2/lib/application.js#L144-L160)。\r\n`app.onerror`为koa默认的错误处理函数。逻辑很简单，不再详述。代码参考[GitHub页面](https://github.com/koajs/koa/blob/1.1.2/lib/application.js#L169-L181)。\r\n\r\n最后的`respond`方法是一个helper方法，用来辅助设置一些response的信息。由于函数体较长，我们分段来看。\r\n```javascript\r\nfunction respond() {\r\n  // allow bypassing koa\r\n  if (false === this.respond) return;\r\n\r\n  var res = this.res;\r\n  if (res.headersSent || !this.writable) return;\r\n```\r\n如果`context`对象的`respond`属性为`false`，则不执行该函数的逻辑。\r\n接下来判断此响应的头部是否已经发出或是该响应是否不可写，如果任何一个条件为真，则跳过此函数逻辑。\r\n\r\n```javascript\r\n  var body = this.body;\r\n  var code = this.status;\r\n\r\n  // ignore body\r\n  if (statuses.empty[code]) {\r\n    // strip headers\r\n    this.body = null;\r\n    return res.end();\r\n  }\r\n```\r\n利用[statuses](https://github.com/jshttp/statuses)判断此响应的状态码是否对应一个空响应体（例如204,304）。如果为真，则直接返回空响应。\r\n\r\n```javascript\r\n  if ('HEAD' == this.method) {\r\n    if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));\r\n    return res.end();\r\n  }\r\n```\r\n如果是`HEAD`请求并且对应的响应资源是一个JSON对象，则通过`JSON.stringify`计算响应体长度。\r\n\r\n\r\n```javascript\r\n  // status body\r\n  if (null == body) {\r\n    this.type = 'text';\r\n    body = this.message || String(code);\r\n    this.length = Buffer.byteLength(body);\r\n    return res.end(body);\r\n  }\r\n```\r\n如果响应体为空，则设置一个默认的响应内容并设置响应体长度。\r\n\r\n```javascript\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  this.length = Buffer.byteLength(body);\r\n  res.end(body);\r\n}\r\n```\r\n最后是针对不同类型的响应体进行处理，如Buffer, 字符串, Stream 和 JSON。\r\n\r\n以上就是`lib/applicaton.js`的主要代码的注释了，其实只有几个方法而已，是不是很简单？\r\n"},"comment":{"url":"https://api.github.com/repos/loveky/Blog/issues/comments/170451197","html_url":"https://github.com/loveky/Blog/issues/3#issuecomment-170451197","issue_url":"https://api.github.com/repos/loveky/Blog/issues/3","id":170451197,"user":{"login":"panyifei","id":7299847,"avatar_url":"https://avatars.githubusercontent.com/u/7299847?v=3","gravatar_id":"","url":"https://api.github.com/users/panyifei","html_url":"https://github.com/panyifei","followers_url":"https://api.github.com/users/panyifei/followers","following_url":"https://api.github.com/users/panyifei/following{/other_user}","gists_url":"https://api.github.com/users/panyifei/gists{/gist_id}","starred_url":"https://api.github.com/users/panyifei/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/panyifei/subscriptions","organizations_url":"https://api.github.com/users/panyifei/orgs","repos_url":"https://api.github.com/users/panyifei/repos","events_url":"https://api.github.com/users/panyifei/events{/privacy}","received_events_url":"https://api.github.com/users/panyifei/received_events","type":"User","site_admin":false},"created_at":"2016-01-11T07:10:27Z","updated_at":"2016-01-11T07:10:27Z","body":"写的很赞，在自己读代码的时候看到这么详细的解释简直太幸福了"}}