一文讲透koa-源码剖析
前言
本文从头开始由浅入深剖析现在十分流行的koa框架的核心源码,适合已经熟练掌握koa框架使用的开发人员阅读
核心机制
现在,让我们从头开始看看koa的内部究竟做了些什么?
// usage
const app = new Koa()
// source code
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
这就是一切的起源,Koa
类的实例从此诞生,继承自 Events
,可得知其子类拥有处理异步事件的能力,然而Koa
如何处理,现在还不得而知,先打个问号。但创建实例的过程中,可得知有三个对象作为实例的属性被初始化,分别为 context
request
response
,还有我们非常熟悉的存放所有全局中间件的数组middleware
koa class 图示
// usage
app.use(async (ctx, next) => {
// 中间件函数
});
当调用use方法时,在确认它是async
函数的情况下,通过push
操作,这个函数会被追加到middleware
数组中
use(fn) {
// 类型检查...
this.middleware.push(fn);
return this;
}
此时我们已经有了处理的操作,但是koa
还并没有真正的跑起来
app.listen(3000);
可以说当开启http
服务器的时候,koa
才真正能够开始处理我们的http
请求,那么这样一个简洁的调用背后具体究竟做了什么呢?
const server = http.createServer(this.callback());
return server.listen(...args);
koa
使用了 node
的原生 http
包来创建http服务
,所有的秘密都藏匿在 callback()
这个方法中
// source code
const fn = compose(this.middleware);
...
return fn(ctx).then(handleResponse).catch(onerror);
koa
自身还依赖于 koa-compose
模块,从 koa
对于 fn
的使用情况来看,middleware
应该是被封装成了一个叫做 fn
的对象,通过传入 context
对象来返回一个 Promise
现在,深入 koa-compose
模块来看看它又对我们的中间件数组做了什么。
// source code
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, function next () {
return dispatch(i + 1) // 递归调用dispatch
}))
} catch (err) {
return Promise.reject(err)
}
}
}
里面的闭包看起来很眼熟是吗?再对比一下
// usage
app.use(async (ctx, next) => {
// 中间件函数
});
这里的 Promise.resolve(fn(..))
帮助我们异步执行的中间件函数,这里的next
函数就解释了为什么Koa
的中间件调用是递归执行的,它递归调用了 dispatch
函数来遍历数组中的,同时,所有的中间件函数享有同一个 ctx
,再回顾一次外部
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;
context
使用node原生的 http
的监听回调函数中的 req
res
来进行进一步的封装,意味着对于每一个 http
请求,koa
都会创建一个 context
并共享给所有的全局中间件使用,当所有的中间件执行完过后,会将最后要返回的所有数据统一再交还给 res
进行返回,所以我们在每一个中间件中才能够从 ctx
中取得自己所需要的 req
中的数据进行处理,最后 ctx
再把要返回的 body
给原生的 res
进行返回
每一个请求都有唯一一个 context
对象,所有的关于请求和返回的东西都统一放在里面
createContext
方法将 req
res
进一步封装
// source code
const context = Object.create(this.context); // 创建一个对象,使之拥有context的原型方法,后面以此类推
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
本着一个请求一个context
的原则,context
必须是作为一个临时对象而存在,所有的东西都必须封进一个对象中,因此 app
req
res
三个属性就此诞生,但不知道大家是否有一个疑问,为什么 app
req
res
也同时被封在了 request
和 response
里 ?
使他们同时共享同一个 app
req
res
和 ctx
,是为了将处理职责进行转移,客户从外部访问,他们只需要一个 ctx
即可获得所有 koa
提供的数据和方法,而 koa
会继续将这些职责进行进一步的划分,比如 request
是用来进一步封装 req
的,response
是用来进一步封装 res
的,这样职责得到了分散,降低了耦合,同时共享所有资源使得整个 context
具有了高内聚的性质,内部元素互相都能够访问得到
// source code
context.state = {};
看得出来,其中的 state
就是专门负责保存单个请求状态的空对象,用户可以根据自己的需要来管理里面的内容
// source code
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
这个巨大的 ctx
被创建出来的时候在一开始就立马挂载了错误监听器,但在 createContext
中并没有发现这个onerror
方法,应该属于其中一个模块的原型方法,经过一番搜索,发现它位于 context.js
这个文件下,这个文件定义了所有的默认 context
对象具有的原型方法
// application.js
// handleRequest()
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
异步函数运行过程中如果有异常抛出且内部没有自行捕获异常的话, koa
会统一使用 Promise
的catch
语句来进行错误处理,用一个图来表示的话那就是这样
koa 请求处理流程图
到这里我们应该也不难解开本文开头时我们心中的一个疑问,那就是为什么要继承 Events
,其实就是为了使用 node
自带的事件监听器来监听一些事件,例如目前所知的 error
,来解耦错误处理这一块的功能
全局的错误处理函数
// source code
// application.js
onerror(err) {
assert(err instanceof Error, `non-error thrown: ${err}`);
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
Koa
的内部核心处理流程已经梳理完毕,现在我们再进入内部模块一探究竟
request 模块
大量的 getter
setter
充分提取出 node
http
所提供的有用的请求相关的属性,还有一些必要的 helper
函数这里不再赘述,API文档中几乎所有的 ctx.req
下的属性都在其下提供,值得一提的是其中的 this
,头疼的 this
, 这一块的代码用了大量的 this.req
的方式来访问node原生提供的 http 请求信息(因为我们把 node 的req
赋给了context
),当在 koa
外部进行使用时我们是通过 ctx.req.propertyName
的形式 … 没错,我想你也发现了,我们需要绑定我们的 this
使之指向的是 ctx.req
,所以在 context.js
模块中使用了代理来将 this
进行正确的绑定,前文提到的使这些模块能够相互访问的赋值代码其实也一定程度减少了this
的麻烦事
// 详见 delegates 包
// https://github.com/tj/node-delegates
// Koa context.js
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.access('query')
.access('path')
.access('url')
.getter('origin')
// delegates 的内部核心实现,通过apply来重新绑定this
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
response 模块
response.js
的内部也同 request.js
中类似,值得一提的是 writable
这个getter,如果请求已经得到相应,那么返回 true
,其中读取的值是 this.res.finished
,许多的报错中我们常常会碰到关于尝试重复写入响应头 header
的错误,这个错误的来源即使是 on-finished
这个包,当结束响应的时候此包能够执行一个回调来保存此请求的状态于 res
response helper 函数
koa有一个统一的响应函数位于 application.js
的末尾,专门负责处理 ctx.body
ctx.status
来进行请求响应
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
值得注意的是返回的body支持 Buffer
string
流,以及最常见的 json
全文完
商业转载请联系作者获得授权,非商业转载请注明出处,谢谢合作!
联系方式:tecker_[email protected]