如何写好Node.JS的后端程序

背景介绍

大家知道,写Node.JS项目的代码都偏向于函数编程,直接上来就与业务代码,很少能看到有体现OO的设计。像什么新建一个类,再初始化一个对象,再调用它的方法,在JavaScript中似乎很冗余,我们直接上来就写action就是了,哪有那么麻烦?!然而,这样像写脚本似的方式,很容易让代码变得复杂难懂,等系统稍微复杂后就会变得难以维护。所以我们更需要在这些项目中重视代码整洁和重构。

这个例子,以后台一个台风数据查询API的重构为例,给大家介绍写Node.JS项目时,非常需要注意的两个地方:代码的层次结构异步回调的处理手法。

以下是原来的代码,它的业务过程大概是

  1. 通过查询语句latestBatchQuery,拿到台风最新批次号的信息
  2. 通过批次号查询台风相关的具体内容,包括:名字信息、路径信息和当前位置等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
exports.search = function(req, res) {
Typhoon.aggregate(latestBatchQuery).exec(function(err, latestBatchQueryResult) {
if(err) {
console.log('[Typhoon] getLatestBatch error.', err);
return res.json([]);
}
let latestBatches = _.get(latestBatchQueryResult[0], 'result');
if(!_.isEmpty(latestBatches)) {
let query = getDataAggrQuery(latestBatches);
Typhoon.aggregate(query).exec(function(err, dataAggrResult) {
if(err) {
console.log('[Typhoon] dataAggregation error.', err);
return res.json([]);
}
res.json(dataAggrResult);
});
} else {
res.json([]);
}
});
}

后端代码的层次

有时写业务的时候,我们会贪方便,把业务逻辑一古脑全写在controller里面。这样的做法其实很不好,一方面业务逻辑和http的req/res混在一起,代码不清晰,另一方面,测试的成本也高,每次跑测试必须把server和db都起起来。比较优雅的做法应该是:

  • 把业务逻辑所需要参数从httpr req 中剥离出来;
  • 把业务逻辑放到service层中,这样我们的测试用例可以只针对service里面的方法,代码的责任和层次就比较分明

这就是所谓的去除代码的坏味道 - 依恋情结(Feature Envy)。

更改后,代码结构为:

  • controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    exports.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
    16
    exports.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
17
let 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
    10
    co(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
    18
    let 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
    10
    exports.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);
    };

实例代码视频演示

以下是完整的代码重构视频:

0%