前言
初期的web端交互還是很簡單,不需要太多的js就能實(shí)現(xiàn)。隨著時代的的發(fā)展,用戶對Web瀏覽器的性能也提出了越來越高的要求,瀏覽器也越來越多的承擔(dān)了更多的交互,不再是寥寥數(shù)語的js就能解決的,那么就造成了前端代碼的日益膨脹,js之間的相互依賴也會越來越多,此時就需要使用一定的規(guī)范來管理js之間的依賴。
本文主要是什么是模塊化,為什么需要模塊化以及現(xiàn)下流行的模塊化規(guī)范:AMD,CMD,CommonJs,ES6。
什么是模塊化
要想理解模塊化是什么可以先理解模塊是什么?
模塊:能夠獨(dú)立命名并且能夠獨(dú)立完成一定功能的集合。
因此在js中就可以理解為模塊就是能夠?qū)崿F(xiàn)特定功能獨(dú)立的一個個js文件。
模塊化:就可以簡單的理解為將原來繁重復(fù)雜的整個js文件按功能或者按模塊拆成一個個單獨(dú)的js文件,然后將每一個js文件中的某些方法拋出去,給別的js文件去引用和依賴。
為什么需要模塊化
一、模塊化的進(jìn)程
1、全局function模式:將不同的功能封裝為不同的函數(shù)
缺點(diǎn):污染全局命名空間,容易引起命名沖突,看不出模塊間的依賴
2、namespace模式:封裝為對象模式
作用:減少全局變量,解決命名沖突
缺點(diǎn):數(shù)據(jù)不安全(外部函數(shù)可以修改模塊內(nèi)的數(shù)據(jù)),看不出模塊之間的依賴
const module = {
data:1,
getData(){console.log(this.data)}
}
module.data = 2; //這樣會直接修改模塊內(nèi)部的數(shù)據(jù)
3、IIFE模式:匿名函數(shù)自調(diào)用(閉包)
作用:解決了數(shù)據(jù)安全,數(shù)據(jù)是私有的,外部只能調(diào)用暴露的信息
缺點(diǎn):需要綁定到一個全局變量上例如window向外暴露,這樣也會有命名沖突的問題
4、IIFE增強(qiáng)模式:引入依賴
作用:解決了模塊直接的依賴問題
缺點(diǎn):引入js的時候需要注意引入的順序,并且當(dāng)依賴很多的時候也會有弊端
// IIFE模式:匿名函數(shù)自調(diào)用(閉包)
(function(window){
let data = '這是IIFE模式';
getData(){
console.log(data);
}
window.module = { getData }
})(window)
// IIFE增強(qiáng)模式
(function(window,$){
let data = '這是IIFE模式';
getData(){
console.log(data);
$('body').css('background', 'red')
}
window.module = { getData }
})(window,jQuery);
//index.html
//需要注意引入的順序
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
//當(dāng)<script>過多的時候的缺點(diǎn):
// 1. 請求過多
// 2. 依賴模糊:不清楚依賴直接是什么關(guān)系,很容易因?yàn)橐氲捻樞驅(qū)е鲁鲥e
// 3. 難以維護(hù)
從以上的發(fā)展歷程來看雖然模塊化還不是那么的完善,但是也不難能發(fā)現(xiàn)模塊化的優(yōu)點(diǎn):
二、模塊化的優(yōu)點(diǎn):
- 避免命名沖突
- 更好的分離模塊,按需加載
- 高復(fù)用性
- 高維護(hù)性
模塊的規(guī)范
AMD
AMD(Asynchronous Module Definition):異步模塊定義。采用異步方式加載模塊,模塊的加載不影響后續(xù)語句的執(zhí)行。所有依賴這個模塊的語句,都定義在一個回調(diào)函數(shù)中,等到加載完成之后,這個回調(diào)函數(shù)才會運(yùn)行。瀏覽器環(huán)境要從服務(wù)器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范。
RequireJs是AMD規(guī)范的最佳實(shí)踐,所以我們用AMD規(guī)范的時候要引入requirejs。
AMD的語法:
define用來定義模塊;
require用來加載模塊,通常AMD框架會以require方法作為入口,進(jìn)行依賴關(guān)系分析并依次有序地進(jìn)行加載。
AMD是依賴前置,就是說,在define或require中方法里傳入的模塊數(shù)組會在一開始就下載并執(zhí)行
//define定義模塊
//第一個參數(shù):依賴的模塊,沒有可以不寫
define(['module1','module2'],function(m1,m2){
//如果引入了module,但是沒有使用,模塊的內(nèi)容也會執(zhí)行
return 模塊;
})
//引入模塊,相當(dāng)于主函數(shù)的引用
require(['module1','module2'],function(m1,m2){
//使用m1,m2
//如果引入了module,但是沒有使用,模塊的內(nèi)容也會執(zhí)行
})
AMD具體使用流程
- 引入requirejs
a. requirejs官網(wǎng)下載:requirejs
b. github:requirejs
c. 或者直接用require的js鏈接放在index.html中引用(只建議在學(xué)習(xí)時候用): https://requirejs.org/docs/release/2.3.6/minified/require.js - 定義模塊
//module1.js
define(function(){
const msg='module1';
return {
msg
}
})
//module2.js
define(function(){
const msg="module2";
return {
msg
}
})
//module3.js
define(['module2'],function(m2) {
const msg="module3";
const msg2 = m2.msg
return {
msg,
msg2
};
});
//main.js,入口文件不需要module2,因此就不需要引入
require.config({
paths:{
jquery:'jquery' //用于引入第三方庫,此處填寫的是路徑,文件的路徑;當(dāng)然也可以在index.html用script標(biāo)簽引入
}
})
require(['module1','module3','jquery'],function(m1,m3,$){
console.log(m1,'module1');
console.log(m3,'module3');
console.log($,'第三方庫');
});
console.log('模塊加載'); //會優(yōu)先打印
- index.html的引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--data-main:文件入口-->
<script data-main="main" src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>
</body>
</html>
-
輸出結(jié)果
image
CMD
CMD(Common Module Definition):通用模塊定義。用于瀏覽器端,是除AMD以外的另一種模塊組織規(guī)范。結(jié)合了AMD與CommonJs(后面會講到)的特點(diǎn)。也是異步加載模塊。
與AMD不同的是:AMD推崇的是依賴前置,而CMD是依賴就近,延遲執(zhí)行。
依賴前置&&依賴就近,延遲執(zhí)行
//依賴前置:AMD
require(['module1','module2'],function(m1,m2){
//依賴的模塊首先加載,無論后續(xù)是否會用到
})
//依賴就近,延遲執(zhí)行
define(funciton(require){
const module1 = require('./module1'); //用到的時候再申明,不需要就不用申明,也就不會加載進(jìn)來
})
CMD具體使用步驟
- 引入sea.js
a. 官網(wǎng):https://seajs.github.io/seajs/docs/#downloads
b. github:https://github.com/seajs/seajs - 模塊定義
//module1.js
define(function(require,exports) {
const msg='這是模塊1';
// const module2 = require('./module2');
exports.msg = msg; // 注意這里是用的exports
// exports.module2 = module2; // 如果是多個就得這么寫,所以如果暴露多個接口不建議用exports
});
//module2.js
define(function(require,exports,module) {
const msg="這是模塊2";
module.exports = { //這里用的是module.exports,跟exports作用一樣
msg
}
});
//module3.js
define(function(require,exports,module) {
const msg="這是模塊3";
const module2 = require('./module2');
module.exports = { //這里用的是module.exports,跟exports作用一樣
msg,
module2
}
});
//main.js
define(function(require, exports,module) {
const module1 = require('./module1');
const module3 = require('./module3');
console.log(module1);
console.log(module3);
});
console.log('模塊加載');
- index.html引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="sea.js"></script>
<script>
seajs.use('main');
</script>
</body>
</html>
- 輸出結(jié)果
image
CommonJS
Node 應(yīng)用由模塊組成,采用 CommonJS 模塊規(guī)范。每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。在服務(wù)器端,模塊的加載是運(yùn)行時同步加載的;在瀏覽器端,模塊需要提前編譯打包處理。
CommonJs有4個畢竟重要的變量:module、require、exports、global
CommonJs的特點(diǎn):
- 所有代碼都運(yùn)行在模塊作用域,不會污染全局作用域;如果想要多個模塊共享一個變量,需要給global添加屬性(不建議這么用)
//module.js
global.data = '共享變量'; //添加到global的屬性是多個文件可以共享的
let x = {
a:5
}
let b=0;
const add = function(val){
x.a+=x.a;
b=++b;
}
module.exports.x=x; //只有對外暴露了變量,在外部引用的時候才能獲取到,否則x,b就是模塊內(nèi)部的私有變量
module.exports.add = add;
- 模塊可以多次加載,但是只有再第一次加載的時候才會執(zhí)行,后續(xù)的加載都是使用的緩存結(jié)果;如果想要再次加載需要清除緩存,或者對外暴露一個函數(shù),加載之后執(zhí)行暴露的函數(shù)
// test1.js
let msg="測試緩存";
console.log(msg);
exports.msg=msg;
// test2.js
const msg = require('./test1');
const msg2 = require('./test1');
// 上面的輸出結(jié)果是:測試緩存
// 會發(fā)現(xiàn)只打印了一次結(jié)果,以此可以證明只有第一次加載的時候才會執(zhí)行,第二次加載的時候使用的是緩存的結(jié)果
// 刪除指定模塊的緩存
delete require.cache[moduleName] // moduleName必須是絕對路徑
// 刪除所有模塊緩存
Object.keys(require.cache).forEach(function(key){
delete require.cache[key]
})
- 模塊加載的順序是按照再代碼中書寫的順序加載的,同步加載模塊。
- 引入的值其實(shí)是輸出值的拷貝(淺拷貝)。也就是說一旦值輸出之后,模塊內(nèi)的改變就不會影響這個值的改變(引用類型除外)
// example.js
let x= {
a:5
};
let b=0;
const add = function(val){
x.a+=x.a;
b=++b;
// return x.a++;
}
module.exports.x = x;
module.exports.b=b;
module.exports.add = add;
// main.js
const example = require('./example');
const add = require('./example').add;
example.add();
console.log(example.x) // {a:10}
console.log(example.b) // b:0
CommonJs的語法
//對外暴露接口
module.exports
exports
//引入模塊
require(模塊的路徑) //模塊的路徑有多種寫法,后續(xù)補(bǔ)充
module.exports&&exports
module.exports: 每個模塊內(nèi)部,module代表了當(dāng)前這個模塊,module是一個對象,對外暴露的就是exports這個屬性,加載某個模塊其實(shí)就相當(dāng)于加載的module.exports這個屬性
exports: 其實(shí)就是module.exports的引用。為了使用方便,node為每個模塊創(chuàng)建了一個exports變量,這個變量就指向了module.exports。因此以下兩種做法是錯誤的。
//錯誤一
exports='msg'; // 此時相當(dāng)于改變了exports的指向,失去了與module.exports的聯(lián)系,也就失去了對外暴露接口的能力
//錯誤二
exports.msg='msg';
module.exports = 'Hello world'; // 上面的msg是無法對外暴露的,因?yàn)閙odule.exports被重新賦值了;此時對外暴露的就是『Hello world』
ES6
ES modules(ESM)是 JavaScript 官方的標(biāo)準(zhǔn)化模塊系統(tǒng)。ES6模塊設(shè)計的思想是盡量的靜態(tài)化,使得編譯時就能知道模塊的依賴關(guān)系,以及輸入和輸出的變量。有兩個主要的命令:export和import。export用于對外暴露接口,import用于引入其他模塊。
ES6模塊的特點(diǎn):
- 嚴(yán)格模式:ES6 的模塊自動采用嚴(yán)格模式
- import read-only特性: import的屬性是只讀的,不能賦值,類似于const的特性
- export/import提升: import/export必須位于模塊頂級,不能位于作用域內(nèi);其次對于模塊內(nèi)的import/export會提升到模塊頂部,這是在編譯階段完成的
- 兼容在node環(huán)境下運(yùn)行
- ES modules 輸出的是值的引用,輸出接口動態(tài)綁定,而 CommonJS 輸出的是值的拷貝
//ES6模塊值的引用 .mjs 主要是為了能用node環(huán)境運(yùn)行:node --experimental-modules
// example.mjs
let x= {
a:5
};
let b=0;
const add = function(val){
x.a+=x.a;
b=++b;
}
export {x,b,add}
// main.mjs
import { x,b, add} from './example.mjs';
add();
console.log(x,b); // {a:10} 1
export&&import用法
export:用于向外暴露接口
import:用于引入外部接口
// 方法一:
//export單個向外暴露接口
export const x = 1;
export const y = {a:1}
export const add = function(){console.log(123)}
//export一起向外暴露接口
const x=1;
const y={a:1};
const add = function(){console.log(123)};
export {x,y,add}
//import引入外部接口
//針對以上兩種方式import可以寫成如下兩種情況
import {x,y,add} from './exmaple'; //使用哪個就引入哪個
import * as moduleName from './exmaple'; // 全部引入,使用的時候使用moduleName.x
// 方法二:
//除了使用export 向外暴露接口外還可以使用export default向外暴露接口:同一個模塊中export可以有多個,但是export default只能有一個
const x=1;
const y={a:1};
export default { x,y}
// 對應(yīng)的import
import moduleName from './exmaple'; //全部引入,使用方式moduleName.x
總結(jié)
- AMD:異步加載模塊,允許指定回調(diào)函數(shù)。AMD規(guī)范是依賴前置的。一般瀏覽器端會采用AMD規(guī)范。但是開發(fā)成本高,代碼閱讀和書寫比較困難。
- CMD:異步加載模塊。CMD規(guī)范是依賴就近,延遲加載。一般也是用于瀏覽器端。
- CommonJs:同步加載模塊,一般用于服務(wù)器端。對外暴露的接口是值的拷貝
- ES6:實(shí)現(xiàn)簡單。對外暴露的接口是值的引用??梢杂糜跒g覽器端和服務(wù)端。
后記
模塊化的一次有一次的變更,讓系統(tǒng)模塊化變得也越來越好,但是響應(yīng)的也引起了一些問題。例如使用模塊的時候,發(fā)現(xiàn)有些模塊引入了但是并沒有真正的使用到,這樣就造成了代碼的冗余,多了一些不必要的代碼。這些模塊有時通過檢查是很難發(fā)現(xiàn)的,因此就要想如何能夠快速的去除這些無用的模塊呢,此時Tree Shaking就出現(xiàn)了;又比如如何能夠在開發(fā)代碼的時候比較便捷,然后在生產(chǎn)中又有高強(qiáng)度的兼容性呢,此時就出現(xiàn)了babel;又比如如何預(yù)處理模塊,此時出現(xiàn)了webpack……
出現(xiàn)了解決問題的辦法,就得繼續(xù)學(xué)習(xí)啦……
參考文章
前端模塊化的十年征程
徹底理清 AMD,CommonJS,CMD,UMD,ES6 modules
前端模塊化—CommonJS、CMD、AMD、UMD和ESM
前端模塊化詳解(完整版)