前言
說到前端模塊化,你第一時間能想到的是什麼?Webpack?ES6 Module?還有嗎?咱們一塊兒來看一下下圖。 javascript
![模塊化關鍵詞](http://static.javashuo.com/static/loading.gif)
1、千絲萬縷
爲了更貼合咱們的平常開發場景(先後端分離),咱們嘗試先從不一樣平臺的維度區分,做爲本文的切入點。php
1. 根據平臺劃分
平臺 | 規範 | 特性 |
---|---|---|
瀏覽器 | AMD、CMD | 存在網絡瓶頸,使用異步加載 |
非瀏覽器 | CommonJS | 直接操做 IO,同步加載 |
能夠看到咱們很是暴力的以是否是瀏覽器做爲劃分標準。仔細分析一下,他們之間最大的差別在於其特性上,是否存在瓶頸。 例如說網絡性能瓶頸,每一個模塊的請求都須要發起一次網絡請求,並等待資源下載完成後再進行下一步操做,那整個用戶體驗是很是糟糕的。 根據該場景,咱們簡化一下,以同步加載和異步加載兩個維度進行區分。前端
特性 | 規範 |
---|---|
同步加載 | CommonJS |
異步加載 | AMD、CMD |
2. AMD、CMD 兩大規範
先忽略 CommonJS,咱們先介紹下,曾經一度盛行的 AMD、CMD 兩大規範。java
規範 | 約束條件 | 表明做 |
---|---|---|
AMD | 依賴前置 | requirejs |
CMD | 就近依賴 | seajs |
AMD、CMD 提供了封裝模塊的方法,實現語法上相近,甚至於 requirejs 在後期也默默支持了 CMD 的寫法。咱們用一個例子,來說清楚這兩個規範之間最大的差別:依賴前置和就近依賴。node
AMD:webpack
web
// 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()); } }; });es6
複製代碼// 輸出 // 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
複製代碼複製代碼
咱們的理論依據是模塊對象,根據該依據咱們進行以下分析。
一、 a.js準備加載,在內存中生成module對象moduleA 二、 a.js執行exports.x = 'a1'; 在moduleA的exports屬性中添加x 三、 a.js執行console.log('a.js', require('./b.js').x); 檢測到require關鍵字,開始加載b.js,a.js執行暫停 四、 b.js準備加載,在內存中生成module對象moduleB 五、 b.js執行exports.x = 'b1'; 在moduleB的exports屬性中添加x 六、 b.js執行console.log('b.js', require('./a.js').x); 檢測到require關鍵字,開始加載a.js,b.js執行暫停 七、 檢測到內存中存在a.js的module對象moduleA,因而能夠將第6步當作console.log('b.js', moduleA.x); 在第二步中moduleA.x賦值爲a1,因而輸出b.js, a1 八、 b.js繼續執行,exports.x = 'b2',改寫moduleBexports的x屬性 九、 b.js執行完成,回到a.js,此時同理能夠將第3步當作console.log('a.js', modulerB.x); 輸出了a.js, b2 十、 a.js繼續執行,改寫exports.x = 'a2' 十一、 輸出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 協議?
2、工具時代
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...
總結
回到開始咱們提出的問題,咱們嘗試使用一張圖彙總上文說起到的一溜模塊化相關詞彙。
![總結](http://static.javashuo.com/static/loading.gif)