關於 JS 模塊化的最佳實踐總結

 

模塊化開發是 JS 項目開發中的必備技能,它如同面向對象、設計模式同樣,能夠兼顧提高軟件項目的可維護性和開發效率。html

 

模塊之間一般以全局對象維繫通信。在小遊戲中,GameGlobal 是全局對象。在小程序中,App 是全局對象,任何頁面均可以使用 getApp() 獲取這個全局對象。在 NodeJS 中,global 是全局對象。在傳統瀏覽器宿主中,window 是全局對象。前端

 

如下是做者總結的模塊化實踐經驗。簡言之,除了在瀏覽器項目中使用 sea.js,其它類型項目均建議直接使用原生的 ES6 模塊規範。jquery

 

目錄編程

  1. CommonJS 規範
  2. AMD 規範
  3. CMD 規範
  4. ES6 模塊規範
  5. 結論

 

CommonJS 規範

 

CommonJS 規範最先在 NodeJS 中實踐並被推廣開來。它使用 module.exports 輸出模塊,一個模塊寫在一個獨立的文件內,一個文件便是一個模塊。在另外一個JS文件中,使用 require 導入模塊。各個模塊相互隔離,模塊之間的通信,經過全局對象 global 完成。小程序

 

值得特別注意的是,CommonJS 這種規範天生是爲 NodeJS 服務的。NodeJS 是一種服務器端編程語言,源碼文件都在硬盤上,讀起來很方便。CommonJS 規範做爲一種同步方案,後續代碼必須等待前面的require指令加載模塊完成。設計模式

 

使用 CommonJS 規範的代碼示例以下:瀏覽器

 

// 定義模塊math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在這裏寫上須要向外暴露的函數、變量
  add: add,
  basicNum: basicNum
}
// 在另外一個文件中,引用自定義的模塊時,參數包含路徑,可省略後綴.js
var math = require('./math');
math.add(2, 5);

 

在小程序與小遊戲的官方文檔中,提到模塊化時官方建議的規範便是 CommonJS 規範。但其實在做者看來,更適合小遊戲/小程序開發的規範是 ES6 模塊規範,緣由稍後便會講到。服務器

 

AMD 規範

 

CommonJS 規範主要是爲服務器端的 NodeJS 服務,服務器端加載模塊文件無延時,可是在瀏覽器上就大不相同了。AMD 便是爲了在瀏覽器宿主環境中實現模塊化方案的規範之一。微信

 

AMD是一種使用JS語言自實現的模塊化規範方案,主要由require.config()、define()、require 三個函數實現。require.config() 用於聲明基本路徑和模塊名稱;define() 用於定義模塊對象;require() 則用於加載模塊並使用。閉包

 

與 CommonJS 規範不一樣,AMD 規範身處瀏覽器環境之中,是一種異步模塊加載規範。在使用時,首先要加載模塊化規範實現文件 require.js 及 JS 主文件,示例以下:

 

/** 網頁中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

 

在上面的 Html 代碼中,"js/require.js" 是實現 AMD 規範的類庫文件,是任何使用 AMD 規範的網頁都須要加載的;"js/main" 是開發者的代碼主文件,在這個文件中加載並使用自定義模塊,示例代碼以下:

 

/** main.js 入口文件/主模塊 **/
// 首先用config()指定各模塊路徑和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //實際路徑爲js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 執行基本操做
require(["jquery","underscore","math"],function($,_,math){//在這裏$表明jqurey、_表明underscore
  var sum = math.add(10,20);
  $("#sum").html(sum);
});

 

而用於模塊的定義,在其它 JS 文件中是這樣聲明的:

 

// 定義math.js模塊
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});

 

若是在一個模塊定義中依賴另外一個模塊對象,能夠這樣聲明:

 

// 定義一個依賴underscore模塊的模塊
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

 

AMD 規範看起來完美解決了瀏覽器模塊化開發的難題。可是它有一個天生的缺陷,對於依賴的模塊不管實際須要與否,都會先加載並執行。以下所示:

 

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
    // 等於在最前面聲明並初始化了要用到的全部模塊
    if (false) {
      // 即使沒用到某個模塊 b,但 b 仍是提早執行了
      b.foo()
    } 
});

 

在上面的代碼中,模塊 a、b、c、d、e、f 都會加載並執行,即便它們在實際的模塊代碼中沒有被用到。爲了解決這個「浪費」的問題,CMD 規範應運而生。

 

CMD 規範

 

CMD 規範單從名字來看,它也與 AMD 規範很像。CMD 與 AMD 規範同樣,一樣是一種 JS 語言自實現的模塊化方案。不一樣之處在於,AMD 規範是依賴前置、模塊提早加載並執行;CMD 是依賴後置、模塊懶惰加載再執行。示例代碼以下:

 

