该博客是个人学习 Koa 总结篇,如理解有误,请留言或在 GitHub 提交 issue 纠正。
转载请标明出处。
Koa 简介
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造,致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。Koa 并没有捆绑任何中间件,而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
Koa 中间件作用
中间件功能是可以访问请求对象(request),响应对象(response)和应用程序的请求-响应周期中通过 next 对下一个中间件函数的调用。通俗来讲,利用这个特性在 next 之前对 request 进行处理,在 next 函数之后对 response 处理。Koa 的中间件模型可以非常方便的实现后置处理逻辑。
图为洋葱模型:
应用程序
以下案例来源(Koa官网: hello world 应用 )
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
现在,一起深入刨析中间件的运行原理。
中间件应用案例
中间件应用案例(简称: demo),下面是 demo 代码:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(async (ctx, next) => {
ctx.body = 'Hello, Koa';
});
app.listen(3001);
开启服务器,端口为 3001。每接受一条客户端请求时,服务器依次输出 1, 3, 4, 2 打印结果。结合 demo 逐步刨析中间件原理。
中间件原理
注册中间件函数
demo 示例使用 use 注册中间件函数, 看下 Koa 源码内部的 use 实现。
use(fn) {
// 省略部分代码...
this.middleware.push(fn);
return this;
}
省略了部分校验和转换的代码,use 函数最核心的就是 this.middleware.push(fn) 这句。将注册的中间件函数都缓存到 middleware 栈中,并且返回了 this 自身,方便进行链式调用。 demo 注册了三个中间件函数,具体这些中间件函数什么时候执行以及如何执行,下文有详细解释。
创建 server 服务
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
内部使用了 Node 原生的 http 模块,通过 createServer 创建 Server 实例并监听指定的端口号。http.createServer(RequestListener) 接受请求侦听器函数作为参数。
所以,this.callback() 函数的调用返回 RequestListener 函数。RequestListener 函数接受请求对象(request)和 响应对象(response)两个参数。
callback 创建 RequestListener 请求侦听器函数
callback() {
// compose 为中间件运行的核心
const fn = compose(this.middleware);
// handleRequest 就是 callback 函数返回的函数
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
callback 函数主要做了两件事情:
-
使用 compose 函数对缓存中间件函数的栈做了一层校验,并 **返回了一个函数。**后面会详细讲解 compose 的实现。
-
创建一个 RequestListener 请求侦听器函数返回。如果客户端发送请求时,会先触发请求侦听器函数(handleRequest)执行,接受每次请求的 request 和 response 对象。
handleRequest 函数中的 const ctx = this.createContext(req, res) 将每次请求的 request(简称:req)和 response(简称:res) 对象结合创建一个 context(简称:ctx)上下文对象,并且创建三者的互相引用关系。当然,这不是本篇文章重点,只是简单带过。
然后将 ctx 和 fn 交给 handleRequest 进行处理,在讲解 handleRequest 之前,先看下通过 compose 生成的 fn 有什么作用。
compose
compose 是一个 koa-compose npm 包,内部核心代码也就20来行,它提供了中间件 next 函数调用的核心承载,看一下内部实现。下面为简化后的代码,方便理清运行逻辑。
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!')
}
/**
* @param {Object} ctx
* @return {Promise}
* @api public
*/
return function fn (ctx, next) {
// 简化了部分代码
return dispatch(0)
function dispatch (i) {
let middlewareFn = middleware[i]
try {
return Promise.resolve(middlewareFn(ctx, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
代码量不多,但实现的很巧妙。
**const fn = compose(this.middleware) **的调用主要对已注册的中间件列表(middleware)栈内每一个中间件函数的校验,并返回 fn 函数。
所以,只要记住传递给 handleRequest 函数的 fn 就是如下代码:
return function fn (ctx, next) {
// 简化了部分代码
return dispatch(0)
function dispatch (i) {
let middlewareFn = middleware[i]
try {
return Promise.resolve(middlewareFn(ctx, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
handleRequest
客户端每次请求时,都会调用 RequestListener 请求侦听器函数,并创建请求响应上下文对象后传递 ctx 和 fn 到 handleRequest 函数处理。
因此,每次请求都需要根据中间件注册的顺序进行中间件函数的执行。
看下 Koa handleRequest 函数内部实现:
handleRequest(ctx, fn) {
// 省略无关代码...
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// 省略无关代码...
return fn(ctx).then(handleResponse).catch(onerror);
}
将上下文对象(ctx)作为参数传入 fn 函数,在 fn 内部所有中间件全部执行完毕后,即调用 resolve 通知外部的 handleResponse 函数进行后续响应数据的处理。fn 内部首次调用 dispatch(0) 根据自定义下标 i 取出 middleware 栈中的第一个中间件函数 middlewareFn 。
demo 中第一个注册的中间件函数:
async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
执行第一个中间件函数,将 **上下文对象(ctx)和 dispatch.bind(null, i+1) **作为参数传递给中间件函数。首先执行 console.log(1) 打印 1,然后执行 await next() 将当前中间件函数的执行权转交给 next(即: dispatch(1) )执行,则继续取出第二个中间件函数执行,如此类推,直到所有中间件都执行完毕。
所有中间件函数依次执行完毕后,最后执行的中间件函数出栈后,将执行权转交给前一个中间件函数的 await 出,继续代码的执行。
结合 demo 代码,通过一张中间件函数入栈-出栈的图,来深刻的体会一下。
当 dispatch(0) 出栈后表示所有的中间件函数依次执行完毕了,如果某个中间件执行过程出现错误,会抛出 reject 由外部 onerror 错误处理函数进行处理,如果没有出现错误则调用 handleResponse 转交给 respond 函数处理 body 的数据格式。
总结
-
中间件函数的 next 不是直接调用的下一个中间件函数,而是调用的 fn 内部的 dispatch 函数,由它来调用下一个中间件函数并传递上下文对象(ctx)和自身(dispatch)。
-
中间件函数的执行顺序和注册顺序一致, 先 use 的中间件函数先执行。
-
一个中间件函数执行完毕,相应的 dispatch 函数将执行权转交给上一个中间件函数的 **await next(),**执行该中间件函数 await 后续的代码。