Egg.js企业级应用开发踩坑总结与最佳实践
今天被Egg.js的中间件顺序坑惨了
又是被Egg.js折磨的一天。今天在开发一个用户认证相关的功能时,突然发现session总是拿不到数据,折腾了快3个小时才搞明白问题所在。
先说结论吧:Egg.js的中间件执行顺序真的太重要了,顺序不对各种奇怪的问题都会出现。特别是当你需要在中间件里获取session的时候,如果放在某些中间件前面,session还没初始化,那你就等着debug到头秃吧。
发现问题的过程就是一言难尽
当时我的middleware配置是这样的:
// config/config.default.js
module.exports = {
middleware: [
'auth', // 自定义认证中间件
'cors',
'bodyParser',
],
};
然后在auth中间件里面想获取用户的session信息:
// app/middleware/auth.js
module.exports = () => {
return async function auth(ctx, next) {
const user = ctx.session.user;
console.log('session user:', user); // 这里始终打印 undefined
if (!user) {
ctx.status = 401;
ctx.body = { message: '请先登录' };
return;
}
await next();
};
};
刚开始我还以为是session配置有问题,各种排查session的key、cookie设置啥的。结果折腾了半天发现,session根本就没初始化!
终于找到问题根源
后来仔细看了下Egg.js的源码和文档,发现session的初始化也是通过中间件完成的,而且是在coreMiddleware里的。当我把自定义的auth中间件放到前面时,session中间件还没执行,所以ctx.session对象虽然存在,但是里面的数据都是空的。
这里我踩了个坑,就是没有仔细看Egg.js内置中间件的执行顺序。Egg.js的中间件执行顺序是这样的:
- app.middleware (应用层自定义中间件)
- plugin.middleware (插件中间件)
- Egg内置coreMiddleware
而session相关的中间件就在coreMiddleware里,所以如果你的应用层中间件要依赖session,那就得想办法调整顺序。
三种解决方案,最后选择了最稳妥的
折腾了半天发现有几种解决办法:
方案一:修改配置顺序
最直接的办法就是把需要session的中间件往后移,但问题是有些中间件又必须在前面执行,比如一些日志收集、请求预处理等。
方案二:在中间件内部延迟执行
这个方案比较tricky,就是在中间件里等待session初始化完成,但感觉不够优雅,也不稳定。
方案三:使用match和ignore进行精细化控制(推荐)
这是我现在采用的方案,也是相对稳妥的做法。只对需要认证的路由应用这个中间件:
// config/config.default.js
module.exports = {
middleware: [
'cors',
'bodyParser',
'auth',
],
auth: {
match: ['/api/user', '/api/profile'], // 只对这些路径生效
ignore: ['/api/login', '/api/register'] // 登录注册不用验证
}
};
// app/middleware/auth.js
module.exports = (options, app) => {
return async function auth(ctx, next) {
// 这时候session肯定已经初始化了
const user = ctx.session.user;
if (!user) {
ctx.status = 401;
ctx.body = { message: '请先登录' };
return;
}
// 把用户信息挂到context上,方便后面的controller使用
ctx.currentUser = user;
await next();
};
};
还有个小坑需要注意
还有一个细节,就是session的配置也很重要。我当时还遇到一个问题,就是本地环境正常,部署到测试环境就session丢失。查了好久才发现是domain配置的问题:
// config/config.default.js
exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 24小时
httpOnly: true,
encrypt: true,
// domain配置要根据具体环境调整
domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : '',
};
本地开发一般不用配domain,但在跨域或者子域名访问的情况下,domain配置错误就会导致session无法正常存储。
调试中间件顺序的小技巧
为了更好地排查中间件顺序问题,我加了一些调试代码:
// app/middleware/debug.js
module.exports = () => {
return async function debug(ctx, next) {
console.log('=== 中间件执行顺序调试 ===');
console.log('当前路径:', ctx.path);
console.log('Session状态:', !!ctx.session);
console.log('Session数据:', ctx.session?.user ? '有数据' : '无数据');
await next();
};
};
把这个中间件放在不同位置,就能清楚地看到各个中间件执行时的session状态了。虽然这种方法有点原始,但在调试复杂中间件流程时还是挺有用的。
生产环境踩坑经验
上线到生产环境后又发现一个小问题。由于我们用了nginx做负载均衡,多个实例之间session共享是个问题。开始用了内存存储,结果用户请求到不同实例时session就丢了。
后来改为redis存储:
// config/plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
// config/config.default.js
exports.redis = {
client: {
port: 6379,
host: '127.0.0.1',
password: '',
db: '0',
},
};
exports.session = {
store: app => {
return {
get: async (key) => {
const res = await app.redis.get(key);
return res ? JSON.parse(res) : null;
},
set: async (key, value, maxAge) => {
await app.redis.setex(key, maxAge / 1000, JSON.stringify(value));
},
del: async (key) => {
await app.redis.del(key);
}
};
}
};
这部分配置花了不少时间调试,主要是redis连接池和session序列化的问题。
总的来说,这次踩坑让我对Egg.js的中间件机制理解更深了。虽然官方文档写了中间件执行顺序,但实际开发中还是会遇到各种意想不到的问题。现在我基本都按照”先通用中间件,再业务中间件,最后用match精确控制”这样的原则来配置。
以上是我踩坑后的总结,如果你也遇到过类似的问题,希望这篇文章能帮到你。有更好的方案欢迎评论区交流。

暂无评论