本文纲要
- express 与 koa 的比照
- Koa1 内核源码
- 简要介绍 Koa2 内核与 koa1 的区别
- 理解 Koa 中 http 协商缓存的实现机制
- koa-router 源码
- koa-view 源码
express
本文咱们不解说 express 的源码。然而 express 的实现机制对于咱们理解 TJ 在设计框架时的思路有肯定的参考意义。express 实现了一个相似于流的申请处理过程,其源码比 Koa 还要略微简单一点(次要是其内置了 Router 概念来实现路由)。如果对 express 的源码感兴趣的能够参考这两篇文章:
从 express 源码中探析其路由机制
NodeJS 框架之 Express4.x 源码剖析
exporess 和 koa 都是用来对 http 申请进行 接管、解决、响应。在这个过程中,express 和 koa 都有提供中间件的能力来对申请和响应进行串联。同时要提供一个封装好的 执行上下文 来串联中间件。
因而,koa 和 express 就是把这些 http 解决能力打包在一起的一个残缺的后端利用框架。波及到了一个申请解决的残缺流程,其中蕴含了这些常识概念:Application、Request、Response、COntext、Session、Cookie。
express 跟 koa 的区别是,express 应用的 ES5 时代的语言能力(没有应用 generator 和 async),因而 express 实现的中间件机制是传统的串行的流式运行(从第一个运行到最初一个后输入响应);而 koa 应用了 generator 或 async 从而实现了一种洋葱模型的中间件机制,所谓洋葱模型实际上就是中间件函数在运行过程中能够停下来,把执行权交给前面的中间件,等到适合的机会再回到函数内持续往下执行
Koa1 内核
本文咱们还是次要剖析 Koa1 的代码(因为 Generator 比 async 要绕一些难一些),我看的代码是基于 Koa 1.6.0。
对于 Koa1 来说,其实现是基于 ES6 的 Generator 函数。Generator 给了咱们用同步代码编写异步的可能,他能够让程序执行流 流向
下方,在异步完结之后再返回之前的中央执行。Generator 就像一个迭代器,能够通过它的 next 办法一直去迭代来实现函数的步进式执行。对于 Generator 函数解决异步问题的学习能够参考 阮一峰的 ES6 教程 Generator 函数与异步
Koa 内核只有 1 千 行左右的代码。共蕴含 4 个文件:
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 对象上(这里就不开展源码解说了)
当初流程根本分明了。但这里有个难点:
- fn 函数是如何可能把所有 Generator 中间件执行的?
- 中间件执行实现后是如何响应给浏览器后果的?
delegates 挂载属性到 context
咱们如果读过 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 对象。
response 如何响应给浏览器的?
这个次要是在 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 内核
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 的 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 源码解读,这里举荐一篇知乎专栏吧
Koa 中协商缓存实现机制
咱们晓得在 http 协定中,服务器端个别应用 http 报文的 if-none-match
if-modify-since
字段来进行缓存协商。Koa 提供了一个 request.fresh 函数来帮忙你确定是否返回 304.
这个 fresh 函数的实现基于 npm 模块 fresh
. 它外部会查看以后 response 响应头的 etag 和 last-modifyed 与 申请头里的 对应字段进行比对判断。
这个能够用在 Node 响应浏览器的最初一环时。
koa-route
咱们先看一个简陋版的 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
koa-view
TODO
KOA 源码学习
Koa1 源码学习 +co 源码学习
Koa2 源码学习
你晓得 koa 中间件执行原理吗