- 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.json中egg的依赖为^2.0.0。 - 检查相关插件是否发布新版本(可选)。
- 重新安装依赖,跑单元测试。
搞定!几乎不需要修改任何一行代码,就已经完成了升级。
插件变更说明
egg-multipart
yield parts 需修改为 await parts() 或 yield parts()
// oldconst parts = ctx.multipart();while ((part = yield parts) != null) {// do something}// yield parts() also workwhile ((part = yield parts()) != null) {// do something}// newconst parts = ctx.multipart();while ((part = await parts()) != null) {// do something}
- egg-multipart#upload-multiple-files
egg-userrole
不再兼容 1.x 形式的 role 定义,因为 koa-roles 已经无法兼容了。请求上下文 Context 从 this 传入改成了第一个参数 ctx 传入,原有的 scope 变成了第二个参数。
// oldapp.role.use('user', function() {return !!this.user;});// newapp.role.use((ctx, scope) => {return !!ctx.user});app.role.use('user', ctx => {return !!ctx.user;});
- 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.xmodule.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.xmodule.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 兼容类型:
- promises
- array (parallel execution)
- objects (parallel execution)
- thunks (functions)
- generators (delegation)
- generator functions (delegation)
尽管 generator 和 async 两者的编程模型基本一模一样,但由于上述的 co 的一些特殊处理,导致在移除 co 后,我们需要根据不同场景自行处理:
promise
直接替换即可:
function echo(msg) {return Promise.resolve(msg);}yield echo('hi egg');// change toawait 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.jsclass BizService extends Service {* list(topic, uid) {return {news: ctx.service.news.list(topic),user: ctx.service.user.get(uid),};}}// app/controller/home.jsconst { news, user } = yield ctx.service.biz.list(topic, uid);
建议修改为 await Promise.all([]) 的方式:
// app/service/biz.jsclass BizService extends Service {list(topic, uid) {return Promise.all([ctx.service.news.list(topic),ctx.service.user.get(uid),]);}}// app/controller/home.jsconst [ news, user ] = await ctx.service.biz.list(topic, uid);
如果无法修改对应的接口,可以临时兼容下:
- 使用我们提供的 Utils 方法 app.toPromise。
- 建议尽量改掉,因为实际上就是丢给 co,会带回对应的性能损失和堆栈问题。
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。
// {app_root}/app/schedule/cleandb.jsexports.task = function* (ctx) {yield ctx.service.db.clean();};// {app_root}/app/schedule/log.jsexports.task = async function splitLog(ctx) {await ctx.service.log.split();};
插件开发者可以简单包装下原始函数:
// https://github.com/eggjs/egg-schedule/blob/80252ef/lib/load_schedule.js#L38task = app.toAsyncFunction(schedule.task);
插件发布规则
- 需要发布大版本
- 除非插件提供的接口都是 promise 的,且代码里面不存在
async,如 egg-view-nunjucks。
- 除非插件提供的接口都是 promise 的,且代码里面不存在
- 修改
package.json- 修改
devDependencies依赖的egg为^2.0.0。 - 修改
engines.node为>=8.0.0。 - 修改
ci.version为8, 9, 并重新安装依赖以便生成新的 travis 配置文件。
- 修改
- 修改
README.md的示例为 async function。 - 编写升级指引。
- 修改
test/fixtures为 async function,可选,建议分开另一个 PR 方便 Review。
一般还会需要继续维护上一个版本,故需要:
- 对上一个版本建立一个
1.x这类的 branch 分支 - 修改上一个版本的
package.json的publishConfig.tag为release-1.x - 这样如果上一个版本有 BugFix 时,npm 版本时就会发布为
release-1.x这个 tag,用户通过npm i egg-xx@release-1.x来引入旧版本。 - 参见 npm 文档。
