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

回復(fù)“加群”,一起學(xué)習(xí)進(jìn)步
