前端模塊化的發(fā)展概述

前端模塊化簡單梳理

本篇簡介

關(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仿粹、gulpwebpack原茅,大致原理如下:

<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è)都可以講很多東西,在這里不過多闡述茎用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遣总,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子轨功,更是在濱河造成了極大的恐慌旭斥,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件古涧,死亡現(xiàn)場離奇詭異垂券,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)羡滑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門菇爪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柒昏,你說我怎么就攤上這事洞辣『撤海” “怎么了乒躺?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵科展,是天一觀的道長诈嘿。 經(jīng)常有香客問我堪旧,道長削葱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任淳梦,我火速辦了婚禮析砸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爆袍。我一直安慰自己首繁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布陨囊。 她就那樣靜靜地躺著弦疮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蜘醋。 梳的紋絲不亂的頭發(fā)上胁塞,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音压语,去河邊找鬼啸罢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胎食,可吹牛的內(nèi)容都是我干的扰才。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼厕怜,長吁一口氣:“原來是場噩夢啊……” “哼衩匣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起粥航,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤舵揭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后躁锡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體午绳,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年映之,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拦焚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杠输,死狀恐怖赎败,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蠢甲,我是刑警寧澤僵刮,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響搞糕,放射性物質(zhì)發(fā)生泄漏勇吊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一窍仰、第九天 我趴在偏房一處隱蔽的房頂上張望汉规。 院中可真熱鬧,春花似錦驹吮、人聲如沸针史。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啄枕。三九已至,卻和暖如春族沃,著一層夾襖步出監(jiān)牢的瞬間射亏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工竭业, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留智润,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓未辆,卻偏偏與公主長得像窟绷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子咐柜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容