《Nodejs開發加密貨幣》之七:入口程序app.js解讀

關於

《Nodejs開發加密貨幣》,是一個加密貨幣產品的詳細開發文檔,涉及到使用Nodejs開發產品的方方面面,從前端到後臺、從服務器到客戶端、從PC到移動、加密解密、區款鏈等各個環節。代碼完全開源、文章免費分享。 相關資源見 http://ebookchain.org

QQ交流羣: 185046161

前言

在入門文章部分,我們已經知道,Nodejs的應用最終都可以合併成一個文件,爲了開發方便,纔將其拆分成多個文件。

被拆分的那個文件,自然是我們重點研究的對象,通常這個文件就是App.js或server.js,大家稱之爲入口程序

顯然Ebookcoin用的就是app.js。這一篇,我們就來閱讀一下該文件,學習研究它的整體架構流程。

源碼

地址: https://github.com/Ebookcoin/ebookcoin/blob/master/app.js

類圖

js原本無類,因此它的類圖並不好處理,僅能大致給出它與其他模塊的關聯關係。

appjs-uml

解讀

直接讀代碼看看。

1.配置處理

任何一個應用,都會提供一些參數。對這些參數的處理,有很多種方案。但總的來說,通常需要提供一種理想環境,即默認配置,同時給你一種方法自行修改。

(1)全局默認配置

通常默認參數較少時,可以硬編碼到代碼裏。但更靈活的方式,就是使用單獨文件。這裏就使用了文件 ./config.json 來保存全局配置,如:

