背景介绍
大家知道,写Node.JS项目的代码都偏向于函数编程,直接上来就与业务代码,很少能看到有体现OO的设计。像什么新建一个类,再初始化一个对象,再调用它的方法,在JavaScript中似乎很冗余,我们直接上来就写action就是了,哪有那么麻烦?!然而,这样像写脚本似的方式,很容易让代码变得复杂难懂,等系统稍微复杂后就会变得难以维护。所以我们更需要在这些项目中重视代码整洁和重构。
这个例子,以后台一个台风数据查询API的重构为例,给大家介绍写Node.JS项目时,非常需要注意的两个地方:代码的层次结构和异步回调的处理手法。
以下是原来的代码,它的业务过程大概是
- 通过查询语句
latestBatchQuery,拿到台风最新批次号的信息 - 通过批次号查询台风相关的具体内容,包括:名字信息、路径信息和当前位置等。
1 | exports.search = function(req, res) { |
后端代码的层次
有时写业务的时候,我们会贪方便,把业务逻辑一古脑全写在controller里面。这样的做法其实很不好,一方面业务逻辑和http的req/res混在一起,代码不清晰,另一方面,测试的成本也高,每次跑测试必须把server和db都起起来。比较优雅的做法应该是:
- 把业务逻辑所需要参数从httpr req 中剥离出来;
- 把业务逻辑放到service层中,这样我们的测试用例可以只针对service里面的方法,代码的责任和层次就比较分明
这就是所谓的去除代码的坏味道 - 依恋情结(Feature Envy)。
更改后,代码结构为:
controller
1
2
3
4
5
6
7
8
9
10
11exports.search = function(req, res) {
let returnResult = function(err, result) {
if(err) {
console.log('[Typhoon] getLatestBatch error.', err);
return res.json([]);
} else {
return res.json(result);
}
};
TyphoonService.getTheLatestTyphoon(returnResult);
};service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16exports.getTheLatestTyphoon = function(returnResult) {
Typhoon.aggregate(latestBatchQuery).exec(function(err, latestBatchQueryResult) {
if(err) {
returnResult(err, []);
}
let latestBatches = _.get(latestBatchQueryResult[0], 'result');
if(!_.isEmpty(latestBatches)) {
let query = getDataAggrQuery(latestBatches);
Typhoon.aggregate(query).exec(function(err, dataAggrResult) {
returnResult(err, dataAggrResult);
});
} else {
returnResult(null, []);
}
});
};
异步回调的处理手法
JavaScript的回调地狱,也是让人生畏的地方,当遇到稍复杂的逻辑,那些层层套嵌的代码会让你看得没有明天。其实,很多手法可以让回调写得优雅。总体来说,有以下几个演化:
Promise Chain
使用promise chain,可以使DB查询(IO操作)用 .then / .catch的方法来继写, 如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let getTheLatestBatchs = function() {
return Typhoon.aggregate(latestBatchQuery);
};
let getTheLatestTyphoonByBatchNos = function(latestBatchQueryResult) {
let latestBatches = _.get(latestBatchQueryResult[0], 'result');
if(!_.isEmpty(latestBatches)) {
let query = getDataAggrQuery(latestBatches);
return Typhoon.aggregate(query);
} else {
return new Promise((resolve, reject) => { resolve([]); });
}
};
exports.getTheLatestTyphoon = function() {
return getTheLatestBatchs().then(getTheLatestTyphoonByBatchNos);
};
这样改后,逻辑变得清晰了,只是得把每个IO操作都写成一个方法,并且终须返回一个Promise对象。
借助ES6的新特性generator和第三方库CO
借助ES6的generator特性和第三方包co,就可以使yield来调用异步函数,便其像写同步函数一样自然。最后,只需要用co包着一个generator函数,里面的异步方法就可以用try/catch包裹起来了,exception的处理也变得简单。
controler
1
2
3
4
5
6
7
8
9
10co(function*() {
let result = [];
try {
result = yield TyphoonService.getTheLatestTyphoon();
} catch (error) {
console.log('[Typhoon] getLatestTyphoon error.', err);
} finally {
res.json(result);
}
});service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let getTheLatestBatchs = function* () {
return yield Typhoon.aggregate(latestBatchQuery);
};
let getTheLatestTyphoonByBatchNos = function* (latestBatchQueryResult) {
let latestBatches = _.get(latestBatchQueryResult[0], 'result');
if(!_.isEmpty(latestBatches)) {
let query = getDataAggrQuery(latestBatches);
return yield Typhoon.aggregate(query);
} else {
return [];
}
};
exports.getTheLatestTyphoon = function* () {
let latestBatchQueryResult = yield getTheLatestBatchs();
return yield getTheLatestTyphoonByBatchNos(latestBatchQueryResult);
};
ES7的async + await方式
如果你不想引用第三方的包,可以用async + await的方式,但这是ES7的特性,请务必使用足够高的node.js版本,或者使用babel来转换执行(常在浏览器端代码使用)。这引改写后,感觉JavaScript就有点像JAVA了。:)
controller
1
2
3
4
5
6
7
8
9
10exports.search = async function(req, res) {
let result = [];
try {
result = await TyphoonService.getTheLatestTyphoon();
} catch (error) {
console.log('[Typhoon] getLatestTyphoon error.', error);
} finally {
res.json(result);
}
};service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let getTheLatestBatchs = async function() {
return await Typhoon.aggregate(latestBatchQuery);
};
let getTheLatestTyphoonByBatchNos = async function(latestBatchQueryResult) {
let latestBatches = _.get(latestBatchQueryResult[0], 'result');
if(!_.isEmpty(latestBatches)) {
let query = getDataAggrQuery(latestBatches);
return await Typhoon.aggregate(query);
} else {
return [];
}
};
exports.getTheLatestTyphoon = async function() {
let latestBatchQueryResult = await getTheLatestBatchs();
return await getTheLatestTyphoonByBatchNos(latestBatchQueryResult);
};
实例代码视频演示
以下是完整的代码重构视频: