Koa 常用中间件和手动实现中间件实战

企业级大型 Koa 应用:常用中间件 + 手动实现中间件实战

在企业级 Koa 应用中,中间件是核心扩展能力,需兼顾「通用性(复用成熟包)」和「定制化(手动实现贴合业务)」。以下先梳理高频常用中间件(按场景分类),再详解「手动实现中间件的场景+实战示例」,所有代码符合企业级规范(可直接集成到大型项目)。

一、企业级 Koa 常用中间件(分类梳理)

按「核心场景」分类,标注必选/可选、作用、推荐包及使用要点,适配大型应用的高可用、高安全、可维护要求:

分类 中间件功能 推荐包 必选/可选 核心使用场景
基础核心 请求体解析(JSON/表单/文件) koa-body 必选 处理 POST/PUT 请求的参数、文件上传,替代老旧的 koa-bodyparser/koa-multer
路由管理(嵌套/分组) koa-router 必选 大型应用拆分模块路由(如 /api/user//api/order),支持路由前缀、中间件绑定
跨域处理 koa2-cors 必选 适配前端跨域请求,定制允许的域名、请求头、Credentials
全局错误处理 自研(结合 winston/pino 日志) 必选 统一错误捕获、格式化响应、区分业务/系统错误(前文已详解)
安全防护 HTTP 头安全配置 koa-helmet 必选 设置 CSP/XSS/CSRF/HTTPS 等头,防注入、跨站攻击
接口限流(防刷) koa-ratelimit/koa2-ratelimit 必选 限制单 IP/用户的请求频率(如 100 次/分钟),避免接口被刷
CSRF 防护 koa-csrf 可选 表单/接口防跨站请求伪造,适用于登录态场景
接口加密/解密 koa-jose/自研 可选 敏感接口(支付/用户信息)的请求体加密、响应体解密
性能与监控 请求耗时统计 自研/koa-response-time 必选 监控接口响应耗时,定位慢接口
结构化日志 koa-logger/winston/pino 必选 记录请求/响应/错误的完整上下文(IP、参数、耗时、堆栈)
链路追踪 koa-opentelemetry 可选 微服务场景下追踪请求全链路(跨服务调用)
业务支撑 JWT 鉴权 koa-jwt 必选 无状态登录鉴权,解析 Token、验证权限范围
参数校验 koa-parameter/joi + 自研 必选 校验请求参数(类型、长度、枚举),返回标准化错误
接口缓存 koa-redis/koa-cache2 可选 高频读接口(如商品列表)缓存,减少数据库压力
部署与运维 响应压缩 koa-compress 必选 压缩响应体(gzip/brotli),减少网络传输量
静态资源托管 koa-static 可选 托管前端静态文件(如打包后的 React/Vue 资源)
健康检查 自研 必选 提供 /health 接口,供运维/容器检测服务存活状态
开发调试 热重载 koa-devtools/nodemon 开发必选 开发阶段代码修改后自动重启服务
接口文档 koa-swagger-decorator 开发必选 自动生成 Swagger 文档,便于前后端联调

关键使用要点(大型应用)

  1. 中间件分层:按「全局中间件(跨域/安全/日志)→ 模块中间件(鉴权/限流)→ 接口中间件(参数校验)」分层注册,避免混乱;
  2. 依赖轻量化:核心安全/解析类用成熟包,业务类优先自研(减少第三方依赖的维护风险);
  3. 配置中心化:中间件配置抽离到 config/middleware.js,支持环境区分(开发/测试/生产)。

二、手动实现 Koa 中间件的场景

大型应用中,以下场景优先手动实现中间件,而非依赖第三方包:

  1. 业务强定制:第三方包无法适配企业独特规则(如自定义限流策略、响应格式);
  2. 轻量化需求:简单功能(如健康检查、耗时统计)无需引入大依赖,减少包体积;
  3. 安全可控:核心业务逻辑(如鉴权、参数校验)自研可避免第三方包的漏洞/后门;
  4. 性能优化:手动实现可剔除第三方包的冗余逻辑,适配应用的性能瓶颈。

三、手动实现中间件实战示例(企业级规范)

以下示例均符合「可配置、可复用、有错误处理、适配大型应用」的要求,附带完整代码和使用说明。

示例1:健康检查中间件(必选,运维刚需)

需求

提供 /health 接口,返回服务状态、版本、数据库连接状态,供运维/容器检测服务可用性。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 健康检查中间件(src/middleware/healthCheck.js)
* 支持配置检查项(数据库、Redis),适配不同环境
*/
class HealthCheckMiddleware {
constructor(options = {}) {
// 配置项:需要检查的依赖、接口路径
this.options = {
path: '/health', // 健康检查接口路径
checkDeps: { // 需要检查的依赖(数据库/Redis)
mysql: false,
redis: false
},
appVersion: process.env.npm_package_version || '1.0.0', // 应用版本
...options
};
}

// 检查数据库连接(示例:mysql2)
async checkMysql() {
if (!this.options.checkDeps.mysql) return { status: 'skip' };
const mysql = require('mysql2/promise');
let connection;
try {
connection = await mysql.createConnection({
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD
});
await connection.ping(); // 测试连接
return { status: 'healthy' };
} catch (err) {
return { status: 'unhealthy', error: err.message };
} finally {
if (connection) await connection.end();
}
}

// 检查Redis连接(示例:ioredis)
async checkRedis() {
if (!this.options.checkDeps.redis) return { status: 'skip' };
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
try {
await redis.ping();
return { status: 'healthy' };
} catch (err) {
return { status: 'unhealthy', error: err.message };
} finally {
await redis.quit();
}
}

// 中间件核心逻辑
middleware() {
return async (ctx, next) => {
// 仅处理健康检查接口
if (ctx.path !== this.options.path) {
await next();
return;
}

// 执行依赖检查
const [mysqlStatus, redisStatus] = await Promise.all([
this.checkMysql(),
this.checkRedis()
]);

// 组装响应(符合企业级规范:结构化、易解析)
const healthStatus = (mysqlStatus.status === 'unhealthy' || redisStatus.status === 'unhealthy')
? 'unhealthy'
: 'healthy';

ctx.status = healthStatus === 'healthy' ? 200 : 503; // 503表示服务不可用
ctx.body = {
status: healthStatus,
version: this.options.appVersion,
timestamp: new Date().toISOString(),
checks: {
mysql: mysqlStatus,
redis: redisStatus
}
};
};
}
}

// 导出实例化方法
module.exports = (options) => new HealthCheckMiddleware(options).middleware();

使用方式(入口文件 app.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Koa = require('koa');
const app = new Koa();
const healthCheck = require('./middleware/healthCheck');

// 注册健康检查中间件(全局前置,优先于路由)
app.use(healthCheck({
path: '/health',
checkDeps: {
mysql: true, // 生产环境检查MySQL
redis: true // 生产环境检查Redis
}
}));

// 其他中间件/路由...
app.listen(3000);

测试效果

请求 http://localhost:3000/health,返回:

1
2
3
4
5
6
7
8
9
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2025-12-05T10:00:00.000Z",
"checks": {
"mysql": { "status": "healthy" },
"redis": { "status": "healthy" }
}
}

示例2:基于令牌桶的接口限流中间件(定制化限流)

需求

第三方限流包(如 koa-ratelimit)仅支持固定频率限流,需实现「令牌桶算法」,适配企业「高峰时段放宽限流、按用户ID限流」的需求。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* 令牌桶限流中间件(src/middleware/rateLimit.js)
* 特性:按IP/用户ID限流、动态令牌生成、支持白名单
*/
class TokenBucketRateLimit {
constructor(options = {}) {
// 配置项:令牌桶参数、限流维度、白名单
this.options = {
capacity: 100, // 桶最大容量(最多存100个令牌)
refillRate: 10, // 令牌补充速率(10个/秒)
limitKey: 'ip', // 限流维度:ip / userId
whiteList: [], // 白名单(IP/用户ID)
errorMsg: '请求过于频繁,请稍后再试', // 限流提示
...options
};
// 存储每个限流维度的令牌桶状态(内存版,大型应用可替换为Redis)
this.buckets = new Map();
// 定时补充令牌(全局单例,避免重复定时器)
this.startRefillTimer();
}

// 初始化令牌桶
initBucket(key) {
if (!this.buckets.has(key)) {
this.buckets.set(key, {
tokens: this.options.capacity, // 初始令牌数=桶容量
lastRefillTime: Date.now() // 最后补充时间
});
}
return this.buckets.get(key);
}

// 定时补充令牌(每秒执行)
startRefillTimer() {
setInterval(() => {
const now = Date.now();
for (const [key, bucket] of this.buckets) {
// 计算自上次补充的时间差(秒)
const timeDiff = (now - bucket.lastRefillTime) / 1000;
// 补充令牌(不超过桶容量)
bucket.tokens = Math.min(
this.options.capacity,
bucket.tokens + timeDiff * this.options.refillRate
);
bucket.lastRefillTime = now;
}
}, 1000);
}

// 尝试获取令牌
tryConsumeToken(key) {
const bucket = this.initBucket(key);
if (bucket.tokens >= 1) {
bucket.tokens -= 1; // 消耗1个令牌
return true;
}
return false;
}

// 中间件核心逻辑
middleware() {
return async (ctx, next) => {
// 1. 获取限流维度的key(IP/用户ID)
let limitKey;
if (this.options.limitKey === 'ip') {
limitKey = ctx.ip;
} else if (this.options.limitKey === 'userId') {
limitKey = ctx.state.user?.id || ctx.ip; // 假设userId存在于ctx.state.user
}

// 2. 白名单跳过限流
if (this.options.whiteList.includes(limitKey)) {
await next();
return;
}

// 3. 尝试获取令牌
if (!this.tryConsumeToken(limitKey)) {
ctx.status = 429; // 429 Too Many Requests
ctx.body = {
code: 429,
msg: this.options.errorMsg,
data: null
};
return;
}

// 4. 允许请求
await next();
};
}
}

// 导出实例化方法
module.exports = (options) => new TokenBucketRateLimit(options).middleware();

使用方式(模块路由中注册,仅对订单接口限流)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
const rateLimit = require('./middleware/rateLimit');

const orderRouter = new Router({ prefix: '/api/order' });

// 仅对订单接口限流:按用户ID,桶容量20,补充速率2个/秒
orderRouter.use(rateLimit({
capacity: 20,
refillRate: 2,
limitKey: 'userId',
whiteList: ['admin123'] // 管理员用户ID白名单
}));

// 订单接口
orderRouter.post('/create', async (ctx) => {
ctx.body = { code: 200, msg: '订单创建成功', data: { orderId: '123456' } };
});

// 注册路由
app.use(orderRouter.routes());
app.listen(3000);

示例3:结构化参数校验中间件(业务定制)

需求

第三方校验包(如 koa-parameter)规则不够灵活,需实现「支持自定义校验规则、多场景校验、标准化错误提示」的参数校验中间件。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 参数校验中间件(src/middleware/validate.js)
* 特性:支持GET/POST参数、自定义规则、标准化错误
*/
class ValidateMiddleware {
/**
* 构造函数
* @param {Object} rules 校验规则:{ 参数名: { type, required, rule, message } }
* @param {string} type 校验类型:query(GET)/body(POST)/params(路由)
*/
constructor(rules = {}, type = 'body') {
this.rules = rules;
this.type = type; // 校验的参数来源
// 内置基础校验规则
this.builtInRules = {
string: (val) => typeof val === 'string',
number: (val) => typeof val === 'number' && !isNaN(val),
integer: (val) => Number.isInteger(val),
email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
mobile: (val) => /^1[3-9]\d{9}$/.test(val)
};
}

// 执行单个参数校验
validateParam(key, rule, value) {
// 1. 必传校验
if (rule.required && (value === undefined || value === null || value === '')) {
return `${key}为必填项`;
}
// 非必传且无值,跳过后续校验
if (!rule.required && (value === undefined || value === null || value === '')) {
return null;
}
// 2. 类型校验
if (rule.type && !this.builtInRules[rule.type](value)) {
return `${key}必须为${rule.type}类型`;
}
// 3. 自定义规则校验
if (rule.rule && typeof rule.rule === 'function') {
const customError = rule.rule(value);
if (customError) return customError;
}
// 校验通过
return null;
}

// 中间件核心逻辑
middleware() {
return async (ctx, next) => {
// 1. 获取待校验的参数对象
let params;
switch (this.type) {
case 'query': params = ctx.query; break;
case 'body': params = ctx.request.body; break;
case 'params': params = ctx.params; break;
default: params = {};
}

// 2. 遍历规则执行校验
const errors = [];
for (const [key, rule] of Object.entries(this.rules)) {
const errorMsg = this.validateParam(key, rule, params[key]);
if (errorMsg) {
errors.push(errorMsg);
}
}

// 3. 校验失败返回错误
if (errors.length > 0) {
ctx.status = 400;
ctx.body = {
code: 400,
msg: '参数校验失败',
data: { errors }
};
return;
}

// 4. 校验通过,执行后续逻辑
await next();
};
}
}

// 导出便捷使用的函数
module.exports = (rules, type) => new ValidateMiddleware(rules, type).middleware();

使用方式(接口级别注册,校验创建用户的参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const Koa = require('koa');
const app = new Koa();
const koaBody = require('koa-body');
const Router = require('koa-router');
const validate = require('./middleware/validate');

app.use(koaBody()); // 解析请求体
const userRouter = new Router({ prefix: '/api/user' });

// 创建用户接口:校验body参数
userRouter.post('/create',
// 校验规则:name(必填字符串)、mobile(必填手机号)、age(可选整数,>18)
validate({
name: { type: 'string', required: true, message: '用户名不能为空' },
mobile: {
type: 'mobile',
required: true,
message: '手机号格式错误'
},
age: {
type: 'integer',
required: false,
rule: (val) => val > 18 ? null : '年龄必须大于18岁'
}
}, 'body'),
async (ctx) => {
ctx.body = { code: 200, msg: '用户创建成功', data: ctx.request.body };
}
);

app.use(userRouter.routes());
app.listen(3000);

测试效果

请求 /api/user/create 传入 { "name": "", "mobile": "123456", "age": 17 },返回:

1
2
3
4
5
6
7
{
"code": 400,
"msg": "参数校验失败",
"data": {
"errors": ["name为必填项", "手机号格式错误", "年龄必须大于18岁"]
}
}

示例4:统一响应格式化中间件(企业规范)

需求

统一所有接口的响应格式({ code, msg, data, timestamp }),避免前端适配多种格式,同时支持自定义忽略某些接口。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 响应格式化中间件(src/middleware/responseFormat.js)
* 特性:统一格式、支持忽略列表、兼容错误响应
*/
class ResponseFormatMiddleware {
constructor(options = {}) {
this.options = {
ignorePaths: ['/health'], // 忽略格式化的接口(如健康检查)
defaultCode: 200, // 成功默认码
defaultMsg: 'success', // 成功默认提示
...options
};
}

middleware() {
return async (ctx, next) => {
// 1. 执行后续中间件(路由逻辑)
await next();

// 2. 忽略列表中的接口,不格式化
if (this.options.ignorePaths.includes(ctx.path)) {
return;
}

// 3. 格式化响应(区分成功/错误)
if (ctx.status >= 200 && ctx.status < 300) {
// 成功响应:补充timestamp,统一结构
ctx.body = {
code: ctx.body?.code || this.options.defaultCode,
msg: ctx.body?.msg || this.options.defaultMsg,
data: ctx.body?.data || ctx.body || null,
timestamp: new Date().getTime()
};
} else {
// 错误响应:确保结构统一
ctx.body = {
code: ctx.body?.code || ctx.status,
msg: ctx.body?.msg || '请求失败',
data: null,
timestamp: new Date().getTime()
};
}
};
}
}

module.exports = (options) => new ResponseFormatMiddleware(options).middleware();

使用方式(全局注册,最后执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Koa = require('koa');
const app = new Koa();
const responseFormat = require('./middleware/responseFormat');

// 全局注册(需放在所有路由之后,保证最后格式化响应)
app.use(responseFormat({
ignorePaths: ['/health'], // 健康检查接口不格式化
defaultMsg: '操作成功'
}));

// 测试接口
app.use(async (ctx) => {
if (ctx.path === '/api/test') {
ctx.body = { data: { name: 'test' } }; // 原始响应
}
});

app.listen(3000);

测试效果

请求 /api/test,返回:

1
2
3
4
5
6
{
"code": 200,
"msg": "操作成功",
"data": { "name": "test" },
"timestamp": 1733426400000
}

四、手动实现中间件的最佳实践

  1. 类封装+可配置:用类封装中间件逻辑,通过构造函数传入配置,适配不同场景(如限流的容量/速率);
  2. 错误边界处理:所有异步操作(如数据库检查)加 try/catch,避免中间件自身抛出未捕获错误;
  3. 内存安全:内存版存储(如限流的令牌桶)需考虑过期清理(大型应用建议替换为Redis);
  4. 复用性设计:抽离通用逻辑(如参数校验的内置规则),避免重复代码;
  5. 环境适配:通过配置区分开发/生产环境(如开发环境关闭限流、放宽校验)。

Koa 常用中间件和手动实现中间件实战
https://zjw93615.github.io/2025/12/06/Koa/koa常用中间件和手动实现中间件实战/
作者
嘉炜
发布于
2025年12月6日
许可协议