前端模塊化進(jìn)化史:從全局 function 到 ES Modules

目前账锹,前端開發(fā)已經(jīng)離不開由 CommonJS萌业、ES Modules 和 Webpack 構(gòu)建的模塊化開發(fā)環(huán)境。無論是 JavaScript奸柬、CSS生年、圖片還是其他資源,都可以作為一個(gè)模塊來處理廓奕。那么抱婉,模塊化究竟是如何發(fā)展到今天的呢?

全局函數(shù)模式

最初的前端模塊化嘗試是通過 全局函數(shù)來實(shí)現(xiàn)的桌粉。例如蒸绩,在一個(gè) util.js 文件中定義了一個(gè)變量 count 和一個(gè)工具函數(shù) formatNumberWithCommas,用于將數(shù)字轉(zhuǎn)換成帶千分位分隔符的字符串:

var count = 1;
function formatNumberWithCommas(number) {
  if (typeof number !== "number") {
    throw new TypeError("Input must be a number.");
  }
  return number.toLocaleString("en-US");
}

index.html 文件中通過 <script> 標(biāo)簽將 util.js 資源引入:

<script src="../src/util.js"></script>

此時(shí) util.js 文件內(nèi)的變量和函數(shù)將掛載到全局對(duì)象 window 上铃肯。

在瀏覽器的 Console 控制臺(tái)上直接輸入 window.formatNumberWithCommas 就可以訪問該函數(shù)棠涮。

1_全局function.png

然而疟赊,這種方式存在一個(gè)問題:不同的 JS 文件間一旦存在相同的變量或函數(shù)名就會(huì)互相覆蓋革答,從而導(dǎo)致某些變量或函數(shù)不可用贾节。

全局命名空間

為了避免全局函數(shù)命名沖突的問題,進(jìn)一步采用了通過對(duì)象封裝模塊的方式食茎。例如,在 util.js 文件中定義了一個(gè)全局對(duì)象 __Util

window.__Util = {
  count: 1,
  formatNumberWithCommas(number) {
    if (typeof number !== "number") {
      throw new TypeError("Input must be a number.");
    }
    return number.toLocaleString("en-US");
  },
};

通過為全局對(duì)象定義一個(gè)較復(fù)雜的名稱,可以減少命名沖突的風(fēng)險(xiǎn)表锻。然而,這種方式下對(duì)象內(nèi)的屬性很容易被外部修改乞娄。例如瞬逊,將 window.__Util 賦值給變量 d,再修改 d 中的 count 時(shí)仪或,window.__Util 中的 count 屬性也會(huì)被修改确镊。

2_namespace.png

IIFE 自執(zhí)行函數(shù)

為了解決模塊內(nèi)的變量容易被外界隨意修改的問題,通過 IIFE(立即執(zhí)行函數(shù)表達(dá)式)創(chuàng)建閉包來實(shí)現(xiàn)模塊化范删。例如:

(function () {
  var count = 1;
  function formatNumberWithCommas(number) {
    if (typeof number !== "number") {
      throw new TypeError("Input must be a number.");
    }
    return number.toLocaleString("en-US");
  }
  function getCount() {
    return count;
  }
  function setCount(num) {
    count = num;
  }
  window.__Util = {
    formatNumberWithCommas,
    getCount,
    setCount,
  };
})();

此時(shí)我們不直接將 count 變量導(dǎo)出蕾域,而是通過 getCount 獲取 count 的值,通過 setCount 修改 count 的值到旦。

3_iife自執(zhí)行.png

這種方式使得模塊內(nèi)的變量不能被外界隨意修改旨巷。然而,這種模式下存在的問題是添忘,如果存在多個(gè)模塊采呐,且它們之間有依賴關(guān)系,就無法很好地支持搁骑。

IIFE 自定義依賴

為了解決 IIFE 無法關(guān)聯(lián)模塊的問題斧吐,可以通過在 IIFE 中傳入?yún)?shù)來將各模塊關(guān)聯(lián)起來。例如仲器,新增一個(gè) verify.js 文件煤率,并在 index.html 中引入:

(function (global) {
  function isNumber(num) {
    return typeof num === "number";
  }
  global.__Verify = {
    isNumber,
  };
})(window);

同時(shí)改造 util.js 文件,接收 verify.js 文件中綁定到全局的 __Verify 屬性乏冀,并調(diào)用 __Verify 中的 isNumber 方法:

