1. Egg.js控制器深度解析:从HTTP到定时任务的全方位实践
作为一名长期使用Egg.js开发企业级应用的全栈工程师,我经常遇到新手对控制器(Controller)的理解停留在表面。今天,我将结合实战经验,带你深入掌握Egg.js控制器的核心用法和设计思想。
1.1 控制器在Egg.js架构中的核心地位
在Egg.js的MVC架构中,控制器扮演着"交通警察"的角色。它接收来自前端的HTTP请求,协调模型(Model)和服务(Service)完成业务逻辑处理,最终返回响应。这种设计遵循了"单一职责原则",使代码结构更清晰、更易维护。
实际项目中,我习惯将控制器分为三类:
- HTTP控制器:处理常规的Web请求
- API控制器:专用于RESTful接口
- 定时任务控制器:处理后台作业
这种分类不是Egg.js强制的,但能显著提升大型项目的可维护性。下面我们重点剖析HTTP控制器的实现细节。
2. HTTP控制器实战指南
2.1 控制器基础配置
在Egg.js中创建HTTP控制器非常简单,但有些细节值得注意:
javascript复制// app/controller/user.js
const { Controller } = require('egg');
class UserController extends Controller {
async index() {
const { ctx } = this;
ctx.body = await ctx.service.user.list();
}
}
module.exports = UserController;
关键点解析:
- 控制器必须放在
app/controller目录下 - 类名采用大驼峰命名,文件名建议小写
- 每个方法对应一个路由,默认支持RESTful风格
经验之谈:我习惯在大型项目中按业务模块划分子目录,比如
app/controller/admin/user.js用于后台用户管理,这样结构更清晰。
2.2 路由配置的艺术
路由是控制器的入口,Egg.js的路由配置非常灵活:
javascript复制// app/router.js
module.exports = app => {
const { router, controller } = app;
// 基本路由
router.get('/user/:id', controller.user.show);
// RESTful路由
router.resources('posts', '/api/posts', controller.post);
// 重定向
router.redirect('/old', '/new', 302);
};
在实际项目中,我总结出几个最佳实践:
- API路由统一加
/api前缀 - 参数路由使用
:param形式 - 复杂路由建议拆分成多个简单路由
- 路由文件超过300行就该考虑拆分
2.3 参数处理的正确姿势
Egg.js提供了丰富的参数获取方式,每种都有其适用场景:
| 参数类型 | 获取方式 | 适用场景 | 示例 |
|---|---|---|---|
| Query | ctx.query | 筛选、分页 | /users?page=1 |
| Params | ctx.params | 资源标识 | /users/123 |
| Body | ctx.request.body | 创建/更新 | POST /users |
| Headers | ctx.headers | 认证、上下文 | Authorization |
一个常见的错误是过度依赖ctx.query,这会导致安全问题。我的建议是:
- 必填参数用Params或Body
- 敏感参数不要放在URL中
- 所有输入都要做校验
javascript复制// 安全的参数处理示例
async update() {
const { ctx } = this;
const { id } = ctx.params; // 路径参数
const data = ctx.request.body; // 请求体
// 参数校验
ctx.validate({
name: { type: 'string', required: true },
age: { type: 'number', required: false }
}, data);
await ctx.service.user.update(id, data);
ctx.body = { success: true };
}
2.4 响应处理的进阶技巧
除了简单的ctx.body,Egg.js还支持多种响应方式:
javascript复制// JSON响应(默认)
ctx.body = { code: 0, data: result };
// 状态码设置
ctx.status = 201; // Created
// 设置Header
ctx.set('X-Powered-By', 'Egg');
// 流式响应
ctx.body = fs.createReadStream('/path/to/file');
// 重定向
ctx.redirect('/new-url');
在大型项目中,我建议统一响应格式:
- 成功响应:
- 错误响应:
- 分页数据:{ code: 0, data: [], pagination: { ... } }
3. 定时任务控制器深度解析
3.1 定时任务的应用场景
定时任务是后台系统的常见需求,Egg.js内置了强大的定时任务功能。根据我的经验,这些场景特别适合使用定时任务:
- 数据统计(每日/每周/每月)
- 缓存刷新
- 状态检查(订单超时等)
- 日志清理
- 报表生成
3.2 定时任务配置详解
Egg.js的定时任务支持两种模式:
| 模式 | 配置 | 特点 | 适用场景 |
|---|---|---|---|
| worker | type: 'worker' | 集群中只有一个worker执行 | 全局任务(如订单超时) |
| all | type: 'all' | 每个worker都会执行 | 进程级任务(如缓存清理) |
一个完整的定时任务配置示例:
javascript复制// app/schedule/clear_log.js
module.exports = {
type: 'worker', // 只有一个worker执行
cron: '0 0 3 * * *', // 每天凌晨3点执行
immediate: true, // 应用启动后立即执行一次
async task(ctx) {
const keepDays = 7;
const before = new Date(Date.now() - keepDays * 86400 * 1000);
await ctx.service.log.clear(before);
}
};
3.3 Cron表达式详解
Cron表达式是定时任务的核心,Egg.js支持6位格式(秒 分 时 日 月 周):
| 字段 | 允许值 | 特殊字符 |
|---|---|---|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 时 | 0-23 | , - * / |
| 日 | 1-31 | , - * / L W |
| 月 | 1-12 | , - * / |
| 周 | 0-7 | , - * / L # |
常用表达式示例:
0 0 3 * * *每天3点0 30 10 * * 1-5工作日10:30*/10 * * * * *每10秒
避坑指南:线上环境避免使用太短的间隔(如每秒),这可能导致任务堆积。我建议最小间隔不低于1分钟。
4. 控制器最佳实践与性能优化
4.1 控制器的分层设计
在复杂项目中,我推荐将控制器分为三层:
- 接入层:处理HTTP协议相关(参数解析、响应格式化)
- 逻辑层:处理业务流程(调用多个Service)
- 适配层:对接不同客户端(Web/App/第三方)
这种分层使代码更易维护,也便于单元测试。
4.2 性能优化技巧
- 精简控制器逻辑:控制器应该薄,复杂逻辑放到Service
- 合理使用缓存:频繁读取的数据考虑缓存
- 批量处理请求:多个操作尽量合并
- 异步非阻塞:耗时操作使用async/await
javascript复制// 优化后的控制器示例
class OptimizedController extends Controller {
async list() {
const { ctx } = this;
// 1. 尝试从缓存读取
const cacheKey = `user_list_${ctx.query.page}`;
let result = await ctx.app.redis.get(cacheKey);
if (!result) {
// 2. 缓存未命中,查询数据库
result = await ctx.service.user.list(ctx.query);
// 3. 设置缓存,10分钟过期
await ctx.app.redis.set(cacheKey, result, 'EX', 600);
}
// 4. 返回响应
ctx.body = {
code: 0,
data: result,
fromCache: !!result
};
}
}
4.3 错误处理的艺术
良好的错误处理能显著提升系统稳定性:
- 参数校验错误:返回400状态码
- 权限错误:返回403
- 业务错误:返回200,但code不为0
- 系统错误:返回500
我通常会在项目中定义一个错误码枚举:
javascript复制// app/enums/error.js
module.exports = {
PARAM_INVALID: { code: 4001, message: '参数无效' },
USER_NOT_FOUND: { code: 4004, message: '用户不存在' },
// ...
};
然后在控制器中统一使用:
javascript复制async show() {
const { ctx } = this;
const user = await ctx.service.user.find(ctx.params.id);
if (!user) {
ctx.throw(404, ctx.enums.error.USER_NOT_FOUND);
}
ctx.body = { code: 0, data: user };
}
5. 实战中的疑难问题解析
5.1 跨域问题解决方案
前端分离架构下,跨域是常见问题。Egg.js提供了多种解决方案:
-
cors插件(简单场景)
javascript复制// config/plugin.js exports.cors = { enable: true, package: 'egg-cors' }; // config/config.default.js exports.cors = { origin: 'https://yourdomain.com', allowMethods: 'GET,POST,PUT,DELETE' }; -
代理方案(复杂场景)
- 开发环境:webpack-dev-server代理
- 生产环境:Nginx反向代理
-
JSONP(老旧系统兼容)
5.2 文件上传的最佳实践
文件上传是控制器的常见需求,需要注意:
- 使用
ctx.getFileStream()获取文件流 - 限制文件类型和大小
- 使用随机文件名防止冲突
- 考虑使用OSS等云存储
javascript复制async upload() {
const { ctx } = this;
const stream = await ctx.getFileStream();
const filename = `${Date.now()}${path.extname(stream.filename)}`;
// 保存到本地
const writer = fs.createWriteStream(path.join(this.config.uploadDir, filename));
stream.pipe(writer);
ctx.body = {
code: 0,
data: {
url: `/uploads/${filename}`
}
};
}
5.3 性能监控与调优
对于高性能要求的应用,我通常会添加监控:
-
响应时间监控
javascript复制// app/middleware/response_time.js module.exports = () => { return async function(ctx, next) { const start = Date.now(); await next(); const cost = Date.now() - start; ctx.set('X-Response-Time', `${cost}ms`); }; }; -
慢请求日志
javascript复制// 在控制器中记录慢请求 if (cost > 500) { ctx.logger.warn(`Slow API: ${ctx.path}, cost ${cost}ms`); } -
性能分析
- 使用egg-development插件
- 使用alinode进行线上诊断
6. 从控制器看Egg.js设计哲学
通过控制器的设计,我们可以看出Egg.js的几个核心理念:
- 约定优于配置:目录结构、命名规范都有明确约定
- 插件化架构:核心功能通过插件实现,可按需组合
- 渐进式开发:简单场景简单写,复杂需求有扩展方案
- 企业级思维:内置了日志、监控、安全等企业级特性
在实际项目中,我建议遵循这些原则:
- 先使用内置功能,再考虑自定义
- 保持代码风格一致
- 合理使用插件机制
- 重视日志和监控
掌握好控制器,就掌握了Egg.js开发的核心模式。希望这些经验能帮助你在实际项目中游刃有余。记住,好的控制器设计应该是简单、清晰、易于维护的。当你的控制器变得复杂时,就是时候考虑重构了。