/** CMD寫法 **/
define(function(require, exports, module) {
    var a = require('./a'),
     b = require('./b'),
     c = require('./c'); //在須要時申明、加載和使用
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

 

在上面的代碼中,模塊 a 在使用時才被聲明並加載。sea.js 是一個模塊加載器,是 AMD 規範的主要實現者之一。使用 sea.js 定義和使用模塊的示例以下所示:

 

/** sea.js **/
// 定義模塊 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加載模塊
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

 

與 AMD 相比,CMD 貌似確實節省了無謂的模塊加載。可是 AMD 規範自己就是一種異步模塊加載方案,是隻有在運行時才被加載並運行的,用則加載,不用不加載,有何浪費可言?何況,比起在代碼中分別以 require 函數加載模塊,直接在 define 方法的第一個參數中聲明,彷佛還更簡潔與瀟灑些。

 

sea.js 做爲 AMD 規範的升級版,簡化了使用方法,在使用上更加方便,值得推崇。可是 sea.js 即是瀏覽器開發中最佳的模塊化解決方案嗎?未必,還要看是什麼類型的項目,後面會講到。

 

ES6 模塊規範

 

在講 ES6 模塊規範以前,咱們先看一下規範前驅 CommonJS 的一個缺陷。以下所示:

 

// 模塊定義代碼:lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// 模塊使用代碼:main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
var mod2 = require('./lib');
console.log(mod2.counter);  // 3

 

在上面的代碼中,爲何三個 mod.counter 的輸出均是3?

 

CommonJS 規範是一種動態加載、拷貝值對象執行的模塊規範。每一個模塊在被使用時,都是在運行時被動態拉取並被拷貝使用的,模塊定義是惟一的,但有幾處引用便有幾處拷貝。因此,對於不一樣的 require 調用,生成的是不一樣的運行時對象。

 

即便如此,在上面的代碼中,mod 只有一個,爲何 mod.incCounter() 對這個模塊對象——即 mod 中的 counter 變量改變無效?相反,對於如下的代碼:

 

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 4

 

第二個輸出是4。只是將 counter 聲明爲一個 getter 存取器屬性,調用便正常了,爲何?

 

這是因爲 CommonJS 的拷貝機制形成的。因爲 CommonJS 規範的拷貝運行機制,在 lib.js 中使用 module.exports 輸出的對象,是從 lib 模塊內拷貝而得,當時 counter 的值是幾,便拷貝了幾。不管執行 incCounter 多少次,改變的都不是輸出對象的 counter 變量。

 

而當定義了 getter 屬性以後,該屬性指向了模塊定義對象中的 counter 變量了嗎?不,是指向了被 incCounter 方法以閉包形式囊括的 counter 變量,這個變量是輸出的模塊對象的一部分。

 

CommonJS 規範的這個缺陷,有時候讓程序很無奈,一不當心就寫出了錯誤的代碼。這個缺陷在 ES6 中獲得了很好的解決。

 

在 ES6 模塊規範中,只有 export 與 import 兩個關鍵字。示例以下:

 

/** 定義模塊 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模塊 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

 

在上面的代碼中,使用 export 關鍵字在 math.js 文件中輸出模塊,這裏使用了對象字面量的屬性名稱簡寫與方法名稱簡寫。在另外一個文件中引用模塊,在 import 關鍵字後面,{basicNum, add} 這是對象變量析構的寫法。

 

若是在 export 模塊時,使用了 default 限定詞,以下所示:

 

//定義輸出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

 

在 import 引入時,即可以省去花括號。這樣看起來代碼更清爽簡潔。

 

ES6 模塊規範與 CommonJS 規範相比,有如下不一樣:

 

(1)ES6 模塊規範是解析(是解析不是編譯)時靜態加載、運行時動態引用,全部引用出去的模塊對象均指向同一個模塊對象。在上面使用 CommonJS 規範聲明的 lib 模塊,若是使用 ES6 模塊規範聲明,根本不會出現 counter 變量含糊不清的問題。

 

(2)CommonJS 規範是運行時動態加載、拷貝值對象使用。每個引用出去的模塊對象,都是一個獨立的對象。

 

結論

 

因此綜上所述,在模塊化方案上最佳選擇是什麼?

 

在小程序(包括小遊戲)開發項目中,因爲支持 ES6,因此小程序最好的模塊化方案即是使用ES6模塊規範。雖然官方文檔中提到的模塊化規範是 CommonJS,但最佳方案做者認爲卻應該是 ES6。

 

小程序在手機端(不管 iOS 仍是 Android)的底層渲染內核都是類 Chrome v8 引擎。v8 引擎在執行JS代碼時,是將代碼先以 MacroAssembler 彙編庫在內存中先編譯成機器碼再送往 CPU 執行的,並非像其它 JS 引擎那樣解析一行執行一行。因此,靜態加載的 ES6 模塊規範,更有助於 v8 引擎發揮價值。而運行時加載的 CommonJS 規範、AMD 規範、CMD 規範等,均不利於 v8 引擎施展拳腳。遇到 CommonJS 代碼,v8 可能會怒罵:「有什麼話能不能一次講完,你這樣貓拉屎式的作法只能讓我更慢!」

 

在 NodeJS 開發項目中,Node9 已經支持 ES6 語法,徹底可使用 ES6 模塊規範。NodeJS 的誕生,自己就基於 Google 的 v8 引擎,沒有理由不考慮發揮 v8 的最大潛能。

 

在瀏覽器 JS 開發項目中,由於從服務器加載文件須要時間,使用 CommonJS 規範確定是不合適了。至因而使用原生的 ES 模塊規範,仍是使用sea.js,要看具體場景。若是想頁面儘快加載,sea.js 適合;若是是單頁面網站,適合使用原生的 ES6 模塊規範。還有一點,瀏覽器並不是只有 Chrome 一家,對於沒有使用 v8 引擎的瀏覽器,使用 ES6 原生規範的優點就又減小了一點。

 

2019年1月21日於北京

 


 

參考資料

  • 瀏覽器已原生支持 ES 模塊,這對前端開發來講意味着什麼?
  • Node 9下import/export的絲般順滑使用
  • Sea.js 是什麼?
  • 前端模塊化:CommonJS,AMD,CMD,ES6
  • Module 的加載實現

 

本文首先於微信公衆號「藝述思惟」:關於 JS 模塊化的最佳實踐總結

相關文章
相關標籤/搜索