轉載自WEB 前端模組化都有什麼?
前言
說到前端模組化,你第一時間能想到的是什麼?Webpack?ES6 Module?還有嗎?我們一起來看一下下圖。
相信大夥兒對上圖的單詞都不陌生,可能用過、看過或者是隻是聽過。那你能不能用一張圖梳理清楚上述所有詞彙之間的關係呢?我們日常編寫程式碼的時候,又和他們之間的誰誰誰有關係呢?一、千絲萬縷
為了更貼合我們的日常開發場景(前後端分離),我們嘗試先從不同平臺的維度區分,作為本文的切入點。
1. 根據平臺劃分
平臺 | 規範 | 特性 |
---|---|---|
瀏覽器 | AMD、CMD | 存在網路瓶頸,使用非同步載入 |
非瀏覽器 | CommonJS | 直接操作 IO,同步載入 |
可以看到我們非常暴力的以是不是瀏覽器作為劃分標準。仔細分析一下,他們之間最大的差異在於其特性上,是否存在瓶頸。 例如說網路效能瓶頸,每個模組的請求都需要發起一次網路請求,並等待資源下載完成後再進行下一步操作,那整個使用者體驗是非常糟糕的。 根據該場景,我們簡化一下,以同步載入和非同步載入兩個維度進行區分。
特性 | 規範 |
---|---|
同步載入 | CommonJS |
非同步載入 | AMD、CMD |
2. AMD、CMD 兩大規範
先忽略 CommonJS,我們先介紹下,曾經一度盛行的 AMD、CMD 兩大規範。
規範 | 約束條件 | 代表作 |
---|---|---|
AMD | 依賴前置 | requirejs |
CMD | 就近依賴 | seajs |
AMD、CMD 提供了封裝模組的方法,實現語法上相近,甚至於 requirejs 在後期也默默支援了 CMD 的寫法。我們用一個例子,來講清楚這兩個規範之間最大的差異:依賴前置和就近依賴。
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
複製程式碼複製程式碼
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
複製程式碼複製程式碼
結論: CMD 的輸出結果中,沒有列印"world init"。但是,需要注意的是,CMD 沒有列印"world init"並是不 world.js 檔案沒有載入。AMD 與 CMD 都是在頁面初始化時載入完成所有模組,唯一的區別就是就近依賴是當模組被 require 時才會觸發執行。
requirejs 和 seajs 的具體實現在這裡就不展開闡述了,有興趣的同學可以到官網瞭解一波,畢竟現在使用 requirejs 和 seajs 的應該很少了吧。
3. CommonJS
回到 CommonJS,寫過 NodeJS 的同學對它肯定不會陌生。CommonJS 定義了,一個檔案就是一個模組。在 node.js 的實現中,也給每個檔案賦予了一個 module 物件,這個物件包括了描述當前模組的所有資訊,我們嘗試列印 module 物件。
// index.js
console.log(module);
複製程式碼
// 輸出
{
id: '/Users/x/Documents/code/demo/index.js',
exports: {},
parent: { module }, // 呼叫該模組的模組,可以根據該屬性查詢呼叫鏈
filename: '/Users/x/Documents/code/demo/index.js',
loaded: false,
children: [...],
paths: [...]
}
複製程式碼複製程式碼
也就是說,在 CommonJS 裡面,模組是用物件來表示。我們通過“迴圈載入”的例子進行來加深瞭解。
// 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
複製程式碼複製程式碼
我們的理論依據是模組物件,根據該依據我們進行如下分析。
1、 a.js準備載入,在記憶體中生成module物件moduleA
2、 a.js執行exports.x = 'a1'; 在moduleA的exports屬性中新增x
3、 a.js執行console.log('a.js', require('./b.js').x); 檢測到require關鍵字,開始載入b.js,a.js執行暫停
4、 b.js準備載入,在記憶體中生成module物件moduleB
5、 b.js執行exports.x = 'b1'; 在moduleB的exports屬性中新增x
6、 b.js執行console.log('b.js', require('./a.js').x); 檢測到require關鍵字,開始載入a.js,b.js執行暫停
7、 檢測到記憶體中存在a.js的module物件moduleA,於是可以將第6步看成console.log('b.js', moduleA.x); 在第二步中moduleA.x賦值為a1,於是輸出b.js, a1
8、 b.js繼續執行,exports.x = 'b2',改寫moduleBexports的x屬性
9、 b.js執行完成,回到a.js,此時同理可以將第3步看成console.log('a.js', modulerB.x); 輸出了a.js, b2
10、 a.js繼續執行,改寫exports.x = 'a2'
11、 輸出index.js a2
複製程式碼複製程式碼
至此,“CommonJS 的模組,是一個物件。”這個概念大夥兒應該能理解吧?
回到這個例子,例子裡面還出現了一個保留字 exports。其實 exports 是指向 module.exports 的一個引用。舉個例子可以說明他們兩個之間的關係。
const myFuns = { a: 1 };
let moduleExports = myFuns;
let myExports = moduleExports;
// moduleExports 重新指向
moduleExports = { b: 2 };
console.log(myExports);
// 輸出 {a : 1}
複製程式碼
// 也就是說在module.exports被重新複製時,exports與它的關係就gg了。解決方法就是重新指向
myExports = modulerExports;
console.log(myExports);
// 輸出 { b: 2 }
複製程式碼複製程式碼
4. ES6 module
對 ES6 有所瞭解的同志們應該都清楚,web 前端模組化在 ES6 之前,並不是語言規範,不像是其他語言 java、php 等存在名稱空間或者包的概念。上文提及的 AMD、CMD、CommonJS 規範,都是為了基於規範實現的模組化,並非 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
複製程式碼複製程式碼
emmmm,沒錯,export 保留字看起來是不是和 CommonJS 的 exports 有點像?我們嘗試 下從保留字對比 ES6 和 CommonJS。
保留字 | CommonJS | ES6 |
---|---|---|
require | 支援 | 支援 |
export / import | 不支援 | 支援 |
exports / module.exports | 支援 | 不支援 |
好吧,除了 require 兩個都可以用之外,其他實際上還是有明顯差別的。那麼問題來了,既然 require 兩個都可以用,那這兩個在 require 使用上,有差異嗎?
我們先對比下 ES6 module 和 CommonJS 之間的差異。
模組輸出 | 載入方式 | |
---|---|---|
CommonJS | 值拷貝 | 物件 |
ES6 | 引用(符號連結) | 靜態解析 |
又多了幾個新穎的詞彙,我們先通過例子來介紹一下值拷貝和引用的區別。
// 值拷貝 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之間,值拷貝和引用的區別吧。
複製程式碼複製程式碼
靜態解析,什麼是的靜態解析呢?區別於 CommonJS 的模組實現,ES6 的模組並不是一個物件,而只是程式碼集合。也就是說,ES6 不需要和 CommonJS 一樣,需要把整個檔案載入進去,形成一個物件之後,才能知道自己有什麼,而是在編寫程式碼的過程中,程式碼是什麼,它就是什麼。
PS:
- 目前各個瀏覽器、node.js 端對 ES6 的模組化支援實際上並不友好,更多實踐同志們有興趣可以自己搞一波。
- 在 ES6 中使用 require 字樣,靜態解析的能力將會丟失!
5. UMD
模組化規範中還有一個 UMD 也不得不提及一下。什麼是 UMD 呢?
UMD = AMD + CommonJS
複製程式碼複製程式碼
沒錯,UMD 就是這麼簡單。常用的場景就是當你封裝的模組需要適配不同平臺(瀏覽器、node.js),例如你寫了一個基於 Date 物件二次封裝的,對於時間的處理工具類,你想推廣給負責前端頁面開發的 A 同學和後臺 Node.js 開發的 B 同學使用,你是不是就需要考慮你封裝的模組,既能適配 Node.js 的 CommonJS 協議,也能適配前端同學使用的 AMD 協議?
二、工具時代
1. webpack
webpack 興起之後,什麼 AMD、CMD、CommonJS、UMD,似乎都變得不重要了。因為 webpack 的模組化能力真的強。
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();
複製程式碼複製程式碼
不僅如此,webpack 識別了你的模組之後,可以將其打包成 UMD、AMD 等等規範的模組重新輸出。例如上文提及到的你需要把 Date 模組封裝成 UMD 格式。只需要在 webpack 的 output 中新增 libraryTarget: 'UMD'即可。
2. more...
總結
回到開始我們提出的問題,我們嘗試使用一張圖彙總上文提及到的一溜模組化相關詞彙。