我們知道,在NodeJS之前,由於沒有過於復雜的開發場景,前端是不存在模塊化的,后端才有模塊化。NodeJS誕生之后,它使用CommonJS的模塊化規范。從此,js模塊化開始快速發展。
模塊化的開發方式可以提供代碼復用率,方便進行代碼的管理。通常來說,一個文件就是一個模塊,有自己的作用域,只向外暴露特定的變量和函數。目前流行的js模塊化規范有CommonJS、AMD、CMD以及ES6的模塊系統。下面開始一一介紹:
CommonJS
NodeJS是CommonJS規范的主要實踐者,它有四個重要的環境變量為模塊化的實現提供支持:module
、exports
、require
、global
。實際使用時,用module.exports
定義當前模塊對外輸出的接口(不推薦直接用exports
),用require
加載模塊。
// 定義模塊math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在這里寫上需要向外暴露的函數、變量
add: add,
basicNum: basicNum
}
/** 必須加./路徑,不加的話只會去node_modules文件找 **/
// 引用自定義的模塊時,參數包含路徑,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模塊時,不需要帶路徑
var http = require('http');
http.createService(...).listen(3000);
CommonJS用同步的方式加載模塊。在服務端,模塊文件都存放在本地磁盤,讀取非常快,所以這樣做不會有問題。但是在瀏覽器端,限於網絡原因,更合理的方案是使用異步加載。
exports
和module.export
區別:
exports
:對於本身來講是一個變量(對象),它不是module的引用,它是{}
的引用,它指向module.exports
的{}模塊。只能使用.
語法 向外暴露變量。
module.exports
:module
是一個變量,指向一塊內存,exports
是module
中的一個屬性,存儲在內存中,然后exports
屬性指向{}
模塊。既可以使用.
語法,也可以使用=
直接賦值。
AMD和require.js
AMD規范采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之后,這個回調函數才會運行。這里介紹用require.js實現AMD規范的模塊化:用require.config()
指定引用路徑等,用definde()
定義模塊,用require()
加載模塊。
首先我們需要引入require.js文件和一個入口文件main.js。main.js中配置require.config()
並規定項目中用到的基礎模塊。
/** 網頁中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模塊 **/
// 首先用config()指定各模塊路徑和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //實際路徑為js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 執行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
引用模塊的時候,我們將模塊名放在[]
中作為reqiure()
的第一參數;如果我們定義的模塊本身也依賴其他模塊,那就需要將它們放在[]
中作為define()
的第一參數。
// 定義math.js模塊
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定義一個依賴underscore.js的模塊
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模塊,將模塊放在[]內
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
CMD和sea.js
AMD的實現者require.js在申明依賴的模塊時,會在第一時間加載並執行模塊內的代碼:
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等於在最前面聲明並初始化了要用到的所有模塊
if (false) {
// 即便沒用到某個模塊 b,但 b 還是提前執行了。**這就CMD要優化的地方**
b.foo()
}
});
CMD是另一種js模塊化方案,它與AMD很類似,不同點在於:AMD推崇依賴前置、提前執行,CMD推崇依賴就近、延遲執行。此規范其實是在sea.js推廣過程中產生的。
/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等於在最前面聲明並初始化了要用到的所有模塊
a.doSomething();
if (false) {
// 即便沒用到某個模塊 b,但 b 還是提前執行了
b.doSomething()
}
});
/** CMD寫法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要時申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定義模塊 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加載模塊
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
ES6 Module
ES6 在語言標准的層面上,實現了模塊功能,而且實現得相當簡單,旨在成為瀏覽器和服務器通用的模塊解決方案。其模塊功能主要由兩個命令構成:export
和import
。export
命令用於規定模塊的對外接口,import
命令用於輸入其他模塊提供的功能。
/** 定義模塊 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模塊 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
如上例所示,使用import
命令的時候,用戶需要知道所要加載的變量名或函數名。其實ES6還提供了export default
命令,為模塊指定默認輸出,對應的import
語句不需要使用大括號。這也更趨近於ADM的引用寫法。
/** export default **/
//定義輸出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
ES6的模塊不是對象,import
命令會被 JavaScript 引擎靜態分析,在編譯時就引入模塊代碼,而不是在代碼運行時加載,所以無法實現條件加載。也正因為這個,使得靜態分析成為可能。
ES6 模塊的特征:
- 嚴格模式:ES6 的模塊自動采用嚴格模式
import
read-only特性:import
的屬性是只讀的,不能賦值,類似於const
的特性export/import
提升:import/export
必須位於模塊頂級,不能位於作用域內;其次對於模塊內的import/export
會提升到模塊頂部,這是在編譯階段完成的
ES6 模塊與 CommonJS 模塊的差異
1. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用
- CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。
- ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令
import
,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import
有點像 Unix 系統的“符號連接”,原始值變了,import
加載的值也會跟着變。因此,ES6 模塊是動態引用,並且不會緩存值,模塊里面的變量綁定其所在的模塊。
2. CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口
- 運行時加載: CommonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,然后再從這個對象上面讀取方法,這種加載稱為“運行時加載”。
- 編譯時加載: ES6 模塊不是對象,而是通過
export
命令顯式指定輸出的代碼,import
時采用靜態命令的形式。即在import
時可以指定加載某個輸出值,而不是加載整個模塊,這種加載稱為“編譯時加載”。模塊內部引用的變化,會反應在外部。
CommonJS 加載的是一個對象(即module.exports
屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
廢話不多說,直接看代碼:
首先看個CommonJS輸出拷貝的例子:
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
a = 2;
b = { num: 2 };
}, 200);
module.exports = {
a,
b,
};
// main.js
// node main.js
let {a, b} = require('./a');
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {
console.log(a); // 1
console.log(b); // { num: 1 }
}, 500);
所謂輸出拷貝,如果了解過 NodeJS 或者 webpack 對 CommonJS 的實現(不了解可以看這篇文章),就會知道:exports
對象是模塊內外的唯一關聯, CommonJS 輸出的內容,就是exports
對象的屬性,模塊運行結束,屬性就確定了。
再看ES6 Module輸出的例子:
// a.mjs
let a = 1;
let b = { num: 1 }
setTimeout(() => {
a = 2;
b = { num: 2 };
}, 200);
export {
a,
b,
};
// main.mjs
// node --experimental-modules main.mjs
import {a, b} from './a';
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {
console.log(a); // 2
console.log(b); // { num: 2 }
}, 500);
以上就是 ES6 Module 輸出引用和 CommonJS 輸出值的區別,模塊內部引用的變化,會反應在外部,這是 ES6 Module 的規范。
總結
-
AMD/CMD/CommonJs 是js模塊化開發的規范,對應的實現是require.js/sea.js/Node.js
-
CommonJs 主要針對服務端,AMD/CMD/ES Module主要針對瀏覽器端,容易混淆的是AMD/CMD。(順便提一下,針對服務器端和針對瀏覽器端有什么本質的區別呢?服務器端一般采用同步加載文件,也就是說需要某個模塊,服務器端便停下來,等待它加載再執行。這里如果有其他后端語言,如java。而瀏覽器端要保證效率,需要采用異步加載,這就需要一個預處理,提前將所需要的模塊文件並行加載好。)
-
AMD/CMD區別,雖然都是並行加載js文件,但還是有所區別,AMD是預加載,在並行加載js文件同時,還會解析執行該模塊(因為還需要執行,所以在加載某個模塊前,這個模塊的依賴模塊需要先加載完成);而CMD是懶加載,雖然會一開始就並行加載js文件,但是不會執行,而是在需要的時候才執行。
-
AMD/CMD的優缺點.一個的優點就是另一個的缺點, 可以對照瀏覽。
AMD優點:加載快速,尤其遇到多個大文件,因為並行解析,所以同一時間可以解析多個文件。
AMD缺點:並行加載,異步處理,加載順序不一定,可能會造成一些困擾,甚至為程序埋下大坑。CMD優點:因為只有在使用的時候才會解析執行js文件,因此,每個JS文件的執行順序在代碼中是有體現的,是可控的。
CMD缺點:執行等待時間會疊加。因為每個文件執行時是同步執行(串行執行),因此時間是所有文件解析執行時間之和,尤其在文件較多較大時,這種缺點尤為明顯。(PS:重新看這篇文章,發現這里寫的不是很准確。確切來說,JS是單線程,所有JS文件執行時間疊加在AMD和CMD中是一樣的。但是CMD是使用時執行,沒法利用空閑時間,而AMD是文件加載好就執行,往往可以利用一些空閑時間。這么來看,CMD比AMD的優點還是很明顯的,畢竟AMD加載好的時候也未必就是JS引擎的空閑時間!)
-
CommonJS 和 ES Module 區別:CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用
-
如何使用?CommonJs 的話,因為 NodeJS 就是它的實現,所以使用 node 就行,也不用引入其他包。AMD則是通過
<script>
標簽引入require.js,CMD則是引入sea.js
參考: