目前账锹,前端開發(fā)已經(jīng)離不開由 CommonJS萌业、ES Modules 和 Webpack 構(gòu)建的模塊化開發(fā)環(huán)境。無論是 JavaScript奸柬、CSS生年、圖片還是其他資源,都可以作為一個(gè)模塊來處理廓奕。那么抱婉,模塊化究竟是如何發(fā)展到今天的呢?
全局函數(shù)模式
最初的前端模塊化嘗試是通過 全局函數(shù)
來實(shí)現(xiàn)的桌粉。例如蒸绩,在一個(gè) util.js
文件中定義了一個(gè)變量 count
和一個(gè)工具函數(shù) formatNumberWithCommas
,用于將數(shù)字轉(zhuǎn)換成帶千分位分隔符的字符串:
var count = 1;
function formatNumberWithCommas(number) {
if (typeof number !== "number") {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
在 index.html
文件中通過 <script>
標(biāo)簽將 util.js
資源引入:
<script src="../src/util.js"></script>
此時(shí) util.js
文件內(nèi)的變量和函數(shù)將掛載到全局對(duì)象 window
上铃肯。
在瀏覽器的 Console
控制臺(tái)上直接輸入 window.formatNumberWithCommas
就可以訪問該函數(shù)棠涮。
然而疟赊,這種方式存在一個(gè)問題:不同的 JS 文件間一旦存在相同的變量或函數(shù)名就會(huì)互相覆蓋革答,從而導(dǎo)致某些變量或函數(shù)不可用贾节。
全局命名空間
為了避免全局函數(shù)命名沖突的問題,進(jìn)一步采用了通過對(duì)象封裝模塊的方式食茎。例如,在 util.js
文件中定義了一個(gè)全局對(duì)象 __Util
:
window.__Util = {
count: 1,
formatNumberWithCommas(number) {
if (typeof number !== "number") {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
},
};
通過為全局對(duì)象定義一個(gè)較復(fù)雜的名稱,可以減少命名沖突的風(fēng)險(xiǎn)表锻。然而,這種方式下對(duì)象內(nèi)的屬性很容易被外部修改乞娄。例如瞬逊,將 window.__Util
賦值給變量 d,再修改 d 中的 count 時(shí)仪或,window.__Util
中的 count 屬性也會(huì)被修改确镊。
IIFE 自執(zhí)行函數(shù)
為了解決模塊內(nèi)的變量容易被外界隨意修改的問題,通過 IIFE(立即執(zhí)行函數(shù)表達(dá)式)創(chuàng)建閉包來實(shí)現(xiàn)模塊化范删。例如:
(function () {
var count = 1;
function formatNumberWithCommas(number) {
if (typeof number !== "number") {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
function getCount() {
return count;
}
function setCount(num) {
count = num;
}
window.__Util = {
formatNumberWithCommas,
getCount,
setCount,
};
})();
此時(shí)我們不直接將 count 變量導(dǎo)出蕾域,而是通過 getCount 獲取 count 的值,通過 setCount 修改 count 的值到旦。
這種方式使得模塊內(nèi)的變量不能被外界隨意修改旨巷。然而,這種模式下存在的問題是添忘,如果存在多個(gè)模塊采呐,且它們之間有依賴關(guān)系,就無法很好地支持搁骑。
IIFE 自定義依賴
為了解決 IIFE
無法關(guān)聯(lián)模塊的問題斧吐,可以通過在 IIFE
中傳入?yún)?shù)來將各模塊關(guān)聯(lián)起來。例如仲器,新增一個(gè) verify.js
文件煤率,并在 index.html
中引入:
(function (global) {
function isNumber(num) {
return typeof num === "number";
}
global.__Verify = {
isNumber,
};
})(window);
同時(shí)改造 util.js
文件,接收 verify.js
文件中綁定到全局的 __Verify
屬性乏冀,并調(diào)用 __Verify
中的 isNumber
方法:
(function (global, verifyModule) {
var count = 1;
function formatNumberWithCommas(number) {
if (!verifyModule.isNumber(number)) {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
function getCount() {
return count;
}
function setCount(num) {
count = num;
}
global.__Util = {
formatNumberWithCommas,
getCount,
setCount,
};
})(window, window.__Verify);
盡管這種方式能夠在一定程度上支持模塊化涕侈,但如果模塊過多,特別是在現(xiàn)代項(xiàng)目中煤辨,模塊數(shù)量動(dòng)輒幾十上百個(gè)裳涛,這種方式就顯得力不從心,而且代碼的可讀性和維護(hù)性也會(huì)受到影響众辨。
commonjs
以上提到的方法都是通過簡(jiǎn)單的代碼實(shí)現(xiàn)模塊化功能端三,但隨著 CommonJS
的出現(xiàn),一套正式的模塊化規(guī)范開始形成鹃彻。CommonJS
使用 module.exports
導(dǎo)出模塊郊闯,并通過 require
加載其他模塊,從而實(shí)現(xiàn)模塊間的交互。
讓我們對(duì)之前的 verify.js
和 util.js
文件進(jìn)行改造以適應(yīng) CommonJS
規(guī)范:
// verify.js
function isNumber(num) {
return typeof num === "number";
}
module.exports = {
isNumber,
};
// util.js
const { isNumber } = require("./verify");
function formatNumberWithCommas(number) {
if (!isNumber(number)) {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
console.log("formatNumberWithCommas", formatNumberWithCommas(123456));
通過命令行工具執(zhí)行 node ./src/util.js
团赁,可以看到 console.log 輸出的結(jié)果育拨。
CommonJS
規(guī)范是為服務(wù)器端設(shè)計(jì)的,它假定所有的模塊加載都是同步的欢摄。然而熬丧,在客戶端環(huán)境中,由于網(wǎng)絡(luò)延遲怀挠,這種方式可能會(huì)導(dǎo)致用戶界面的阻塞析蝴,從而影響用戶體驗(yàn)。
AMD
AMD(Asynchronous Module Definition)
規(guī)范則是為了解決瀏覽器端模塊加載的異步需求而設(shè)計(jì)的绿淋。AMD
規(guī)范使用 define
來定義模塊闷畸,并且通過 return
導(dǎo)出模塊內(nèi)容,同時(shí)使用 require
來加載其他模塊吞滞。
以下是 verify.js
和 util.js
改造后的 AMD 規(guī)范代碼:
// verify.js
define(function () {
function isNumber(num) {
return typeof num === "number";
}
return {
isNumber: isNumber,
};
});
// util.js
define(['verify'], function(verify) {
function formatNumberWithCommas(number) {
if (!verify.isNumber(number)) {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
return {
formatNumberWithCommas: formatNumberWithCommas
};
});
此外佑菩,定義一個(gè) index.js 文件來使用這些模塊:
define(function (require) {
var util = require("util");
console.log("formatNumberWithCommas", util.formatNumberWithCommas(123456));
});
在 HTML 頁面中,可以通過 RequireJS 來解析 AMD 規(guī)范的代碼裁赠,并通過 HTML 屬性 data-main
指定入口文件:
<script data-main="../src/index.js" src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"></script>
打開 HTML 頁面時(shí)倘待,可以在瀏覽器控制臺(tái)中看到輸出結(jié)果。
CMD
CMD(Common Module Definition)
規(guī)范在 AMD
的基礎(chǔ)上進(jìn)行了改進(jìn)组贺,尤其是在異步加載和延遲執(zhí)行方面。CMD 規(guī)范同樣使用 define 來定義模塊祖娘,但導(dǎo)出模塊時(shí)使用的是 exports失尖。
下面是 verify.js
和 util.js
按照 CMD
規(guī)范的代碼示例:
// verify.js
define(function (require, exports, module) {
function isNumber(num) {
return typeof num === "number";
}
exports.isNumber = isNumber;
});
// util.js
define(function (require, exports, module) {
var verify = require("verify");
function formatNumberWithCommas(number) {
if (!verify.isNumber(number)) {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
exports.formatNumberWithCommas = formatNumberWithCommas;
});
為了在瀏覽器中運(yùn)行 CMD
規(guī)范的代碼,可以使用 Sea.js
渐苏。在 HTML 文件中添加以下代碼:
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script>
<script>
seajs.config({
alias: {
verify: "../src/verify",
util: "../src/util",
},
});
seajs.use(["util"], function (util) {
console.log(
"formatNumberWithCommas",
util.formatNumberWithCommas(123456)
);
});
</script>
ES Modules
相比之下掀潮,ES Modules(ESM)
作為 ECMAScript
標(biāo)準(zhǔn)的一部分,不僅提供了更為簡(jiǎn)潔的語法用于模塊的導(dǎo)入和導(dǎo)出琼富,還具備動(dòng)態(tài)加載的能力仪吧,提高了模塊間協(xié)作的效率與靈活性。
下面是如何用 ESM
來重寫 verify.js
和 util.js
:
// verify.js
export function isNumber(num) {
return typeof num === "number";
}
// util.js
import { isNumber } from "./verify.js";
export function formatNumberWithCommas(number) {
if (!isNumber(number)) {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
}
為了測(cè)試 formatNumberWithCommas
函數(shù)鞠眉,我們定義一個(gè) index.js 文件:
// index.js
import { formatNumberWithCommas } from "./util.js";
console.log("formatNumberWithCommas", formatNumberWithCommas(123456));
在 index.html 文件中引入 index.js薯鼠,瀏覽器本身就支持 ESModule,只需要將 type 需要定義成 module械蹋。
<script type="module" src="../src/index.js"></script>
盡管現(xiàn)代瀏覽器原生支持 ES Modules
出皇,但瀏覽器自身并不具備有效的模塊管理機(jī)制。這意味著哗戈,每一個(gè)模塊都會(huì)作為一個(gè)獨(dú)立的 JS 資源文件加載郊艘,這不僅導(dǎo)致資源文件過于分散,而且每次加載模塊都會(huì)產(chǎn)生新的服務(wù)器請(qǐng)求,從而增加了加載時(shí)間纱注,降低了性能畏浆,這在大型項(xiàng)目中尤其明顯。
為了解決這些問題狞贱,開發(fā)者社區(qū)引入了 npm
和 webpack
這樣的工具刻获。npm 作為最流行的 JavaScript 包管理器之一,能夠有效地管理和組織模塊依賴關(guān)系斥滤,確保項(xiàng)目的模塊化組件能夠被正確地安裝和更新将鸵。另一方面,webpack 則是一個(gè)模塊打包工具佑颇,它可以將多個(gè)模塊和它們的依賴合并成單個(gè)文件或一組優(yōu)化過的文件顶掉,同時(shí)還能進(jìn)行壓縮等優(yōu)化操作,以減少最終輸出文件的大小挑胸,提高加載速度和應(yīng)用的整體性能痒筒。
關(guān)于 npm
和 webpack
的相關(guān)內(nèi)容,大家可以查看我其他的博客茬贵,持續(xù)更新中~