前言
說到前端模塊化,你第一時間能想到的是什么?Webpack?ES6 Module?還有嗎?我們一起來看一下下圖。

相信大伙兒對上圖的單詞都不陌生,可能用過、看過或者是只是聽過。那你能不能用一張圖梳理清楚上述所有詞匯之間的關(guān)系呢?我們?nèi)粘>帉懘a的時候,又和他們之間的誰誰誰有關(guān)系呢?一、千絲萬縷
為了更貼合我們的日常開發(fā)場景(前后端分離),我們嘗試先從不同平臺的維度區(qū)分,作為本文的切入點。
1. 根據(jù)平臺劃分
平臺 規(guī)范 特性 瀏覽器 AMD、CMD 存在網(wǎng)絡(luò)瓶頸,使用異步加載 非瀏覽器 CommonJS 直接操作 IO,同步加載 可以看到我們非常暴力的以是不是瀏覽器作為劃分標(biāo)準(zhǔn)。仔細(xì)分析一下,他們之間最大的差異在于其特性上,是否存在瓶頸。 例如說網(wǎng)絡(luò)性能瓶頸,每個模塊的請求都需要發(fā)起一次網(wǎng)絡(luò)請求,并等待資源下載完成后再進(jìn)行下一步操作,那整個用戶體驗是非常糟糕的。 根據(jù)該場景,我們簡化一下,以同步加載和異步加載兩個維度進(jìn)行區(qū)分。
特性 規(guī)范 同步加載 CommonJS 異步加載 AMD、CMD 2. AMD、CMD 兩大規(guī)范
先忽略 CommonJS,我們先介紹下,曾經(jīng)一度盛行的 AMD、CMD 兩大規(guī)范。
規(guī)范 約束條件 代表作 AMD 依賴前置 requirejs CMD 就近依賴 seajs AMD、CMD 提供了封裝模塊的方法,實現(xiàn)語法上相近,甚至于 requirejs 在后期也默默支持了 CMD 的寫法。我們用一個例子,來講清楚這兩個規(guī)范之間最大的差異:依賴前置和就近依賴。
AMD:
// hello.js define(function() { console.log('hello init'); return { getMessage: function() { return 'hello'; } }; }); // world.js define(function() { console.log('world init'); }); // main define(['./hello.js', './world.js'], function(hello) { return { sayHello: function() { console.log(hello.getMessage()); } }; }); // 輸出 // hello init // world init 復(fù)制代碼
CMD:
// hello.js define(function(require, exports) { console.log('hello init'); exports.getMessage = function() { return 'hello'; }; }); // world.js define(function(require, exports) { console.log('world init'); exports.getMessage = function() { return 'world'; }; }); // main define(function(require) { var message; if (true) { message = require('./hello').getMessage(); } else { message = require('./world').getMessage(); } }); // 輸出 // hello init 復(fù)制代碼
結(jié)論: CMD 的輸出結(jié)果中,沒有打印"world init"。但是,需要注意的是,CMD 沒有打印"world init"并是不 world.js 文件沒有加載。AMD 與 CMD 都是在頁面初始化時加載完成所有模塊,唯一的區(qū)別就是就近依賴是當(dāng)模塊被 require 時才會觸發(fā)執(zhí)行。
requirejs 和 seajs 的具體實現(xiàn)在這里就不展開闡述了,有興趣的同學(xué)可以到官網(wǎng)了解一波,畢竟現(xiàn)在使用 requirejs 和 seajs 的應(yīng)該很少了吧。
3. CommonJS
回到 CommonJS,寫過 NodeJS 的同學(xué)對它肯定不會陌生。CommonJS 定義了,一個文件就是一個模塊。在 node.js 的實現(xiàn)中,也給每個文件賦予了一個 module 對象,這個對象包括了描述當(dāng)前模塊的所有信息,我們嘗試打印 module 對象。
// index.js console.log(module); // 輸出 { id: '/Users/x/Documents/code/demo/index.js', exports: {}, parent: { module }, // 調(diào)用該模塊的模塊,可以根據(jù)該屬性查找調(diào)用鏈 filename: '/Users/x/Documents/code/demo/index.js', loaded: false, children: [...], paths: [...] } 復(fù)制代碼
也就是說,在 CommonJS 里面,模塊是用對象來表示。我們通過“循環(huán)加載”的例子進(jìn)行來加深了解。
// a.js exports.x = 'a1'; console.log('a.js ', require('./b.js').x); exports.x = 'a2'; //b.js exports.x = 'b1'; console.log('b.js ', require('./a.js').x); exports.x = 'b2'; //main console.log('index.js', require('./a.js').x); // 輸出 b.js a1 a.js b2 index.js a2 復(fù)制代碼
我們的理論依據(jù)是模塊對象,根據(jù)該依據(jù)我們進(jìn)行如下分析。
1、 a.js準(zhǔn)備加載,在內(nèi)存中生成module對象moduleA 2、 a.js執(zhí)行exports.x = 'a1'; 在moduleA的exports屬性中添加x 3、 a.js執(zhí)行console.log('a.js', require('./b.js').x); 檢測到require關(guān)鍵字,開始加載b.js,a.js執(zhí)行暫停 4、 b.js準(zhǔn)備加載,在內(nèi)存中生成module對象moduleB 5、 b.js執(zhí)行exports.x = 'b1'; 在moduleB的exports屬性中添加x 6、 b.js執(zhí)行console.log('b.js', require('./a.js').x); 檢測到require關(guān)鍵字,開始加載a.js,b.js執(zhí)行暫停 7、 檢測到內(nèi)存中存在a.js的module對象moduleA,于是可以將第6步看成console.log('b.js', moduleA.x); 在第二步中moduleA.x賦值為a1,于是輸出b.js, a1 8、 b.js繼續(xù)執(zhí)行,exports.x = 'b2',改寫moduleBexports的x屬性 9、 b.js執(zhí)行完成,回到a.js,此時同理可以將第3步看成console.log('a.js', modulerB.x); 輸出了a.js, b2 10、 a.js繼續(xù)執(zhí)行,改寫exports.x = 'a2' 11、 輸出index.js a2 復(fù)制代碼
至此,“CommonJS 的模塊,是一個對象。”這個概念大伙兒應(yīng)該能理解吧?
回到這個例子,例子里面還出現(xiàn)了一個保留字 exports。其實 exports 是指向 module.exports 的一個引用。舉個例子可以說明他們兩個之間的關(guān)系。
const myFuns = { a: 1 }; let moduleExports = myFuns; let myExports = moduleExports; // moduleExports 重新指向 moduleExports = { b: 2 }; console.log(myExports); // 輸出 {a : 1} // 也就是說在module.exports被重新復(fù)制時,exports與它的關(guān)系就gg了。解決方法就是重新指向 myExports = modulerExports; console.log(myExports); // 輸出 { b: 2 } 復(fù)制代碼
4. ES6 module
對 ES6 有所了解的同志們應(yīng)該都清楚,web 前端模塊化在 ES6 之前,并不是語言規(guī)范,不像是其他語言 JAVA、php 等存在命名空間或者包的概念。上文提及的 AMD、CMD、CommonJS 規(guī)范,都是為了基于規(guī)范實現(xiàn)的模塊化,并非 JavaScript 語法上的支持。 我們先簡單的看一個 ES6 模塊化寫法的例子:
// a.js export const a = 1; // b.js export const b = 2; // main import { a } from './a.js'; import { b } from './b.js'; console.log(a, b); //輸出 1 2 復(fù)制代碼
emmmm,沒錯,export 保留字看起來是不是和 CommonJS 的 exports 有點像?我們嘗試 下從保留字對比 ES6 和 CommonJS。
保留字 CommonJS ES6 require 支持 支持 export / import 不支持 支持 exports / module.exports 支持 不支持 好吧,除了 require 兩個都可以用之外,其他實際上還是有明顯差別的。那么問題來了,既然 require 兩個都可以用,那這兩個在 require 使用上,有差異嗎?
我們先對比下 ES6 module 和 CommonJS 之間的差異。
模塊輸出 加載方式 CommonJS 值拷貝 對象 ES6 引用(符號鏈接) 靜態(tài)解析 又多了幾個新穎的詞匯,我們先通過例子來介紹一下值拷貝和引用的區(qū)別。
// 值拷貝 vs 引用 // CommonJS let a = 1; exports.a = a; exports.add = () => { a++; }; const { add, a } = require('./a.js'); add(); console.log(a); // 1 // ES6 export const a = 1; export const add = () => { a++; }; import { a, add } from './a.js'; add(); console.log(a); // 2 // 顯而易見CommonJS和ES6之間,值拷貝和引用的區(qū)別吧。 復(fù)制代碼
靜態(tài)解析,什么是的靜態(tài)解析呢?區(qū)別于 CommonJS 的模塊實現(xiàn),ES6 的模塊并不是一個對象,而只是代碼集合。也就是說,ES6 不需要和 CommonJS 一樣,需要把整個文件加載進(jìn)去,形成一個對象之后,才能知道自己有什么,而是在編寫代碼的過程中,代碼是什么,它就是什么。
PS:
- 目前各個瀏覽器、node.js 端對 ES6 的模塊化支持實際上并不友好,更多實踐同志們有興趣可以自己搞一波。
- 在 ES6 中使用 require 字樣,靜態(tài)解析的能力將會丟失!
5. UMD
模塊化規(guī)范中還有一個 UMD 也不得不提及一下。什么是 UMD 呢?
UMD = AMD + CommonJS 復(fù)制代碼
沒錯,UMD 就是這么簡單。常用的場景就是當(dāng)你封裝的模塊需要適配不同平臺(瀏覽器、node.js),例如你寫了一個基于 Date 對象二次封裝的,對于時間的處理工具類,你想推廣給負(fù)責(zé)前端頁面開發(fā)的 A 同學(xué)和后臺 Node.js 開發(fā)的 B 同學(xué)使用,你是不是就需要考慮你封裝的模塊,既能適配 Node.js 的 CommonJS 協(xié)議,也能適配前端同學(xué)使用的 AMD 協(xié)議?
二、工具時代
1. webpack
webpack 興起之后,什么 AMD、CMD、CommonJS、UMD,似乎都變得不重要了。因為 webpack 的模塊化能力真的強(qiáng)。
webpack 在定義模塊上,可以支持 CommonJS、AMD 和 ES6 的模塊聲明方式,換句話說,就是你的模塊如果是使用 CommonJS、AMD 或 ES6 的語法寫的,webpack 都支持!我們看下例子:
//say-amd.js define(function() { 'use strict'; return { sayHello: () => { console.log('say hello by AMD'); } }; }); //say-commonjs.js exports.sayHello = () => { console.log('say hello by commonjs'); }; //say-es6.js export const sayHello = () => { console.log('say hello in es6'); }; //main import { sayHello as sayInAMD } from './say-amd'; import { sayHello as sayInCommonJS } from './say-commonjs'; import { sayHello as sayInES6 } from './say-es6'; sayInAMD(); sayInCommonJS(); sayInES6(); 復(fù)制代碼
不僅如此,webpack 識別了你的模塊之后,可以將其打包成 UMD、AMD 等等規(guī)范的模塊重新輸出。例如上文提及到的你需要把 Date 模塊封裝成 UMD 格式。只需要在 webpack 的 output 中添加 libraryTarget: 'UMD'即可。
2. more...
總結(jié)
回到開始我們提出的問題,我們嘗試使用一張圖匯總上文提及到的一溜模塊化相關(guān)詞匯。
