我最早接觸前端應(yīng)該是在 2013 年左右频敛,雖然那個(gè)時(shí)候還在讀大二,但已經(jīng)和同學(xué)開始折騰一些校園創(chuàng)業(yè)項(xiàng)目。當(dāng)時(shí)希望開發(fā)一個(gè)面向校園的網(wǎng)上零食商城蜂厅,我們從批發(fā)市場進(jìn)貨然后在校園內(nèi)網(wǎng)上以低于「小賣部」的價(jià)格出售愉择。由于人手不足劫乱,寫后端的我也同時(shí)兼顧了前端的開發(fā)工作[1]。
[1] 這種情況在讀研甚至工作的時(shí)候也在不斷發(fā)生锥涕,以至于現(xiàn)在我都懷疑自己的前端代碼量可能快要超過后端代碼量了衷戈。
雖然當(dāng)時(shí)沒有系統(tǒng)性的學(xué)習(xí)過前端,但得益于諸多開發(fā)者在網(wǎng)上分享和開源自己的前端組件层坠,使得我在開發(fā)過程中能夠隨時(shí)使用現(xiàn)成組件殖妇,從而快速搭建出了系統(tǒng)的第一版。
海量的前端組件庫為當(dāng)時(shí)的開發(fā)帶來了極大的便利破花,但也帶來了一些困擾谦趣。這些組件很多時(shí)候需要使用 script
命令引入,例如:
<!-- a 組件需要用到的庫 -->
<script src="a1.js"></script>
<!-- b 組件需要用到的庫 -->
<script src="b1.js"></script>
<script src="b2.js"></script>
<!-- c 組件需要用到的庫 -->
<script src="c1.js"></script>
在開發(fā)過程中座每,經(jīng)常會(huì)遇到:
- 引入的
.js
文件越來越多前鹅,越來越復(fù)雜 - 上述
.js
文件引入順序被不小心改變導(dǎo)致某個(gè)庫不可用 - 引入一個(gè)新的
.js
文件導(dǎo)致某個(gè)庫不可用
隨著使用的組件越來越多,引入的庫越來越復(fù)雜峭梳,上面的問題反復(fù)出現(xiàn)舰绘,很多時(shí)候需要花很多時(shí)間去理清各個(gè)組件庫的依賴,必要時(shí)更新庫的版本、調(diào)整引入順序捂寿,甚至修改組件內(nèi)部的沖突代碼才能使得頁面正常運(yùn)行口四。可以說當(dāng)時(shí)我有大量的時(shí)間都花費(fèi)在了這些不必要的調(diào)試上秦陋,最終導(dǎo)致我的前端開發(fā)初體驗(yàn)并不好窃祝,甚至可以說糟糕。
直到后面接觸了 Node.js
踱侣,我才意識(shí)到之前在瀏覽器端遇到的種種問題的根本原因是:模塊化的缺失粪小。
模塊化歷史與演化
函數(shù)封裝
樸實(shí)無華:全局函數(shù)寫法
上面提及的通過 script
引入的 .js
文件通常都是第三方 UI 組件所需要的模塊,這些模塊內(nèi)封裝了對(duì)應(yīng)的函數(shù)或功能抡句,為的是提高程序的復(fù)用性和可維護(hù)性探膊。
那么該如何封裝一個(gè)可被復(fù)用的模塊?最為原始和簡單的方式就是將公用函數(shù)放到一個(gè) xxx.js
文件中:
/* utils.js */
function HandleDog() {
// ...
}
function HandleCat() {
// ...
}
使用時(shí)通過 script
語句導(dǎo)入皆可:
<script src="utils.js"/>
但是通過 script
語句引入之后待榔,HandleDog
函數(shù) 和 HandleCat
函數(shù) 以及內(nèi)部的其他變量實(shí)際就成為了全局變量逞壁。
全局變量很容易遇到命名沖突問題,即很有可能你引用的另一個(gè)模塊 utils2.js
內(nèi)有同名的 HandleDog
函數(shù)锐锣。同時(shí)自己編寫代碼的時(shí)候更要處處當(dāng)心自己的函數(shù)腌闯、變量和模塊內(nèi)的函數(shù)、變量產(chǎn)生沖突雕憔。
小小優(yōu)化:對(duì)象寫法
為了在一定程度上解決或改進(jìn)「全局函數(shù)寫法」姿骏,可以將函數(shù)、變量封裝成一個(gè)對(duì)象斤彼,如:
var Utils = {
_name: 'utils',
_desc: 'utils for dog and cat',
HandleDog: function() {/* ... */},
HandleCat: function() {/* ... */},
}
因?yàn)閷?duì) HandleDog
分瘦、HandleCat
等變量封裝了一層,所以減少了產(chǎn)生沖突的概率琉苇。但這依然不能從根本上解決命名沖突這個(gè)問題嘲玫,所以當(dāng)時(shí)有一些模塊如 Yahoo! 的 YUI2
進(jìn)一步引入了「命名空間」的概念,即給模塊再加上一個(gè)類似「Java 包名」的唯一前綴:
var cn = {};
cn.front = {};
cn.front.Utils = {};
cn.front.Utils._name = 'utils';
cn.front.Utils._desc = 'utils for dog and cat';
cn.front.Utils.HandleDog = function() {/* ... */};
cn.front.Utils.HandleCat = function() {/* ... */};
上述代碼確實(shí)算是解決了命名沖突的問題并扇,但由于沒有 Java 中的 import
關(guān)鍵字實(shí)現(xiàn)前綴省略去团,所以每次調(diào)用模塊內(nèi)函數(shù)或變量都得寫全命名空間:
if (type === 'dog') {
var d_result = cn.front.Utils.HandleDog(dog);
} else {
var c_result = cn.front.Utils.HandleCat(cat);
}
這樣的編程體驗(yàn)著實(shí)令人痛苦。同時(shí)當(dāng)前的寫法依然存在一個(gè)重大的缺陷穷蛹,即變量和函數(shù)實(shí)際沒能實(shí)現(xiàn)真正的封裝土陪,變量和函數(shù)對(duì)外都是公開暴露的,我們隨時(shí)可以訪問甚至修改模塊內(nèi)部的屬性:
// 修改模塊內(nèi)部變量
cn.front.Utils._name = 'changed';
有點(diǎn)意思:立即執(zhí)行函數(shù)寫法 IIFE
立即執(zhí)行函數(shù)(Immediately-Invoked Function Expression俩莽,IIFE)旺坠,能夠?qū)崿F(xiàn)函數(shù)聲明的同時(shí)立即被執(zhí)行。在 JS 中扮超,一個(gè)函數(shù)被執(zhí)行時(shí)將創(chuàng)建一個(gè)新的上下文取刃,在這個(gè)函數(shù)內(nèi)定義的變量處于一個(gè)單獨(dú)的作用域內(nèi)蹋肮,只能被函數(shù)內(nèi)部訪問,不會(huì)暴露到函數(shù)外部璧疗。這樣再結(jié)合閉包[2]坯辩,就能夠?qū)崿F(xiàn)私有變量封裝的效果:
function createModule() {
var _name = 'utils';
var _desc = 'utils for dog and cat';
var HandleDog = function() {/* ... */};
var HandleCat = function() {/* ... */};
var GetName = function() {/* ... */};
const ret = {
HandleDog: HandleDog,
HandleCat: HandleCat,
GetName: GetName,
};
return ret;
}
const moduleA = createModule();
// 如果想要訪問 _name,只能通過對(duì)外開放的 GetName 函數(shù)
// console.log(moduleA._name); // 錯(cuò)誤崩侠,moduleA 無法直接訪問內(nèi)部私有的 _name
[2] 有關(guān)閉包可以閱讀這篇文章: 一個(gè)故事講閉包
我們需要再進(jìn)一步漆魔,上面的 createModule
實(shí)際也是對(duì)這個(gè)模塊的不必要命名,因?yàn)槲覀冎皇窍氲玫缴厦婧瘮?shù)返回的 ret
結(jié)果 而已却音,因?yàn)檫@個(gè) ret
結(jié)果 就是模塊本身改抡。既然如此我們希望 createModule
函數(shù)是匿名函數(shù)且能夠立即執(zhí)行,從而獲取到我們需要的模塊系瓢。那么該如何讓這個(gè)函數(shù)立即執(zhí)行呢阿纤?調(diào)用形式是否可以如下呢:
// 錯(cuò)誤寫法
function createModule() {
/* ... */
}();
調(diào)用一個(gè)函數(shù)的語法,是在函數(shù)名的后面添加上 ()
符號(hào)夷陋,所以可能自然的想到上面的寫法士败。但是上面寫法是錯(cuò)誤的攒钳,因?yàn)?JavaScript 引擎在解析時(shí),在行首遇到 funciton
關(guān)鍵字時(shí)會(huì)將其視「函數(shù)聲明」吕粹,而函數(shù)調(diào)用語句 funcA()
則是一個(gè)「函數(shù)表達(dá)式」稽煤。為了讓 JavaScript 引擎知曉后面是一個(gè)函數(shù)表達(dá)式富玷,我們可以在行首加上 (
败潦,也就是用 ()
將函數(shù)聲明包起來:
// 當(dāng)然還有其他寫法讓 JavaScript 引擎知曉后面是一個(gè)函數(shù)表達(dá)式
// 例如行首添加 `+`啃擦、`-` 等符號(hào),這里不做擴(kuò)展
(function createModule() {
/* ... */
})();
同時(shí) createModule
可以是一個(gè)匿名函數(shù)诺凡,最終得到:
var module = (function() {
/* ... */
})();
上述的立即執(zhí)行函數(shù) IIFE东揣,最終實(shí)現(xiàn)了對(duì)模塊內(nèi)函數(shù)和私有變量的真正封裝践惑,我們可以通過 module
調(diào)用模塊內(nèi)的公開函數(shù)或公開變量腹泌,但同時(shí)又無法訪問任何不該被外部知曉的私有變量。
立即執(zhí)行函數(shù) IIFE 比起之前的幾種封裝方案尔觉,已經(jīng)有相當(dāng)大的進(jìn)步了凉袱,當(dāng)年的 JQuery
就是大量采用 IIFE 實(shí)現(xiàn)模塊封裝。但模塊化并不是一個(gè)簡單的事情侦铜,要考慮的事情是多方面的专甩。IIFE 方案依然面臨一個(gè)最基本的問題:模塊之間的依賴問題。
如果我們想要在一個(gè)模塊中使用另一個(gè)模塊钉稍,其實(shí)可以通過立即執(zhí)行函數(shù)傳參的方式實(shí)現(xiàn):
var module = (function($) {
/* ... */
$('.class').css("color","red");
})(jQuery);
// 通過傳入 window涤躲,將模塊掛在 window 下
(function(window, $) {
/* ... */
$('.class').css("color","red");
window.utils = utils;
})(window, jQuery);
如上所示,將一個(gè)模塊 jQuery
通過參數(shù)傳入 module
模塊贡未,就能在 module
模塊內(nèi)部使用 jQuery
模塊了种樱。但是將 jQuery 這個(gè)參數(shù)傳遞給 module
的前提是:jQuery 已經(jīng)被加載初始化蒙袍。也就是在通過 script
引入模塊時(shí),需要將 jQuery 引入寫在 module 模塊引入之前嫩挤,此時(shí)這個(gè)潛在的次序關(guān)系必須由程序員手動(dòng)管理害幅。這也就是上文我提到的現(xiàn)象:「需要經(jīng)常調(diào)整和管理 script 腳本之間的先后順序」。
隨著模塊的數(shù)量不斷增加岂昭,模塊之前的依賴變得愈發(fā)復(fù)雜以现,看來 JavaScript 的模塊化[3]依然任重道遠(yuǎn)。
[3] 因?yàn)?JavaScript 的設(shè)計(jì)發(fā)明過于倉促和簡化(Brendan Eich 僅用了十天就推出了第一版)约啊,所以在設(shè)計(jì)之處留下了很多的「坑」邑遏。模塊化就是眾多的「坑」之一,這些不完善的模塊化方案都是由于 JavaScript 一開始沒能提供模塊化規(guī)范導(dǎo)致的恰矩。包括下文將要介紹的各種模塊化規(guī)范有很多都是「民間」自發(fā)推行的模塊化規(guī)范无宿,實(shí)際上這多少增加了 JavaScript 語言的繁雜性。
最后再對(duì)立即執(zhí)行函數(shù) IIFE 做一些補(bǔ)充枢里,由于 ES6 正式引入了模塊化規(guī)范孽鸡,所以立即執(zhí)行函數(shù) IIFE 實(shí)現(xiàn)模塊化也就沒有什么必要了。
同時(shí)立即執(zhí)行函數(shù) IIFE 由于作用域封裝的特性栏豺,能夠達(dá)到避免污染全局變量或外部變量的效果彬碱,所以在 ES6 之前經(jīng)常被用來實(shí)現(xiàn)「塊作用域」。但由于 ES6 引入了 let
奥洼、const
關(guān)鍵字實(shí)現(xiàn)了「塊作用域」巷疼,所以也沒必要通過立即執(zhí)行函數(shù) IIFE 來實(shí)現(xiàn)「塊作用域」了。
模塊化規(guī)范
前端領(lǐng)域在模塊化這件事磕磕碰碰的前行了很久灵奖,雖然摸索出了上文提及的一些方案嚼沿,但每種方案距離真正的模塊化都有一定的距離。更重要的是瓷患,模塊化需要每個(gè)人采用同一種方案骡尽,也就是模塊化最終還是需要定下規(guī)范。
CommonJS
概述
時(shí)間來到了 2009 年擅编,JavaScript 在瀏覽器端已經(jīng)運(yùn)行了十多年攀细,逐步扎穩(wěn)了腳跟。此時(shí)一個(gè)有趣的想法正在不斷發(fā)酵中:即讓 JavaScript 運(yùn)行在服務(wù)器端爱态。當(dāng)然在具體實(shí)現(xiàn)之前谭贪,如果能夠先統(tǒng)一出一個(gè)規(guī)范的話,JavaScript 在服務(wù)器端的發(fā)展可能會(huì)更加可期锦担。
于是 2009 年 1 月俭识,Mozilla 的工程師 Kevin Dangoor 發(fā)起了制定這個(gè)規(guī)范的提案,這個(gè)規(guī)范最初被命名為 ServerJS
洞渔。服務(wù)器端的 JavaScript 規(guī)范自然會(huì)涉及到文件系統(tǒng)套媚、IO理盆、HTTP 客戶端等方方面面,當(dāng)然其中最重要的是不會(huì)再重蹈瀏覽器端的覆轍凑阶,所以 ServerJS
一開始就引入了模塊化規(guī)范并命名為 Modules/1.0
猿规。
大概是同年的 2 月份左右,Ryan Dahl 開始開發(fā) JavaScript 服務(wù)器端的運(yùn)行時(shí)環(huán)境 Node.js
宙橱,而在模塊化這一方面則采用了 ServerJS
的模塊化規(guī)范姨俩。
2009 年的下半年,由于 ServerJS
規(guī)范推動(dòng)的很不錯(cuò)师郑,同時(shí)看到瀏覽器端一直缺少模塊化規(guī)范的窘境环葵,所以社區(qū)開始期望將已經(jīng)得到 Node.js
實(shí)踐的模塊化規(guī)范推廣到瀏覽器端,社區(qū)也改名為 CommonJS宝冕。
之后我們也將 CommonJS 社區(qū)推行张遭,Node.js
實(shí)踐的模塊化規(guī)范稱為 CommonJS 規(guī)范。
CommonJS 規(guī)定了一個(gè)文件就是一個(gè)模塊地梨,每個(gè)模塊內(nèi)定義的變量菊卷、函數(shù)等都是私有的,對(duì)其他模塊不可見宝剖。但可通過 global
來定義對(duì)其他文件可見的變量:
/* utils.js */
var _name = 'utils';
var _desc = 'utils for dog and cat';
var HandleDog = function() {/* ... */};
var HandleCat = function() {/* ... */};
var GetName = function() {/* ... */};
global.isGlobal = true;
上述代碼定義的 _name
洁闰、_desc
等變量或 HandleDog
等函數(shù)是 utils.js
模塊私有的,其他文件(模塊)不可見万细。但 isGlobal
則可以在其他文件訪問[4]扑眉。
[4] 但一般情況下,不推薦使用赖钞。
模塊導(dǎo)出
CommonJS
規(guī)范可使用 module.exports
實(shí)現(xiàn)模塊導(dǎo)出:
/* utils.js */
var _name = 'utils';
var _desc = 'utils for dog and cat';
var HandleDog = function() {/* ... */};
var HandleCat = function() {/* ... */};
var GetName = function() {/* ... */};
module.exports.HandleDog = HandleDog;
module.exports.HandleCat = HandleCat;
module.exports.GetName = GetName;
console.log('模塊信息:', module);
每個(gè)模塊內(nèi)部都自帶了一個(gè)變量 module
腰素,表示當(dāng)前模塊,上述代碼的 console.log
將輸出如下內(nèi)容:
模塊信息: Module {
id: '.', // 模塊的識(shí)別符
exports: // 本模塊導(dǎo)出的值
{ HandleDog: [Function: HandleDog],
HandleCat: [Function: HandleCat],
GetName: [Function: GetName] },
parent: null, // 調(diào)用本模塊的模塊雪营,null 表明此文件為入口文件
filename: '/Users/xxxx/work/git_repository/demo/js-demo/es6_module.js', // 模塊的文件名(帶絕對(duì)路徑)
loaded: false, // 模塊是否已經(jīng)被加載
children: [], // 本模塊引用的其他模塊
paths: // 模塊的搜索路徑
[ '/Users/xxxx/work/git_repository/demo/js-demo/node_modules',
'/Users/xxxx/work/git_repository/demo/node_modules',
'/Users/xxxx/work/git_repository/node_modules',
'/Users/xxxx/work/node_modules',
'/Users/xxxx/node_modules',
'/Users/node_modules',
'/node_modules' ] }
由上述結(jié)果可知 module
對(duì)象存儲(chǔ)了本文件對(duì)應(yīng)的模塊信息弓千,而其中 module.exports
屬性更是重點(diǎn),它表示的實(shí)際上就是本模塊對(duì)外導(dǎo)出了什么卓缰。當(dāng)我們進(jìn)行所謂的導(dǎo)入加載模塊時(shí)计呈,實(shí)際上就是在獲取 module.exports
屬性。
同時(shí)為了寫法的一點(diǎn)點(diǎn)簡化征唬,CommonJS
在每個(gè)模塊內(nèi)還設(shè)置一個(gè)屬性 exports
指向 module.exports
,相當(dāng)于在每個(gè)模塊的頭部幫我們執(zhí)行了以下代碼:
var exports = module.exports;
需要注意的是茁彭,我們可以在 exports
上掛載各種屬性或函數(shù)总寒,因?yàn)檫@相當(dāng)于掛載在 module.exports
下,但不可以直接對(duì) exports
進(jìn)行賦值理肺,因?yàn)檫@將導(dǎo)致 exports 指向另一個(gè)地方摄闸,也就沒有導(dǎo)出的效果了:
/* utils.js */
var exports = module.exports; // exports 指向 module.exports;
var funcA = function() {/* ... */};
exports.funcA = funcA(); // 有效善镰,導(dǎo)出 {funcA: funcA}
module.exports.funcA = funcA(); // 有效,與上條等價(jià)
module.exports = { funcA: funcA }; // 有效年枕,與上面等價(jià)
module.exports = funcA(); // 有效炫欺,導(dǎo)出函數(shù) funcA()
// 無效,exports 原先指向 module.exports熏兄,現(xiàn)在指向 funcA()
// 而 module.exports 內(nèi)容依然為空品洛,固最終導(dǎo)出的是 {}
exports = funcA();
exports
和 module.exports
混在一起有時(shí)候令人迷惑,在實(shí)際開發(fā)時(shí)摩桶,也可以全部統(tǒng)一使用 module.exports
桥状。
模塊導(dǎo)入
上文介紹了 CommonJS
如何編寫模塊并導(dǎo)出后,我們來看看 CommonJS
如何導(dǎo)入加載一個(gè)模塊硝清。
CommonJS
使用 require
來導(dǎo)入加載模塊辅斟,如下所示:
/* main.js */
// 導(dǎo)入加載模塊
var Utils = require('./utils.js');
// 使用模塊的函數(shù)
Utils.HandleDog();
上述代碼中的 require('./utils.js')
將加載并執(zhí)行 utils.js 文件,然后返回 module.exports
屬性芦拿。
require
的模塊加載還具有以下幾個(gè)特點(diǎn):
-
同步加載:
CommonJS
的require
進(jìn)行的是「同步加載」士飒,依次同步加載模塊。 -
緩存機(jī)制:
Node.js
在第一次加載時(shí)執(zhí)行模塊并對(duì)模塊進(jìn)行緩存蔗崎,之后的加載將直接從緩存中獲取变汪。 -
值拷貝與引用拷貝:網(wǎng)上不少中文資料將
require
的輸出視為「值拷貝」,但實(shí)際上是不嚴(yán)謹(jǐn)?shù)恼f法蚁趁。這里需要區(qū)分導(dǎo)出的是基本類型還是復(fù)合類型裙盾,如果導(dǎo)出的是布爾、數(shù)字等原始基本類型他嫡,那么得到的是「值拷貝」番官,對(duì)這份值拷貝進(jìn)行修改不會(huì)影響到模塊內(nèi)變量。但是如果是對(duì)象钢属、數(shù)組等引用類型徘熔,那么得到的是「引用拷貝」,這份「引用拷貝」和模塊內(nèi)引用變量指向同一塊內(nèi)存區(qū)域(即一個(gè)閉包空間淆党,Node.js 實(shí)際會(huì)將每個(gè)文件包裝在一個(gè)函數(shù)中實(shí)現(xiàn)閉包)酷师,所以通過「引用拷貝」改變值將會(huì)影響到模塊內(nèi)的變量。
/* utils.js 模塊 */
var count = 1;
var moreInfo = {
innerCount: 1,
};
module.exports = {
count: count,
moreInfo: moreInfo
}
/* main.js 使用 utils 模塊*/
const Utils = require('./es6_module_utils.js');
let count = Utils.count;
count++;
console.log('導(dǎo)出 count染乌,自增后: ', count);
console.log('模塊內(nèi) count 不受影響: ', Utils.count);
let moreInfo = Utils.moreInfo;
moreInfo.innerCount++;
console.log('導(dǎo)出 inner_count, 自增后: ', moreInfo.innerCount);
console.log('模塊內(nèi) inner_count 受影響: ', Utils.moreInfo.innerCount);
AMD
在上一節(jié)對(duì) CommonJS
的介紹中提及了 2009 年由于 ServerJS
模塊化規(guī)范在服務(wù)器端推動(dòng)的較好山孔,于是社區(qū)改名為 CommonJS
,期望將服務(wù)器端的成功經(jīng)驗(yàn)推廣至瀏覽器端荷憋。
但由于瀏覽器端和服務(wù)器端還是存在許多差異台颠,所以這個(gè)推廣過程中產(chǎn)生了諸多不同的觀點(diǎn)和看法,社區(qū)逐步形成了三種流派:
Modules/1.x 流派勒庄。
這個(gè)流派認(rèn)為服務(wù)器端的Modules/1.0
規(guī)范(即上文介紹的CommonJS
規(guī)范)已經(jīng)夠用了串前,直接移植到瀏覽器端即可瘫里。當(dāng)然還需要添加一個(gè)所謂的「轉(zhuǎn)換」規(guī)范即Modules/Transport
,用來將服務(wù)器端模塊翻譯為瀏覽端模塊荡碾。Modules/2.0 流派谨读。
這個(gè)流派則認(rèn)為瀏覽器端有自己的特點(diǎn)(例如模塊可能通過網(wǎng)絡(luò)加載等),直接使用Modules/1.0
規(guī)范不合適坛吁,需要針對(duì)瀏覽器端的特點(diǎn)做更改劳殖,但另一方面應(yīng)該盡可能的接近 Modules/1.0。Modules/Async 流派阶冈。
這個(gè)流派則是更為激進(jìn)一些闷尿,認(rèn)為瀏覽器端和服務(wù)器端差異較大,所以需要對(duì)瀏覽器端進(jìn)行相對(duì)獨(dú)立的模塊化規(guī)范設(shè)計(jì)女坑。這個(gè)流派的代表就是本節(jié)介紹的 AMD 規(guī)范填具。
上述不同流派都有相應(yīng)的實(shí)現(xiàn),但最終在瀏覽器端被廣泛應(yīng)用的是 Modules/Async 流派的 AMD 規(guī)范匆骗,即異步模塊定義規(guī)范 Asynchronous Module Definition劳景。
再次回看當(dāng)初各個(gè)流派的觀點(diǎn),不難發(fā)現(xiàn)其中大家關(guān)注的關(guān)鍵點(diǎn)在于瀏覽器端與服務(wù)器端存在差異碉就。
服務(wù)器端的模塊通常從本地加載盟广,所以同步加載的方式不會(huì)影響程序運(yùn)行的效率和性能。但瀏覽器的模塊通常需要從網(wǎng)絡(luò)加載瓮钥,如果一個(gè)模塊加載完成才能執(zhí)行后續(xù)代碼筋量,那么將會(huì)影響瀏覽器端程序的執(zhí)行效率,甚至直接影響用戶的瀏覽體驗(yàn)碉熄。
針對(duì)上述差異桨武,AMD
規(guī)范提倡采用異步加載的方式加載模塊,模塊加載時(shí)不影響其他代碼執(zhí)行锈津,在加載完成之后呀酸,再通過回調(diào)函數(shù)的方式執(zhí)行相關(guān)邏輯。
模塊定義與導(dǎo)出
不同于 CommonJS
的一個(gè)文件對(duì)應(yīng)一個(gè)模塊琼梆,AMD
規(guī)定了一個(gè)全局函數(shù) define
來實(shí)現(xiàn)模塊定義[5]:
[5]: 需要注意的是性誉,雖然可以將多個(gè)模塊 define 寫在同一個(gè)文件里,但 requirejs 官方文檔 還是提倡一個(gè)文件里只寫一個(gè)模塊 define
define(id?, dependencies?, factory);
其中:
- id:表示定義的模塊的唯一標(biāo)識(shí)符茎杂,可選错览。默認(rèn)為腳本名稱。
-
dependencies:數(shù)據(jù)類型蛉顽,聲明本模塊所需要的依賴模塊蝗砾,可選。默認(rèn)為
["require", "exports", "module"]
- factory:模塊初始化需要執(zhí)行的函數(shù)或?qū)ο笮H绻呛瘮?shù)悼粮,則會(huì)被執(zhí)行,其中的返回值為模塊的輸出值曾棕。如果是對(duì)象扣猫,則該對(duì)象即為模塊的輸出值。
AMD
定義一個(gè)簡單模塊如下所示:
// id -> utils 本模塊名稱(標(biāo)識(shí)符)為 utils
// dependencies -> 本模塊依賴 jquery 模塊
// factory -> 本模塊初始化執(zhí)行函數(shù)翘地,且函數(shù)的參數(shù)依次為第二個(gè)參數(shù)聲明的依賴的模塊
define('utils', ['jquery'], function ($) {
/* ... */
// 本模塊輸出值(導(dǎo)出值)
return {
HandleDog: function() { /* ... */ }
};
});
注意 dependencies
處模塊的定義風(fēng)格是「前置聲明所有模塊」申尤,而且不管模塊實(shí)際執(zhí)行時(shí)是否會(huì)用到某個(gè)依賴模塊,所以模塊都會(huì)被提前執(zhí)行衙耕。
例如代碼實(shí)際執(zhí)行路徑中未用到模塊昧穿,但依然會(huì)被加載執(zhí)行:
// AMD 依賴模塊加載的兩個(gè)特點(diǎn)
// 1. 本模塊所依賴的模塊都需要一次性「前置聲明」
// 2. 且無論是否被使用到,都會(huì)被提前執(zhí)行
define('utils', ['jquery'], function ($) {
/* ... */
// 只有當(dāng) flag === xxx 時(shí)才會(huì)使用到 jquery
// 但無論代碼如何執(zhí)行橙喘,AMD 在一開始就已經(jīng)加載和執(zhí)行 jquery
if (flag === xxx) {
$('.class').css("color","red");
}
// 本模塊輸出值(導(dǎo)出值)
return {
HandleDog: function() { /* ... */ }
};
});
AMD
后續(xù)補(bǔ)充了一些更為接近 CommonJS
的寫法时鸵,仿佛能夠?qū)崿F(xiàn)「就近聲明」:
/* utils.js */
// 省略模塊 id,則 id 默認(rèn)為腳本文件名稱 utils
define(function(require, exports, module) {
var a = require('a'); // 在函數(shù)體內(nèi)通過 require 加載模塊
var b = require('b');
//Return the module value
return function () {};
}
);
但是上述代碼只是為滿足當(dāng)時(shí)開發(fā)者對(duì) CommonJS
風(fēng)格的訴求實(shí)現(xiàn)的「CommonJS 偽支持」厅瞎,其底層依然是 AMD
的「前置聲明和執(zhí)行」饰潜。
如果不依賴其他模塊,還可簡化為如下形式:
/* utils.js */
// 模塊 id 也同樣是可選和簸,不填則默認(rèn)為腳本文件名稱 utils
define(function () {
/* ... */
// 本模塊輸出值(導(dǎo)出值)
return {
HandleDog: function() { /* ... */ }
};
});
模塊導(dǎo)入
AMD
使用 require
函數(shù)實(shí)現(xiàn)模塊的導(dǎo)入和加載彭雾,如下所示:
require(modules?, callback);
其中:
- modules: 數(shù)組形式,聲明需要使用的模塊锁保。
- callback: 所需模塊加載完成后的回調(diào)函數(shù)薯酝。
一個(gè)簡單的模塊加載實(shí)例如下所示:
require(['utils'], function (Utils) {
/* ... */
Utils.HandleDog();
})
一點(diǎn)補(bǔ)充
AMD
規(guī)范的主要踐行者是 RequireJS,或者反過來說 RequireJS
是 AMD
規(guī)范的主要推動(dòng)者和制定者爽柒。對(duì)更多 RequireJS
語法和細(xì)節(jié)興趣的讀者可以參閱其 官方文檔吴菠。
雖然 RequireJS
在當(dāng)時(shí)具有很好的推廣度,但是由上文的介紹不難看出霉赡,RequireJS
和 CommonJS
在模塊定義和使用的風(fēng)格有較大差異橄务,雖然補(bǔ)充了對(duì) CommonJS
風(fēng)格的支持,但底層加載執(zhí)行方式依然是「前置執(zhí)行」[6]穴亏。這些差異導(dǎo)致 AMD
不太受 CommonJS
認(rèn)可蜂挪,甚至于后期 AMD
流派從 CommonJS
社區(qū)獨(dú)立出去成立了自己的 AMD
社區(qū),由 RequireJS
實(shí)際推動(dòng)嗓化。
[6] RequireJS 從 2.0 開始實(shí)現(xiàn)了下文即將介紹的 CMD 所一直推崇的「延遲執(zhí)行」
CMD
CMD(Common Module Definition)規(guī)范是 玉伯 在推廣自己的前端模塊加載庫 seajs 的過程中產(chǎn)出的模塊化規(guī)范棠涮。
上面提及了早期 AMD
依賴模塊加載的兩個(gè)關(guān)鍵特點(diǎn):
-
模塊前置聲明。
AMD
的依賴模塊需要通過參數(shù)在模塊定義時(shí)給出刺覆。 - 模塊提前執(zhí)行严肪。不管函數(shù)體內(nèi)執(zhí)行時(shí)是否會(huì)用到模塊,所有依賴模塊都會(huì)被提前加載執(zhí)行。
而 CMD
規(guī)范則是主要針對(duì)這兩點(diǎn)實(shí)踐了自己的理念:
-
模塊就近聲明驳糯。
CMD
的模塊可以在代碼實(shí)際需要的地方聲明導(dǎo)入篇梭。 -
模塊延遲執(zhí)行。
CMD
的模塊在代碼被實(shí)際需要的時(shí)候才會(huì)執(zhí)行酝枢。
模塊定義與導(dǎo)出
在 CMD
規(guī)范中恬偷,規(guī)定一個(gè)模塊對(duì)應(yīng)一個(gè)文件,使用 define
函數(shù)進(jìn)行模塊定義:
define(factory);
其中:
- factory: 可以為對(duì)象或函數(shù)帘睦。當(dāng)傳入對(duì)象是袍患,該模塊對(duì)外輸出就是該對(duì)象。當(dāng)傳入函數(shù)時(shí)竣付,則函數(shù)相當(dāng)于該模塊的構(gòu)造函數(shù)诡延,被執(zhí)行后返回的值即為該模塊對(duì)外的輸出。
CMD
定義一個(gè)簡單的模塊:
// factory 函數(shù)默認(rèn)傳入三個(gè)參數(shù):require, exports, module
// require:用來加載模塊
// exports:導(dǎo)出模塊
// module:含有模塊基本信息的對(duì)象
define(function(require, exports, module) {
// 模塊加載
var utils = require('./utils');
var HandleAnimal = function(type) {
if (type === 'dog') {
// 就近聲明古胆,延遲執(zhí)行
var module1 = require('./module1');
module1.handle();
utils.HandleDog();
/* ... */
} else if (type === 'cat') {
// 異步加載可使用:require.async(id, callback?)
var module2 = require.async('./module2', function() {/**/});
module2.handle();
utils.HandleCat();
/* ... */
}
}
exports.HandleAnimal = HandleAnimal; // 模塊對(duì)外輸出
});
CMD
也支持 define(id?, deps?, factory)
的寫法:
但這不是 CMD 默認(rèn)或推崇的寫法肆良,只是對(duì) Modules/Transport 規(guī)范的支持
// define(id?, deps?, factory)
define('animal', ['utils', 'module1', 'module2'], function(require, exports, module) {
/* ... */
});
模塊導(dǎo)入
CMD
使用 use
函數(shù)在頁面中導(dǎo)入一個(gè)或多個(gè)模塊:
// 加載多個(gè)模塊,加載完成后執(zhí)行回調(diào)
seajs.use('./utils', './module1', './module2', function(utils, module1, module2) {
utils.HandleDog();
module1.handle();
module2.handle();
});
一點(diǎn)補(bǔ)充
更多有關(guān) CMD
的語法和細(xì)節(jié)可查閱 官方文檔赤兴。
UMD 模式
UMD(Universal Module Definition)通用模塊定義妖滔。網(wǎng)上不少文章將其稱為 UMD
規(guī)范,實(shí)際上 UMD 與其說是一種規(guī)范桶良,不如說是一種編程模式座舍,一種為了兼容 CommonJS、AMD陨帆、CMD 等規(guī)范的編程模式曲秉。且實(shí)現(xiàn)原理也非常簡單,本質(zhì)就是通過 if-else
判斷當(dāng)前環(huán)境支持哪種規(guī)范疲牵,支持哪種規(guī)范就按某種規(guī)范做相應(yīng)的模塊封裝承二。按照這種模式封裝后,可以讓代碼運(yùn)行在不同模塊化規(guī)范的服務(wù)器或?yàn)g覽器環(huán)境中纲爸。
UMD
作為編程模式亥鸠,在不同場景可以有不同的寫法:
(function (root, factory) {
if (typeof define === 'function' && define.amd) { // 如果支持 AMD 規(guī)范
// 則注冊(cè)為一個(gè) AMD 模塊
define(['b'], function (b) {
return (root.returnExportsGlobal = factory(b));
});
} else if (typeof module === 'object' && module.exports) { // 不支持 AMD 但支持 CommonJS 規(guī)范的 Node 環(huán)境
// 則注冊(cè)一個(gè) CommonJS 模塊
module.exports = factory(require('b'));
} else {
// 都不支持,則將模塊輸出注冊(cè)到瀏覽器的全局變量
root.returnExportsGlobal = factory(root.b);
}
}(typeof self !== 'undefined' ? self : this, function (b) {
/* 模塊構(gòu)造函數(shù)本體 */
/* 使用 b */
// 模塊輸出值
return {/* ... */};
}));
更多的 UMD
寫法可以查閱 umdjs识啦,其中收集和維護(hù)了一些常用的 UMD
寫法负蚊。
ESM
從 CommonJS
到 AMD
、CMD
甚至于 UMD
颓哮,前端模塊化規(guī)范可謂「百家爭鳴」家妆。但天下大勢(shì)分久必合,群雄并起的時(shí)代終究是要落下帷幕冕茅。無論是上面哪種模塊化規(guī)范都只是各種前端社區(qū)推行的「民間方案」伤极,做的再好也只是割據(jù)一方罷了蛹找。最終一統(tǒng)天下的還是 JavaScript 的官方標(biāo)準(zhǔn),即 ECMA
標(biāo)準(zhǔn)哨坪。
2015 年 ECMA
的 39 號(hào)技術(shù)委員會(huì)(Technical Committee 39庸疾,簡稱 TC39) 終于推出 ECMAScript 2015 也就是耳熟能詳?shù)?ES6 規(guī)范。其中就包含了 JavaScript 模塊化標(biāo)準(zhǔn) ECMAScript Module齿税,也被簡稱為 ESM彼硫。
模塊定義與導(dǎo)出
在 ESM
中炊豪,一個(gè)模塊對(duì)應(yīng)一個(gè)文件凌箕,所以直接在文件中定義模塊。同時(shí)使用 export
實(shí)現(xiàn)模塊導(dǎo)出:
/* utils.js */
export const _name = 'utils';
export const _desc = 'utils for dog and cat';
export function HandleDog() {/*...*/};
export function HandleCat() {/*...*/};
或者將導(dǎo)出值統(tǒng)一寫在一處:
// ......
// 導(dǎo)出非 default 變量時(shí)必須加括號(hào)词渤,以下語法是錯(cuò)誤的
// 錯(cuò)誤用法:export _name;
// 錯(cuò)誤用法:export 123;
export { _name, _desc, HandleDog, HandleCat };
模塊導(dǎo)出的同時(shí)牵舱,可以對(duì)導(dǎo)出的內(nèi)容進(jìn)行重命名:
// ......
export {
_name as name,
_name as another_name,
_desc as desc,
HandleDog as handleDog,
HandleCat as handleCat,
};
ESM
中還規(guī)定了一個(gè)「默認(rèn)導(dǎo)出」:
const HandleDefault = function() {/* ... */};
// export default 無需添加大括號(hào) {}
// 相當(dāng)于將 HandleDefault 賦值給 default 變量
export default HandleDefault;
對(duì)于模「非默認(rèn)輸出值」缺虐,我們需要使用如下方式導(dǎo)入:
// 想要導(dǎo)入的是模塊導(dǎo)出的 name
// name 必須和 export 導(dǎo)出的名稱對(duì)應(yīng)
import { name } from '模塊'; // name 就是 export 時(shí)起的名字
而對(duì)于模塊的「默認(rèn)輸出值」芜壁,我們無需指定名稱即可獲得:
// 從模塊獲取值,由于沒有指定名稱高氮,所以獲取的是默認(rèn)輸出
// xxx 不與模塊導(dǎo)出值的名稱對(duì)應(yīng)慧妄,xxx 是給默認(rèn)輸出起的任意名稱
import xxx from '模塊';
模塊導(dǎo)入
ESM
使用 import
命令實(shí)現(xiàn)導(dǎo)入加載模塊:
import { name, another_name, handleDog, handleCat } from './utils.js';
// 如上所示,使用 import 導(dǎo)入時(shí)剪芍,括號(hào)內(nèi)的變量名需要和 utils 模塊導(dǎo)出的變量名相同
// 當(dāng)然也可以在導(dǎo)入的同時(shí)使用 as 起一個(gè)新名稱
import { name as old_name, another_name as new_name } from './utils.js';
也可以不按名稱一一導(dǎo)入塞淹,使用 *
符號(hào)將模塊導(dǎo)出的所有變量作為一個(gè)整體一次性導(dǎo)入到一個(gè)對(duì)象:
import * as Utils from './utils.js';
console.log(Utils.name);
console.log(Utils.another_name);
Utils.handleDog();
對(duì)于「默認(rèn)輸出」,其對(duì)應(yīng)的變量名為 default
罪裹,所以在導(dǎo)入時(shí)無需指定特定名稱饱普,還可以直接為其自定義一個(gè)任意名稱:
// 模塊內(nèi)導(dǎo)出的 HandleDefault 已經(jīng)被賦值為默認(rèn) default,所以導(dǎo)出的是 default 變量名
// 獲取模塊的默認(rèn) default 時(shí)無需通過模塊內(nèi)的定義特定名稱獲取
// 直接給這個(gè)默認(rèn) default 自定義一個(gè)任意名稱即可状共,例如 HandleOther
import HandleOther from './utils.js';
一點(diǎn)補(bǔ)充
ESM
規(guī)范中的 import
具有一些特性套耕,如導(dǎo)入的變量是只讀的、import
具有提升效果峡继、比起 CommonJS
冯袍、AMD
等 import 為編譯期執(zhí)行(語句分析階段) 等。同時(shí) ESM
中還有 import
和 export
混合使用等語法碾牌,關(guān)于這些更多內(nèi)容可以查閱 ES6~ES11 特性介紹之 ES6 篇 中的第 4 節(jié)「模塊 Module」康愤。
小結(jié)
在那個(gè) JavaScript 還在野蠻生長的「遠(yuǎn)古」時(shí)代,全局函數(shù)封裝小染、對(duì)象封裝以及立即函數(shù)執(zhí)行 IIFE 這些模塊化的「不完全體」陪伴著無數(shù)開發(fā)者度過了一段漫長的靜默期翘瓮。但隨著 JavaScript 的快速成長,無數(shù) JavaScript 開發(fā)人員在日益龐大和復(fù)雜的系統(tǒng)中磕磕碰碰裤翩,艱難前行资盅。
時(shí)間來到 2009调榄,CommonJS
社區(qū)建立,Node.js
更是橫空出世呵扛,從此開啟了 CommonJS
每庆、AMD
、CMD
今穿、UMD
百家爭鳴的時(shí)代缤灵,這一晃就是 6 個(gè)春秋,最終這一切隨著 2015 年 ES 官方規(guī)范的推出而落下帷幕蓝晒。
原本 ESM
規(guī)范推出后腮出,各個(gè)瀏覽器對(duì)此實(shí)現(xiàn)支持需要一定的開發(fā)時(shí)間。然而時(shí)代浪潮滾滾向前芝薇,這次不再給我們留下回味 AMD
胚嘲、CMD
等規(guī)范的時(shí)間。
2014 年洛二,babel 誕生馋劈。這款能夠?qū)?「ECMAScript 2015+ 語法代碼」編譯成「運(yùn)行在舊平臺(tái)的舊語法代碼」的工具使得開發(fā)人員能夠使用還未被瀏覽器支持的最新語法進(jìn)行開發(fā)。甚至于將前端帶入了一個(gè)新的階段晾嘶,一個(gè)開發(fā)階段代碼與生產(chǎn)階段代碼不同的階段妓雾,即預(yù)編譯階段。
同時(shí)隨著 webpack
的發(fā)明垒迂,前端領(lǐng)域后續(xù)又進(jìn)入全新的自動(dòng)化構(gòu)建階段械姻。這個(gè)過程中當(dāng)然還有 broswerify
、webpack
娇斑、grunt
策添、gulp
等工具的發(fā)展和興衰,這又是另一個(gè)「前端故事」了毫缆。
如同 CommonJS
唯竹、AMD
、CMD
苦丁、UMD
一樣浸颓,前端領(lǐng)域每年都有新的技術(shù)不斷產(chǎn)生、更有新生力量不斷注入旺拉,也有舊的技術(shù)被淘汰产上,而這故事仍然在不斷上演。技術(shù)更替會(huì)不斷的被后人整理記錄蛾狗,但絕大多數(shù)的程序員恐怕只會(huì)被淹沒在歷史的浪潮中了吧晋涣。
參考資料
前端模塊化開發(fā)那點(diǎn)歷史
前端模塊化的前世
前端模塊化的十年征程
RequireJS 官方文檔
CMD 模塊定義規(guī)范
JavaScript for impatient programmers - 24 Modules
汪
汪