背景
隨著 Node.js 8 LTS 的發(fā)布, 內(nèi)建了對(duì) ES2017 Async Function 的支持。
在這之前,TJ 的 co 使我們可以提前享受到 async/await 的編程體驗(yàn),但同時(shí)它不可避免的也帶來(lái)一些問(wèn)題:
現(xiàn)在 Egg 正式發(fā)布了 2.x 版本:
- 保持了對(duì) Egg 1.x 以及 generator function 的完全兼容。
- 基于 Koa 2.x,異步解決方案基于 async function。
- 只支持 Node.js 8 及以上版本。
- 去除 co 后堆棧信息更清晰,帶來(lái) 30% 左右的性能提升(不含 Node 帶來(lái)的性能提升),詳細(xì)參見(jiàn):benchmark。
Egg 的理念之一是漸進(jìn)式增強(qiáng),故我們?yōu)殚_(kāi)發(fā)者提供漸進(jìn)升級(jí)的體驗(yàn)。
快速升級(jí)
- Node.js 使用最新的 LTS 版本(>=8.9.0)。
- 修改 package.json 中 egg 的依賴為 ^2.0.0。
- 檢查相關(guān)插件是否發(fā)布新版本(可選)。
- 重新安裝依賴,跑單元測(cè)試。
搞定!幾乎不需要修改任何一行代碼,就已經(jīng)完成了升級(jí)。
插件變更說(shuō)明
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 定義,因?yàn)?koa-roles 已經(jīng)無(wú)法兼容了。 請(qǐng)求上下文 Context 從 this 傳入改成了第一個(gè)參數(shù) ctx 傳入,原有的 scope 變成了第二個(gè)參數(shù)。
// 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; });
|
進(jìn)一步升級(jí)
得益于 Egg 對(duì) 1.x 的完全兼容,我們可以如何非??焖俚耐瓿缮?jí)。
不過(guò),為了更好的統(tǒng)一代碼風(fēng)格,以及更佳的性能和錯(cuò)誤堆棧,我們建議開(kāi)發(fā)者進(jìn)一步升級(jí):
中間件使用 Koa2 風(fēng)格
2.x 仍然保持對(duì) 1.x 風(fēng)格的中間件的兼容,故不修改也能繼續(xù)使用。
- 返回的函數(shù)入?yún)⒏臑?Koa 2 的 (ctx, next) 風(fēng)格。第一個(gè)參數(shù)為 ctx,代表當(dāng)前請(qǐng)求的上下文,是 Context 的實(shí)例。第二個(gè)參數(shù)為 next,用 await 執(zhí)行它來(lái)執(zhí)行后續(xù)中間件的邏輯。
- 不建議使用 async (ctx, next) => {} 格式,避免錯(cuò)誤堆棧丟失函數(shù)名。
- yield next 改為函數(shù)調(diào)用 await next() 的方式。
// 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 格式的中間件不同,此時(shí) next 是一個(gè)方法,必須要調(diào)用它 await next(); const delta = Math.ceil(Date.now() - start); ctx.set('X-Response-Time', delta + 'ms'); }; };
|
yieldable to awaitable
我們?cè)缭?Egg 1.x 時(shí)就已經(jīng)支持 async,故若應(yīng)用層已經(jīng)是 async-base 的,就可以跳過(guò)本小節(jié)內(nèi)容了。
co 支持了 yieldable 兼容類型:
- promises
- array (parallel execution)
- objects (parallel execution)
- thunks (functions)
- generators (delegation)
- generator functions (delegation)
盡管 generator 和 async 兩者的編程模型基本一模一樣,但由于上述的 co 的一些特殊處理,導(dǎo)致在移除 co 后,我們需要根據(jù)不同場(chǎng)景自行處理:
promise
直接替換即可:
function echo(msg) { return Promise.resolve(msg); }
yield echo('hi egg'); // change to await echo('hi egg');
|
array - yield []
yield [] 常用于并發(fā)請(qǐng)求,如:
const [ news, user ] = yield [ ctx.service.news.list(topic), ctx.service.user.get(uid), ];
|
這種修改起來(lái)比較簡(jiǎn)單,用 Promise.all() 包裝下即可:
const [ news, user ] = await Promise.all([ ctx.service.news.list(topic), ctx.service.user.get(uid), ]);
|
object - yield {}
yield {} 和 yield map 的方式也常用于并發(fā)請(qǐng)求,但由于 Promise.all 不支持 Object,會(huì)稍微有點(diǎn)復(fù)雜。
// 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);
|
如果無(wú)法修改對(duì)應(yīng)的接口,可以臨時(shí)兼容下:
- 使用我們提供的 Utils 方法 app.toPromise。
- 建議盡量改掉,因?yàn)閷?shí)際上就是丟給 co,會(huì)帶回對(duì)應(yīng)的性能損失和堆棧問(wèn)題。
const { news, user } = await app.toPromise(ctx.service.biz.list(topic, uid));
|
其他
- thunks (functions)
- generators (delegation)
- generator functions (delegation)
修改為對(duì)應(yīng)的 async function 即可,如果不能修改,則可以用 app.toAsyncFunction 簡(jiǎn)單包裝下。
注意
@sindresorhus 編寫(xiě)了許多基于 promise 的 helper 方法,靈活的運(yùn)用它們配合 async function 能讓代碼更加具有可讀性。
插件升級(jí)
應(yīng)用開(kāi)發(fā)者只需升級(jí)插件開(kāi)發(fā)者修改后的依賴版本即可,也可以用我們提供的命令 egg-bin autod 快速更新。
以下內(nèi)容針對(duì)插件開(kāi)發(fā)者,指導(dǎo)如何升級(jí)插件:
升級(jí)事項(xiàng)
- 完成上面章節(jié)提到的升級(jí)項(xiàng)。所有的 generator function 改為 async function 格式。升級(jí)中間件風(fēng)格。
- 接口兼容(可選),如下。
- 發(fā)布大版本。
接口兼容
某些場(chǎng)景下,插件開(kāi)發(fā)者提供給應(yīng)用開(kāi)發(fā)者的接口是同時(shí)支持 generator 和 async 的,一般是會(huì)用 co 包裝一層。
譬如 egg-schedule 插件,支持應(yīng)用層使用 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(); };
|
插件開(kāi)發(fā)者可以簡(jiǎn)單包裝下原始函數(shù):
// https://github.com/eggjs/egg-schedule/blob/80252ef/lib/load_schedule.js#L38 task = app.toAsyncFunction(schedule.task);
|
插件發(fā)布規(guī)則
- 需要發(fā)布大版本除非插件提供的接口都是 promise 的,且代碼里面不存在 async,如 egg-view-nunjucks。
- 修改 package.json修改 devDependencies 依賴的 egg 為 ^2.0.0。修改 engines.node 為 >=8.0.0。修改 ci.version 為 8, 9, 并重新安裝依賴以便生成新的 travis 配置文件。
- 修改 README.md 的示例為 async function。
- 編寫(xiě)升級(jí)指引。
- 修改 test/fixtures 為 async function,可選,建議分開(kāi)另一個(gè) PR 方便 Review。
一般還會(huì)需要繼續(xù)維護(hù)上一個(gè)版本,故需要:
- 對(duì)上一個(gè)版本建立一個(gè) 1.x 這類的 branch 分支
- 修改上一個(gè)版本的 package.json 的 publishConfig.tag 為 release-1.x
- 這樣如果上一個(gè)版本有 BugFix 時(shí),npm 版本時(shí)就會(huì)發(fā)布為 release-1.x 這個(gè) tag,用戶通過(guò) npm i egg-xx@release-1.x 來(lái)引入舊版本。
- 參見(jiàn) npm 文檔。
更多建議: