• Egg@2 升级指南
  • 背景
  • 快速升级
  • 插件变更说明
    • egg-multipart
    • egg-userrole
  • 进一步升级
    • 中间件使用 Koa2 风格
    • yieldable to awaitable
      • promise
      • array - yield []
      • object - yield {}
      • 其他
  • 插件升级
    • 升级事项
    • 接口兼容
    • 插件发布规则

    Egg@2 升级指南

    背景

    随着 Node.js 8 LTS 的发布, 内建了对 ES2017 Async Function 的支持。

    在这之前,TJ 的 co 使我们可以提前享受到 async/await 的编程体验,但同时它不可避免的也带来一些问题:

    • 性能损失
    • 错误堆栈不友好

    现在 Egg 正式发布了 2.x 版本:

    • 保持了对 Egg 1.x 以及 generator function完全兼容
    • 基于 Koa 2.x,异步解决方案基于 async function
    • 只支持 Node.js 8 及以上版本。
    • 去除 co 后堆栈信息更清晰,带来 30% 左右的性能提升(不含 Node 带来的性能提升),详细参见:benchmark。

    Egg 的理念之一是渐进式增强,故我们为开发者提供渐进升级的体验。

    • 快速升级
    • 插件变更说明
    • 进一步升级
    • 针对插件开发者的升级指南

    快速升级

    • Node.js 使用最新的 LTS 版本(>=8.9.0)。
    • 修改 package.jsonegg 的依赖为 ^2.0.0
    • 检查相关插件是否发布新版本(可选)。
    • 重新安装依赖,跑单元测试。

    搞定!几乎不需要修改任何一行代码,就已经完成了升级。

    插件变更说明

    egg-multipart

    yield parts 需修改为 await parts()yield parts()

    1. // old
    2. const parts = ctx.multipart();
    3. while ((part = yield parts) != null) {
    4. // do something
    5. }
    6. // yield parts() also work
    7. while ((part = yield parts()) != null) {
    8. // do something
    9. }
    10. // new
    11. const parts = ctx.multipart();
    12. while ((part = await parts()) != null) {
    13. // do something
    14. }
    • egg-multipart#upload-multiple-files

    egg-userrole

    不再兼容 1.x 形式的 role 定义,因为 koa-roles 已经无法兼容了。请求上下文 Context 从 this 传入改成了第一个参数 ctx 传入,原有的 scope 变成了第二个参数。

    1. // old
    2. app.role.use('user', function() {
    3. return !!this.user;
    4. });
    5. // new
    6. app.role.use((ctx, scope) => {
    7. return !!ctx.user
    8. });
    9. app.role.use('user', ctx => {
    10. return !!ctx.user;
    11. });
    • koajs/koa-roles#13
    • eggjs/egg-userrole#9

    进一步升级

    得益于 Egg 对 1.x 的完全兼容,我们可以如何非常快速的完成升级。

    不过,为了更好的统一代码风格,以及更佳的性能和错误堆栈,我们建议开发者进一步升级:

    • 修改为推荐的代码风格,传送门:代码风格指南
    • 中间件使用 Koa2 风格
    • 函数调用的 yieldable 转为 awaitable

    中间件使用 Koa2 风格

    2.x 仍然保持对 1.x 风格的中间件的兼容,故不修改也能继续使用。

    • 返回的函数入参改为 Koa 2 的 (ctx, next) 风格。
      • 第一个参数为 ctx,代表当前请求的上下文,是 Context 的实例。
      • 第二个参数为 next,用 await 执行它来执行后续中间件的逻辑。
    • 不建议使用 async (ctx, next) => {} 格式,避免错误堆栈丢失函数名。
    • yield next 改为函数调用 await next() 的方式。
    1. // 1.x
    2. module.exports = () => {
    3. return function* responseTime(next) {
    4. const start = Date.now();
    5. yield next;
    6. const delta = Math.ceil(Date.now() - start);
    7. this.set('X-Response-Time', delta + 'ms');
    8. };
    9. };
    10. // 2.x
    11. module.exports = () => {
    12. return async function responseTime(ctx, next) {
    13. const start = Date.now();
    14. // 注意,和 generator function 格式的中间件不同,此时 next 是一个方法,必须要调用它
    15. await next();
    16. const delta = Math.ceil(Date.now() - start);
    17. ctx.set('X-Response-Time', delta + 'ms');
    18. };
    19. };

    yieldable to awaitable

    我们早在 Egg 1.x 时就已经支持 async,故若应用层已经是 async-base 的,就可以跳过本小节内容了。

    co 支持了 yieldable 兼容类型:

    • promises
    • array (parallel execution)
    • objects (parallel execution)
    • thunks (functions)
    • generators (delegation)
    • generator functions (delegation)

    尽管 generatorasync 两者的编程模型基本一模一样,但由于上述的 co 的一些特殊处理,导致在移除 co 后,我们需要根据不同场景自行处理:

    promise

    直接替换即可:

    1. function echo(msg) {
    2. return Promise.resolve(msg);
    3. }
    4. yield echo('hi egg');
    5. // change to
    6. await echo('hi egg');

    array - yield []

    yield [] 常用于并发请求,如:

    1. const [ news, user ] = yield [
    2. ctx.service.news.list(topic),
    3. ctx.service.user.get(uid),
    4. ];

    这种修改起来比较简单,用 Promise.all() 包装下即可:

    1. const [ news, user ] = await Promise.all([
    2. ctx.service.news.list(topic),
    3. ctx.service.user.get(uid),
    4. ]);

    object - yield {}

    yield {}yield map 的方式也常用于并发请求,但由于 Promise.all 不支持 Object,会稍微有点复杂。

    1. // app/service/biz.js
    2. class BizService extends Service {
    3. * list(topic, uid) {
    4. return {
    5. news: ctx.service.news.list(topic),
    6. user: ctx.service.user.get(uid),
    7. };
    8. }
    9. }
    10. // app/controller/home.js
    11. const { news, user } = yield ctx.service.biz.list(topic, uid);

    建议修改为 await Promise.all([]) 的方式:

    1. // app/service/biz.js
    2. class BizService extends Service {
    3. list(topic, uid) {
    4. return Promise.all([
    5. ctx.service.news.list(topic),
    6. ctx.service.user.get(uid),
    7. ]);
    8. }
    9. }
    10. // app/controller/home.js
    11. const [ news, user ] = await ctx.service.biz.list(topic, uid);

    如果无法修改对应的接口,可以临时兼容下:

    • 使用我们提供的 Utils 方法 app.toPromise。
    • 建议尽量改掉,因为实际上就是丢给 co,会带回对应的性能损失和堆栈问题。
    1. const { news, user } = await app.toPromise(ctx.service.biz.list(topic, uid));

    其他

    • thunks (functions)
    • generators (delegation)
    • generator functions (delegation)

    修改为对应的 async function 即可,如果不能修改,则可以用 app.toAsyncFunction 简单包装下。

    注意

    • toAsyncFunction 和 toPromise 实际使用的是 co 包装,因此会带回对应的性能损失和堆栈问题,建议开发者还是尽量全链路升级。
    • toAsyncFunction 在调用 async function 时不会有损失。

    @sindresorhus 编写了许多基于 promise 的 helper 方法,灵活的运用它们配合 async function 能让代码更加具有可读性。

    插件升级

    应用开发者只需升级插件开发者修改后的依赖版本即可,也可以用我们提供的命令 egg-bin autod 快速更新。

    以下内容针对插件开发者,指导如何升级插件:

    升级事项

    • 完成上面章节提到的升级项。
      • 所有的 generator function 改为 async function 格式。
      • 升级中间件风格。
    • 接口兼容(可选),如下。
    • 发布大版本。

    接口兼容

    某些场景下,插件开发者提供给应用开发者的接口是同时支持 generator 和 async 的,一般是会用 co 包装一层。

    • 在 2.x 里为了更好的性能和错误堆栈,我们建议修改为 async-first
    • 如有需要,使用 toAsyncFunction 和 toPromise 来兼容。

    譬如 egg-schedule 插件,支持应用层使用 generator 或 async 定义 task。

    1. // {app_root}/app/schedule/cleandb.js
    2. exports.task = function* (ctx) {
    3. yield ctx.service.db.clean();
    4. };
    5. // {app_root}/app/schedule/log.js
    6. exports.task = async function splitLog(ctx) {
    7. await ctx.service.log.split();
    8. };

    插件开发者可以简单包装下原始函数:

    1. // https://github.com/eggjs/egg-schedule/blob/80252ef/lib/load_schedule.js#L38
    2. task = app.toAsyncFunction(schedule.task);

    插件发布规则

    • 需要发布大版本
      • 除非插件提供的接口都是 promise 的,且代码里面不存在 async,如 egg-view-nunjucks。
    • 修改 package.json
      • 修改 devDependencies 依赖的 egg^2.0.0
      • 修改 engines.node>=8.0.0
      • 修改 ci.version8, 9, 并重新安装依赖以便生成新的 travis 配置文件。
    • 修改 README.md 的示例为 async function。
    • 编写升级指引。
    • 修改 test/fixtures 为 async function,可选,建议分开另一个 PR 方便 Review。

    一般还会需要继续维护上一个版本,故需要:

    • 对上一个版本建立一个 1.x 这类的 branch 分支
    • 修改上一个版本的 package.jsonpublishConfig.tagrelease-1.x
    • 这样如果上一个版本有 BugFix 时,npm 版本时就会发布为 release-1.x 这个 tag,用户通过 npm i egg-xx@release-1.x 来引入旧版本。
    • 参见 npm 文档。