深入淺出 JavaScript 模塊化

—— 打通規(guī)范、工具鏈與現(xiàn)代架構的任督二脈


0 引言

模塊化的本質是“分而治之 + 明確邊界 + 可組合復用”。
JavaScript 從最初嵌入的“全局腳本”時代一路演進:

  • 早期:全局變量橫飛、依賴順序需手動維護;
  • 社區(qū)百花:CommonJS、AMD、UMD 各自維優(yōu);
  • ESM 標準化:語言層面原生支持、構建工具深度優(yōu)化;
  • 遠程/邊端/微前端場景:揭示對加載時機、體積、隔離性的更高要求。

本文按照時間線 + 工程落地雙主線,既講原理也給實戰(zhàn)示例,幫助你把碎片化知識串成系統(tǒng)。


1 原始階段:Script 標簽 + 命名空間

1.1 全局腳本模式

<script src="jquery.js"></script>
<script src="app.js"></script>

缺點:全局污染、依賴順序、人肉維護。

1.2 IIFE / 揭示式模塊模式

const utils = (function () {
  function add(a, b) { return a + b }
  return { add }
})()
console.log(utils.add(1, 2))
  • 通過立即執(zhí)行函數(shù)隔離私有變量。
  • 仍依賴全局單例名 utils,易沖突。

1.3 命名沖突與依賴地獄

  • 多人協(xié)作時難以保證唯一前綴。
  • 依賴鏈一長就出現(xiàn)“誰先加載誰后加載”的 腳本加載地獄。

2 社區(qū)方案:CommonJS、AMD、UMD

維度 CommonJS AMD UMD
加載方式 同步(阻塞) 異步(回調) 同步 / 異步
運行環(huán)境 Node 瀏覽器 通吃
核心語法 require() / module.exports define([], fn) 內部判斷 module.exports 是否存在
優(yōu)缺點 簡單直觀,但瀏覽器需打包 網絡并發(fā)好,回調繁瑣 只是一層

2.1 CommonJS 示例

// math.js
exports.add = (a, b) => a + b

// app.js
const { add } = require('./math')
console.log(add(3, 4))
  • 模塊執(zhí)行一次后即緩存;再次 require 直接取緩存。
  • 在瀏覽器需 Browserify/Webpack 打成 bundle。

2.2 AMD 示例

define(['jquery'], function ($) {
  return function mount() {
    $('#root').text('Hello AMD')
  }
})
  • 依賴數(shù)組 聲明,自帶異步加載。
  • 為了解決瀏覽器中多個腳本并行加載的問題而誕生,但存在大量回調與配置繁瑣的問題。

2.3 UMD

(function (root, factory) {
  if (typeof module === 'object' && module.exports) {
    module.exports = factory()
  } else if (typeof define === 'function' && define.amd) {
    define(factory)
  } else {
    root.myLib = factory()
  }
})(this, function () { /* ... */ })
  • 典型“同時支持 Node、AMD、全局”的包裝。
  • 本質還是“打補丁”,漸漸被 ESM 取代。

3 標準化新紀元:ESM

3.1 語法速覽

// math.js
export function add(a, b) { return a + b }
export const PI = 3.14

// app.mjs
import { add, PI } from './math.js'
console.log(add(PI, 1))
  • 靜態(tài)結構import/export 需位于頂層,編譯期即可解析依賴圖。

3.2 靜態(tài)優(yōu)勢

  • Tree?Shaking:未被引用的導出在打包階段可被刪除;
  • 聲明提升:循環(huán)依賴時獲取到是“引用”而非值拷貝。

3.3 瀏覽器原生加載

<script type="module" src="/main.js"></script>
  • 默認 延遲執(zhí)行,等同 defer;
  • 模塊腳本默認啟用 嚴格模式 + CORS

3.4 Node.js ESM

  • package.json"type":"module" 或使用后綴 .mjs。
  • import.meta.url、import.meta.resolve 給出運行時信息。

3.5 CommonJS ? ESM 互操作

  • ESM → CJSimport pkg from 'cjs-lib' 實際拿到 module.exports
  • CJS → ESMrequire('./esm-file.mjs') 會返回一個 Promise。
    ??注意“默認導出”差異:CJS 只有 module.exports 一個出口。

3.6 高級用法

動態(tài)導入

button.onclick = async () => {
  const { lazy } = await import('./heavy.js')
  lazy()
}

支持 代碼分割 + 按需加載。

Top?Level Await

// fetch-data.mjs
const data = await fetch('/api').then(r => r.json())
export default data

使“模塊依賴異步資源”變得自然,但會 阻塞整個依賴圖的評估。

Import Maps(瀏覽器端)

<script type="importmap">
{
  "imports": {
    "vue": "https://cdn.skypack.dev/vue@3.5.0"
  }
}
</script>

