史上很全的 JavaScript 模塊化方案和工具
模塊化是大型前端項(xiàng)目的必備要素。JavaScript 從誕生至今,出現(xiàn)過(guò)各種各樣的模塊化方案,讓我們一起來(lái)盤(pán)點(diǎn)下吧。
IIFE 模塊
默認(rèn)情況下,在瀏覽器宿主環(huán)境里定義的變量都是全局變量,如果頁(yè)面引用了多個(gè)這樣的 JavaScript 文件,很容易造成命名沖突。
- // 定義全局變量
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- // 使用全局變量
- increase();
- reset();
為了避免全局污染,可以用匿名函數(shù)包裹起來(lái),這就是最簡(jiǎn)單的 IIFE 模塊(立即執(zhí)行的函數(shù)表達(dá)式):
- // 定義 IIFE 模塊
- const iifeCounterModule = (() => {
- let count = 0;
- return {
- increase: () => ++count,
- reset: () => {
- count = 0;
- console.log("Count is reset.");
- }
- };
- })();
- // 使用 IIFE 模塊
- iifeCounterModule.increase();
- iifeCounterModule.reset();
IIFE 只暴露了一個(gè)全局的模塊名,內(nèi)部都是局部變量,大大減少了全局命名沖突。
每個(gè) IIFE 模塊都是一個(gè)全局變量,這些模塊通常有自己的依賴(lài)。可以在模塊內(nèi)部直接使用依賴(lài)的全局變量,也可以把依賴(lài)作為參數(shù)傳給 IIFE:
- // 定義帶有依賴(lài)的 IIFE 模塊
- const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
- let count = 0;
- return {
- increase: () => ++count,
- reset: () => {
- count = 0;
- console.log("Count is reset.");
- }
- };
- })(dependencyModule1, dependencyModule2);
一些流行的庫(kù)在早期版本都采用這模式,比如大名鼎鼎的 jQuery(最新版本也開(kāi)始用 UMD 模塊了,后面會(huì)介紹)。
還有一種 IIFE,在 API 聲明上遵循了一種格式,就是在模塊內(nèi)部提前定義了這些 API 對(duì)應(yīng)的變量,方便 API 之間互相調(diào)用:
- // Define revealing module.
- const revealingCounterModule = (() => {
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- return {
- increase,
- reset
- };
- })();
- // Use revealing module.
- revealingCounterModule.increase();
- revealingCounterModule.reset();
CommonJS 模塊(Node.js 模塊)
CommonJS 最初叫 ServerJS,是由 Node.js 實(shí)現(xiàn)的模塊化方案。默認(rèn)情況下,每個(gè) .js 文件就是一個(gè)模塊,模塊內(nèi)部提供了一個(gè)module和exports變量,用于暴露模塊的 API。使用 require 加載和使用模塊。下面這段代碼定義了一個(gè)計(jì)數(shù)器模塊:
- // 定義 CommonJS 模塊: commonJSCounterModule.js.
- const dependencyModule1 = require("./dependencyModule1");
- const dependencyModule2 = require("./dependencyModule2");
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- exports.increase = increase;
- exports.reset = reset;
- // 或者這樣:
- module.exports = {
- increase,
- reset
- };
使用這個(gè)模塊:
- // 使用 CommonJS 模塊
- const { increase, reset } = require("./commonJSCounterModule");
- increase();
- reset();
- // 或者這樣:
- const commonJSCounterModule = require("./commonJSCounterModule");
- commonJSCounterModule.increase();
- commonJSCounterModule.reset();
在運(yùn)行時(shí),Node.js 會(huì)將文件內(nèi)的代碼包裹在一個(gè)函數(shù)內(nèi),然后通過(guò)參數(shù)傳遞exports、module變量和require函數(shù)。
- // Define CommonJS module: wrapped commonJSCounterModule.js.
- (function (exports, require, module, __filename, __dirname) {
- const dependencyModule1 = require("./dependencyModule1");
- const dependencyModule2 = require("./dependencyModule2");
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- module.exports = {
- increase,
- reset
- };
- return module.exports;
- }).call(thisValue, exports, require, module, filename, dirname);
- // Use CommonJS module.
- (function (exports, require, module, __filename, __dirname) {
- const commonJSCounterModule = require("./commonJSCounterModule");
- commonJSCounterModule.increase();
- commonJSCounterModule.reset();
- }).call(thisValue, exports, require, module, filename, dirname);
AMD 模塊(RequireJS 模塊)
AMD(異步模塊定義)也是一種模塊格式,由 RequireJS 這個(gè)庫(kù)實(shí)現(xiàn)。它通過(guò)define函數(shù)定義模塊,并接受模塊名和依賴(lài)的模塊名作為參數(shù)。
- // 定義 AMD 模塊
- define("amdCounterModule", ["dependencyModule1", "dependencyModule2"],
- (dependencyModule1, dependencyModule2) => {
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- return {
- increase,
- reset
- };
- });
也用 require加載和使用模塊:
- require(["amdCounterModule"], amdCounterModule => {
- amdCounterModule.increase();
- amdCounterModule.reset();
- });
跟 CommonJS 不同,這里的 requrie接受一個(gè)回調(diào)函數(shù),參數(shù)就是加載好的模塊對(duì)象。
AMD 的define函數(shù)還可以動(dòng)態(tài)加載模塊,只要給它傳一個(gè)回調(diào)函數(shù),并帶上 require參數(shù):
- // Use dynamic AMD module.
- define(require => {
- const dynamicDependencyModule1 = require("dependencyModule1");
- const dynamicDependencyModule2 = require("dependencyModule2");
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- return {
- increase,
- reset
- };
- });
AMD 模塊還可以給define傳遞module和exports,這樣就可以在內(nèi)部使用 CommonJS 代碼:
- // 定義帶有 CommonJS 代碼的 AMD 模塊
- define((require, exports, module) => {
- // CommonJS 代碼
- const dependencyModule1 = require("dependencyModule1");
- const dependencyModule2 = require("dependencyModule2");
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- exports.increase = increase;
- exports.reset = reset;
- });
- // 使用帶有 CommonJS 代碼的 AMD 模塊
- define(require => {
- // CommonJS 代碼
- const counterModule = require("amdCounterModule");
- counterModule.increase();
- counterModule.reset();
- });
UMD 模塊
UMD(通用模塊定義),是一種支持多種環(huán)境的模塊化格式,可同時(shí)用于 AMD 和 瀏覽器(或者 Node.js)環(huán)境。
兼容 AMD 和瀏覽器全局引入:
- ((root, factory) => {
- // 檢測(cè)是否存在 AMD/RequireJS 的 define 函數(shù)
- if (typeof define === "function" && define.amd) {
- // 如果是,在 define 函數(shù)內(nèi)調(diào)用 factory
- define("umdCounterModule", ["deependencyModule1", "dependencyModule2"], factory);
- } else {
- // 否則為瀏覽器環(huán)境,直接調(diào)用 factory
- // 導(dǎo)入的依賴(lài)是全局變量(window 對(duì)象的屬性)
- // 導(dǎo)出的模塊也是全局變量(window 對(duì)象的屬性)
- root.umdCounterModule = factory(root.deependencyModule1, root.dependencyModule2);
- }
- })(typeof self !== "undefined" ? self : this, (deependencyModule1, dependencyModule2) => {
- // 具體的模塊代碼
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- return {
- increase,
- reset
- };
- });
看起來(lái)很復(fù)雜,其實(shí)就是個(gè) IIFE。代碼注釋寫(xiě)得很清楚了,可以看看。
下面來(lái)看兼容 AMD 和 CommonJS(Node.js)模塊的 UMD:
- (define => define((require, exports, module) => {
- // 模塊代碼
- const dependencyModule1 = require("dependencyModule1");
- const dependencyModule2 = require("dependencyModule2");
- let count = 0;
- const increase = () => ++count;
- const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- module.export = {
- increase,
- reset
- };
- }))(// 判斷 CommonJS 里的 module 變量和 exports 變量是否存在
- // 同時(shí)判斷 AMD/RequireJS 的define 函數(shù)是否存在
- typeof module === "object" && module.exports && typeof define !== "function"
- ? // 如果是 CommonJS/Node.js,手動(dòng)定義一個(gè) define 函數(shù)
- factory => module.exports = factory(require, exports, module)
- : // 否則是 AMD/RequireJS,直接使用 define 函數(shù)
- define);
同樣是個(gè) IIFE,通過(guò)判斷環(huán)境,選擇執(zhí)行對(duì)應(yīng)的代碼。
ES 模塊(ES6 Module)
前面說(shuō)到的幾種模塊格式,都是用到了各種技巧實(shí)現(xiàn)的,看起來(lái)眼花繚亂。終于,在 2015 年,ECMAScript 第 6 版(ES 2015,或者 ES6 )橫空出世!它引入了一種全新的模塊格式,主要語(yǔ)法就是 import和epxort關(guān)鍵字。來(lái)看 ES6 怎么定義模塊:
- // 定義 ES 模塊:esCounterModule.js 或 esCounterModule.mjs.
- import dependencyModule1 from "./dependencyModule1.mjs";
- import dependencyModule2 from "./dependencyModule2.mjs";
- let count = 0;
- // 具名導(dǎo)出:
- export const increase = () => ++count;
- export const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- // 默認(rèn)導(dǎo)出
- export default {
- increase,
- reset
- };
瀏覽器里使用該模塊,在 script標(biāo)簽上加上type="module",表明引入的是 ES 模塊。在 Node.js 環(huán)境中使用時(shí),把擴(kuò)展名改成 .mjs。
- // Use ES module.
- //瀏覽器: <script type="module" src="esCounterModule.js"></script> or inline.
- // 服務(wù)器:esCounterModule.mjs
- import { increase, reset } from "./esCounterModule.mjs";
- increase();
- reset();
- // Or import from default export:
- import esCounterModule from "./esCounterModule.mjs";
- esCounterModule.increase();
- esCounterModule.reset();
瀏覽器如果不支持,可以加個(gè)兜底屬性:
- <script nomodule>
- alert("Not supported.");
- </script>
ES 動(dòng)態(tài)模塊(ECMAScript 2020)
2020 年最新的 ESCMA 標(biāo)準(zhǔn)11版中引入了內(nèi)置的 import函數(shù),用于動(dòng)態(tài)加載 ES 模塊。import函數(shù)返回一個(gè) Promise,在它的then回調(diào)里使用加載后的模塊:
- // 用 Promise API 加載動(dòng)態(tài) ES 模塊
- import("./esCounterModule.js").then(({ increase, reset }) => {
- increase();
- reset();
- });
- import("./esCounterModule.js").then(dynamicESCounterModule => {
- dynamicESCounterModule.increase();
- dynamicESCounterModule.reset();
- });
由于返回的是 Promise,那肯定也支持await用法:
- // 通過(guò) async/await 使用 ES 動(dòng)態(tài)模塊
- (async () => {
- // 具名導(dǎo)出的模塊
- const { increase, reset } = await import("./esCounterModule.js");
- increase();
- reset();
- // 默認(rèn)導(dǎo)出的模塊
- const dynamicESCounterModule = await import("./esCounterModule.js");
- dynamicESCounterModule.increase();
- dynamicESCounterModule.reset();
- })();
各平臺(tái)對(duì)import、export和動(dòng)態(tài)import的兼容情況如下:
image.png
image.png
System 模塊
SystemJS 是一個(gè) ES 模塊語(yǔ)法轉(zhuǎn)換庫(kù),以便支持低版本的 ES。例如,下面的模塊是用 ES6 語(yǔ)法定義的:
- // 定義 ES 模塊
- import dependencyModule1 from "./dependencyModule1.js";
- import dependencyModule2 from "./dependencyModule2.js";
- dependencyModule1.api1();
- dependencyModule2.api2();
- let count = 0;
- // Named export:
- export const increase = function () { return ++count };
- export const reset = function () {
- count = 0;
- console.log("Count is reset.");
- };
- // Or default export:
- export default {
- increase,
- reset
- }
如果當(dāng)前的運(yùn)行環(huán)境(比如舊瀏覽器)不支持 ES6 語(yǔ)法,上面的代碼就無(wú)法運(yùn)行。一種方案是把上面的模塊定義轉(zhuǎn)換成 SystemJS 庫(kù)的一個(gè) API, System.register:
- // Define SystemJS module.
- System.register(["./dependencyModule1.js", "./dependencyModule2.js"],
- function (exports_1, context_1) {
- "use strict";
- var dependencyModule1_js_1, dependencyModule2_js_1, count, increase, reset;
- var __moduleName = context_1 && context_1.id;
- return {
- setters: [
- function (dependencyModule1_js_1_1) {
- dependencyModule1_js_1 = dependencyModule1_js_1_1;
- },
- function (dependencyModule2_js_1_1) {
- dependencyModule2_js_1 = dependencyModule2_js_1_1;
- }
- ],
- execute: function () {
- dependencyModule1_js_1.default.api1();
- dependencyModule2_js_1.default.api2();
- count = 0;
- // Named export:
- exports_1("increase", increase = function () { return ++count };
- exports_1("reset", reset = function () {
- count = 0;
- console.log("Count is reset.");
- };);
- // Or default export:
- exports_1("default", {
- increase,
- reset
- });
- }
- };
- });
這樣,import/export關(guān)鍵字就不見(jiàn)了。Webpack、TypeScript 等可以自動(dòng)完成這樣的轉(zhuǎn)換(后面會(huì)講)。
SystemJS 也支持動(dòng)態(tài)加載模塊:
- // Use SystemJS module with promise APIs.
- System.import("./esCounterModule.js").then(dynamicESCounterModule => {
- dynamicESCounterModule.increase();
- dynamicESCounterModule.reset();
- });
Webpack 模塊(打包 AMD,CJS,ESM)
Webpack 是個(gè)強(qiáng)大的模塊打包工具,可以將 AMD、CommonJS 和 ES Module 格式的模塊轉(zhuǎn)換并打包到單個(gè) JS 文件。
Babel 模塊
Babel 是也個(gè)轉(zhuǎn)換器,可將 ES6+ 代碼轉(zhuǎn)換成低版本的 ES。前面例子中的計(jì)數(shù)器模塊用 Babel 轉(zhuǎn)換后的代碼是這樣的:
- // Babel.
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
- exports["default"] = void 0;
- function _interopRequireDefault(obj)
- { return obj && obj.__esModule ? obj : { "default": obj }; }
- // Define ES module: esCounterModule.js.
- var dependencyModule1 = _interopRequireDefault(require("./amdDependencyModule1"));
- var dependencyModule2 = _interopRequireDefault(require("./commonJSDependencyModule2"));
- dependencyModule1["default"].api1();
- dependencyModule2["default"].api2();
- var count = 0;
- var increase = function () { return ++count; };
- var reset = function () {
- count = 0;
- console.log("Count is reset.");
- };
- exports["default"] = {
- increase: increase,
- reset: reset
- };
引入該模塊的index.js將會(huì)轉(zhuǎn)換成:
- // Babel.
- function _interopRequireDefault(obj)
- { return obj && obj.__esModule ? obj : { "default": obj }; }
- // Use ES module: index.js
- var esCounterModule = _interopRequireDefault(require("./esCounterModule.js"));
- esCounterModule["default"].increase();
- esCounterModule["default"].reset();
以上是 Babel 的默認(rèn)轉(zhuǎn)換行為,它還可以結(jié)合其他插件使用,比如前面提到的 SystemJS。經(jīng)過(guò)配置,Babel 可將 AMD、CJS、ES Module 轉(zhuǎn)換成 System 模塊格式。
TypeScript 模塊
TypeScript 是 JavaScript 的超集,可以支持所有 JavaScript 語(yǔ)法,包括 ES6 模塊語(yǔ)法。它在轉(zhuǎn)換時(shí),可以保留 ES6 語(yǔ)法,也可以轉(zhuǎn)換成 AMD、CJS、UMD、SystemJS 等格式,取決于配置:
- {
- "compilerOptions": {
- "module": "ES2020", // None, CommonJS, AMD, System, UMD, ES6, ES2015, ES2020, ESNext.
- }
- }
TypeScript 還支持 module和namespace關(guān)鍵字,表示內(nèi)部模塊。
- module Counter {
- let count = 0;
- export const increase = () => ++count;
- export const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- }
- namespace Counter {
- let count = 0;
- export const increase = () => ++count;
- export const reset = () => {
- count = 0;
- console.log("Count is reset.");
- };
- }
都可以轉(zhuǎn)換成 JavaScript 對(duì)象:
- var Counter;
- (function (Counter) {
- var count = 0;
- Counter.increase = function () { return ++count; };
- Counter.reset = function () {
- count = 0;
- console.log("Count is reset.");
- };
- })(Counter || (Counter = {}));
總結(jié)
以上提到的各種模塊格式是在 JavaScript 語(yǔ)言演進(jìn)過(guò)程中出現(xiàn)的模塊化方案,各有其適用環(huán)境。隨著標(biāo)準(zhǔn)化推進(jìn),Node.js 和最新的現(xiàn)代瀏覽器都開(kāi)始支持 ES 模塊格式。如果要在舊環(huán)境中使用模塊化,可以通過(guò) Webpack、Babel、TypeScript、SystemJS 等工具進(jìn)行轉(zhuǎn)換。