(function (global, verifyModule) {
  var count = 1;
  function formatNumberWithCommas(number) {
    if (!verifyModule.isNumber(number)) {
      throw new TypeError("Input must be a number.");
    }
    return number.toLocaleString("en-US");
  }
  function getCount() {
    return count;
  }
  function setCount(num) {
    count = num;
  }
  global.__Util = {
    formatNumberWithCommas,
    getCount,
    setCount,
  };
})(window, window.__Verify);
4_iife傳入?yún)?shù).png

盡管這種方式能夠在一定程度上支持模塊化涕侈,但如果模塊過多,特別是在現(xiàn)代項(xiàng)目中煤辨,模塊數(shù)量動(dòng)輒幾十上百個(gè)裳涛,這種方式就顯得力不從心,而且代碼的可讀性和維護(hù)性也會(huì)受到影響众辨。

commonjs

以上提到的方法都是通過簡(jiǎn)單的代碼實(shí)現(xiàn)模塊化功能端三,但隨著 CommonJS 的出現(xiàn),一套正式的模塊化規(guī)范開始形成鹃彻。CommonJS 使用 module.exports 導(dǎo)出模塊郊闯,并通過 require 加載其他模塊,從而實(shí)現(xiàn)模塊間的交互。

讓我們對(duì)之前的 verify.jsutil.js 文件進(jìn)行改造以適應(yīng) CommonJS 規(guī)范:

// verify.js
function isNumber(num) {
  return typeof num === "number";
}
module.exports = {
  isNumber,
};
// util.js
const { isNumber } = require("./verify");
function formatNumberWithCommas(number) {
  if (!isNumber(number)) {
    throw new TypeError("Input must be a number.");
  }
  return number.toLocaleString("en-US");
}
console.log("formatNumberWithCommas", formatNumberWithCommas(123456));

通過命令行工具執(zhí)行 node ./src/util.js团赁,可以看到 console.log 輸出的結(jié)果育拨。

CommonJS 規(guī)范是為服務(wù)器端設(shè)計(jì)的,它假定所有的模塊加載都是同步的欢摄。然而熬丧,在客戶端環(huán)境中,由于網(wǎng)絡(luò)延遲怀挠,這種方式可能會(huì)導(dǎo)致用戶界面的阻塞析蝴,從而影響用戶體驗(yàn)。

AMD

AMD(Asynchronous Module Definition)規(guī)范則是為了解決瀏覽器端模塊加載的異步需求而設(shè)計(jì)的绿淋。AMD 規(guī)范使用 define 來定義模塊闷畸,并且通過 return 導(dǎo)出模塊內(nèi)容,同時(shí)使用 require 來加載其他模塊吞滞。

以下是 verify.jsutil.js 改造后的 AMD 規(guī)范代碼:

// verify.js
define(function () {
  function isNumber(num) {
    return typeof num === "number";
  }
  return {
    isNumber: isNumber,
  };
});
// util.js
define(['verify'], function(verify) {
  function formatNumberWithCommas(number) {
    if (!verify.isNumber(number)) {
      throw new TypeError("Input must be a number.");
    }
    return number.toLocaleString("en-US");
  }
  return {
    formatNumberWithCommas: formatNumberWithCommas
  };
});

此外佑菩,定義一個(gè) index.js 文件來使用這些模塊:

define(function (require) {
  var util = require("util");
  console.log("formatNumberWithCommas", util.formatNumberWithCommas(123456));
});

在 HTML 頁面中,可以通過 RequireJS 來解析 AMD 規(guī)范的代碼裁赠,并通過 HTML 屬性 data-main 指定入口文件:

 <script data-main="../src/index.js" src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"></script>

打開 HTML 頁面時(shí)倘待,可以在瀏覽器控制臺(tái)中看到輸出結(jié)果。

CMD

CMD(Common Module Definition)規(guī)范在 AMD 的基礎(chǔ)上進(jìn)行了改進(jìn)组贺,尤其是在異步加載和延遲執(zhí)行方面。CMD 規(guī)范同樣使用 define 來定義模塊祖娘,但導(dǎo)出模塊時(shí)使用的是 exports失尖。

下面是 verify.jsutil.js 按照 CMD 規(guī)范的代碼示例:

// verify.js
define(function (require, exports, module) {
  function isNumber(num) {
    return typeof num === "number";
  }
  exports.isNumber = isNumber;
});
// util.js
define(function (require, exports, module) {
  var verify = require("verify");
  function formatNumberWithCommas(number) {
    if (!verify.isNumber(number)) {
      throw new TypeError("Input must be a number.");
    }
    return number.toLocaleString("en-US");
  }
  exports.formatNumberWithCommas = formatNumberWithCommas;
});

