bluebird带给我的惊喜

背景

Node.js开发App服务端Api.框架:Express,Promise库:bluebird.

局限

我没用过ES7的await,async语法,所以无法比较.另外,bluebird也了解的不详细,如有写的不对的地方,实属正常,欢迎讨论.

惊喜出现的过程

从callback到async

开始只用Node的传统callback嵌套方式.后来有的api终于变成了callback hell,变得难以阅读,理解,调试,于是只好寻找解决方法.首先找到了async.async没有引入新的概念,也用callback,但只用一层.开始用的还不错,用它的waterfall,parallel等控制流api组织代码块.看一个例子:

async写的无嵌套n步业务逻辑接口

这是用async的waterfall写的不带嵌套的一个接口(每步内没有asycn的api嵌套),每步的实现收起了.基本上就是一直在查询,往respEntity的data里放数据.这样的业务逻辑用callback写已经无法忍受了,用async算是基本解决了callback hell.

从async到Promise(bluebird)

后来有些api,总体是串行结构,但有些步骤可以并行运行,得到所有结果以后再进行下一步(或总体并行,其中有串行),这就需要串并行嵌套,开始不知道async行不行,就没嵌套用,后来为了性能最优,试了一下,发现可以嵌套写,但非常痛苦.因为async每步内用的还是callback方式,一旦嵌套,就是2层callback加上2层async自身的模版代码,每步的结果还需要往后传,缠绵悱恻,完全无法直视.

于是开始学习Promise,不熟悉Promise是什么的可以看看这里,并找到了bluebird.bluebird是一个全功能的Promise库,性能据说也不错.参考官网:Why bluebird?.这个阶段持续了一段时间,用Promise.props,Promise.all,和then解决了async的嵌套痛点,异步代码全部撸平.

看一个路由层串并行例子,业务逻辑:检测参数后,开始用bluebird走异步串并行,先验证用户,如果参数错误直接返回异常,否则继续并行查询2种数据,再拼结果,返回数据或中间有任意步骤出错返回异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const uid = req.query.uid;
// valid是同步辅助函数
if (!valid(uid)) {
return res.send({
code: 1,
msg: '无效参数',
data: {}
})
}

// validUser, queryUser, queryMessage都是异步查库
// 可以直接用bluebird写成返回Promise
// 也可以写成传统的callback,再用Promise.promisify或Promise.promisifyAll包起来返回Promise
Promise.resolve()
.then(validUser(uid))
.then(() => {
return Promise.props({
user: queryUser(uid),
message: queryMessage(uid)
});
})
.then(data => {
const user = data.user;
const message = data.message;
res.send({
code: 0,
msg: 'success'
data: {
user: formatUser(user),
message: formatMsg(message)
}
})
})
.catch(err => {
res.send({
code: 2,
msg: err.message ? err.message : '服务器异常',
data: {}
})
})

从上面例子可以看出,用bluebird写异步串并行嵌套业务逻辑,使每个逻辑变成了一个独立的函数.这就使开发,调试,修改都变得轻松愉快了很多,极大提高了效率.开发过程中,完全符合人类思维过程,处理完一个,再处理下一个.整个异步过程完全可以先写出所有业务的stub函数,把路由走通,再挨个实现具体功能.调试过程理解代码和断点选择都变的一目了然.业务更改时候,不需要的串行处理只要注释掉相应的then代码块就行,不需要的并行处理只要注释掉Promise.props里对象的某一项就行.添加业务逻辑同理.

从bluebird作为异步代码工具到bluebird作为代码控制流工具

但实际工作中上面的代码模式会遇上一些问题,比如参数检查本身的异常检测和返回,经常需要不止一个.这样就需要重复写多次返回异常.加上异步部分也有返回异常,导致异常处理没那么清晰,也不符合DRY原则.有一次写了3个参数检查加三个返回,看着实在难受,于是尝试了一下把整个业务逻辑都用一句Promise串起来,用最后的catch捕获从头到尾的所有同步,异步和内部嵌套异常,发现可以,于是每个路由的主体就变成一句Promise.

惊喜: 作为代码控制流工具,把业务逻辑用一条语句串起来了

按这个思路,上面的例子可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const uid = req.query.uid;

Promise.resolve()
.then(testUid(uid))
.then(validUser(uid))
.then(() => {
return Promise.props({
user: queryUser(uid),
message: queryMessage(uid)
});
})
.then(data => {
const user = data.user;
const message = data.message;
res.send({
code: 0,
msg: 'success',
data: {
user: formatUser(user),
message: formatMsg(message)
}
})
})
.catch(err => {
sendError(err, res);
})

function testUid(uid) {
if (!valid(uid)) {
const err = new Error('无效参数');
err.code = 1;
throw err;
}
}

其中返回异常函数sendError可以独立成一个模块供所有路由使用,每处异常只需要定义自己的错误编码和文字,然后抛出.

1
2
3
4
5
6
7
8
'use strict';
module.exports = function sendError(res, err) {
res.send({
code: err.code ? err.code : 999,
message: err.message ? err.message : '程序异常',
data: {}
});
};

这样就解决了重复返回异常.

然后呢?就没有然后了,所有路由全都按这个套路重写,自我感觉逼格高了些.有些函数式连续transform数据的感觉,整个流程以请求参数为输入,以返回请求结束,一句完成,中间异步并行产生的结果通过return Promise.props()或return单一Promise往后传,都全了在最后一个then里整理,格式化数据返回.后面最后一个.catch捕获所有异常.

常用的bluebird Api,参考

  1. 流控制api
    then(单一异步或同步),Promise.props(有限个数的并行),Promise.all,Prommise.map(不定个数的并行)

  2. 数据库api
    Promise.using(访问数据库,自动释放连接)

  3. 其他
    Promise.resolve()(便于传递值),catch(捕获异常)

希望

有很多bluebird的api,不知道干啥的,大致看了下,也没看懂,如果您有分享,一定要告诉我,万分感谢.