Egg.js企业级应用开发踩坑总结与最佳实践

设计师景荣 前端 阅读 2,027
赞 17 收藏
二维码
手机扫码查看
反馈

今天被Egg.js的中间件顺序坑惨了

又是被Egg.js折磨的一天。今天在开发一个用户认证相关的功能时,突然发现session总是拿不到数据,折腾了快3个小时才搞明白问题所在。

Egg.js企业级应用开发踩坑总结与最佳实践

先说结论吧: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精确控制”这样的原则来配置。

以上是我踩坑后的总结,如果你也遇到过类似的问题,希望这篇文章能帮到你。有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论