{
    "port": 7000,
    "address": "0.0.0.0",
    "serveHttpAPI": true,
    "serveHttpWallet": true,
    "version": "0.1.1",
    "fileLogLevel": "info",
    "consoleLogLevel": "log",
    "sharePort": true,
    ...

使用時,只需要require就可以了。源碼:

var appConfig = require("./config.json"); // app.js 4行

不過,爲了靈活性,默認值通常允許用戶修改。

(2)使用commander組件,引入命令行選項

上一篇已經分享,commander是Nodejs第三方組件(使用npm安裝),常被用來開發命令行工具,用法極爲簡單。源碼:

// 1行
var program = require('commander');

// 19行
program
    .version(packageJson.version)
    .option('-c, --config <path>', 'Config file path')
    .option('-p, --port <port>', 'Listening port number')
    .option('-a, --address <ip>', 'Listening host name or ip')
    .option('-b, --blockchain <path>', 'Blockchain db path')
    .option('-x, --peers [peers...]', 'Peers list')
    .option('-l, --log <level>', 'Log level')
    .parse(process.argv);

這樣,就可以在命令行執行命令時,加帶-c,-p等選項,例如:

node app.js -p 8888

這時,該選項就以program.port的形式被保存,於是手動修改一下:

// 39行
if (program.port) {
    appConfig.port = program.port;
}

這是處理Nodejs應用全局配置的一種常用且簡單的方式,值得學習。

更多內容,請閱讀上一篇對commander組件的詳細介紹。

2.異常捕捉

我們在第一部分總結時,特意提到異常要捕捉,這裏我們很輕鬆就可以看出來,代碼對全局異常處理的方式。

注意:對於domain模塊,已經不提倡使用,這部分代碼將再後續的更新中去除,這裏僅做了解就是了。

(1)使用uncaughtException捕捉進程異常

// 65行
process.on('uncaughtException', function (err) {
    // handle the error safely
    logger.fatal('System error', { message: err.message, stack: err.stack });
    process.emit('cleanup');
});

(2)使用domain模塊捕獲全局異常

// 96行
var d = require('domain').create();
d.on('error', function (err) {
    logger.fatal('Domain master', { message: err.message, stack: err.stack });
    process.exit(0);
});
d.run(function () {
...

另外,對各個模塊,也使用了domain

// 415行
var d = require('domain').create();

d.on('error', function (err) {
scope.logger.fatal('domain ' + name, {message: err.message, stack: err.stack});
});
...

3.模塊加載

這纔是真正的重點,不過看過代碼,發現一切都那麼幹淨利略,也沒有多層回調那些大坑,其實是用了async流程管理組件。

整體使用async.auto進行順序調用;在加載modules時,又使用async.parallel,使其並行運作;當發生錯誤時,清理工作用到了async.eachSeries

下圖是手工簡單畫的,說明了從代碼103-438行之間,各模塊的加載運行順序。後面針對具體代碼會使用UML圖來代替。

async-for-modules

下篇,我們也有必要對async組件進行詳解梳理。這裏,您只要能猜出代碼意圖,就不用太操心async的用法。下面,讀讀有關源碼:

(1)初始網絡

packages.json裏看到使用了Express框架。通過前面部分的介紹,知道必須在入口程序裏,初始化纔對。具體如何調用的?下面的代碼,顯然十分熟悉。

Express是Nodejs重要的web開發框架,這裏的網絡network本質上就是以Express爲基礎的web應用,自然白皮書纔會宣揚基於Http協議

// 215行
network: ['config', function (cb, scope) {
    var express = require('express');
    var app = express();
    var server = require('http').createServer(app);
    var io = require('socket.io')(server);

    if (scope.config.ssl.enabled) {
        var privateKey = fs.readFileSync(scope.config.ssl.options.key);
        var certificate = fs.readFileSync(scope.config.ssl.options.cert);

        var https = require('https').createServer({
    ...

說明:這是async.auto常用的方法,network用到的任何需要回調的方法(這裏是config),都放在這個數組裏,最後的回調函數(function (cb, scope) {//code}),可以巧妙的調用,如: scope.config

這裏的代碼,僅僅初始化服務,沒有做太多實質的事情,真正的動作在下面。

(2)構建鏈接

從下面的代碼開始,我們才能看到這個應用的本質。270行代碼用到了network等,如下:

// 270行
connect: ['config', 'public', 'genesisblock', 'logger', 'build', 'network', function (cb, scope) {

接着,下面的代碼,加載了幾個中間件,告訴我們,該應用接受ejs模板驅動的html文件。視圖文件和圖片、樣式等靜態文件都在public文件夾等,這些信息絕對比官方文檔還有用。

// 277行
scope.network.app.engine('html', require('ejs').renderFile);
scope.network.app.use(require('express-domain-middleware'));
scope.network.app.set('view engine', 'ejs');
scope.network.app.set('views', path.join(__dirname, 'public'));
scope.network.app.use(scope.network.express.static(path.join(__dirname, 'public')));
...

再下來,就是對請求參數和響應數據的處理,包括對節點peers中黑名單、白名單的過濾等,最後啓動服務操作:

// 336行
scope.network.server.listen(scope.config.port, scope.config.address, function (err) {

(3)加載邏輯

看代碼知道,其核心邏輯功能應該是:賬戶管理、交易和區塊鏈。這些模塊,有其執行順序,我們需要在後面的篇章中,分別單獨介紹。

// 379行
logic: ['dbLite', 'bus', 'scheme', 'genesisblock', function (cb, scope) {

    // 嵌套了async.auto
    async.auto({
        ...
        account: ["dbLite", "bus", "scheme", 'genesisblock', function (cb, scope) {
            new Account(scope, cb);
        }],
        transaction: ["dbLite", "bus", "scheme", 'genesisblock', "account", function (cb, scope) {
            new Transaction(scope, cb);
        }],
        block: ["dbLite", "bus", "scheme", 'genesisblock', "account", "transaction", function (cb, scope) {
            new Block(scope, cb);
        }]
    }, cb);
    ...

(4)加載模塊

上面所有代碼的執行結果,都要被這裏的各模塊共享。下面的代碼說明,各個模塊都採用一致(不一定一樣)的參數和處理方法,這樣處理起來簡單方便:

// 411行
modules: ['network', 'connect', 'config', 'logger', 'bus', 'sequence', 'dbSequence', 'balancesSequence', 'dbLite', 'logic', function (cb, scope) {

    // 對每個模塊都使用`domain`監控其錯誤
    Object.keys(config.modules).forEach(function (name) {
        tasks[name] = function (cb) {
            var d = require('domain').create();

            d.on('error', function (err) {
                ...
            });

            d.run(function () {
                ...
            });
        }
    });

    // 讓各個模塊並行運行
    async.parallel(tasks, function (err, results) {
        cb(err, results);
    });

這裏的模塊既然都是並行處理,研究它們就不需要分先後了。

總結

這篇文章開始深入代碼,但是仍然較爲粗略。不過,對整個應用的基本架構已經瞭然。繼續深入研究,方向路線也已然清晰。

代碼中還有很多細節,我們並沒有逐行介紹。個人認爲,讀代碼就像看文章,先要概覽,逐步深入,不一定一開始就逐字逐句去讀,那樣效率低、效果差。

對於這個app.js文件,成手讀它可能就是分分鐘的事情,而寫出來卻要羅嗦這麼多。如果,你並沒有覺得很輕鬆,甚至理解很困難,那麼可能缺少對commanderdomainasync等組件或模塊的瞭解,請看相關分享或官方文檔。

鏈接

本系列文章即時更新,若要掌握最新內容,請關注下面的鏈接

本源文地址: https://github.com/imfly/bitcoin-on-nodejs

電子書閱讀: http://bitcoin-on-nodejs.ebookchain.org

電子書下載: 下載頁面 PDF文件 ePub文件 Mobi文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章