Egg@2 升级指南

# 背景

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

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

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

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

# 快速升级

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

# 插件变更说明

# egg-multipart

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

// old
const parts = ctx.multipart();
while ((part = yield parts) != null) {
// do something
}

// yield parts() also work
while ((part = yield parts()) != null) {
// do something
}

// new
const parts = ctx.multipart();
while ((part = await parts()) != null) {
// do something
}

# egg-userrole

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

// old
app.role.use('user', function() {
return !!this.user;
});

// new
app.role.use((ctx, scope) => {
return !!ctx.user
});

app.role.use('user', ctx => {
return !!ctx.user;
});

# 进一步升级

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

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

# 中间件使用 Koa2 风格

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

// 1.x
module.exports = () => {
return function* responseTime(next) {
const start = Date.now();
yield next;
const delta = Math.ceil(Date.now() - start);
this.set('X-Response-Time', delta + 'ms');
};
};

// 2.x
module.exports = () => {
return async function responseTime(ctx, next) {
const start = Date.now();
// 注意,和 generator function 格式的中间件不同,此时 next 是一个方法,必须要调用它
await next();
const delta = Math.ceil(Date.now() - start);
ctx.set('X-Response-Time', delta + 'ms');
};
};

# yieldable to awaitable

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

co 支持了 yieldable 兼容类型:

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

# promise

直接替换即可:

function echo(msg) {
return Promise.resolve(msg);
}

yield echo('hi egg');
// change to
await echo('hi egg');

# array - yield []

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

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

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

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

# object - yield {}

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

// app/service/biz.js
class BizService extends Service {
* list(topic, uid) {
return {
news: ctx.service.news.list(topic),
user: ctx.service.user.get(uid),
};
}
}

// app/controller/home.js
const { news, user } = yield ctx.service.biz.list(topic, uid);

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

// app/service/biz.js
class BizService extends Service {
list(topic, uid) {
return Promise.all([
ctx.service.news.list(topic),
ctx.service.user.get(uid),
]);
}
}

// app/controller/home.js
const [ news, user ] = await ctx.service.biz.list(topic, uid);

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

const { news, user } = await app.toPromise(ctx.service.biz.list(topic, uid));

# 其他

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

注意

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

# 插件升级

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

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

# 升级事项

# 接口兼容

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

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

// {app_root}/app/schedule/cleandb.js
exports.task = function* (ctx) {
yield ctx.service.db.clean();
};

// {app_root}/app/schedule/log.js
exports.task = async function splitLog(ctx) {
await ctx.service.log.split();
};

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

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

# 插件发布规则

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