Koa 是 Express 团队设计的一个新的 web 框架,旨在为 web 应用程序和 api 打造一个更小、更具表现力和更健壮的基础。通过异步函数,Koa 允许您丢弃回调并大大增加错误处理。Koa 没有在其核心中捆绑任何中间件,它提供了一套优雅的方法,使编写服务器变得快速和愉快。 Koa 应用包含一组中间件,这些中间件以类似于堆栈的方式组合和执行的。 尽管提供了大量有用的方法,Koa 仍然保持了较小的内存占用,因为没有捆绑中间件。
关键词:异步函数、中间件。这是 Koa “优雅”“愉快”“轻量”的核心
小广告:长期内推滴滴:why318why@gmail.com
1 基本流程
我们先准备了一个最简单的 koa2 搭建的app,包括基本的:
- 创建 Koa 实例
- 监听接口
// app.js
const Koa = require('koa')
const app = new Koa()
const port = 3000
app.listen(port)
1.1 app 实例创建
const app = new Koa()
通过 new Koa,我们先获得了一个 koa 实例 —— app。那么这个 app 是如何拼装的呢?
// application.js
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
...
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
这里主要把三种东西挂到 app 实例上:
- 配置
- 中间件池
- 伴随请求的上下文、请求对象、响应对象
1.2 app.listen
在获得 app 后,我们可以立即 listen,就启动了个什么也不做的 Koa 了。
app.listen(port, () => console.log('listen...'))
// listen 参数和 node 自带 http.createServer 返回实例的 listen 参数完全一致
现在我们看源码
// application.js
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
参数是透传下去的,只是多了 http.createServer 调 this.callback。
注意这个this.callback
是执行的,也就是只在listen
时执行一次,返回的函数才是每次请求都执行的。
this.callback
// application.js
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
...
};
return handleRequest;
}
this.callback 只做了两件事情:
- 用 compose 合并中间件,返回单一的中间件调用函数 fn,每次调用这个函数,就会把中间件池走一遍。(具体实现后面看)
- 返回一个函数 handleRequest,用来处理请求
1.3 请求处理
现在我们拿到了中间件调用函数 fn,并在每次请求调用 handleRequest
// application.js
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
handleRequest 做了几件事
- 调用 this.createContext,把原生 req, res 构造成 koa 的 ctx(请求上下文)
- 调用 this.handleRequest,把 ctx、fn 提供过去,处理请求并返回。
this.createContext
代码就不贴了,最终构造出一个这样的结构:
this.handleRequest
收到构造好的「上下文」和「中间件调用函数」,这里终于开始干活了。
代码核心在:
return fn(ctx).then(handleResponse).catch(onerror);
- 先过中间件。给 fn 传入 ctx,通过中间件对 ctx 进行各种加工,返回了一个 Promise
- 再处理返回。handleResponse 解构 ctx,处理status、body、header 等 HTTP 特性,最终调用原生
res.end()
返回
2 中间件机制
我们在 listen 前加两个中间件
const app = new Koa()
app.use(async (ctx, next) => {
console.log('1-pre')
await next()
ctx.response.body += ' hello middleware1'
console.log('1-next')
})
app.use(async (ctx, next) => {
console.log('2-pre')
await next()
ctx.response.body += ' hello middleware2'
console.log('2-next')
})
app.listen(port)
结果很明显
- console 打出:
1-pre 2-pre 2-next 1-next
- body 返回:
undefined hello middleware2 hello middleware1
前面我们了解到:
- app 构造了
this.middleware
中间件池,并可以通过app.use
注册中间件 - compose 函数把所有中间件组合成一个异步函数,并在请求到来时调用
下面我们分别来看看这两部分做了什么。
2.1 中间件注册 —— app.use
use(fn) {
...
this.middleware.push(fn);
}
很简单的 push
2.2 中间件执行 —— compose
compose 是 koa 中间件管理的精髓,最有特点的是支持异步,进而实现了洋葱圈。
compose 返回的函数被这样调用,进去以后再逐个取中间件执行,加工 ctx
fn(ctx)
所谓支持异步,就是:
- 一个中间件 A 执行的时候,可以调一个异步的 next 函数,next 返回 Promise,并在 then 后继续执行 A 的逻辑
- next 函数可以嵌套调另外一个中间件 B
这里的灵魂是 next 函数的实现,它作为一个指针递归取中间件,返回一个「取下个中间件并执行」的 Promise。
next 函数就是中间件执行的触发器
尝试简单实现下上面思路:
// compose 返回的函数,传入 ctx 过中间件
function whatComposeReturn (ctx) {
let index = -1;
function next() {
index++;
const fn = middleware[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, next));
}
next();
}
但这里还有些边边角角的问题:
- 如果一个中间件调用了两次 next 怎么办?——指定index,阻止重复调用
- 如果 next 抛错了怎么办?——try catch
所以compose的实现:
function (context, next) {
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 (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
3 流程图
最后,我们用一张图回顾下 Koa 从初始化到响应请求的整体流程
小结
我们了解到:
- 初始化和app 实例创建过程
- 请求到来的处理流程:
构造ctx —> 过中间件 --> 处理返回
- 初始化阶段,
app.use
把中间件函数推入中间件池 - compose 封装了一个函数,通过递归调用 next 实现中间件的逐个异步调用