中间件是 Koa 中的核心概念,必须完全掌握。
一、什么是中间件
先来看下面这段代码:
app.use(async (ctx, next) => {
await next();
ctx.response.type = "text/html";
ctx.response.body = "<h1>Hello World</h1>";
});
上述代码的作用是:每收到一个 HTTP 请求,Koa 应用服务端都会调用 async 箭头函数进行响应,然后返回响应内容给客户端。
在上述代码中,app 是一个 Koa 实例对象,它用 use 方法注册了一个函数,这个函数就是中间件。所以,我们可以下定义,中间件就是一个函数,而且 Koa 应用自动给这个函数传入了两个默认参数。那么,Koa 应用程序其实就是一个包含一组中间件函数的对象。
中间件参数
- 第一个参数:ctx
这是一个上下文对象,用于处理 HTTP 请求和响应。
- 第二个参数:next
这是一个函数,执行后返回一个 Promise 对象。它的作用是将程序的执行权交给下一个中间件,等下一个以及后面的中间件全部执行结束后,再回到当前中间件继续执行。
二、中间件的最佳实践
2.1 命名中间件
我们可以给中间件进行命名,这样在 Debug 时就会显示函数名,有助于定位调试。
// 定义中间件,命名为 logger
async function logger(ctx, next) {
// do something
};
}
// 注册中间件
app.use(logger)
2.2 中间件选项
在创建公共中间件时,可以将中间件包装在另外一个函数中,这有利于功能扩展。
// 定义 logger 函数,用于扩展功能
function logger(format) {
format = format || ":method :url";
// 返回一个中间件函数
return async function (ctx, next) {
const str = format
.replace(":method", ctx.method)
.replace(":url", ctx.url);
console.log(str);
await next();
};
}
// 注册中间件
app.use(logger());
app.use(logger(":method"));
app.use(logger(":method :url"));
问题:运行上述代码,控制台会输出哪些内容?
三、执行顺序
下面通过具体代码来演示中间件的执行顺序,代码如下:
const Koa = require("koa");
const app = new Koa();
// the first middleware
app.use(async function (ctx, next) {
console.log(">> one"); // 1
await next(); // 2
console.log("<< one"); // 3
});
// the second middleware
app.use(async function (ctx, next) {
console.log(">> two"); // 4
ctx.body = "two"; // 5
await next(); // 6
console.log("<< two"); // 7
});
// the third middleware
app.use(async function (ctx, next) {
console.log(">> three");// 8
await next(); // 9
console.log("<< three");// 10
});
app.listen(3000, () => {
console.log("[demo] server is running at http://localhost:3000");
});
用 Postman 访问 http://localhost:3000
,控制台输出结果如下:
>> one
>> two
>> three
<< three
<< two
<< one
通过输入结果发现,三个中间件函数体的具体执行顺序如下:
1 ➡️ 2 ➡️ 4 ➡️ 5 ➡️ 6 ➡️ 8 ➡️ 9 ➡️ 10 ➡️ 7 ➡️ 3
问题:将第二个中间件中的 next 那以上代码注释后再次运行,控制台输出结果是什么?
解析:执行顺序为 1 ➡️ 2 ➡️ 4 ➡️ 5 ➡️ 7 ➡️ 3
,第三个中间件没有执行到
提示
使用浏览器访问,会发现控制台输出了两次结果,这是因为访问http://localhost:3000
后,浏览器还自动请求了http://localhost:3000/favicon.ico
通过上述代码演示,我们知道了中间件的处理流程大致分为三部分:
- 前期处理
- 交给其他中间件处理并等待(这一步就是调用了 next 方法)
- 后期处理
Koa 应用程序由一组中间件组成,所以整个的处理过程就类似于先进后出的堆栈结构,可以用如下这张洋葱切面图形象地来解释多个不同功能的中间件的执行顺序。
四、中间件组合
有时候需要将多个中间件组合起来作为一个中间件,以便于重用和导出,这就需要使用 koa-compose
提示
koa 依赖koa-compose
,安装 koa 时已经自动下载到node_modules
中,无须单独安装
下面看如下代码:
const compose = require("koa-compose");
// random 中间件
async function random(ctx, next) {
if ("/random" == ctx.path) {
ctx.body = Math.floor(Math.random() * 10);
} else {
console.log("random middleware");
await next();
}
}
// backwards 中间件
async function backwards(ctx, next) {
if ("/backwards" == ctx.path) {
ctx.body = "sdrawkcab";
} else {
console.log("backwards middleware");
await next();
}
}
// pi 中间件
async function pi(ctx, next) {
if ("/pi" == ctx.path) {
ctx.body = String(Math.PI);
} else {
console.log("backwards middleware");
await next();
}
}
// 将三个中间件组合成一个中间件
const all = compose([random, backwards, pi]);
// 注册中间件
app.use(all);
- 用浏览器访问
http://localhost:3000
,控制台输出结果如下:
random middleware
backwards middleware
pi middleware
- 用浏览器访问
http://localhost:3000/backwards
,控制台输出结果如下:
random middleware
- 当变换组合顺序,比如
compose([random, pi, backwards])
,然后再次访问上述两个 URL 地址,控制台输出结果是什么?
五、实战演练
在实际应用中,经常需要记录服务器的响应时间,即服务器从接收到 HTTP 请求到返回响应内容给客户端所耗的时长。下面使用 Koa 的中间件机制实现这一功能,具体代码如下:
const Koa = require("koa");
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get("X-Response-Time");
console.log(`${ctx.method} ${ctx.url} - 响应时间: ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.response.set("X-Response-Time", `${ms}ms`);
});
// response
app.use(ctx => {
ctx.body = "<h1>Hello World</h1>";
});
app.listen(3000, () => {
console.log("[demo] server is running at http://localhost:3000")
})
按照业务需求,上述代码中使用了三个中间件,各自的都有不同的功能:
- logger 中间件:用于控制台打印请求的响应时间
- x-response-time 中间件:用于设置响应头信息
- response 中间件:用于返回响应内容给客户端
用浏览器访问 http://loacalhost:3000/user
,控制台输出结果如下:
GET /user - 响应时间: 0ms