允許在瀏覽器端指定模塊路徑映射,簡化依賴管理。目前仍處于實驗階段,需瀏覽器支持。

3.7? 循環(huán)依賴解析

  • ESM 先創(chuàng)建“模塊記錄”再執(zhí)行;相互引用時拿到 暫時未賦值的綁定。

4? 構建工具與打包生態(tài)

4.1? Bundler 進化

年份 工具 特征
2011 Browserify CommonJS to IIFE
2012 Webpack Loader & Plugin 生態(tài)、HMR
2015 Rollup ESM 優(yōu)化、極致 tree?shaking
2020 Vite / ESBuild Dev Server + ESM 原生 + 極快構建
2023 Bun JS Runtime + Bundler 一體

4.2? Tree?Shaking & Scope?Hoisting

  • Rollup 靜態(tài)分析導出使用情況;
  • Webpack sideEffects:false + TerserPlugin;
  • Scope?Hoisting 把多模塊函數(shù)折疊成一個 IIFE,減少閉包開銷。

4.3? 輸出格式

export default {
  format: ['esm', 'cjs', 'umd'],
  dts: true
}
  • Library 場景通常 多格式發(fā)布
  • App 場景僅需瀏覽器友好的 esm + polyfill 方案。

4.4? 源映射與 Chunk 策略

  • //# sourceMappingURL= 幫助線上錯誤定位到源碼行;
  • webpackChunkName 注釋或 rollup.output.manualChunks 手工拆包。

4.5? Monorepo / 多包管理

  • pnpm workspaces + nx / rush 實現(xiàn) Hoist 依賴 + 原子發(fā)布。

5? 跑在云端與邊緣的模塊化

平臺 特征 模塊格式
Deno 原生 ESM、URL 導入 ESM
Cloudflare Workers V8 Isolate + ESM-only ESM
Vercel Edge 基于 V8 的無服務器 ESM
  • Service Worker 支持ESM:new Worker('./worker.js', { type:'module' })。
  • HTTP/2?Push、Early Hints 搭配 ESM preload 減少白屏。

6? 微前端與模塊聯(lián)邦

6.1? 需求背景

  • 多團隊并行交付;
  • 漸進式重構舊系統(tǒng)。

6.2? Webpack Module?Federation

// host webpack.config.js
plugins:[
  new ModuleFederationPlugin({
    remotes: { shop: 'shop@https://cdn.xxx.com/remoteEntry.js' }
  })
]
// remote app
module.exports = {
  name:'shop', exposes:{ './Header':'./src/Header.tsx' }
}

運行時拉取遠程 bundle 并注入共享依賴版本。

6.3? CDN + import() 方案

const Header = await import('https://unpkg.com/shop/Header.js')

無打包器耦合,更貼近瀏覽器原生。

6.4? 共享依賴治理

  • singleton: true 指定依賴只加載一次;
  • 版本不匹配降級到 fiber 沙箱或 iframe 隔離。

7? 安全、性能與最佳實踐

  1. 供應鏈安全

    • Subresource?Integrity:<script src="..." integrity="sha384?...">
    • 鎖文件審計:npm?audit / snyk。
  2. Side?Effects 字段

    • package.json?里 "sideEffects": false 協(xié)助 tree?shaking。
  3. 條件導入(Node ≥?20)

    import pkg from './lib/index.js' with { type:'json' }
    
  4. 類型 & Lint

    • TypeScript?+?JSDoc 雙保險;
    • ESLint import/no?cycle 檢測循環(huán)依賴。
  5. 性能指標

    • 首屏 JS ≤?100?kB、Chunk ≤?50?kB;
    • 長任務切分、動態(tài) import 保持主線程流暢。

8? 未來展望

提案 目標 進度
Package?Exports/Imports 更安全的包入口映射 Stage?4, 已被 Node 采納
Module?Fragments 內嵌 HTML 模塊化 Stage?2
WASM Modules JS ?? Wasm 無縫導入 試驗中
  • 零打包時代:ESBuild/Bun 利用極快編譯 + 瀏覽器原生 ESM。

9? 結語

模塊化是一條“控制復雜度”的主線:
從命名空間到 CommonJS,再到 ESM 與 Module?Federation,每一次演進都在拉高抽象層,降低協(xié)作成本。
理解背后的 依賴解析、作用域隔離、加載策略,你才能在任何規(guī)模的項目中做出最優(yōu)權衡。

至此,你已擁有 JavaScript 模塊化全景地圖。
未來無論構建多包庫、落地微前端,抑或在 Edge Runtime 編寫函數(shù),你都能以最合適的模塊方案駕輕就熟。

希望本文能幫助你更好地理解和應用 JavaScript 模塊化的相關知識。
如有任何問題或建議,歡迎在評論區(qū)交流討論!

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容