1. Node.js中间件与控制器设计实战指南
在构建企业级Node.js应用时,中间件和控制器是架构设计的核心要素。很多开发者在面对复杂业务逻辑时,常常陷入中间件滥用或控制器臃肿的困境。本文将基于Egg.js框架,分享我在多个生产项目中总结出的中间件与控制器最佳实践。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 中间件设计原理与实现
2.1 中间件的本质与执行机制
中间件本质上是洋葱圈模型的实现,每个中间件都能访问请求上下文(Context)并决定是否继续传递控制权。在Egg.js中,中间件执行顺序由config/config.default.ts中的配置数组决定:
typescript复制// 配置文件中声明的顺序决定了执行顺序
config.middleware = ['auth', 'robot', 'logger'];
重要提示:中间件执行顺序直接影响功能逻辑。例如认证中间件必须放在业务中间件之前,否则会导致未授权访问。
2.2 认证中间件完整实现
下面是一个生产级认证中间件的实现示例,包含JWT验证和权限校验:
typescript复制// app/middleware/auth.ts
import { Context, Next } from 'egg';
import * as jwt from 'jsonwebtoken';
interface AuthOptions {
required?: boolean;
permissions?: string[];
}
export default function auth(options: AuthOptions = {}) {
return async (ctx: Context, next: Next) => {
const token = ctx.get('authorization')?.replace('Bearer ', '');
// 非必验路由直接放行
if (options.required === false && !token) {
await next();
return;
}
try {
// 1. 验证Token有效性
const decoded = jwt.verify(token, ctx.app.config.jwt.secret) as {
userId: string;
role: string;
};
// 2. 检查用户状态(数据库查询)
const user = await ctx.service.user.findById(decoded.userId);
if (!user || user.status !== 'active') {
throw new Error('用户状态异常');
}
// 3. 权限校验
if (options.permissions && !options.permissions.includes(user.role)) {
ctx.status = 403;
ctx.body = { error: '权限不足' };
return;
}
// 4. 挂载用户信息到上下文
ctx.user = user;
await next();
} catch (err) {
ctx.logger.error('认证失败:', err);
ctx.status = 401;
ctx.body = { error: '认证失败', detail: err.message };
}
};
}
2.3 中间件配置进阶技巧
在config/config.default.ts中,我们可以针对不同环境配置中间件:
typescript复制// config/config.default.ts
export default () => {
const config: PowerPartial<EggAppConfig> = {};
// 开发环境禁用部分中间件
config.middleware = ['auth'];
if (process.env.NODE_ENV === 'production') {
config.middleware.push('ratelimit', 'security');
}
// 中间件细粒度配置
config.auth = {
required: true,
ignore: ['/api/login', '/api/register'] // 白名单路由
};
config.ratelimit = {
duration: 60000, // 1分钟
max: 100, // 最大请求数
disableHeader: false
};
return config;
};
3. 控制器最佳实践
3.1 控制器分层设计
良好的控制器应该保持"瘦"状态,主要职责是:
- 接收请求参数
- 调用服务层处理业务
- 返回响应
typescript复制// app/controller/user.ts
import { Controller } from 'egg';
export default class UserController extends Controller {
// 获取用户详情
async show() {
const { ctx } = this;
// 1. 参数校验
const { id } = ctx.params;
if (!id || !/^\d+$/.test(id)) {
ctx.throw(400, 'ID参数不合法');
}
// 2. 调用服务层
const user = await ctx.service.user.getDetail(parseInt(id));
// 3. 返回响应
ctx.body = {
success: true,
data: user
};
}
}
3.2 异常处理统一方案
推荐使用Egg.js的中间件实现全局异常处理:
typescript复制// app/middleware/error_handler.ts
export default () => {
return async (ctx: Context, next: Next) => {
try {
await next();
} catch (err) {
// 记录完整错误堆栈
ctx.logger.error(err);
// 业务异常
if (err.code && err.code >= 400 && err.code < 500) {
ctx.status = err.code;
ctx.body = {
code: err.code,
message: err.message
};
return;
}
// 系统异常
ctx.status = 500;
ctx.body = {
code: 500,
message: '服务器内部错误'
};
}
};
};
3.3 RESTful API设计规范
遵循RESTful风格时,控制器方法应对应标准HTTP方法:
| HTTP方法 | 控制器方法 | 用途 | 示例路由 |
|---|---|---|---|
| GET | index | 获取资源列表 | GET /users |
| GET | show | 获取单个资源 | GET /users/1 |
| POST | create | 创建资源 | POST /users |
| PUT | update | 更新整个资源 | PUT /users/1 |
| PATCH | modify | 部分更新资源 | PATCH /users/1 |
| DELETE | destroy | 删除资源 | DELETE /users/1 |
4. 高级技巧与性能优化
4.1 中间件性能调优
对于高频调用的中间件,可以采用以下优化策略:
- 缓存验证结果:将认证结果缓存在内存中,设置合理的TTL
- 异步并行处理:使用Promise.all处理无依赖的检查项
- 条件执行:通过ctx.path判断是否需要执行当前中间件
typescript复制// 优化后的认证中间件
export default function auth() {
return async (ctx: Context, next: Next) => {
// 跳过静态资源请求
if (ctx.path.startsWith('/public/')) {
await next();
return;
}
const start = Date.now();
const cacheKey = `auth:${ctx.get('authorization')}`;
// 尝试从缓存获取
const cachedUser = await ctx.app.redis.get(cacheKey);
if (cachedUser) {
ctx.user = JSON.parse(cachedUser);
ctx.logger.info(`[Auth] Cache hit, cost: ${Date.now() - start}ms`);
await next();
return;
}
// 并行执行验证
const [tokenValid, userInfo] = await Promise.all([
verifyToken(ctx.get('authorization')),
getUserFromDB(ctx.get('authorization'))
]);
if (!tokenValid || !userInfo) {
ctx.throw(401, '认证失败');
}
// 设置缓存
await ctx.app.redis.setex(cacheKey, 300, JSON.stringify(userInfo));
ctx.user = userInfo;
await next();
};
}
4.2 控制器代码复用
通过基类控制器实现通用逻辑:
typescript复制// app/core/base_controller.ts
import { Controller } from 'egg';
export default class BaseController extends Controller {
protected success(data?: any) {
this.ctx.body = {
success: true,
data
};
}
protected error(message: string, code = 400) {
this.ctx.throw(code, message);
}
protected pagination(list: any[], total: number) {
return {
list,
total,
page: this.ctx.query.page || 1,
pageSize: this.ctx.query.pageSize || 10
};
}
}
// 使用示例
class UserController extends BaseController {
async index() {
const { list, total } = await this.service.user.list();
this.success(this.pagination(list, total));
}
}
5. 常见问题排查
5.1 中间件执行顺序问题
症状:某些中间件未按预期顺序执行
解决方案:
- 检查config.default.ts中的middleware数组顺序
- 确保没有在router.ts中重复注册中间件
- 使用app.middleware.ready()查看中间件加载顺序
5.2 控制器方法未被调用
症状:请求返回404但路由配置正确
排查步骤:
- 检查控制器方法是否为public(TypeScript默认方法为public)
- 确认路由HTTP方法与控制器方法匹配(GET对应index/show等)
- 使用app.controller检查控制器是否正常加载
5.3 上下文污染问题
症状:请求间数据互相影响
解决方案:
- 避免在中间件中直接修改ctx对象原型
- 使用Symbol作为自定义属性的key
- 确保每次请求都初始化必要的上下文属性
typescript复制// 安全的自定义上下文扩展
declare module 'egg' {
interface Context {
[Symbol.for('user')]?: User;
}
}
// 中间件中使用
ctx[Symbol.for('user')] = userInfo;
6. 测试策略
6.1 中间件单元测试
使用supertest和egg-mock进行测试:
typescript复制// test/middleware/auth.test.ts
import { app, mock } from 'egg-mock/bootstrap';
describe('auth middleware', () => {
it('should reject unauthorized request', async () => {
await app.httpRequest()
.get('/protected')
.expect(401);
});
it('should accept valid token', async () => {
const token = app.jwt.sign({ userId: 1 });
await app.httpRequest()
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
});
6.2 控制器集成测试
模拟完整请求链路:
typescript复制// test/controller/user.test.ts
describe('GET /users/:id', () => {
it('should return user detail', async () => {
// 准备测试数据
await app.factory.create('user', { id: 1, name: 'test' });
const res = await app.httpRequest()
.get('/users/1')
.expect(200);
assert(res.body.data.name === 'test');
});
it('should return 404 for non-existent user', async () => {
await app.httpRequest()
.get('/users/999')
.expect(404);
});
});
在实际项目中,我通常会为每个中间件编写独立的测试用例,覆盖各种边界条件。对于控制器,则更关注业务逻辑的正确性和异常处理。记住,良好的测试覆盖率是保证中间件和控制器稳定运行的关键。
