【總結】1775- 聊聊前端模塊化
在上古時期,曾經(jīng)的 Web 開發(fā)者們,應該會因為在一個龐大的 JavaScript 文件中尋找一個小小的函數(shù)而感到絕望?或者因為修改一個變量而不得不查找整個代碼庫?
當下的前端開發(fā)中,webpack,rollup,vite 等構建打包工具大家應該都用的飛起了。它們都基于一個非常重要的概念 - 前端模塊化。
在這篇文章中,我們將聊聊前端模塊化的發(fā)展歷程以及主流的一些方案。
什么是模塊化前端模塊化是指將一個大型的前端應用程序分解為小的、獨立的模塊,每個模塊都有自己的功能和接口,可以被其他模塊使用。前端模塊化的出現(xiàn)是為了解決前端開發(fā)中代碼復雜度和可維護性的問題。在前端模塊化的架構下,開發(fā)人員可以更加專注于各自的模塊開發(fā),提高了代碼的復用性和可維護性。
為什么需要前端模塊化在傳統(tǒng)的前端開發(fā)中,所有的代碼都是寫在同一個文件中,這樣做的問題在于:
- 可維護性差:當應用程序變得越來越大時,代碼變得越來越難以維護。
- 可重用性差:相同的代碼可能會被多次復制和粘貼到不同的文件中,這樣會導致代碼冗余,增加了代碼量。
- 可測試性差:在傳統(tǒng)的前端開發(fā)中,很難對代碼進行單元測試。
- 可擴展性差:在傳統(tǒng)的前端開發(fā)中,很難對應用程序進行擴展。
全局 function 模式
將不同功能封裝成不同的函數(shù)
function fetchData() {
...
}
function handleData() {
...
}
缺陷:這個是將方法掛在 window 下,會污染全局命名空間,容易引起命名沖突且數(shù)據(jù)不安全等問題。
全局 namespace 模式
既然全局 function 模式下,會有命名沖突等問題,那么我們可以通過對象來封裝模塊
var myModule = {
fetchData() {
...
},
handleData() {
...
}
};
缺陷:這個方案確實減少了全局變量,解決命名沖突的問題,但是外部可以直接修改模塊內(nèi)部的數(shù)據(jù)。
IIFE 模式,通過自執(zhí)行函數(shù)創(chuàng)建閉包
function(global) {
var data = 1
function fetchData() {
...
}
function handleData() {
...
}
window.myModule = {fetchData, handleData}
}(window)
缺陷:這個方案下,數(shù)據(jù)是私有的,外部只能通過暴露的方法操作,但無法解決模塊間相互依賴問題。
IIFE 模式增強,傳入自定義依賴
我們可以通過傳入依賴的方式來解決模塊間引用的問題
function(global, otherModule) {
var data = 1
function fetchData() {
...
}
function handleData() {
...
}
window.myModule = {fetchData, handleData, otherApi: otherModule.api}
}(window, window.other_module)
缺陷:但仍然有以下幾個缺點
- 多依賴傳入時,代碼閱讀困難
- 無法支持大規(guī)模模塊化開發(fā)
- 無特定語法支持,代碼簡陋
經(jīng)過以上過程的演進,我們確實可以實現(xiàn)前端模塊化開發(fā)了,但是仍然有幾個問題,一是請求過多,我們都是通過 script 標簽來引入各個模塊文件的,依賴多個模塊,那樣就會發(fā)送多個請求。二是依賴模糊,很容易因為不了解模塊之間的依賴關系導致加載先后順序出錯,模塊之間的依賴關系比較難以管理,也沒有明確的接口和規(guī)范。因此模塊化規(guī)范應運而生。
模塊化規(guī)范CommonJS
1. 概述
CommonJS 是一個 JavaScript 模塊化規(guī)范,它最初是為了解決 JavaScript 在服務器端的模塊化問題而提出的。是 NodeJS 的默認模塊飯規(guī)范,該規(guī)范定義了模塊的基本結構、模塊的加載方式以及模塊的導出和導入方式等內(nèi)容。
2. 模塊的基本結構
在 CommonJS 規(guī)范中,一個模塊就是一個文件。每個文件都是一個獨立的模塊,文件內(nèi)部定義的變量、函數(shù)和類等只在該文件內(nèi)部有效。
每個模塊都有自己的作用域,模塊內(nèi)部的變量、函數(shù)和類等只在該模塊內(nèi)部有效。如果想在其他模塊中使用該模塊內(nèi)部的變量、函數(shù)和類等,需要將其導出。
3. 模塊的加載方式
在 CommonJS 規(guī)范中,模塊的加載方式是同步的。也就是說,當一個模塊被引入時,會立即執(zhí)行該模塊內(nèi)部的代碼,并將該模塊導出的內(nèi)容返回給引入該模塊的代碼。
模塊可以多次加載,第一次加載時會運行模塊,模塊輸出結果會被緩存,再次加載時,會從緩存結果中直接讀取模塊輸出結果。模塊加載的順序,按照其在代碼中出現(xiàn)的順序。模塊輸出的值是值的拷貝,類似 IIFE 方案中的內(nèi)部變量。
這種同步加載方式可以保證模塊內(nèi)部的代碼執(zhí)行完畢后再執(zhí)行外部代碼,從而避免了異步加載所帶來的一些問題。但同時也會影響頁面加載速度,因此在瀏覽器端使用時需要注意。
4. 模塊的導出和導入方式
在 CommonJS 規(guī)范中,一個模塊可以通過module.exports 或者 exports 對象來導出內(nèi)容。module.exports 是真正的導出對象,而 exports 對象只是對 module.exports 的一個引用。
一個模塊可以導出多個內(nèi)容,可以通過 module.exports 或者 exports 對象分別導出。例如:
// 導出一個變量
module.exports.name = 'Tom';
// 導出一個函數(shù)
exports.sayHello = function() {
console.log('Hello!');
};
在另一個模塊中,可以通過 require 函數(shù)來引入其他模塊,并訪問其導出的內(nèi)容。例如:
// 引入其他模塊
var moduleA = require('./moduleA');
// 訪問其他模塊導出的變量
console.log(moduleA.name);
// 訪問其他模塊導出的函數(shù)
moduleA.sayHello();
5. 特點
- CommonJS 模塊由 JS 運行時實現(xiàn)。
- CommonJS 模塊輸出的是值的拷貝,本質(zhì)上導出的就是 exports 屬性。
- CommonJS 是可以動態(tài)加載的,對每一個加載都存在緩存,可以有效的解決循環(huán)引用問題。
- CommonJS 模塊同步加載并執(zhí)行模塊文件。
ES6 模塊化
1. 概述
在 ES6 之前,JavaScript 并沒有原生支持模塊化,因此開發(fā)者們需要使用一些第三方庫或者自己實現(xiàn)一些模塊化方案來解決代碼復用和管理問題。但是這些方案都有一些問題,比如命名沖突、依賴管理等。ES6 引入了 ESModule 模塊化規(guī)范來解決這些問題。
ESModule 模塊化規(guī)范是一種靜態(tài)的模塊化方案,它允許開發(fā)者將代碼分割成小的、獨立的模塊,每個模塊都有自己的作用域。ESModule 規(guī)范是基于文件的,每個文件都是一個獨立的模塊。
ESModule 的模塊解析規(guī)則是基于 URL 解析規(guī)則的。當我們使用 import 語句導入一個模塊時,模塊加載器會根據(jù) import 語句中指定的路徑解析出對應的 URL,并將其作為唯一標識符來加載對應的模塊文件。在瀏覽器中,URL 解析規(guī)則是基于當前頁面的 URL 進行解析;在 Node.js 中,URL 解析規(guī)則是基于當前運行腳本的路徑進行解析。
2. 模塊的加載方式
ESModule 規(guī)范是基于文件的,每個文件都是一個獨立的模塊。在瀏覽器中,可以使用<script type="module">標簽來加載 ESModule 模塊。在 Node.js 中,可以使用 import 關鍵字來加載 ESModule 模塊。
<!-- 在瀏覽器中加載ESModule模塊 -->
<script type="module" src="./module.js">
</script>
// 在Node.js中加載ESModule模塊
import { name } from './module';
3. 模塊的導出和導入方式
在 ESModule 中,使用 export 關鍵字將變量或者函數(shù)導出,使用 import 關鍵字導入其他模塊中導出的變量或者函數(shù)。導出和導入方式有以下幾種:
- 命名導出和命名導入
命名導出和命名導入是最常見的一種方式。可以將多個變量或者函數(shù)命名導出,也可以將多個變量或者函數(shù)命名導入。
// module.js
export const name = '張三';
export function sayHello() {
console.log('Hello');
}
// app.js
import { name, sayHello } from './module';
- 默認導出和默認導入
默認導出和默認導入是一種簡單的方式,可以將一個變量或者函數(shù)作為默認導出,也可以將一個變量或者函數(shù)作為默認導入。
// module.js
export default 'Hello World';
// app.js
import message from './module';
- 混合命名和默認導出
混合命名和默認導出也是一種常見的方式,可以將多個變量或者函數(shù)命名導出,同時將一個變量或者函數(shù)作為默認導出。
// module.js
export const name = '張三';
export function sayHello() {
console.log('Hello');
}
export default 'Hello World';
// app.js
import message, { name, sayHello } from './module';
4. 特點:
- ES6 Module 靜態(tài)的,不能放在塊級作用域內(nèi),代碼發(fā)生在編譯時。
- ES6 模塊輸出的是值的引用,如果一個模塊修改了另一個模塊導出的值,那么這個修改會影響到原始模塊。
- ES6 Module 可以導出多個屬性和方法,可以單個導入導出,混合導入導出。
- ES6 模塊提前加載并執(zhí)行模塊文件,
AMD
1. 概述
AMD 是 Asynchronous Module Definition 的縮寫,即異步模塊定義。它是由 RequireJS 的作者 James Burke 提出的一種模塊化規(guī)范。AMD 規(guī)范的主要特點是:異步加載、提前執(zhí)行。
2. 基本語法
在 AMD 規(guī)范中,一個模塊通常由以下幾個部分組成:
define(id?, dependencies?, factory);
其中:
-
id:可選參數(shù),表示模塊標識符,一般為字符串類型。 -
dependencies:可選參數(shù),表示當前模塊所依賴的其他模塊。它是一個數(shù)組類型,每個元素表示一個依賴模塊的標識符。 -
factory:必需參數(shù),表示當前模塊的工廠函數(shù)。它是一個函數(shù)類型,用于定義當前模塊的行為。
一個典型的 AMD 模塊定義如下所示:
define('module1', ['module2', 'module3'], function(module2, module3) {
// 模塊1的代碼邏輯
return {
// 暴露給外部的接口
};
});
AMD 規(guī)范采用異步加載方式,它通過require函數(shù)來加載一個或多個模塊。require函數(shù)接受一個數(shù)組類型的參數(shù),每個元素表示一個待加載的模塊標識符。當所有依賴模塊加載完成后,require函數(shù)才會執(zhí)行回調(diào)函數(shù)。
require(['module1', 'module2'], function(module1, module2) {
// 所有依賴模塊加載完成后執(zhí)行的回調(diào)函數(shù)
});
AMD 模式可以用于瀏覽器環(huán)境,并且允許非同步加載模塊,也可以根據(jù)需要動態(tài)加載模塊。
CMD
1. 概述
CMD 是 Common Module Definition 的縮寫,即通用模塊定義。CMD 規(guī)范的主要特點是:按需加載、延遲執(zhí)行。
2. 基本語法
//定義沒有依賴的模塊
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定義有依賴的模塊
define(function(require, exports, module){
//引入依賴模塊(同步)
var module2 = require('./module2')
//引入依賴模塊(異步)
require.async('./module3', function (m3) {
})
//暴露模塊
exports.xxx = value
})
// 引入該模塊
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
CMD 規(guī)范專門用于瀏覽器端,模塊的加載是異步的,模塊使用時才會加載執(zhí)行。CMD 規(guī)范整合了 CommonJS 和 AMD 規(guī)范的特點。
往期回顧
#
如何使用 TypeScript 開發(fā) React 函數(shù)式組件?
# # # #6 個你必須明白 Vue3 的 ref 和 reactive 問題
# # 
回復“加群”,一起學習進步
