前端模塊化是指將一個大型的前端應用程序分解為小的、獨立的模塊,每個模塊都有自己的功能和接口,可以被其他模塊使用。
前端模塊化的出現主要是為了解決以下幾個問題:
- 代碼複用:通過模塊化,可以在多個地方重複使用同一個模塊,而不需要重複編寫相同的代碼。
- 代碼維護:模塊化後的代碼更加清晰,每個模塊負責的功能明確,便於維護和升級。
- 依賴管理:模塊化可以很好地處理模塊間的依賴關係,確保模塊使用時其依賴已經被正確加載。
- 私有化:模塊內部具有私有化內容,對外只提供暴露的通信接口
- 提高加載效率:模塊化允許按需加載,只有需要的模塊才會被加載,減少了不必要的資源加載,提高了頁面的加載速度。
- 隔離命名空間:每個模塊都有自己的命名空間,避免了全局變量的污染,減少了命名衝突的可能性。
普通腳本與模塊化的區別:
- 普通腳本:只有一個index.js文件,所有的業務邏輯都在這個一個js文件中
- 模塊化:以一個entry.js作為入口,然後去引用若干個其他模塊
接下來就用一個簡單的案例來講解模塊化的演變歷程
第一階段:函數調用
對於一個複雜業務,一般都會拆分成多個小任務,每個任務就編寫成一個函數,下面列舉一個簡單的例子:
// 獲取一個隨機的座標
function getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
}
// 把橫縱座標向下整
function handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
}
// 求和
function sum(a, b) {
return a + b;
}
const coordinate = getCoordinate();
const data = handleData(coordinate);
const result = sum(data[0], data[1]);
console.log(result); // 99
這裏的每個函數就可以看做是不同的模塊,這些方法都是掛在全局window上的
這就會出現一個嚴重的問題,如果引入了其他的庫,其他的庫也在全局定義了同樣的方法,尤其是那種比較通用的方法名。就會導致同名函數相互覆蓋,最終只有一個是可用的。
缺點:容易引發全局命名衝突
第二階段:全局namespace模式
其本質就是通過對象封裝模塊,在window上定義一個全局對象,然後把所有函數都掛到這個對象上
window.__Module = {
name: "module",
getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
},
handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
},
sum(a, b) {
return a + b;
},
};
const module = window.__Module;
const coordinate = module.getCoordinate();
const data = module.handleData(coordinate);
const result = module.sum(data[0], data[1]);
這種方式可以大大的降低命名衝突的概率,以前有很多JS庫都是用這種方式實現的。
缺點:對象裏面的屬性可以被外部修改,缺少了私有屬性的功能
console.log(module.name); // module
module.name = "new_module";
console.log(module.name); // new_module
第三階段:IIFE模式+函數作用域+閉包
關注我的公眾號【前端筱園】,不錯過每一篇推送
加入【前端筱園交流羣】,與大家一起交流,共同進步!
IIFE(Immediately Invoked Function Expression),即立即調用函數表達式,是一種在定義後立即被執行的JavaScript函數。
IIFE的主要作用包括:
- 創建獨立的作用域:IIFE可以創建一個獨立的作用域,防止變量污染全局作用域。通過這種方式,可以在函數內部定義私有變量,而不影響外部環境。
- 避免變量提升:在JavaScript中,傳統的函數聲明會進行變量提升,即在代碼執行前被提前至作用域頂部。而IIFE由於是表達式,不會被提升,因此可以避免變量提升帶來的問題。
- 保持代碼封裝性:IIFE有助於保持代碼的封裝性,使得一些只需要在特定作用域內運行的代碼得以隔離,減少全局命名空間的衝突。
- 模擬塊級作用域:在ES6之前,JavaScript不支持塊級作用域,IIFE常被用來模擬塊級作用域的效果,尤其是在循環和條件語句中需要臨時的變量時非常有用。
// 函數作用域+閉包
function fun() {
let name = "module";
return {
get() {
return name;
},
set(newName) {
name = newName;
},
};
}
console.log(name); // undefind
const Name = fun();
console.log(Name.get()); // module
如果要改變函數內屬性的值,只有通過暴露出來的方法進行修改,否則無法修改,這就符合了模塊化的標準
Name.set("new_module");
console.log(Name.get()); // new_module
接下來就使用閉包進行模塊化的改造,創建一個自執行的閉包函數。
(() => {
let name = "module";
function getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
}
function handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
}
function sum(a, b) {
return a + b;
}
function getName() {
return name;
}
function setName(newName) {
name = newName;
}
window.__Module = {
name,
getCoordinate,
handleData,
sum,
getName,
setName,
};
})();
const module = window.__Module;
const coordinate = module.getCoordinate();
const data = module.handleData(coordinate);
const result = module.sum(data[0], data[1]);
console.log(result); // 125
console.log(module.name); // module
module.name = "new_module";
console.log(module.name); // new_module
在上面的代碼中,對 module.name 的值進行修改,然後打印發現結果為修改後的值,這與之前提到的私有行相矛盾:
module.name = "new_module";
console.log(module.name); // new_module
這個問題本質上是函數作用域與對象屬性的區別,在閉包方法中,name 屬性添加到了返回結果中,這裏其實是對name的一個拷貝,而不是函數內部的name。
只有通過getName才能拿到內部屬性name的值,也只有通過 setName 才能改變內部屬性 name的值。
console.log(module.getName()); // module
module.setName("new_module")
console.log(module.getName()); // new_module
缺點:無法解決模塊間相互依賴的問題
第四階段:IIFE模式增強,支持傳入自定義依賴
將模塊進行拆分,不同的模塊放在不同的自執行函數中
((global) => {
function handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
}
function sum(a, b) {
return a + b;
}
global.__Module_utils = {
handleData,
sum,
};
})(window);
iief_entry.js
((global, module) => {
function getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
}
global.__Module = {
getCoordinate,
handleData: module.handleData,
sum: module.sum,
};
})(window, window.__Module_utils);
const module = window.__Module;
const coordinate = module.getCoordinate();
const data = module.handleData(coordinate);
const result = module.sum(data[0], data[1]);
console.log(result);
缺點:
- 傳入了多個參數依賴,代碼閲讀變得困難
- 大規模的模塊開發會非常的麻煩,很容易出錯
- 無特定語法支持,代碼簡陋
經過這四個階段的演變,前端模塊化的標準逐步形成,和前面提到的自執行函數原理非常接近,下期講解CommonJS規範(關注我的公眾號,不錯過推送哦)。
寫在最後
歡迎到我的個人網站(www.dengzhanyong.com)
關注我的公眾號【前端筱園】,不錯過每一篇推送
加入【前端筱園交流羣】,與大家一起交流,共同進步!