Koa是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数, Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件,而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
中间件的功能是可以访问请求对象( request ),响应对象( response )和应用程序的请求-响应周期中的通过 next 对下一个中间件函数的调用。通俗来讲, 利用这一特性在 next 之前对 request 进行处理, 而在 next 之后对 response 进行处理。
const Koa = require('koa');
const App = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
以上代码是 Koa 官网上面的 简单示例 , 接下来一起深入中间件机制的运行原理。
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);
结合上面应用demo, 逐步剖析中间件运行原理。每当服务器接收一个客户端请求时, 都会依次打印: 1, 3, 4, 2 。
上面应用使用 use 进行注册中间件函数, 看下 Koa 内部中间件的实现。
use(fn) {
// 省略部分代码...
this.middleware.push(fn);
return this;
}
省略了部分校验和转换的代码, use 函数最核心的就是 this.middleware.push(fn) 这一句。将我们注册的中间件函数都缓存到 middleware 栈中, 并且返回了 this 自身, 方便进行链式调用。
上面的 demo 应用注册了三个中间件函数,具体这些中间件函数什么时候执行以及如何执行, 继续看。
上面 demo 引用调用 Koa 实例的 listen 方法, 开启端口号为 3001 的服务, 看下 Koa 内部 listen 方法的实现。
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
内部使用了 Node 原生的 http 模块, 通过 createServer 创建一个 Server 实例并监听指定的端口号。 http.createServer(RequestListener) 接受请求侦听器函数作为参数, RequestListener 函数接受 request 和 response 对象两个参数。
所以, 知道 this.callback() 函数的调用返回一个函数, 并且这个函数接受 request 和 response 请求和响应对象。
上面说到, callback 函数的调用返回一个 RequestListener 请求侦听器函数, 并且接受 请求对象( request )和响应对象( response )。
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函数主要做了两件事情:
const ctx = this.createContext(req, res) 纯碎做了一件根据请求的 request 和 response 创建了一个 ctx 上下文对象, 创建它们三者的互相引用关系等, 这不是这篇文章的重点, 可自行了解。。
然后通过 handleRequest 函数将 ctx 上下文对象和 compose 函数的结果作为参数进行处理, 那么 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 及 middleware 栈内每一个中间件函数的校验, 并返回 fn 函数。
下面结合 handleRequest 函数内部的处理来深入理解 fn 函数的执行过程。
每次客户端有请求时, 都会调用 RequestListener 请求侦听器函数, 并创建请求响应上下文对象后, 传递 上下文对象 和 fn 函数到 handleRequest 函数处理。所以每次请求都会处理一次, 每次请求都会依次触发已注册的中间件函数。
handleRequest(ctx, fn) {
// 省略无关代码...
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// 省略无关代码...
return fn(ctx).then(handleResponse).catch(onerror);
}
fn(ctx) 接受上下文对象参数,执行的结果可以调用 .then , 不用想了吧, 八成返回一个 Promise 对象, 下面再进入到看下 fn 函数内部的实现。
内部调用了 dispatch(0) 根据下标取出 middleware 栈中的第一个中间件函数 middlewareFn :
async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
希望你对 bing 有深刻的理解。 MDN bind
然后执行第一个中间件函数, 将上下文对象( ctx ) 和 next ( dispatch.bind(null, i + 1) ) 作为参数传递给中间件函数。首先会执行 console.log(1) 打印 1 , 然后执行 await next() 将当前函数的 执行权 转交给 dispatch.bind(null, i + 1) 函数执行。
相当于调用了 dispatch(1) , 则取出第二个中间件函数执行, 依次类推。
洋葱模型
当 dispatch(0) 出栈后则表示所有的中间件函数已依次执行完毕, 如果某个中间件执行出现错误, 就会抛出 Promise.reject 由外部的 onerror 函数处理, 如果没有出现错误则调用 handleResponse 函数并转交给 respond 函数处理 body 的数据格式, 这些不是本篇幅的重点。