前端模塊化簡單梳理
本篇簡介
關(guān)于前端模塊化的一些知識垄琐,如CMD/AMD/Webpack等订讼,之前都進(jìn)行過專門學(xué)習(xí)钧汹,但經(jīng)驗(yàn)尚欠淫僻,無法從上層理解模塊化處于前端工程的哪一層诱篷,所以此篇文章暫且拋開之前所學(xué)內(nèi)容,不做細(xì)化研究雳灵,先單獨(dú)對模塊化大致脈絡(luò)進(jìn)行梳理棕所,等待后續(xù)工程化、設(shè)計(jì)模式等學(xué)習(xí)完成后再進(jìn)行整體的梳理悯辙。
整體脈絡(luò)圖
1. 無模塊化時(shí)期
Web初期并沒有模塊化的工具琳省,且前端在當(dāng)時(shí)相對輕量,甚至后端通過模板就能完全勝任全棧工程師的角色躲撰,所以當(dāng)時(shí)并沒有前端工程的概念针贬,也就沒有模塊化。為了組織代碼拢蛋,采用的是不同功能代碼通過文件區(qū)分:
<!-- 不同功能代碼通過文件區(qū)分桦他,導(dǎo)入模板,但作用域并沒分離 -->
<script src="layer.js"></script>
<script src="init.js"></script>
<script src="login.js"></script>
2. 【幼年期】語法層面模塊化
由于無模塊化會導(dǎo)致全局變量污染問題谆棱,不利于團(tuán)隊(duì)開發(fā)快压,因此 IIFE 自然成了語法層面模塊化的唯一選擇;其原理是函數(shù)作用域內(nèi)變量若存在外部引用垃瞧,則函數(shù)產(chǎn)生引用時(shí)的執(zhí)行環(huán)境不會銷毀蔫劣,也就是此時(shí)函數(shù)作用域一直生效,且由于外部無法訪問函數(shù)內(nèi)部作用域个从,因此就形成了即封閉又長效的“模塊”脉幢。IIFE只是將匿名函數(shù)立即執(zhí)行,是對上述原理的一種語法簡化嗦锐,用它實(shí)現(xiàn)模塊化的方式是返回一個(gè)暴露 api 的對象(模塊):
// IIFE
var moduleA = (() => {
var count = 0;
return {
printCount: function() {
console.log(count); // 對函數(shù)作用域中的count進(jìn)行引用
},
increase: function() {
count++;
}
};
})();
// 調(diào)用模塊
moduleA.printCount(); // 0
moduleA.increase();
moduleA.printCount(); // 1
面試題1:有額外依賴時(shí)嫌松,如何優(yōu)化 IIFE?
由于 IIFE 原理是借助函數(shù)作用域奕污,所以額外依賴可以通過參數(shù)傳入函數(shù)中供模塊使用:
// IIFE
var moduleA = ((dependB, dependC) => {
var count = dependB.count || dependC.count || 0; // 使用依賴
return {
printCount: function() {
console.log(count); // 對函數(shù)作用域中的count進(jìn)行引用
},
increase: function() {
count++;
}
};
})(moduleB, moduleC);
面試題2:了解 jQuery早期依賴處理及模塊化加載方案嗎萎羔?
使用了揭示模式(Revealing)寫法,原理仍然是 IIFE 傳參菊值,匿名函數(shù)暴露 api 對象外驱,api 寫法是指針形式:
const moduleA = ((dep1, dep2) => {
var count = dep1.count || dep2.count || 0;
var getCount = function() {
return count;
};
return {
getCount, // 揭示模式寫法育灸,用指針代替具體函數(shù)
};
})(moduleB, moduleC);
3. 【成熟期】CommonJS 的出現(xiàn)
Node 作為服務(wù)端語言出現(xiàn)后腻窒,自然少不了模塊化的需求,因此出現(xiàn)了 CommonJS磅崭。其特性如下:
- require 引入依賴模塊
- module儿子、exports 對象暴露 api
模塊組織方式如下:
/* NodeJS模塊,基于CommonJS規(guī)范 */
// 引入依賴
const dep1 = require('./dep1.js');
// coding
let count = de1.count || 0; // 使用模塊
const getCount = () => count;
// 暴露api
exports.getCount = getCount;
// 也可以直接重寫module.exports
module.exports = {
getCount,
}
優(yōu)缺點(diǎn)如下:
- 優(yōu)點(diǎn):從框架層面首次實(shí)現(xiàn)了真正意義的模塊化
- 缺點(diǎn):為服務(wù)端設(shè)計(jì)砸喻,所以起初并未考慮異步依賴問題
面試題:上述CommonJS模塊實(shí)際執(zhí)行過程是柔逼?
由于對異步依賴支持不足蒋譬,所以不難想到早期原理是 IIFE 的語法糖:
(function(thisValue, exports, require, module) {
const dep1 = require('./dep1.js');
// ...
}).call(thisValue, exports, require, module);
4. AMD規(guī)范
CommonJS 解決了模塊化的問題,但又有了如何處理異步依賴的新問題愉适,而 AMD規(guī)范應(yīng)運(yùn)而生犯助。原理是 “異步加載后,執(zhí)行回調(diào)函數(shù)”维咸,經(jīng)典框架是 require.js
剂买。示例如下:
/**
* @function define函數(shù)能夠定義模塊,require函數(shù)加載模塊
* @params 模塊名癌蓖,依賴列表瞬哼,模塊工廠函數(shù)
*/
// 定義模塊
define(id, [...dependList], factory);
// 引入模塊
require([...moduleList], callback); // 加載模塊,完成后執(zhí)行callback
demo如下:
// 定義moduleA模塊
define('moduleA', ['dep1', 'dep2'], (dep1, dep2) => {
let count = dep1.count || dep2.count || 0;
const getCount = () => count;
return {
getCount,
};
});
// 引入并使用模塊
require(['moduleA'], moduleA => {
moduleA.getCount(); // 0
});
面試題1:若希望AMD兼容之前CommonJS代碼租副,怎么辦坐慰?
AMD引入依賴的方式除了傳入列表,還給工廠函數(shù)提供了 require方法用僧,能夠兼容CommonJS的寫法:
define('moduleA', [], require => {
let dep1 = require('./dep1.js');
let dep2 = require('./dep2.js');
let count = dep1.count || dep2.count || 0;
const getCount = () => count;
return {
getCount,
};
});
面試題2:AMD使用 revealing 寫法结胀?
除了返回對象,也可以使用工廠函數(shù)的 export 對象掛載指針永毅,雖然是一種設(shè)計(jì)模式把跨,但其實(shí)寫法沒太大區(qū)別:
define('moduleA', [], (require, exports, module) => {
let dep1 = require('./dep1.js');
let dep2 = require('./dep2.js');
let count = dep1.count || dep2.count || 0;
const getCount = () => count;
exports.getCount = getCount; // 直接掛載export對象
});
AMD的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):可在瀏覽器中加載模塊,且支持異步沼死、并行得加載多個(gè)模塊
- 缺點(diǎn):無法按需加載
還有一種能夠兼容 AMD 和 CommonJS 的規(guī)范叫 UMD着逐,原理就是在工廠函數(shù)外包裹一層兼容函數(shù),通過不同傳參實(shí)現(xiàn)兼容意蛀,在這里不過多展開耸别。
5. CMD規(guī)范
由于AMD無法按需加載,國內(nèi)團(tuán)隊(duì)做了CMD規(guī)范進(jìn)行優(yōu)化县钥,主流框架是 seaJS秀姐,示例如下:
/**
* @function 省去依賴列表參數(shù),依賴動態(tài)引入若贮,與AMD區(qū)分是能按需加載
*/
define('moduleA', (require, exports, module) => {
let $ = require('jquery');
//...jquery相關(guān)邏輯
let dep1 = require('./dep1');
// ...dep1相關(guān)邏輯
});
優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):在打包時(shí)能夠?qū)崿F(xiàn)按需加載省有,且依賴就近,方便維護(hù)
- 缺點(diǎn):按需加載會使打包過程變慢谴麦,且按需加載邏輯放入每個(gè)模塊蠢沿,模塊體積反而會增大
面試題:AMD & CMD 的區(qū)別?
CMD能夠按需加載
6. 【新時(shí)代】ES模塊化
從 ES6 開始匾效,實(shí)現(xiàn)了JS模塊化的標(biāo)準(zhǔn)語法舷蟀,且能實(shí)現(xiàn)上述舊工具的所有功能,并得到了良好支持。示例如下:
// 引入依賴
import dep1 from './dep1'
let count = 0;
function getCount() {
return count;
}
// 導(dǎo)出接口
export default {
getCount,
}
瀏覽器中引入模塊的方法是 type=module 的script標(biāo)簽:
<script type="module" src="./moduleA.js"></script>
新版 Node中直接使用 ES6語法引入即可:
// 模塊文件一般用 mjs 后綴
import moduleA from './moduleA.mjs'
// 使用模塊
moduleA.getCount();
面試題:ES6 如何實(shí)現(xiàn)動態(tài)加載模塊野宜?
Webpack 支持使用 CMD寫法或 import('./moduleA')
扫步,不過 ES11 已原生支持該特性:
// 原理就是包一層promise,等模塊加載完就引入
import('./moduleA').then(dynamicModule => {
dynamicModule.getCount();
});
ES6模塊化的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):通過統(tǒng)一且標(biāo)準(zhǔn)的方式實(shí)現(xiàn)了模塊化
- 缺點(diǎn):若不使用Webpack 等工程化工具匈子,其本質(zhì)仍然是運(yùn)行時(shí)依賴分析
7. 【完備方法】工程化
為了解決 ES6 是運(yùn)行時(shí)依賴分析的痛點(diǎn)河胎,使用工程化構(gòu)建工具,使依賴能夠在代碼構(gòu)建階段就完成虎敦。典型工具有 grunt
仿粹、gulp
、webpack
原茅,大致原理如下:
<script>
// 構(gòu)建工具的占位符
// require.config(__FRAME_CONFIG__);
</script>
<script>
import A from './modA'
define('B', () => {
let c = require('C');
// 業(yè)務(wù)邏輯
});
</script>
編譯時(shí)會掃描依賴關(guān)系并生成map:
{
a: [],
b: ['c'],
}
然后會根據(jù)依賴關(guān)系替換占位符:
<script>
require.config({
a: [],
b: ['c'],
})
</script>
接著會根據(jù)模塊化工具的配置處理依賴并生成符合兼容要求的代碼:
// 同步方案吭历,加載進(jìn)C
define('b', ['c'], () => {
// ...
})
完成。
這種方案的優(yōu)點(diǎn):
- 構(gòu)建時(shí)分析依賴
- 方便拓展擂橘,比如同時(shí)使用3種不同的模塊化方案
總結(jié)
大概梳理一下模塊化的脈絡(luò)晌区,有助于理解現(xiàn)在為何Webpack、Vite 等工具流行的原因通贞,因?yàn)槟芙鉀Q足夠多的問題朗若,將多規(guī)范進(jìn)行統(tǒng)一并且可以拓展。若想研究具體的規(guī)范使用昌罩,可以參考對應(yīng)熱門框架的實(shí)現(xiàn)哭懈,每一個(gè)都可以講很多東西,在這里不過多闡述茎用。