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:

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 in package.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 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.
  • Using async (ctx, next) => {} is not recommended, which prevents anonymous function in error stack.
  • Change yield next to 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();
// 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 with async function.
    • Upgrade middlewares.
  • 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
  • Modify package.json
    • Change egg in devDependencies to ^2.0.0.
    • Change engines.node to >=8.0.0.
    • Change ci.version to 8, 9, reinstall dependencies to generate new travis config files.
  • 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 in package.json to release-1.x in previous version.
  • If the previous version has new Bugfix, tag it as release-1.x when publishing, so users may use npm i egg-xx@release-1 to import the old version.
  • See npm documentations.