本文咱们不讲解express的源码。可是express的实现机制对于咱们了解 TJ 在设计框架时的思路有必定的参考意义。express 实现了一个相似于流的请求处理过程,其源码比 Koa 还要稍微复杂一点(主要是其内置了Router概念来实现路由)。若是对 express 的源码感兴趣的能够参考这两篇文章:node
exporess和koa都是用来对http请求进行 接收、处理、响应。在这个过程当中,express和koa都有提供中间件的能力来对请求和响应进行串联。同时要提供一个封装好的 执行上下文 来串联中间件。github
所以,koa和express就是把这些http处理能力打包在一块儿的一个完整的后端应用框架。涉及到了一个请求处理的完整流程,其中包含了这些知识概念:Application、Request、Response、COntext、Session、Cookie。express
express跟koa的区别是,express使用的ES5时代的语言能力(没有使用generator和async),所以express实现的中间件机制是传统的串行的流式运行(从第一个运行到最后一个后输出响应);而koa使用了generator或async从而实现了一种洋葱模型的中间件机制,所谓洋葱模型实际上就是中间件函数在运行过程当中能够停下来,把执行权交给后面的中间件,等到合适的时机再回到函数内继续往下执行npm
本文咱们仍是主要分析 Koa1 的代码(由于 Generator 比 async 要绕一些难一些),我看的代码是基于 Koa 1.6.0。json
对于 Koa1 来讲,其实现是基于 ES6 的 Generator 函数。Generator 给了咱们用同步代码编写异步的可能,他可让程序执行流 流向
下方,在异步结束以后再返回以前的地方执行。Generator 就像一个迭代器,能够经过它的 next 方法不断去迭代来实现函数的步进式执行。对于 Generator 函数解决异步问题的学习能够参考 阮一峰的 ES6 教程 Generator函数与异步后端
Koa 内核只有 1千 行左右的代码。共包含 4 个文件:api
application.js request.js response.js context.js
咱们从 package.json
中能够看到 Koa 的主入口是 lib/application.js
. 这个入口作的事情即是导出了一个 Application 的class类。(能够看到 Koa 的实现相比express已经比较面向对象了)数组
而 Application 的 prototype 上被挂载了咱们经常使用的 application 的方法,例如 use
, listen
, callback
, onerror
。
咦?是否是少了点 API? app.env,app.proxy这些呢?
原来,这些是 Application 的实例属性,在 Application 实例化的时候会同步初始化。来看一下 Application 构造函数的代码:
function Application() { if (!(this instanceof Application)) return new Application; // 支持工厂函数模式建立 this.env = process.env.NODE_ENV || 'development'; // 设置当前环境 this.subdomainOffset = 2; // 应用的子域偏移(这个主要是控制request.subdomain如何返回当前域名的哪一个部分;具体可参考文档的request.subdomains) this.middleware = []; // 存放应用中间件的数组 this.proxy = false; // 是否信任代理。为true时会让request.ips/hosts等字段读取X-Forward-*头 this.context = Object.create(context); // 在app挂载一个继承 context 对象的对象。 this.request = Object.create(request); // 在app挂载一个继承 request 对象的对象 this.response = Object.create(response); // 在app挂载一个继承 response 对象的对象 }
Application 类型的实现就是如此简单,除此以外,还继承了 EventEmitter 从而提供事件能力:
Object.setPrototypeOf(Application.prototype, Emitter.prototype);
至此,Application 上的方法和属性咱们都找到源头了,暂且先不分析其方法的具体实现。咱们再来看看 request 和 response 对象。
而从 request.js
中能够看到,该文件就仅仅导出了一个Object对象,对象中全部函数和属性便是 Koa 中间件中 request api
的全部方法。简要摘录下该文件源码结构:
// request.js module.exports = { get header() { return this.req.headers; } }
注意到这里面的 api 基本上就是对 Node.js 原生的 http.IncomingMessage 类型 API 的封装; response.js 也是相似的; context.js 也是相似的,并代理挂载了 request 和 response 的一些方法。那这里问题就来了: 上面代码中 this.req
为何能够拿到 IncomingMessage 对象呢?这就要从 Koa 中间件是如何运行提及了。
咱们先看下中间件是如何注入到应用中的。咱们在开发 Koa 应用时,一般是使用 app.use 来注册中间件。
app.use(function * (next) { this.body = '123' yield next })
而 use 函数作了一件很简单的事情: 把你的中间件置入 app.middleware 数组。
// 简化后的 use 函数 app.use = function(fn){ this.middleware.push(fn); return this; };
因为 use 函数同时返回了 this 指针,所以 app.use 得以能够链式调用。再回到咱们的话题: 中间件是如何运行起来的。 咱们看下 Koa 的启动代码:
http.createServer(app.callback()).listen(3000); // 或 app.listen(3000)
因为 listen 是一个语法糖,所以 http请求 最终都是被 app.callback() 函数返回的一个 function 来执行。 咱们看看 callback 到底返回了一个什么函数, 下面是我去掉了一些可有可无的 error 处理代码以后的源码:
app.callback = function(){ var fn = co.wrap(compose(this.middleware)); // 把全部中间件包装成一个fn函数 var self = this; // 返回一个闭包 return function handleRequest(req, res){ var ctx = self.createContext(req, res); // 把Node原生的req和res包装成Koa的context对象 self.handleRequest(ctx, fn); // 开始执行中间件 } };
其实原理很简单了,就是把 http 请求利用 createContext函数包装为 context 对象,而后调用 app.handleRequest 把应用内全部中间件执行一遍并返回结果给浏览器。
还记得上文提到的一个问题: 为何 request对象内能够用 this.req
拿到原生请求? 原理在这里就显而易见了,正是 self.createContext 把原生 req 设置在了 ctx 对象上(这里就不展开源码讲解了)
如今流程基本清楚了。但这里有个难点:
咱们若是读过koa文档,会发如今中间件中 this/ctx 上是能够访问到 ctx.request 对象上的属性的。这个是由于 koa 在初始化context对象的过程当中,把request上相关的属性挂载到了ctx.
这是中间件执行以前建立ctx的过程:
app.createContext = function(req, res){ var context = Object.create(this.context); var request = context.request = Object.create(this.request); // ctx能够访问request对象 var response = context.response = Object.create(this.response); context.app = request.app = response.app = this; // ctx能够访问app对象 context.req = request.req = response.req = req; //ctx能够访问原生req对象 context.res = request.res = response.res = res; request.ctx = response.ctx = context; // request对象能够访问ctx request.response = response; // request和rewspinse能够互相访问 response.request = request; context.onerror = context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); context.accept = request.accept = accepts(req); context.state = {}; return context; };
这段代码仍是没法解释为何ctx上能够访问request对象的上的属性。可是这里有一点是有做用的:ctx对象上面挂载了request对象。所以,在ctx的方法中能够经过 this.request
访问到request对象,这为ctx提供了访问request属性的基础。
上述的问题的答案,其实在context对象初始化的过程中。咱们看看context对象的初始化时作了个什么事情:
delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .access('querystring') .access('idempotent') .access('socket') .access('search') ...
能够看到,这里调用delegate这个库,给context对象添加了不少方法。实际上从deleteate源码中得知,delegate原型是这样的:
Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; this.methods.push(name); proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; return this; };
这个很明显就是给ctx添加方法函数,函数内调用目标对象的方法。access是经过getter,setter来访问this[target]的属性。
至此,ctx能够访问request和response属性的谜底就解开了。
中间件的执行流程和koa2是一致的。把中间件想做一个栈,请求会从顶部的第一个中间件开始处理,遇到yield next调用,就会进入下一个中间件中,直到最后没有yield next调用,再从栈底反弹,一个一个执行以前next以后的代码。
上文讲到了中间件执行主要靠这句代码合并为一个fn函数:
var fn = co.wrap(compose(this.middleware))
这里 compose 是来自 koa-compose
这个模块。 在前文《Koa教程-经常使用中间件》中,咱们已经了解了中间件的合并方式以及 koa-compose
的运做原理:总之就是经过 不断实例化Generator并做为参数传递给前一个Generator函数
的方式把多个 Generator 串联起来,最终执行第一个中间件就至关于串联执行全部中间件。
那么,co.wrap 是什么呢? 这里看下 co 源码 (co源码4.6.0加注释总共才237行):
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } }; function co(gen) { ... }
能够看到,co.wrap 仅仅就是返回了一个闭包,该闭包用于利用 co 来执行原函数(关于co是如何执行Generator的,本文暂不讲解)。看到这里,会有点疑惑,wrap包裹一层这是否是有点画蛇添足啊?实际上我上面省略了一点点代码,这里 Koa 是为了兼容 ES7 可能不须要co来运行中间件的状况。这里fn函数赋值的原始代码以下:
// ES7 合并后的中间件函数能够直接执行。ES6 generator的方式须要借助CO执行。 fn函数屏蔽了底层差别 var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
至此,咱们已经梳理出整个 http 请求的流程,即: Koa 收到 http 链接回调后,对InCompingMessage进行包装为 ctx, 并调用中间件合并后的函数 fn 进行业务处理。业务处理的代码很是简单,就是以ctx为上下文执行中间件:
// handleRequest即是收到网络请求后中间件运行的起点 app.handleRequest = function(ctx, fnMiddleware){ ctx.res.statusCode = 404; onFinished(ctx.res, ctx.onerror); // 注意这里: fnMiddleware.call(ctx) 就把中间件执行上下文设置为了 context 对象 fnMiddleware.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror); };
在开发 Koa 应用时,咱们知道在中间件中使用 this
就是在访问 context 对象. 正是由于在 Koa 执行中间件函数时将上下文设置为了 context 对象。
这个主要是在 co 执行中间件 resolve 以后利用了上文代码中看到的 respond 函数来实现。
fnMiddleware.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror);
respond函数主要是对ctx上设置的body内容进行解析,并选择合适的方法响应给浏览器。这里限于篇幅再也不具体讲解了。
yield *
另外咱们发现,在 Koa 的中间件里,咱们一般用:
yield next
来运行下一个中间件。经过上面的原理,咱们了解到所谓的 next 变量 实际上就是下一个中间件 Generator函数的实例,但是咱们会疑惑右值是一个Generator对象的时候 运行 Generator实例为何没有使用 yield *
? 理论上,若是按照 Generator的原始执行方式,没有使用 yield *
的话,这个语句只会返回 next 这个遍历器对象而已,是没法运行 next 函数的
这个问题的答案就在 co 源码里面,若是看过 co 的源码,会发现它是经过 右值.then(data=>{...})
回调里不断递归调用 gen.next(data)
来实现自动执行。而 gen.next 又会返回一个新的右值 {value: xx, done:false} ,co经过 toPromise 函数对右值进行Promise化从而能够调用 then,而 toPromise函数中若是检测到这个右值是一个Generator遍历器对象,则会从新用 co 来 run 这个对象。
所以,co 里面能够支持使用了 yield *
的方式(这种方式 Generator默认会展开下一层的遍历器);也能够支持 yield + 遍历器
的方式,这种方式是 co 本身检测到并运行这个迭代器的。
Koa2 主要是用 async await 代替了 Generator,用起来更方便了。async await 是 Generator 的语法糖,能够这样理解:
await --> 等于 yield async --> 等于 Generator函数声明: function * () {} 调用 async 函数 --> 等于利用 co 来自动执行 Generator: co(function*(){})
co 运行器返回的是一个 Promise, async 函数运行后也是返回一个 Promise。
Koa2 里面直接使用了 ES6 语法来建立 Application 类型:
module.exports = class Application extends Emitter {}
Koa2 中的 app.use、app.listen 等实现与 Koa1 基本彻底一致。区别开始出如今 app.callback 函数里:
callback() { const fn = compose(this.middleware); // 合并中间件 if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
能够看到 handleRequest 里面调用中间件函数 fnMiddleware 时再也不设置上下文,而是直接传递 ctx 到中间件中。所以 Koa 在中间件里不是经过 this 获取上下文,而是用 ctx 变量。
另一个主要区别就在于上面代码中这个 compose 了。
Koa2 使用了 4.x 版本的 koa-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!') } 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) } } } }
这个 compose 就是专门针对 async 函数而设计的了。 它最终返回的是个 function (context, next)
这样的闭包函数。 在上文讲到的 Koa2 的请求入口里 fnMiddleware(ctx)
的 fnMiddleware 实际上就是在调用这个函数。能够看到这个函数只接收了 context 参数,而 next 参数是 undefined。
咱们再来看看这个函数内部作了啥。它实际上从 middleware数组的第 0 项开始触发执行(dispath(0)),至关于主动在调用中间件async函数:
yourmid(ctx, next)
而这个 next 实际上传入的是下一个中间件函数。由此造成了递归调用。直到最后中间件没有了, fn = next 被赋值为undefined(由于next的值是undefined),而后回溯。回溯后返回的 Promise 交给 handleResponse 响应或错误处理:
const onerror = err => ctx.onerror(err); return fnMiddleware(ctx).then(handleResponse).catch(onerror);
能够看到 Koa2 的错误处理机制,跟 Koa1 也是同样的,都是中间件中一旦发生 throw Error,则会触发 fnMiddleware 的 catch,进而触发 context 对象的 onerror,在 ctx.onerror 里面会作出浏览器响应并调用 app.onerror 兜底。
以上就是 Koa2 的执行流程。跟 Koa1 差异不是很大,这里没有再过多展开了,若是但愿了解更详细的 Koa2 源码解读,这里推荐一篇知乎专栏吧
咱们知道在 http 协议中, 服务器端通常使用 http 报文的 if-none-match
if-modify-since
字段来进行缓存协商。 Koa 提供了一个 request.fresh 函数来帮助你肯定是否返回 304.
这个 fresh 函数的实现基于 npm 模块 fresh
. 它内部会检查当前 response 响应头的 etag 和 last-modifyed 与 请求头里的 对应字段进行比对判断。
这个能够用在 Node 响应浏览器的最后一环时。
咱们先看一个简陋版的router是怎么作的。这个库叫作 koa-route (注意不是koa-router哦)
这个route库只作了一件事,就是帮咱们生成简单的 generator 中间件,中间件的内容就是判断当前请求的路径是不是符合咱们的配置要求,符合才执行。
其用法以下:
var _ = require('koa-route'); app.use(_.get('/pets', pets.list)); app.use(_.get('/pets/:name', pets.show));
其中pets.list假设就是咱们针对 /pets
路径的处理函数。
实际上 _.get() 会把你传入的path包装成一个对其进行判断的generator中间件。相似于:
function * (next) { if (this.path === '/pets' && this.method === 'get') { ... } else { yield next } }
看他的源码也只有聊聊几行,核心在于这个create函数:
这个红圈圈出来的部分就是实际的 _.get返回值,做为中间件给注册进了 Koa
TODO