隨著前端js代碼復(fù)雜度的提高,JavaScript模塊化這個(gè)概念便被提出來,前端社區(qū)也不斷地實(shí)現(xiàn)前端模塊化,直到es6對其進(jìn)行了規(guī)范,下面就介紹JavaScript模塊化。
第一階段:無模塊化
JavaScript最初的作用僅僅是驗(yàn)證表單,后來會(huì)添加一些動(dòng)畫,但是這些js代碼很多在一個(gè)文件中就可以完成了,所以,我們只需要在html文件中添加一個(gè)script標(biāo)簽。
后來,隨著前端復(fù)雜度提高,為了能夠提高項(xiàng)目代碼的可讀性、可擴(kuò)展性等,我們的js文件逐漸多了起來,不再是一個(gè)js文件就可以解決的了,而是把每一個(gè)js文件當(dāng)做一個(gè)模塊。那么,這時(shí)的js引入方式是怎樣的呢?大概是下面這樣:
<script src="jquery.js"></script>
<script src="jquery_scroller.js"></script>
<script src="main.js"></script>
<script src="other1.js"></script>
<script src="other2.js"></script>
<script src="other3.js"></script>
即簡單的將所有的js文件統(tǒng)統(tǒng)放在一起。但是這些文件的順序還不能出錯(cuò),比如jquery需要先引入,才能引入jquery插件,才能在其他的文件中使用jquery。
優(yōu)點(diǎn):
- 相比于使用一個(gè)js文件,這種多個(gè)js文件實(shí)現(xiàn)最簡單的模塊化的思想是進(jìn)步的。
缺點(diǎn):
- 污染全局作用域。 因?yàn)槊恳粋€(gè)模塊都是暴露在全局的,簡單的使用,會(huì)導(dǎo)致全局變量命名沖突,當(dāng)然,我們也可以使用命名空間的方式來解決。
- 對于大型項(xiàng)目,各種js很多,開發(fā)人員必須手動(dòng)解決模塊和代碼庫的依賴關(guān)系,后期維護(hù)成本較高。
- 依賴關(guān)系不明顯,不利于維護(hù)。比如main.js需要使用jquery,但是,從上面的文件中,我們是看不出來的,如果jquery忘記了,那么就會(huì)報(bào)錯(cuò)。
第二階段: CommonJS規(guī)范(require導(dǎo)入加載 , exports 輸出,modul.exports輸出)
CommonJS就是一個(gè)JavaScript模塊化的規(guī)范,該規(guī)范最初是用在服務(wù)器端的node的,前端的webpack也是對CommonJS原生支持的。
根據(jù)這個(gè)規(guī)范,每一個(gè)文件就是一個(gè)模塊,其內(nèi)部定義的變量是屬于這個(gè)模塊的,不會(huì)對外暴露,也就是說不會(huì)污染全局變量。
CommonJS的核心思想就是通過 require 方法來同步加載所要依賴的其他模塊,然后通過 exports 或者 module.exports 來導(dǎo)出需要暴露的接口。如下所示:
// a.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
這里的a.js就是一個(gè)CommonJS規(guī)范的模塊了。 這里的module就代表了這個(gè)模塊,module的exports屬性就是對外暴露的接口,可以對外導(dǎo)出外部可以訪問的變量,比如這里的x和addX。
然后我們就可以在其他模塊中引入這個(gè)模塊使用了:
var a = require('./a.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
這里的require就會(huì)獲取到a.js所暴露的module.exports變量,然后就可以使用其暴露的x和addX了。
module.exports和exports的區(qū)別
穿插一個(gè)必備小知識:在文件a.js中用exports或module.exports導(dǎo)出的對象(方法、變量),可以在另一個(gè)文件b.js中通過require('./a')引用。
console.log(exports);//輸出:{}
console.log(module);//輸出:Module {..., exports: {}, ...} (注:...代表省略了其他一些屬性)
從打印我們可以看出,module.exports和exports一開始都是一個(gè)空對象{},實(shí)際上,這兩個(gè)對象指向同一塊內(nèi)存。這也就是說module.exports和exports是等價(jià)的(有個(gè)前提:不去改變它們指向的內(nèi)存地址)。
例如:exports.age = 18和module.exports.age = 18,這兩種寫法是一致的(都相當(dāng)于給最初的空對象{}添加了一個(gè)屬性,通過require得到的就是{age: 18})。
注意,不能直接將exports變量指向一個(gè)值,因?yàn)檫@樣等于切斷了exports與module.exports的聯(lián)系。
require引入的對象本質(zhì)上是module.exports。所以如果直接給exports賦值 改變了指針,就會(huì)造成引用的時(shí)候結(jié)果是空對象{};
1.js
exports = {name: 'lokka'}
2.js
let a = require('./1');
console.log(a);//輸出:{} 因?yàn)?1.js 中的module.exports = {}
優(yōu)點(diǎn):
- CommonJS規(guī)范在服務(wù)器端率先完成了JavaScript的模塊化,解決了依賴、全局變量污染的問題,這也是js運(yùn)行在服務(wù)器端的必要條件。
缺點(diǎn):
- 此文主要是瀏覽器端js的模塊化, 由于 CommonJS
是同步加載模塊的,在服務(wù)器端,文件都是保存在硬盤上,所以同步加載沒有問題,但是對于瀏覽器端,需要將文件從服務(wù)器端請求過來,那么同步加載就不適用了,所以,CommonJS是不適用于瀏覽器端的。
第三階段: AMD規(guī)范(define, require)
之前提到: CommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。AMD規(guī)范則是非同步加載模塊,允許指定回調(diào)函數(shù)。由于Node.js主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規(guī)范比較適用。但是,如果是瀏覽器環(huán)境,要從服務(wù)器端加載模塊,這時(shí)就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范。而AMD規(guī)范的實(shí)現(xiàn),就是大名鼎鼎的 require.js 了。
AMD標(biāo)準(zhǔn)中,定義了下面兩個(gè)API:
1. define(id, [depends], callback)
2.require([module], callback)
即通過define來定義一個(gè)模塊,然后使用require來加載一個(gè)模塊。 并且,require還支持CommonJS的模塊導(dǎo)出方式。
1. 獨(dú)立模塊
- 我們使用 RequireJS 定義一個(gè)不依賴其他模塊得獨(dú)立模塊,文件名:a.js
define(function(){
var add = function(x,y) {
return x + y;
};
return {
add : add
}
});
- 接著創(chuàng)建一個(gè) html 頁面,其內(nèi)部加載并調(diào)用這個(gè)模塊。
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="require.js"></script>
<script type="text/javascript">
require(['a'], function (m){
console.log(m.add(2,3));
});
</script>
</head>
<body>
</body>
</html>
2. 存在依賴的函數(shù)式定義
define(["./cart", "./inventory"], function(cart, inventory) {
//return an object to define the "my/shirt" module.
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
}
);
但是,在使用require.js的時(shí)候,我們必須要提前加載所有的依賴,然后才可以使用,而不是需要使用時(shí)再加載。
優(yōu)點(diǎn):
適合在瀏覽器環(huán)境中異步加載模塊??梢圆⑿屑虞d多個(gè)模塊。
缺點(diǎn):
提高了開發(fā)成本,并且不能按需加載,而是必須提前加載所有的依賴。
第四階段:CMD規(guī)范( exports 暴露模塊接口、modul.exports 暴露模塊對象, seajs.use引入)
CMD規(guī)范是阿里的玉伯提出來的,實(shí)現(xiàn)js庫為sea.js。 它和requirejs非常類似,即一個(gè)js文件就是一個(gè)模塊,但是CMD的加載方式更加優(yōu)秀,是通過按需加載的方式,而不是必須在模塊開始就加載所有的依賴。如下:
創(chuàng)建一個(gè)js a.js
define(function(require, exports, module) {
// 對外提供name屬性
exports.name = 'lokka';
// 對外提供hello方法
exports.hello = function() {
console.log('Hello lokka');
};
})
創(chuàng)建一個(gè) html 頁面,其內(nèi)部加載并調(diào)用這個(gè)模塊 a.js。
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="sea.js"></script>
<script type="text/javascript">
//加載一個(gè)模塊,在加載完成時(shí),執(zhí)行回調(diào)
seajs.use('a', function(a) {
a.hello(); // Hello lokka
});
</script>
</head>
<body>
</body>
</html>
優(yōu)點(diǎn):
同樣實(shí)現(xiàn)了瀏覽器端的模塊化加載。
可以按需加載,依賴就近。
缺點(diǎn):
依賴SPM打包,模塊的加載邏輯偏重。
其實(shí),這時(shí)我們就可以看出AMD和CMD的區(qū)別了,前者是對于依賴的模塊提前執(zhí)行,而后者是延遲執(zhí)行。 前者推崇依賴前置,而后者推崇依賴就近,即只在需要用到某個(gè)模塊的時(shí)候再require。 如下:
// AMD
define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好
a.doSomething()
// 此處略去 100 行
b.doSomething()
...
});
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此處略去 100 行
var b = require('./b')
// 依賴可以就近書寫
b.doSomething()
// ...
});
第五階段: ES6模塊化(export導(dǎo)出,export default導(dǎo)出,import導(dǎo)入加載)
之前的幾種模塊化方案都是前端社區(qū)自己實(shí)現(xiàn)的,只是得到了大家的認(rèn)可和廣泛使用,而ES6的模塊化方案是真正的規(guī)范。 在ES6中,我們可以使用 import 關(guān)鍵字引入模塊,通過 export 關(guān)鍵字導(dǎo)出模塊,功能較之于前幾個(gè)方案更為強(qiáng)大,也是我們所推崇的,但是由于ES6目前無法在瀏覽器中執(zhí)行,所以,我們只能通過babel將不被支持的import編譯為當(dāng)前受到廣泛支持的 require。
雖然目前import和require的區(qū)別不大,但是還是推薦使用使用es6,因?yàn)槲磥韊s6必定是主流,對于代碼的遷移成本還是非常容易的。 如:
1. 用 export 導(dǎo)出
- a.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
- b.js
逐一加載 必須有{}包起來
import { area, circumference } from './circle';
console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
整體加載
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
2. 用 export default 導(dǎo)出
- a.js
默認(rèn)輸出
export default function () {
console.log('foo');
}
指定函數(shù)名輸出
// export default命令用在非匿名函數(shù)前,也是可以的。
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者寫成
function foo() {
console.log('foo');
}
export default foo;
- b.js
import customName from './export-default';
customName(); // 'foo'
export 和 export default 的區(qū)別
// -----------------------------------------------------------
// 1.當(dāng)用export default people導(dǎo)出時(shí),就用 import people 導(dǎo)入(不帶大括號)
// 2.一個(gè)文件里,有且只能有一個(gè)export default。但可以有多個(gè)export。
// 3.當(dāng)用export name 時(shí),就用import { name }導(dǎo)入(記得帶上大括號)
// 4.當(dāng)一個(gè)文件里,既有一個(gè)export default people, 又有多個(gè)export name 或者 // export age時(shí),導(dǎo)入就用 import people, { name, age }
// 5.當(dāng)一個(gè)文件里出現(xiàn)n多個(gè) export // 導(dǎo)出很多模塊,導(dǎo)入時(shí)除了一個(gè)一個(gè)導(dǎo)入,也可以用import * as example
// -----------------------------------------------------------