Egg@2 Upgrade guideline
Background
With the official release of Node.js 8 LTS, egg now comes with built-in ES2017 Async Function support.
Though the TJ co has brought async/await
programming experience before this, but it also has some inevitable problems:
- performance lost
- obscure error logs
In the official Egg 2.x:
- Full compatibility to Egg 1.x and
generator function
. - Koa 2.x based
async function
solutions. - Only support Node.js 8 and above.
- Better error stack messages without co, approximately 30% performance improvement (do not include the performance improvement brought by Node), see benchmark for more details.
One of the Egg's concept is progressive
, hence we provide progressive programming experiences to developers.
Quick upgrade
- Use the latest Node LTS version (
>=8.9.0
). - Change
egg
version to^2.0.0
inpackage.json
. - Check if included plugins are the latest version (optional).
- Reinstall the dependencies, and run unit tests again.
Done! Barely with any code changes
Plugin update
egg-multipart
yield parts
needs to change to await parts()
or 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
DO NOT support 1.x role definition, because koa-roles is no longer compatible. The Context
has changed from this
to the first argument ctx
, the original scope
now is the second argument.
// 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;
});
Further upgrade
Due to the complete compatibility to Egg 1.x, we can finish the upgrade quickly.
But in order to keep the coding style consistent, as well as a better performance improvement and more developer-friendly error stack logs, we suggest developers to make a further upgrade:
- Use recommended code style, see Style guide
- Use Koa style middleware
- Change
yieldable
toawaitable
in function invoke
Use Koa2-styled middleware
2.x is compatible to 1.x-styled middleware, so it's still functional without any changes.
- Use Koa 2's
(ctx, next)
arguments style in callback function- The 1st argument is
ctx
, means context, it is an instance of Context - The 2nd argument is
next
, use await to execute it for the coming logics.
- The 1st argument is
- Using
async (ctx, next) => {}
is not recommended, which prevents anonymous function in error stack. - Change
yield next
toawait 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();
// Note, differ from the generator function middleware, next is a function, we're executing it here
await next();
const delta = Math.ceil(Date.now() - start);
ctx.set('X-Response-Time', delta + 'ms');
};
};
yieldable to awaitable
async was supported in Egg 1.x, thus if the middleware is already async-base, we could skip this section.
co supports yieldable
compatibility types:
- promises
- array (parallel execution)
- objects (parallel execution)
- thunks (functions)
- generators (delegation)
- generator functions (delegation)
Despite both generator
and async
have the same program models, but we may still need to refactor our codes correspondingly after removing co
because of the above special handling from co
.
promise
We can replace it directly:
function echo(msg) {
return Promise.resolve(msg);
}
yield echo('hi egg');
// change to
await echo('hi egg');
array - yield []
yeild []
is normally used to send concurrent requests, such as:
const [ news, user ] = yield [
ctx.service.news.list(topic),
ctx.service.user.get(uid),
];
In this case, use Promise.all()
to wrap it:
const [news, user] = await Promise.all([
ctx.service.news.list(topic),
ctx.service.user.get(uid),
]);
object - yield {}
Sometimes yield {}
and yield map
can also be used to send concurrent requests, but it may be a bit complex in this place because Promise.all
doesn't support Object argument.
// 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);
It's recommended to use 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);
If the interfaces are unchangeable, e can do things below as a workaround:
- Use app.toPromise method provided by our Utils.
- This is built on top of the co, so it may still cause performance issue and returning inaccurate error stacks, so this is not recommended.
const { news, user } = await app.toPromise(ctx.service.biz.list(topic, uid));
Others
- thunks (functions)
- generators (delegation)
- generator functions (delegation)
Use async function
to replace the above functions, or use app.toAsyncFunction alternatively.
Note
- toAsyncFunction and toPromise are wrappers of co, thus it may cause performance lost and error stack problems. So we're recommending developers to use all-chain upgrade.
- toAsyncFunction doesn't have performance lost when invokes async function.
@sindresorhus has written a lot promise-based helpers, use them together with async function could make source code more readable.
Plugin update
App developers
just need to update the upgraded plugins by plugin developers
, or use egg-bin autod
command we've prepared to quickly update.
The following content is for plugin developers
, it shows how to update the plugins:
Update precautions
- Finish the upgrade items above.
- Replace all
generator function
withasync function
. - Upgrade middlewares.
- Replace all
- Interfaces compatibility (optional), see following.
- Release a major version.
Interfaces compatibility
In some cases, the interface provided by Plugin developers
supports both generator and async, normally it's wrapped by co.
- In 2.x, we suggest
async-first
to get a better performance and clearer error stacks. - If necessary, please use toAsyncFunction and toPromise for compatibility.
Like egg-schedule plugin, it supports generator or async to define the tasks in application level.
// {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();
};
Plugin developers
could simply wrap the following raw function:
// https://github.com/eggjs/egg-schedule/blob/80252ef/lib/load_schedule.js#L38
task = app.toAsyncFunction(schedule.task);
Rules of Plugin release
- Major version releasement
- All the APIs are promise based, and there's no
async
in source code. e.g. egg-view-nunjucks
- All the APIs are promise based, and there's no
- Modify
package.json
- Change
egg
indevDependencies
to^2.0.0
. - Change
engines.node
to>=8.0.0
. - Change
ci.version
to8, 9
, reinstall dependencies to generate new travis config files.
- Change
- Update examples in
README.md
with async function. - Write instructions for upgrade.
- (optional) Change
test/fixtures
to async function, and it's recommended to create a PR for code preview.
In case the previous versions still requires maintenance:
- Create a new branch which based on previous
1.x
version. - Change the
publishConfig.tag
property inpackage.json
torelease-1.x
in previous version. - If the previous version has new Bugfix, tag it as
release-1.x
when publishing, so users may usenpm i egg-xx@release-1
to import the old version. - See npm documentations.