前言
使用Koa已有一段时间,为什么会从Express转向Koa呢,那还是得从Express上说起。对于服务端的Web框架来说,Express更为贴近「Web Framework」这一概念,比如自带的路由,经过多年的运行,也使其生态丰富稳定。
但是说到Express的坏处,大家可能都会想起它的callback,使用不当必然会引起回调地狱。而此时此刻的Koa,正是解决了这个问题,不仅如此,Koa是基于Node的下一代Web框架,由Express团队打造,特点是「优雅、简洁、灵活、体积小」,几乎所有功能都需要通过中间件实现。
「Promise」和「Async/Await」是未来主流的异步编程方式,Node应用中需要优雅的异步处理方式,而Koa恰好来得很是时候。下面以小白的角度对Koa源码进行一次解读。
解读目标
Koa的源码非常精简,代码量非常少。正所谓「短小精悍」,在Koa上体现得淋漓尽致。读完源码后发现Koa还有很多插件的源码也值得一读,这篇文章先从基础解读开始,理解Koa最核心的东西。
- 中间件调用顺序:「洋葱模型」
- 理解Koa源码
洋葱模型
在了解洋葱模型之前,我们需要知道每一个中间件的周期:
- 前期处理
- 交给并等待下一个中间件处理
- 后期处理
多个中间件处理的过程,就形成了洋葱模型。这样实现的好处在于可非常方便的实现后续处理逻辑,而第一个中间件也能得到最后一个中间件的处理结果。
Koa使用app.use()
方法来加载中间件,功能基本都由中间件实现。加载完多个中间件后,跟栈的执行顺序一样,以「先进后出」的顺序执行。中间件带有2个参数:ctx对象
、next函数
。
Koa将request
与response
封装进ctx
,当调用了next()
就会执行下一个中间件。当执行完最后一个中间件,就会执行上一级调用的中间件。整个过程可理解成一刀切洋葱,切的顺序就是中间件的调用顺序,非常有趣。
洋葱模型的具体实现原理可通过插件「Koa-Compose」的源码理解,这里只做一下简单的介绍。app.use()
的作用是将中间件添加到中间件数组middleware
,将中间件数组middleware
传入Compose()
函数,Compose()
函数返回一个匿名函数,匿名函数返回Promise对象,第一个参数是context
,第二个参数是next()
,在有下一个中间件需要执行的情况下,next()
其实就是下一个要运行的中间件函数。返回Promise
,是为了方便await
调用。
说到context
,可与「Express」做一下小比较。对Express来说,并没有提供上下流信息,需要手动处理。Express不支持洋葱模型那样的数据流入流出处理能力,需要引入插件。因此,Koa就胜在此处。
下面用一个简单的例子来理解洋葱模型:
代码语言:javascript复制const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
console.log("第一个中间件");
next();
console.log("第一个中间件执行完毕");
});
app.use((ctx, next) => {
console.log("第二个中间件");
next();
console.log("第二个中间件执行完毕");
});
app.use((ctx, next) => {
console.log("第三个中间件");
});
app.listen(8090);
// 输出结果:
// 第一个中间件
// 第二个中间件
// 第三个中间件
// 第二个中间件执行完毕
// 第一个中间件执行完毕
理解源码
下载Koa的源码,主要代码都在lib文件下,仅有4个文件,分别是:request.js
、resopnse.js
、context.js
、application.js
。对于这4个文件,可大致分成3类:req/res
(请求与响应)、context
(上下文)、application
。
❝req和res ❞
对应的是request.js
和response.js
,分别代表着请求信息和返回信息。2个文件都是对外暴露一个对象,使用getter
和setter
来读写对象的属性。
request.js部分源码:
代码语言:javascript复制module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
}
};
❝context ❞
运行环境的上下文信息存在context.js
。上下文包括了request
和response
,在context.js
里引用了delegate.js
库来对request和response的代理。上面说到洋葱模型时,中间件的第一个参数ctx
,其实就是context的缩写。因此有ctx.req=ctx.request=context.request
和ctx.res=ctx.response=context.response
。
const delegate = require("delegates");
delegate(proto, "response")
.method("attachment")
.method("redirect");
delegate(proto, "request")
.method("acceptsLanguages")
.method("acceptsEncodings");
❝application ❞
对应的是application.js
文件,是最重要的一个文件。在这里将各个函数拆分,分析,理解。
- 「listen()」:Koa通过
app.listen(8090)
来启动端口,可看到listen函数
,http.createServer()
用于创建一个服务器,接受一个请求监听函数this.callback()
listen(...args) {
debug("listen");
const server = http.createServer(this.callback());
return server.listen(...args);
}
- 「callback()」:
callback
负责合并中间件,通过compose()
合并存在this.middleware
里的所有中间件。compose函数由插件koa-compose
引入。callback函数
返回handleRequest()
处理函数,handleRequest函数
作为创建服务器之后接受的处理函数
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()」:
handleRequest函数
通过createContext()
创建请求的上下文,将状态设为404。onFinished(res, onerror)
通过引入第三方库on-finished
来监听服务器的失败响应,传入的onerror就是ctx.onerror(err)
。最后返回fnMiddleware(ctx).then(handleResponse).catch(onerror)
,就是将所有合并起来的中间件成功执行完后就执行handleResponse响应函数
,异常则执行onerror,就是ctx.onerror(err)
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);
}
- 「respond()」:
respond函数
其实就是所有中间件执行成功后的响应函数,这里对不同的响应主体进行了响应的处理。为了更好的理解,每一行代码相应的理解注释在代码下面。
function respond(ctx) {
// allow bypassing koa
// 这里说明可通过设置上下文的respond为false,则会跳过respond处理
if (ctx.respond === false) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
// 如果状态码表示没有响应主体时,则设置响应主体为null
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if (ctx.method == "HEAD") {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (body == null) {
// 如果响应主体为空,这里将body设置成状态码,或者设置成message
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
// 如果响应头为发送时需要设置Content-Type与Content-Length
if (!res.headersSent) {
ctx.type = "text";
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// 这里针对不同类型的响应主体,做相应的处理
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if (typeof body === "string") 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);
}
总结
对Koa源码的简单解析就写到这啦。读完源码之后发现,不能只停留在使用上面,更应该花点时间来理解背后的源码,在解读源码的时候,也许会让自己有意外的收获哦。Koa还有很多插件的源码值得去探究,比如koa-compose
,koa-router
这些插件的源码都值得一读。不断学习进步是消除焦虑的唯一方法,继续努力呀~
结语
欢迎在下方进行评论,喜欢本文的「点个赞」或「收个藏」,同时也希望各位朋友对文章里的要点进行补充或提出自己的见解。
关注IQ前端
「关注公众号IQ前端
,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔」