文章目錄
- 前言
- 一、CommonJS (CJS) - Node.js 的同步模塊系統
- 1.1 設計背景
- 1.2 瀏覽器兼容性問題
- 1.3 Webpack 如何轉換 CJS
- 1.4 適用場景
- 二、AMD (Asynchronous Module Definition) - 瀏覽器異步加載方案
- 2.1 設計背景
- 2.2 為什么現代瀏覽器不原生支持 AMD
- 2.3 Webpack/Rollup 如何處理 AMD
- 2.4 適用場景
- 三、UMD (Universal Module Definition) - 兼容瀏覽器 + Node.js 的"縫合怪"
- 3.1 設計背景
- 3.2 為什么 UMD 代碼看起來這么"丑"
- 3.3 構建工具如何生成 UMD
- 3.4 適用場景
- 四、ES Modules (ESM) - 現代 JavaScript 標準
- 4.1 設計背景
- 4.2 為什么舊瀏覽器不支持 ESM
- 4.3 構建工具如何處理 ESM
- 4.4 ESM 模塊生命周期的三個階段
- 4.5 適用場景
- 五、模塊系統對比總結
- 總結
前言
模塊系統是 JavaScript 生態演化的核心部分,不同的模塊規范(CJS/AMD/UMD/ESM)針對不同的運行環境設計,它們的加載機制、語法規則和構建工具處理方式都有顯著差異。本文將結合具體案例,詳細解析它們的設計初衷、運行環境適配、構建工具轉換規則,并解釋為什么需要不同的打包策略。
一、CommonJS (CJS) - Node.js 的同步模塊系統
1.1 設計背景
- 目標環境:Node.js(服務器端)
- 核心需求:同步加載模塊,無需考慮網絡延遲。(設計初衷是 Node.js 的本地文件系統)
- 關鍵特性:
require() 同步阻塞:模塊立即執行。
module.exports 導出模塊:用于模塊化開發。
模塊緩存:相同路徑的 require() 只執行一次。
1.2 瀏覽器兼容性問題
- 為什么瀏覽器不能直接運行 CJS?
// 瀏覽器直接運行會報錯!
const fs = require('fs'); // Uncaught ReferenceError: require is not defined
原因:
- API 不兼容:require 是 Node.js 的 API,瀏覽器沒有實現。
- 加載方式差異:CJS 是同步加載,瀏覽器需要異步加載(否則阻塞渲染)。
1.3 Webpack 如何轉換 CJS
// 原始代碼 (CJS)
const lodash = require('lodash');
// Webpack 轉換后(簡化版)
const __webpack_modules__ = {'lodash': (module) => { module.exports = _; }
};
function __webpack_require__(moduleId) {// 1. 檢查緩存// 2. 執行模塊代碼// 3. 返回 module.exports
}
const lodash = __webpack_require__('lodash');
關鍵轉換策略
- 包裹模塊:每個模塊被包裹成函數,避免全局污染。
- 實現自己的 require 系統:webpack_require 模擬 Node.js 的模塊加載。
- 依賴分析:構建時靜態分析 require() 調用。
1.4 適用場景
- Node.js 后端開發:適用于服務器端的模塊化開發。
- 舊版工具鏈:如 Webpack 4 默認使用 CJS。
二、AMD (Asynchronous Module Definition) - 瀏覽器異步加載方案
2.1 設計背景
- 目標環境:瀏覽器(RequireJS)
- 核心需求:異步加載,避免阻塞渲染
- 關鍵特性:
define() 定義模塊
require([], callback) 動態加載依賴
依賴前置:所有依賴必須在回調函數之前聲明
獨立模塊
立即執行,依賴模塊
按序加載、回調執行
2.2 為什么現代瀏覽器不原生支持 AMD
<!-- 必須手動加載 RequireJS -->
<script src="require.js"></script>
<script>require(['jquery'], function($) {// 回調函數內才能使用 jQuery});
</script>
原因:
- AMD 是社區規范,不是 ECMAScript 標準
- 現代瀏覽器原生支持 ESM,不再需要 AMD
2.3 Webpack/Rollup 如何處理 AMD
// 原始 AMD 代碼
define(['jquery'], function($) {return { init: () => $('body').css('color', 'red') };
});
// Webpack 轉換后(Promise 化)
__webpack_require__.e("jquery").then(function() {const $ = __webpack_require__("jquery");return { init: () => $('body').css('color', 'red') };
});
關鍵轉換策略:
- 轉為 Promise 鏈:適配現代異步編程
- 代碼拆分:動態加載的模塊會被拆分為單獨 chunk
2.4 適用場景
- 舊版瀏覽器項目(IE 8+)
- 按需加載的復雜 SPA(如 2015 年前的 AngularJS 項目)
三、UMD (Universal Module Definition) - 兼容瀏覽器 + Node.js 的"縫合怪"
3.1 設計背景
- 目標環境:同時支持瀏覽器、Node.js、AMD
- 核心需求:一份代碼,多環境運行(適配所有規范)
- 關鍵特性:
環境嗅探:判斷當前是 CJS/AMD/全局變量
手動適配:通過 if-else 實現多環境兼容
3.2 為什么 UMD 代碼看起來這么"丑"
// UMD 模板代碼(jQuery 風格)
(function (global, factory) {if (typeof define === 'function' && define.amd) {// AMD 環境define(['jquery'], factory);} else if (typeof exports === 'object') {// CJS 環境 (Node.js)module.exports = factory(require('jquery'));} else {// 瀏覽器全局變量global.myLib = factory(global.jQuery);}
}(typeof self !== 'undefined' ? self : this, function ($) {// 實際模塊代碼return { init: () => $('body').css('color', 'red') };
}));
</script>
原因:
- 需要手動判斷運行環境
- 必須兼容多種模塊加載方式
3.3 構建工具如何生成 UMD
# Rollup 生成 UMD
rollup -i src/index.js -o dist/bundle.umd.js -f umd -n myLib
// 輸出結構
(function (global, factory) {// 環境檢測邏輯...
})(this, function() {return /* 模塊內容 */;
});
關鍵轉換策略:
- 包裹 IIFE(Immediately Invoked Function Expression,立即調用函數表達式):避免污染全局作用域
- 注入環境判斷:運行時動態選擇模塊系統
3.4 適用場景
- 開源庫開發(如 Lodash、Moment.js)
- 需要同時支持
<script>
標簽和npm 安裝
的項目
export function kInstallScript(src: string): Promise<void> {return new Promise((resolve, reject) => {const script = document.createElement('script');script.src = src;script.onload = resolve as () => void;script.onerror = reject;document.head.append(script);});
}
await kInstallScript(kIsMobile? '/lib/mobileFilePreview/file-preview.umd.min.js': '/file-preview/filePreview.umd.min.js');kFilePreviewSDK = kIsMobile? (globalThis as any)['file-preview'].FilePreviewSDK: (globalThis as any).FilePreview;// globalThis 在瀏覽器環境中會將模塊掛載到全局對象(如 window)上,屬性名就是在打包配置中指定的 name
// UMD 模塊的全局變量名
傳統瀏覽器用戶:希望通過
<script src="awesome-lib.js">
直接使用
現代前端工程:希望通過npm install awesome-lib
引入
用戶環境不可控,而 UMD 就是最好的兼容方案
當使用 RollupWebpack 之類的打包器時,UMD 通常用作備用模塊
四、ES Modules (ESM) - 現代 JavaScript 標準
4.1 設計背景
- 目標環境:現代瀏覽器 + Node.js(ES6+)
- 核心需求:官方標準、靜態分析、Tree Shaking
- 關鍵特性:
import / export 語法
靜態加載:依賴關系在編譯時確定
原生支持:瀏覽器和 Node.js 均可直接運行
靜態分析
(Static Analysis)是編程語言和構建工具在 不實際執行代碼的情況下,通過分析代碼的結構、語法、依賴關系來推導代碼行為的技術。它在 Tree Shaking(搖樹優化)
中扮演核心角色,直接影響打包工具的無用代碼消除能力。
靜態分析是 現代前端工具鏈的基石
- Tree Shaking 能安全刪除未使用代碼
- 類型系統 能提前發現錯誤
- 壓縮工具 能極致優化體積
CJS 的動態特性破壞了靜態分析的前提,而 ESM 的嚴格靜態結構讓工具能精確推導代碼行為。這就是為什么現代前端生態(Vite/Rollup/Snowpack)都基于 ESM 設計。
模塊系統 | 靜態分析可行性 | 根本原因 |
---|---|---|
ESM | ? 完美支持 | 語言標準強制靜態結構 |
CJS | ?? 有限支持 | require() 動態性破壞分析前提 |
AMD | ? 基礎支持 | 依賴數組顯式聲明 |
UMD | ? 幾乎不可用 | 混合模式導致邏輯分裂 |
SystemJS | ? 不可用 | 動態注冊機制 |
4.2 為什么舊瀏覽器不支持 ESM
<!-- 現代瀏覽器 -->
<script type="module">import { add } from './math.js'; // 正常工作
</script>
<!-- 舊瀏覽器 -->
<script>import { add } from './math.js'; // SyntaxError
</script>
原因:
- import/export 是 ES6 語法,IE 11 及更早版本不支持
- 傳統
<script>
默認是全局腳本,不解析模塊語法
4.3 構建工具如何處理 ESM
webpack:
// 原始 ESM
import React from 'react';
// Webpack 轉換后(CJS 風格)
const React = __webpack_require__('react');
- 策略:默認轉為 CJS,兼容舊環境
Vite:
// 開發模式:直接返回 ESM
import React from '/node_modules/react/index.js'; // 瀏覽器發起請求
// 生產模式:Rollup 打包
import { r as React } from './chunk-abc123.js';
- 策略:
開發模式:開發時不需要打包,利用瀏覽器原生 ESM
生產模式:Rollup 打包優化
ESM 的模塊處理機制與傳統 AMD/CJS 有本質區別,主要體現在
編譯時靜態分析
與運行時執行控制
兩個階段。
4.4 ESM 模塊生命周期的三個階段
(1) 解析階段(Parsing)
靜態分析所有 import(無論是否會被執行)
// main.js
import { unused } from './unused.js'; // 即使從未使用也會被分析
import { core } from './core.js';
if (false) unused(); // 死代碼
構建不可變的依賴圖:
(2) 加載階段(Loading)
瀏覽器/Node.js 的行為:
- 立即并行請求所有依賴模塊(包括unused.js)
- 阻塞性:必須所有依賴加載完成才會進入執行階段
示例網絡請求:
GET /main.js
GET /unused.js (并行)
GET /core.js (并行)
(3) 執行階段(Evaluation)
嚴格按拓撲順序初始化(從葉子節點
開始):
執行順序:unused.js → core.js → main.js
關鍵特性:
即使模塊導出未被使用,該模塊仍會被執行(包括其頂層代碼)
但不會執行未被調用的函數
模塊初始化 ≠ 導出被調用
即使導出未被使用,模塊的頂層代碼仍會執行(打包工具可能通過Tree Shaking移除)
4.5 適用場景
- 現代前端項目(React/Vue 3+)
- Node.js 14+ 后端項目
五、模塊系統對比總結
模塊系統 | 加載方式 | 環境支持 | 構建工具轉換策略 | 典型應用場景 |
---|---|---|---|---|
CJS | 同步 | Node.js | 包裹為函數,模擬 require | Node.js 后端 |
AMD | 異步 | 瀏覽器 (RequireJS) | 轉為 Promise + 代碼拆分 | 舊版瀏覽器 SPA |
UMD | 兼容多種 | 瀏覽器 + Node.js | IIFE + 環境嗅探 | 開源庫開發 |
ESM | 靜態 | 現代瀏覽器 + Node | 原生支持或轉為 CJS | 現代前端/Node 項目 |
特性 | ESM | AMD/RequireJS | CommonJS |
---|---|---|---|
依賴獲取時機 | 并行請求所有發現的依賴 | 按需并行請求 | 同步阻塞加載 |
執行觸發條件 | 整個依賴圖就緒后拓撲序執行 | 串行回調(按照申明順序) | 遇到 require 時立即執行 |
循環依賴處理 | 語言標準明確定義(引用未初始化值) | 依賴加載器實現(可能不一致) | 部分導出可能為 undefined |
典型場景 | import './module.js' | require(['module'], callback) | const m = require('./module') |
總結
- CJS:Node.js 專用,同步加載,需構建工具轉換才能在瀏覽器運行
- AMD:舊瀏覽器異步加載方案,已被 ESM 取代
- UMD:兼容瀏覽器 + Node.js 的過渡方案,適合庫開發
- ESM:現代標準,支持 Tree Shaking,未來唯一選擇
構建工具的作用就是
抹平環境差異
,讓開發者可以用任意模塊規范編寫代碼,最終輸出目標環境可運行的版本。