目前,前端開發已經離不開由 CommonJS、ES Modules 和 Webpack 構建的模塊化開發環境。無論是 JavaScript、CSS、圖片還是其他資源,都可以作為一個模塊來處理。那麼,模塊化究竟是如何發展到今天的呢?
全局函數模式
最初的前端模塊化嘗試是通過 全局函數來實現的。例如,在一個 util.js 文件中定義了一個變量 count 和一個工具函數 formatNumberWithCommas,用於將數字轉換成帶千分位分隔符的字符串:
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> 標籤將 util.js 資源引入:
<script src="../src/util.js"></script>
此時 util.js 文件內的變量和函數將掛載到全局對象 window 上。
在瀏覽器的 Console 控制枱上直接輸入 window.formatNumberWithCommas 就可以訪問該函數。
然而,這種方式存在一個問題:不同的 JS 文件間一旦存在相同的變量或函數名就會互相覆蓋,從而導致某些變量或函數不可用。
全局命名空間
為了避免全局函數命名衝突的問題,進一步採用了通過對象封裝模塊的方式。例如,在 util.js 文件中定義了一個全局對象 __Util:
window.__Util = {
count: 1,
formatNumberWithCommas(number) {
if (typeof number !== "number") {
throw new TypeError("Input must be a number.");
}
return number.toLocaleString("en-US");
},
};
通過為全局對象定義一個較複雜的名稱,可以減少命名衝突的風險。然而,這種方式下對象內的屬性很容易被外部修改。例如,將 window.__Util 賦值給變量 d,再修改 d 中的 count 時,window.__Util 中的 count 屬性也會被修改。
IIFE 自執行函數
為了解決模塊內的變量容易被外界隨意修改的問題,通過 IIFE(立即執行函數表達式)創建閉包來實現模塊化。例如:
(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,
};
})();
此時我們不直接將 count 變量導出,而是通過 getCount 獲取 count 的值,通過 setCount 修改 count 的值。
這種方式使得模塊內的變量不能被外界隨意修改。然而,這種模式下存在的問題是,如果存在多個模塊,且它們之間有依賴關係,就無法很好地支持。
IIFE 自定義依賴
為了解決 IIFE 無法關聯模塊的問題,可以通過在 IIFE 中傳入參數來將各模塊關聯起來。例如,新增一個 verify.js 文件,並在 index.html 中引入:
(function (global) {
function isNumber(num) {
return typeof num === "number";
}
global.__Verify = {
isNumber,
};
})(window);
同時改造 util.js 文件,接收 verify.js 文件中綁定到全局的 __Verify 屬性,並調用 __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);
儘管這種方式能夠在一定程度上支持模塊化,但如果模塊過多,特別是在現代項目中,模塊數量動輒幾十上百個,這種方式就顯得力不從心,而且代碼的可讀性和維護性也會受到影響。
commonjs
以上提到的方法都是通過簡單的代碼實現模塊化功能,但隨着 CommonJS 的出現,一套正式的模塊化規範開始形成。CommonJS 使用 module.exports 導出模塊,並通過 require 加載其他模塊,從而實現模塊間的交互。
讓我們對之前的 verify.js 和 util.js 文件進行改造以適應 CommonJS 規範:
// 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));
通過命令行工具執行 node ./src/util.js,可以看到 console.log 輸出的結果。
CommonJS 規範是為服務器端設計的,它假定所有的模塊加載都是同步的。然而,在客户端環境中,由於網絡延遲,這種方式可能會導致用户界面的阻塞,從而影響用户體驗。
AMD
AMD(Asynchronous Module Definition)規範則是為了解決瀏覽器端模塊加載的異步需求而設計的。AMD 規範使用 define 來定義模塊,並且通過 return 導出模塊內容,同時使用 require 來加載其他模塊。
以下是 verify.js 和 util.js 改造後的 AMD 規範代碼:
// 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
};
});
此外,定義一個 index.js 文件來使用這些模塊:
define(function (require) {
var util = require("util");
console.log("formatNumberWithCommas", util.formatNumberWithCommas(123456));
});
在 HTML 頁面中,可以通過 RequireJS 來解析 AMD 規範的代碼,並通過 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 頁面時,可以在瀏覽器控制枱中看到輸出結果。
CMD
CMD(Common Module Definition)規範在 AMD 的基礎上進行了改進,尤其是在異步加載和延遲執行方面。CMD 規範同樣使用 define 來定義模塊,但導出模塊時使用的是 exports。
下面是 verify.js 和 util.js 按照 CMD 規範的代碼示例:
// 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;
});
為了在瀏覽器中運行 CMD 規範的代碼,可以使用 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 標準的一部分,不僅提供了更為簡潔的語法用於模塊的導入和導出,還具備動態加載的能力,提高了模塊間協作的效率與靈活性。
下面是如何用 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");
}
為了測試 formatNumberWithCommas 函數,我們定義一個 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>
儘管現代瀏覽器原生支持 ES Modules,但瀏覽器自身並不具備有效的模塊管理機制。這意味着,每一個模塊都會作為一個獨立的 JS 資源文件加載,這不僅導致資源文件過於分散,而且每次加載模塊都會產生新的服務器請求,從而增加了加載時間,降低了性能,這在大型項目中尤其明顯。
為了解決這些問題,開發者社區引入了 npm 和 webpack 這樣的工具。npm 作為最流行的 JavaScript 包管理器之一,能夠有效地管理和組織模塊依賴關係,確保項目的模塊化組件能夠被正確地安裝和更新。另一方面,webpack 則是一個模塊打包工具,它可以將多個模塊和它們的依賴合併成單個文件或一組優化過的文件,同時還能進行壓縮等優化操作,以減少最終輸出文件的大小,提高加載速度和應用的整體性能。
關於 npm 和 webpack 的相關內容,大家可以查看我其他的博客,持續更新中~