Egg 2.x 升級(jí)指南

2020-02-06 10:54 更新

背景

隨著 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)單包裝下。

注意

  • toAsyncFunction 和 toPromise 實(shí)際使用的是 co 包裝,因此會(huì)帶回對(duì)應(yīng)的性能損失和堆棧問(wèn)題,建議開(kāi)發(fā)者還是盡量全鏈路升級(jí)。
  • toAsyncFunction 在調(diào)用 async function 時(shí)不會(huì)有損失。

@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 包裝一層。

  • 在 2.x 里為了更好的性能和錯(cuò)誤堆棧,我們建議修改為 async-first。
  • 如有需要,使用 toAsyncFunction 和 toPromise 來(lái)兼容。

譬如 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 文檔


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)