前言
隨著 Web 技術的蓬勃發展和依賴的基礎設施日益完善,前端領域逐漸從瀏覽器擴充套件至服務端(Node.js),桌面端(PC、Android、iOS),乃至於物聯網裝置(IoT),其中 JavaScript 承載著這些應用程式的核心部分,隨著其規模化和複雜度的成倍增長,其軟體工程體系也隨之建立起來(協同開發、單元測試、需求和缺陷管理等),模組化程式設計的需求日益迫切;
JavaScript 對模組化程式設計的支援尚未形成規範,難以堪此重任;一時間,江湖俠士挺身而出,一路披荊斬棘,從刀耕火種過渡到面向未來的模組化方案;
概念
模組化程式設計就是通過組合一些__相對獨立可複用的模組__來進行功能的實現,其最核心的兩部分是__定義模組__和__引入模組__;
- 定義模組時,每個模組內部的執行邏輯是不被外部感知的,只是匯出(暴露)出部分方法和資料;
- 引入模組時,同步 / 非同步去載入待引入的程式碼,執行並獲取到其暴露的方法和資料;
刀耕火種
儘管 JavaScript 語言層面並未提供模組化的解決方案,但利用其可__物件導向__的語言特性,外加__設計模式__加持,能夠實現一些簡單的模組化的架構;經典的一個案例是利用單例模式模式去實現模組化,可以對模組進行較好的封裝,只暴露部分資訊給需要使用模組的地方;
// Define a module
var moduleA = (function ($, doc) {
var methodA = function() {};
var dataA = {};
return {
methodA: methodA,
dataA: dataA
};
})(jQuery, document);
// Use a module
var result = moduleA.mehodA();
複製程式碼
直觀來看,通過立即執行函式(IIFE)來宣告依賴以及匯出資料,這與當下的模組化方案並無巨大的差異,可本質上卻有千差萬別,無法滿足的一些重要的特性;
- 定義模組時,宣告的依賴不是強制自動引入的,即在定義該模組之前,必須手動引入依賴的模組程式碼;
- 定義模組時,其程式碼就已經完成執行過程,無法實現按需載入;
- 跨檔案使用模組時,需要將模組掛載到全域性變數(window)上;
AMD & CMD 二分天下
題外話:由於年代久遠,這兩種模組化方案逐漸淡出歷史舞臺,具體特性不再細聊;
為了解決”刀耕火種”時代存留的需求,AMD 和 CMD 模組化規範問世,解決了在瀏覽器端的非同步模組化程式設計的需求,其最核心的原理是通過動態載入 script 和事件監聽的方式來非同步載入模組;
AMD 和 CMD 最具代表的兩個作品分別對應 require.js 和 sea.js;其主要區別在於依賴宣告和依賴載入的時機,其中 require.js 預設在宣告時執行, sea.js 推崇懶載入和按需使用;另外值得一提的是,CMD 規範的寫法和 CommonJS 極為相近,只需稍作修改,就能在 CommonJS 中使用。參考下面的 Case 更有助於理解;
// AMD
define(['./a','./b'], function (moduleA, moduleB) {
// 依賴前置
moduleA.mehodA();
console.log(moduleB.dataB);
// 匯出資料
return {};
});
// CMD
define(function (requie, exports, module) {
// 依賴就近
var moduleA = require('./a');
moduleA.mehodA();
// 按需載入
if (needModuleB) {
var moduleB = requie('./b');
moduleB.methodB();
}
// 匯出資料
exports = {};
});
複製程式碼
CommonJS
2009 年 ty 釋出 Node.js 的第一個版本,CommonJS 作為其中最核心的特性之一,適用於服務端下的場景;歷年來的考察和時間的洗禮,以及前端工程化對其的充分支援,CommonJS 被廣泛運用於 Node.js 和瀏覽器;
// Core Module
const cp = require('child_process');
// Npm Module
const axios = require('axios');
// Custom Module
const foo = require('./foo');
module.exports = { axios };
exports.foo = foo;
複製程式碼
規範
- module (Object): 模組本身
- exports (*): 模組的匯出部分,即暴露出來的內容
- require (Function): 載入模組的函式,獲得目標模組的匯出值(基礎型別為複製,引用型別為淺拷貝),可以載入內建模組、npm 模組和自定義模組
實現
1、模組定義
預設任意 .node .js .json 檔案都是符合規範的模組;
2、引入模組
首先從快取(require.cache)優先讀取模組,如果未命中快取,則進行路徑分析,然後按照不同型別的模組處理:
- 內建模組,直接從記憶體載入;
- 外部模組,首先進行檔案定址定位,然後進行編譯和執行,最終得到對應的匯出值;
其中在編譯的過程中,Node對獲取的JavaScript檔案內容進行了頭尾包裝,結果如下:
(function (exports, require, module, __filename, __dirname) {
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
});
複製程式碼
特性總結
- 同步執行模組宣告和引入邏輯,分析一些複雜的依賴引用(如迴圈依賴)時需注意;
- 快取機制,效能更優,同時限制了記憶體佔用;
- Module 模組可供改造的靈活度高,可以實現一些定製需求(如熱更新、任意檔案型別模組支援);
ES Module(推薦使用)
ES Module 是語言層面的模組化方案,由 ES 2015 提出,其規範與 CommonJS 比之 ,匯出的值都可以看成是一個具備多個屬性或者方法的物件,可以實現互相相容;但寫法上 ES Module 更簡潔,跟 Python 接近;
import fs from 'fs';
import color from 'color';
import service, { getArticles } from '../service';
export default service;
export const getArticles = getArticles;
複製程式碼
主要差異在於:
- ES Module 會對靜態程式碼分析,即在程式碼編譯時進行模組的載入,在執行時之前就已經確定了依賴關係(可解決迴圈引用的問題);
- ES Module 關鍵字:
import
export
以及獨有的default
關鍵字,確定預設的匯出值; - ES Module 中匯入模組的屬性或者方法是強繫結的,包括基礎型別;
UMD
通過一層自執行函式來相容各種模組化規範的寫法,相容 AMD / CMD / CommonJS 等模組化規範,貼上程式碼勝過千言萬語,需要特別注意的是 ES Module 由於會對靜態程式碼進行分析,故這種執行時的方案無法使用,此時通過 CommonJS 進行相容;
(function (global, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
this.eventUtil = factory();
}
})(this, function (exports) {
// Define Module
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = 42;
});
複製程式碼
構建工具中的實現
為了在瀏覽器環境中執行模組化的程式碼,需要藉助一些模組化打包的工具進行打包( 以 webpack 為例),定義了專案入口之後,會先快速地進行依賴的分析,然後將所有依賴的模組轉換成瀏覽器相容的對應模組化規範的實現;
模組化的基礎
從上面的介紹中,我們已經對其規範和實現有了一定的瞭解;在瀏覽器中,要實現 CommonJS 規範,只需要實現 module / exports / require / global 這幾個屬性,由於瀏覽器中是無法訪問檔案系統的,因此 require 過程中的檔案定位需要改造為載入對應的 JS 片段(webpack 採用的方式為通過函式傳參實現依賴的引入)。具體實現可以參考:tiny-browser-require。
webpack 打包出來的程式碼快照如下,注意看註釋中的時序;
(function (modules) {
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {}
return __webpack_require__(0); // ---> 0
})
({
0: function (module, exports, __webpack_require__) {
// Define module A
var moduleB = __webpack_require__(1); // ---> 1
},
1: function (module, exports, __webpack_require__) {
// Define module B
exports = {}; // ---> 2
}
});
複製程式碼
實際上,ES Module 的處理同 CommonJS 相差無幾,只是在定義模組和引入模組時會去處理 __esModule 標識,從而相容其在語法上的差異。
非同步和擴充套件
1、瀏覽器環境下,網路資源受到較大的限制,因此打包出來的檔案如果體積巨大,對頁面效能的損耗極大,因此需要對構建的目標檔案進行拆分,同時模組也需要支援動態載入;
webpack 提供了兩個方法 require.ensure() 和 import() (推薦使用)進行模組的動態載入,至於其中的原理,跟上面提及的 AMD & CMD 所見略同,import() 執行後返回一個 Promise 物件,其中所做的工作無非也是動態新增 script 標籤,然後通過 onload / onerror 事件進一步處理。
2、由於 require 函式是完全自定義的,我們可以在模組化中實現更多的特性,比如通過修改 require.resolve 或 Module._extensions 擴充套件支援的檔案型別,使得 css / .jsx / .vue / 圖片等檔案也能為模組化所使用;
附錄1:特性一覽表
模組化規範 | 載入方式 | 載入時機 | 執行環境 | 備註 |
---|---|---|---|---|
AMD | 非同步 | 執行時 | 瀏覽器 | |
CMD | 非同步 | 執行時 | 瀏覽器 | |
CommonJS | 同步/非同步 | 執行時 | 瀏覽器 / Node | |
ES Module | 同步/非同步 | 編譯階段 | 瀏覽器 / Node | 通過 import() 實現非同步載入 |
附錄2:參考
- AMD 模組化規範: github.com/amdjs/amdjs…
- CMD 模組定義規範:github.com/seajs/seajs…
- webpack 模組相關文件: webpack.js.org/concepts/mo…
- 瀏覽器載入 CommonJS 模組的原理與實現:www.ruanyifeng.com/blog/2015/0…