如何設計大規模 JavaScript 應用

背景

我一直很關注如何設計大規模的 JavaScript 應用因爲我一直在做的都是大規模的JavaScript 應用。從百度 Hi 網頁版到百度地圖,從 Yahoo Search Direct 到豌豆莢客戶端。好吧……Yahoo Search Direct 本身的規模不大,但作爲一個網頁插件它要能跑在任何宿主頁 面上,其複雜度也不低。在大規模 JavaScript 應用開發和維護的過程中,有兩個問題尤其值得關注:設計和性能。前者是必須在開發階段之前做好的,開發開始後就來不及改了,只能事後重構;後者則更多發生 在開發的中後期,等 profiling 結果出來了,再針對瓶頸做優化。在這個文章系列中,我們的關注點是設計。

儘管我一直在寫跟設計相關的文章,不過現在看來我過去寫下的文章都有點山寨了,也不夠 update。在我看到 Large-scale JavaScript Application Architecture 這個幻燈片後,我決定重新寫一個系列來說說我在大規模 JavaScript應用方面的經驗,也包括一些設想。

如果說大規模 JavaScript 應用設計方面有什麼核心原則的話,我覺得核心原則就是這一條:永遠不要嘗試構建大規模應用。構建小應用,保證它們的可測性,然後把它們組裝成大應用。因此,我覺得在討論任何設計模式之前,我們先要討論一下如何把大應用拆分成小應用。

“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application”

- Justin Meyer

模塊化

CommonJS Modules

先說最理想的模塊化方式,那就是 CommonJS Modules。一個模塊系統至少要能解決兩個問題:依賴項的加載、私有作用域和公有導出成員的區分。CommonJS Modules 通過 require 和 exports 很好地實現了上述兩個功能。下面是一個 CommonJS Modules模塊定義和使用的例子:

util.js

var util = {
    extend: function(target, source) {
        /* implementation */
    }
};
util.extend(exports, util);

feature.js

var util = require('util');var features = {
    core: {
        start: function() {
            /* implementation */
        }
    }
};
util.extend(exports,features);

app.js

var features = require('features');
features.core.start();
AMD

考慮到瀏覽器裏面沒有 CommonJS 的 Modules 接口,也不可能完整實現這樣的Modules 接口,所以就有了 AMD (Asynchronous Module Definition) 這樣的解決方案。AMD 使用 define 函數定義模塊,要求模塊提前聲明依賴項,然後通過回調加載模塊,解決了瀏覽器無法同步加載模塊的問題。下面是一個 AMD 模塊定義和使用的例子:

util.js

define('util', [], function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
};

feature.js

define('features', ['util'], function(util) {
    return {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
});

app.js

define('app', ['feature'], function(feature) {
    features.core.start();
});
CommonJS Modules/Wrappings

考慮到 AMD 的寫法跟 CommonJS Modules 的寫法區別十分之大,要把已有的 CommonJS Module 寫法模塊改爲兼容 AMD 不容易,所以又有人設計一些改動不那麼大的寫法,如 AMD 工廠函數的 function(require, exports, ...) {...} 簽名,或 CommonJS Modules/Wrappings (意思是 Modules 的 Wrappings)。由於瀏覽器必須異步加載依賴項,所以這些寫法只能通過對工廠函數源代碼做靜態分析提前找出依賴項,在異步加載好之後再執行工廠函數。這樣做的壞處是工廠函數內部對 require 的調用缺乏靈活性。下面是一個 CommonJS Modules/Wrappings 模塊定義和使用的例子:

util.js

define(function(require, exports, module) {
    var util = {
        extend: function(target, source) {
            /* implementation */
        }
    };
    util.extend(exports, util);
});

feature.js

define(function(require, exports, module) {
    var util = require('util');
    var features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

define(function(require, exports, module) {
    var features = require('features');
    features.core.start();
});
UMD

儘管 Node.js 寫好的 CommonJS Modules 模塊可以通過 CommonJS Modules/Wrapping 包裝一下使得它能用在瀏覽器內,儘管包裝過的模塊通過 r.js 也能用於 Node.js 環境下,不過這不是完美的解決方案。因此,又有人提出 UMD (Universal Module Definition),希望提供跨平臺的模塊定義方案。

UMD 現在還沒有定稿,不同的人提出了不同的解決方案。最全面的方案同時支持 AMD 和 Node.js,順便還把傳統的瀏覽器腳本順序加載模式也兼容了。具體的做法就是判斷環境中是否存在 AMD 所依賴的 define,如果存在的話就使用 AMD 加載,不存在的話就使用別的方式模擬 AMD 加載。

util.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define('util', [], factory);
    } else {        root.util = factory();
    }
})(this, function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
});

feature.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('util'));
    } else if (typeof define === 'function' && define.amd) {
        define('feature', ['util'], factory);
    } else {        root.feature = factory(root.util);
    }
})(this, function(util) {
    return features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('feature'));
    } else if (typeof define === 'function' && define.amd) {
        define('app', ['feature'], factory);
    } else {        root.app = factory(root.feature);
    }
})(this, function(feature) {
    var features = require('features');
    features.core.start();
});

小結

在這篇文章裏面,我們瞭解了大規模 JavaScript 應用設計的核心原則:使用模塊化的方式把大應用分解爲小應用來編寫和維護。同時我們也看了不同的模塊定義方式,我們可以針對平臺來選擇一種合適的方式,也可以選擇通用的方式但需要維護更多的代碼。


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