為了在瀏覽器中運(yùn)行 CMD 規(guī)范的代碼,可以使用 Sea.js渐苏。在 HTML 文件中添加以下代碼:

<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script>
<script>
  seajs.config({
    alias: {
      verify: "../src/verify",
      util: "../src/util",
    },
  });
  seajs.use(["util"], function (util) {
    console.log(
      "formatNumberWithCommas",
      util.formatNumberWithCommas(123456)
    );
  });
</script>

ES Modules

相比之下掀潮,ES Modules(ESM) 作為 ECMAScript 標(biāo)準(zhǔn)的一部分,不僅提供了更為簡(jiǎn)潔的語法用于模塊的導(dǎo)入和導(dǎo)出琼富,還具備動(dòng)態(tài)加載的能力仪吧,提高了模塊間協(xié)作的效率與靈活性。

下面是如何用 ESM 來重寫 verify.jsutil.js

// verify.js
export function isNumber(num) {
  return typeof num === "number";
}
// util.js
import { isNumber } from "./verify.js";
export function formatNumberWithCommas(number) {
  if (!isNumber(number)) {
    throw new TypeError("Input must be a number.");
  }
  return number.toLocaleString("en-US");
}

為了測(cè)試 formatNumberWithCommas 函數(shù)鞠眉,我們定義一個(gè) index.js 文件:

// index.js
import { formatNumberWithCommas } from "./util.js";
console.log("formatNumberWithCommas", formatNumberWithCommas(123456));

在 index.html 文件中引入 index.js薯鼠,瀏覽器本身就支持 ESModule,只需要將 type 需要定義成 module械蹋。

  <script type="module" src="../src/index.js"></script>

盡管現(xiàn)代瀏覽器原生支持 ES Modules出皇,但瀏覽器自身并不具備有效的模塊管理機(jī)制。這意味著哗戈,每一個(gè)模塊都會(huì)作為一個(gè)獨(dú)立的 JS 資源文件加載郊艘,這不僅導(dǎo)致資源文件過于分散,而且每次加載模塊都會(huì)產(chǎn)生新的服務(wù)器請(qǐng)求,從而增加了加載時(shí)間纱注,降低了性能畏浆,這在大型項(xiàng)目中尤其明顯。

為了解決這些問題狞贱,開發(fā)者社區(qū)引入了 npmwebpack 這樣的工具刻获。npm 作為最流行的 JavaScript 包管理器之一,能夠有效地管理和組織模塊依賴關(guān)系斥滤,確保項(xiàng)目的模塊化組件能夠被正確地安裝和更新将鸵。另一方面,webpack 則是一個(gè)模塊打包工具佑颇,它可以將多個(gè)模塊和它們的依賴合并成單個(gè)文件或一組優(yōu)化過的文件顶掉,同時(shí)還能進(jìn)行壓縮等優(yōu)化操作,以減少最終輸出文件的大小挑胸,提高加載速度和應(yīng)用的整體性能痒筒。

關(guān)于 npmwebpack 的相關(guān)內(nèi)容,大家可以查看我其他的博客茬贵,持續(xù)更新中~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末簿透,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子解藻,更是在濱河造成了極大的恐慌老充,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件螟左,死亡現(xiàn)場(chǎng)離奇詭異啡浊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胶背,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門巷嚣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钳吟,你說我怎么就攤上這事廷粒。” “怎么了红且?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵坝茎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我暇番,道長(zhǎng)景东,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任奔誓,我火速辦了婚禮斤吐,結(jié)果婚禮上搔涝,老公的妹妹穿的比我還像新娘。我一直安慰自己和措,他們只是感情好庄呈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著派阱,像睡著了一般诬留。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贫母,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天文兑,我揣著相機(jī)與錄音,去河邊找鬼腺劣。 笑死绿贞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的橘原。 我是一名探鬼主播籍铁,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼趾断!你這毒婦竟也來了拒名?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤芋酌,失蹤者是張志新(化名)和其女友劉穎增显,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脐帝,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡同云,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腮恩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡温兼,死狀恐怖秸滴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情募判,我是刑警寧澤荡含,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站届垫,受9級(jí)特大地震影響释液,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜装处,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一误债、第九天 我趴在偏房一處隱蔽的房頂上張望浸船。 院中可真熱鬧,春花似錦寝蹈、人聲如沸李命。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽封字。三九已至,卻和暖如春耍鬓,著一層夾襖步出監(jiān)牢的瞬間阔籽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工牲蜀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笆制,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓各薇,卻偏偏與公主長(zhǎng)得像项贺,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子峭判,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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