Use Generator in Express

今年 (2015 年) 上半年,对 JavaScript 开发界最重要的新闻无疑是这两条:

由于 node.js 在新版本和新特性的更新上的消极态度,所以 io.js 才独立出来,这对当时的 JS 开发者来说是双刃剑:

  • 使用 io.js 意味着可以用新的 V8 引擎的新特性,比如 ES 6 的 generator
  • 因为 V8 的底层 API 发生了更改,使用 io.js 意味着会遇到 node modules 无法在 io.js 上使用 (研究过 0.10 和 0.11 的人应该明白,而且由于 0.11 是 beta,所以模块开发者不一定愿意修复) ,当然,0.12 也有这个问题

因此,希望在生产环境使用 node.js 的企业大多数都选择继续在 0.10 (即使 0.12 出来后) ,毕竟对那些企业来说,io.js 并不是成熟的产物。但总体来说,往 io.js (严格来说是新的 V8 引擎) 迁移是趋势,社区对此都是非常认同,所以到目前为止,主流的 node modules 大多数都更新了支持的 io.js 的版本。

与此同时,JavaScript 毕竟是为了在浏览器运行才设计出来的,一旦进入后端的领域,特别是企业级应用中,它的缺点就更明显了。虽然各大团队都在研究可以解决这些缺点的方案,比如 TypeScript,但始终比不上作为 JavaScript 根基的 ECMAScript——一旦作为正式的标准写入 ECMAScript 里,JavaScript 引擎开发者 (主要是浏览器厂商) 就需要去实现,同时不会出现实现差异。

这也是那两条新闻显得如此重要的原因。

但遗憾的是,大部分开发者的习惯是很难改变的,比如在 2013 年发布第一个版本的 koa,它向开发者展示了如何使用 generator 来解决回调地狱,但直到今天,Express 依旧是最主流的 web app 框架。

因为 Express 的使用者是如此守旧,以至于我在 Google 都搜不到在 Express 里使用 generator 的内容。因此就有了这篇教你如何享受 generator 的便利的文章。

首先,先介绍一下 co 这个库,它可以封装 generator function,并以 Promise 的形式返回结果:

co(function* () {
return yield A_Promise
}).then(function (result) {})

假设有这样的程序:

app.use(function (req, res, next) {
User.find({ id: req.params.id })
.then(function (user) {
Book.find({ userId: user.id })
.then(function (books) {
user.books = books
res.json(user)
next()
})
.catch(next)
})
.catch(next)
})

如果用 koa 的话,就是这样:

app.use(function* (next) {
let user = yield User.find({ id: this.params.id })
user.books = yield Book.find({ userId: user.id })
this.body = user
yield next
})

是不是更简洁更易懂?

虽然 koa 很好,但现实情况却不是你说迁移过去就能迁移的。我在 2013 年底左右开始用 koa 对飞飞商城移动版进行重构,遇到了很多 modules 不支持或者需要自己写的问题。如果要用在一些大型项目中,我估计额外开发量会非常大,同时维护成本也很高,而 Express 有很多很活跃的 modules,这也是一大优势。

既然不能迁移到 koa,那就对 Express 做一下改造吧:

app.use(function () {
co.wrap(function* (req, res, next) {
let user = yield User.find({ id: req.params.id })
user.books = yield Book.find({ userId: user.id })
res.json(user)
)).call(this, arguments).then(next, next)
})

看,还是相当简单的。但作为一个有追求的开发者,对这样代码有一种天然的抵触,不够简洁优雅,因此我写了一个 express-next 来解决这个问题。

app.use(require('cookie-session')({ key: KES }))
app.get('/user/:id', function* (req, res, next) {
let user = yield User.find({ id: req.params.id })
user.books = yield Book.find({ userId: user.id })
res.json(user)
next()
})
app.use(function* (err, req, res, next) {
var result = yield Logger.captureException(err, req)
res.locals.err = result.friendlyMessage
res.status(result.statusCode).render(result.template)
})

既能用 generator,也能用传统的 middlewares,enjoy it!