隨着 Web 技術的蓬勃發展和依賴的基礎設施日益完善,前端領域逐漸從瀏覽器擴展至服務端(Node.js),桌面端(PC、Android、iOS),乃至於物聯網設備(IoT),其中 JavaScript 承載着這些應用程序的核心部分,隨着其規模化和複雜度的成倍增加,其軟件工程體系也隨之創建起來(協同開發、單元測試、需求和缺陷管理等),模塊化編程的需求日益迫切。javascript
JavaScript 對模塊化編程的支持還沒有造成規範,難以堪此重任;一時間,江湖俠士自告奮勇,一路披荊斬棘,從刀耕火種過渡到面向將來的模塊化方案;css
模塊化編程就是經過組合一些__相對獨立可複用的模塊__來進行功能的實現,其最核心的兩部分是__定義模塊__和__引入模塊__;html
儘管 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)來聲明依賴以及導出數據,這與當下的模塊化方案並沒有巨大的差別,可本質上卻有千差萬別,沒法知足的一些重要的特性;vue
題外話:因爲年代久遠,這兩種模塊化方案逐漸淡出歷史舞臺,具體特性再也不細聊;java
爲了解決」刀耕火種」時代存留的需求,AMD 和 CMD 模塊化規範問世,解決了在瀏覽器端的異步模塊化編程的需求,其最核心的原理是經過動態加載 script 和事件監聽的方式來異步加載模塊;node
AMD 和 CMD 最具表明的兩個做品分別對應 require.js 和 sea.js;其主要區別在於依賴聲明和依賴加載的時機,其中 require.js 默認在聲明時執行, sea.js 推崇懶加載和按需使用;另外值得一提的是,CMD 規範的寫法和 CommonJS 極爲相近,只需稍做修改,就能在 CommonJS 中使用。參考下面的 Case 更有助於理解;webpack
// 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 = {};
});
複製代碼
2009 年 ry 發佈 Node.js 的第一個版本,CommonJS 做爲其中最核心的特性之一,適用於服務端下的場景;歷年來的考察和時間的洗禮,以及前端工程化對其的充分支持,CommonJS 被普遍運用於 Node.js 和瀏覽器;ios
// 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;
複製代碼
一、模塊定義git
默認任意 .node .js .json 文件都是符合規範的模塊;
二、引入模塊
首先從緩存(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));
});
複製代碼
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;
複製代碼
主要差別在於:
import
export
以及獨有的 default
關鍵字,肯定默認的導出值;只讀的值的引用
,不管基礎類型和複雜類型,而在 CommonJS 中 require 的是值的拷貝,其中複雜類型是值的淺拷貝;// a.js
export let a = 1;
export function caculate() {
a++;
};
// b.js
import { a, caculate } from 'a.js';
console.log(a); // 1
caculate();
console.log(a); // 2
a = 2; // Syntax Error: "a" is read-only
複製代碼
經過一層自執行函數來兼容各類模塊化規範的寫法,兼容 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 標識,從而兼容其在語法上的差別。
一、瀏覽器環境下,網絡資源受到較大的限制,所以打包出來的文件若是體積巨大,對頁面性能的損耗極大,所以須要對構建的目標文件進行拆分,同時模塊也須要支持動態加載;
webpack 提供了兩個方法 require.ensure() 和 import() (推薦使用)進行模塊的動態加載,至於其中的原理,跟上面說起的 AMD & CMD 所見略同,import() 執行後返回一個 Promise 對象,其中所作的工做無非也是動態新增 script 標籤,而後經過 onload / onerror 事件進一步處理。
二、因爲 require 函數是徹底自定義的,咱們能夠在模塊化中實現更多的特性,好比經過修改 require.resolve 或 Module._extensions 擴展支持的文件類型,使得 css / .jsx / .vue / 圖片等文件也能爲模塊化所使用;
模塊化規範 | 加載方式 | 加載時機 | 運行環境 | 備註 |
---|---|---|---|---|
AMD | 異步 | 運行時 | 瀏覽器 | |
CMD | 異步 | 運行時 | 瀏覽器 | 依賴基於靜態分析,require 時已經 module ready |
CommonJS | 同步/異步 | 運行時 | 瀏覽器 / Node | |
ES Module | 同步/異步 | 編譯階段 | 瀏覽器 / Node | 經過 import() 實現異步加載 |