深入浅出 JavaScript 模块化

—— 打通规范、工具链与现代架构的任督二脉


0 引言

模块化的本质是“分而治之 + 明确边界 + 可组合复用”。
JavaScript 从最初嵌入的“全局脚本”时代一路演进:

  • 早期:全局变量横飞、依赖顺序需手动维护;
  • 社区百花:CommonJS、AMD、UMD 各自维优;
  • ESM 标准化:语言层面原生支持、构建工具深度优化;
  • 远程/边端/微前端场景:揭示对加载时机、体积、隔离性的更高要求。

本文按照时间线 + 工程落地双主线,既讲原理也给实战示例,帮助你把碎片化知识串成系统。


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))
  • 通过立即执行函数隔离私有变量。
  • 仍依赖全局单例名 utils,易冲突。

1.3 命名冲突与依赖地狱

  • 多人协作时难以保证唯一前缀。
  • 依赖链一长就出现“谁先加载谁后加载”的 脚本加载地狱

2 社区方案:CommonJS、AMD、UMD

维度 CommonJS AMD UMD
加载方式 同步(阻塞) 异步(回调) 同步 / 异步
运行环境 Node 浏览器 通吃
核心语法 require() / module.exports define([], fn) 内部判断 module.exports 是否存在
优缺点 简单直观,但浏览器需打包 网络并发好,回调繁琐 只是一层

2.1 CommonJS 示例

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

// app.js
const { add } = require('./math')
console.log(add(3, 4))
  • 模块执行一次后即缓存;再次 require 直接取缓存。
  • 在浏览器需 Browserify/Webpack 打成 bundle。

2.2 AMD 示例

define(['jquery'], function ($) {
  return function mount() {
    $('#root').text('Hello AMD')
  }
})
  • 依赖数组 声明,自带异步加载。
  • 为了解决浏览器中多个脚本并行加载的问题而诞生,但存在大量回调与配置繁琐的问题。

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))
  • 静态结构import/export 需位于顶层,编译期即可解析依赖图。

3.2 静态优势

  • Tree‑Shaking:未被引用的导出在打包阶段可被删除;
  • 声明提升:循环依赖时获取到是“引用”而非值拷贝。

3.3 浏览器原生加载

<script type="module" src="/main.js"></script>
  • 默认 延迟执行,等同 defer
  • 模块脚本默认启用 严格模式 + CORS

3.4 Node.js ESM

  • package.json"type":"module" 或使用后缀 .mjs
  • import.meta.urlimport.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 高级用法

动态导入

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  循环依赖解析

  • ESM 先创建“模块记录”再执行;相互引用时拿到 暂时未赋值的绑定

4  构建工具与打包生态

4.1  Bundler 进化

年份 工具 特征
2011 Browserify CommonJS to IIFE
2012 Webpack Loader & Plugin 生态、HMR
2015 Rollup ESM 优化、极致 tree‑shaking
2020 Vite / ESBuild Dev Server + ESM 原生 + 极快构建
2023 Bun JS Runtime + Bundler 一体

4.2  Tree‑Shaking & Scope‑Hoisting

  • Rollup 静态分析导出使用情况;
  • Webpack sideEffects:false + TerserPlugin
  • Scope‑Hoisting 把多模块函数折叠成一个 IIFE,减少闭包开销。

4.3  输出格式

export default {
  format: ['esm', 'cjs', 'umd'],
  dts: true
}
  • Library 场景通常 多格式发布
  • App 场景仅需浏览器友好的 esm + polyfill 方案。

4.4  源映射与 Chunk 策略

  • //# sourceMappingURL= 帮助线上错误定位到源码行;
  • webpackChunkName 注释或 rollup.output.manualChunks 手工拆包。

4.5  Monorepo / 多包管理

  • pnpm workspaces + nx / rush 实现 Hoist 依赖 + 原子发布

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  微前端与模块联邦

6.1  需求背景

  • 多团队并行交付;
  • 渐进式重构旧系统。

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 协助 tree‑shaking。
  3. 条件导入(Node ≥ 20)

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

    • TypeScript + JSDoc 双保险;
    • ESLint import/no‑cycle 检测循环依赖。
  5. 性能指标

    • 首屏 JS ≤ 100 kB、Chunk ≤ 50 kB;
    • 长任务切分、动态 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,每一次演进都在拉高抽象层,降低协作成本
理解背后的 依赖解析、作用域隔离、加载策略,你才能在任何规模的项目中做出最优权衡。

至此,你已拥有 JavaScript 模块化全景地图。
未来无论构建多包库、落地微前端,抑或在 Edge Runtime 编写函数,你都能以最合适的模块方案驾轻就熟。

希望本文能帮助你更好地理解和应用 JavaScript 模块化的相关知识。
如有任何问题或建议,欢迎在评论区交流讨论!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容