当前位置: 首页 > news >正文

模块化演进史:从 IIFE / CommonJS / AMD 到 ES Modules(含 Tree Shaking 原理)

目标:把“为什么会这样设计、现在应该怎么写、旧项目如何迁移”一次说清。你将获得一套可操作的迁移清单与代码模板。


0. 大图先立:四代模块化的一句话印象

时代代表运行环境特征痛点
IIFE立即执行函数浏览器用函数形成私有作用域,避免全局变量冲突无依赖管理、可维护性差
CommonJSrequire/module.exportsNode同步加载、按执行时解析浏览器不可用、难以摇树(动态依赖)
AMDdefine/require浏览器异步加载、声明依赖模板冗长、生态式微
ESMimport/export浏览器 & Node静态依赖图、原生标准、支持 Tree Shaking与 CJS 互操作有边界,需要理解打包器行为

1. IIFE(Immediately Invoked Function Expression)

1.1 核心写法

<script>
(function(global){const _cache = {};function add(a,b){ return a+b; }global.math = { add };
})(window);
</script><script>
console.log(window.math.add(1,2));
</script>

1.2 要点与局限

  • 通过函数立即执行 + 形参承载“模块导出”(挂 window)。

  • 没有标准的依赖声明与模块边界,资源顺序强耦合、可维护性差。

  • 现在仅用于极少量“无需构建的小脚本”或“兼容极老旧环境”。


2. CommonJS(CJS):Node 的事实标准

2.1 写法与特性

// add.js
function add(a, b){ return a+b; }
module.exports = add;// index.js
const add = require('./add');     // 同步解析与执行
console.log(add(1,2));
  • 同步加载:面向服务器端,磁盘读取几乎“常量时间”,同步合理。

  • 依赖是运行时决定:require(someVar) 可能是动态值。

  • 导出可变:module.exports = objexports.foo = …

2.2 痛点

  • 浏览器不支持,需要打包器把 require “编译掉”。

  • 因为依赖是动态可变,打包器很难做强确定的静态分析Tree Shaking 效果很差(通常按整包引入)。


3. AMD:浏览器时代的异步模块

3.1 写法

// add.js
define(function(){return function add(a,b){ return a+b; };
});// index.js
require(['./add'], function(add){console.log(add(1,2));
});
  • 适应浏览器网络环境,异步拉取依赖。

  • 模块定义 冗长、社区转向 ESM 后逐渐式微(历史项目仍有存量)。


4. UMD:一次发布同时兼容 CJS/AMD/全局变量

4.1 模板(简化版)

(function (root, factory) {if (typeof define === 'function' && define.amd) {define([], factory);                 // AMD} else if (typeof module === 'object' && module.exports) {module.exports = factory();          // CJS} else {root.MyLib = factory();              // 浏览器全局}
}(this, function () {function add(a,b){ return a+b; }return { add };
}));
  • 产物形态,不是源代码写法。

  • 现在更推荐直接产出 ESM + CJS 双包,UMD 主要为旧环境兜底。


5. ES Modules(ESM):现在与未来

5.1 语法与特性

// math.js
export function add(a,b){ return a+b; }
export const PI = 3.14159;
export default function square(x){ return x*x; }// index.mjs / 浏览器 <script type="module">
import square, { add, PI } from './math.js';
console.log(add(1,2), square(3), PI);
  • 静态依赖图import/export 必须在顶层,编译阶段即可解析依赖。

  • 只读绑定:导出的是绑定而非值拷贝,有助于优化器与工具链推理。

  • 原生支持:现代浏览器 <script type="module">、Node 也支持(.mjspackage.json: { "type": "module" })。

5.2 浏览器直用

<script type="module">import { add } from './math.js';console.log(add(1,2));
</script>
  • 模块脚本天然 defer,按依赖并行加载;跨域需要 CORS 头。

  • 支持 import() 动态分包加载(代码分割)。

5.3 Node 下的 ESM

  • 两种开启方式:
    1)文件后缀 .mjs
    2)package.json"type": "module"

  • CJS ↔ ESM 互操作要点(常见坑):

    • ESM 中不能直接 require,需要:

      import { createRequire } from 'node:module';
      const require = createRequire(import.meta.url);
      const pkg = require('./pkg.json');
      
    • ESM import CJS:大多数情况下 CJS 的 module.exports 会变成 ESM 的 default

      // CJS: module.exports = fn
      import fn from './legacy.cjs'; // OK
      
    • CJS require ESM:不被允许(动态);应改写为 import() 或拆分产物。

5.4 package.json 现代导出

{"name": "lib","type": "module","exports": {".": {"import": "./dist/index.esm.js",   // ESM"require": "./dist/index.cjs"      // CJS},"./feature": {"import": "./dist/feature.esm.js","require": "./dist/feature.cjs"}},"types": "./dist/index.d.ts"
}
  • 使用 exports 显式声明入口,避免深路径耦合。

  • 现代打包器与 Node 都优先解析 exports 字段。


6. Tree Shaking 原理:为什么 ESM 能“摇掉”没用的代码?

6.1 静态分析 + DCE(Dead Code Elimination)

  • 前提:打包器在构建阶段就能准确知道“导入了哪些符号”“哪些导出未被使用”。

  • ESM 静态import/export 语义固定、顶层出现 → 可进行精确的依赖图构建

  • CJS 动态require(expr) 与对 module.exports 的任意赋值让静态分析困难 → 很难做到细粒度摇树。

6.2 影响摇树的关键因素

  1. 副作用(Side Effects)

    • 例如模块顶层执行 console.log、修改全局、给原型打补丁,这些不能被删除

    • 包的作者需在 package.json 声明:

    { "sideEffects": false }
    

    或:

    { "sideEffects": ["*.css", "polyfills/*.js"] }
    

    表示哪些文件保留副作用、其他可安全摇掉。

  2. 纯函数与可推导性

    • 访问动态属性、witheval 等都会降低优化空间。

    • getters/setters 的副作用也会阻碍摇树。

  3. 最小可分割单元

    • 导出应按功能颗粒拆分,避免“大对象聚合导出”导致整包被保留:

    // 反例:坏的聚合导出
    export default { add, sub, mul, div };
    // 正例:按需导出
    export { add, sub, mul, div };
    

6.3 打包器如何做

  • Rollup:以 ESM 为一等公民,Tree Shaking 表现最好。

  • Webpackmode=production + optimization.usedExports + Terser DCE;结合 sideEffects 更稳。

  • esbuild / Vite:以 ESM 为核心,默认开启摇树,速度快。


7. 迁移与落地:从 CJS/AMD 到 ESM 的路线图

7.1 旧项目迁移清单(渐进式)

  1. 源代码层:优先把内部模块改为 ESM(export/import),必要时保留 CJS 入口做过渡。

  2. 打包配置:Rollup/Vite 优先;Webpack 需开启:

    // webpack.config.js
    module.exports = {mode: 'production',optimization: {usedExports: true,               // 标记可摇树符号concatenateModules: true,        // 作用域提升sideEffects: true                // 配合 package.json}
    }
    
  3. package.json:添加

    {"type": "module","sideEffects": false,"exports": {".": { "import": "./dist/index.esm.js", "require": "./dist/index.cjs" }}
    }
    
  4. 互操作

    • ESM 中引入 CJS → 默认拿 default

    • CJS 引入 ESM → 用 import() 或拆分产物。

  5. 单测与工具链:Jest 28+、Vitest 对 ESM 友好;Babel/TS 配置 module: esnext,交给打包器处理模块。

7.2 包作者的产物策略(库/组件库)

  • 同时产出 ESM + CJS(双入口),并配 types

  • 按需导出,避免默认导出聚合对象。

  • 声明 sideEffects,并把有副作用的文件列入白名单。

  • 提供 exports 子路径,鼓励精确导入:

    import { Button } from 'ui-kit/feature'; // 代替 'ui-kit'
    

8. 代码示例:一眼看懂“可摇 vs 难摇”

8.1 可摇(ESM + 纯导出)

// math.js
export function add(a,b){ return a+b; }
export function sub(a,b){ return a-b; }
// index.js
import { add } from './math.js';          // 只用 add
console.log(add(1,2));                    // 构建后 sub 被剔除

8.2 难摇(聚合默认导出 + 动态属性)

// anti-math.js
const api = { add(a,b){return a+b;}, sub(a,b){return a-b;} };
export default api;
// 使用方:
import math from './anti-math.js';
console.log(math.add(1,2));
// 打包器往往无法证明“访问不到 sub”,容易整包保留

8.3 顶层副作用阻碍摇树

// side.js
console.log('init');           // 顶层副作用
export const x = 1;
// 即使不使用 x,包含本文件的分包也很难被删除

9. 实战 Tips & 避坑清单

  1. 浏览器原生 ESM

    • 注意 CORS;跨域需正确的 Access-Control-Allow-Origin

    • 预加载:<link rel="modulepreload" href="/entry.js"> 改善瀑布。

  2. Node 与 ESM 的路径解析

    • ESM 必须写显式扩展名(.js/.mjs/.cjs),或通过 exports 定义;

    • __dirname/__filename 在 ESM 不存在,用:

      import { fileURLToPath } from 'node:url';
      import { dirname } from 'node:path';
      const __filename = fileURLToPath(import.meta.url);
      const __dirname = dirname(__filename);
      
  3. TypeScript

    • tsconfig.json 建议:

      {"module": "esnext","moduleResolution": "bundler",   // 或 node16"target": "ES2020"
      }
      
    • 交给打包器处理模块格式;库产物用 tsup/rollup 产 ESM+CJS。

  4. 动态 import() 的价值

    • 代码分割、懒加载、条件加载(例如路由级分包、可视区内组件)。

    • 支持 try/catch,错误处理友好。

  5. 面向库的稳定性

    • 不要在顶层做环境探测带副作用(如把东西挂到 window)。

    • 尽量无副作用:更易摇树、缓存可预测、测试更简单。


10. 一页速查:现在新项目怎么选?

  • 浏览器/Web 前端:ESM 源码 + Vite/Rollup 构建;按需 import();严格声明 sideEffects

  • Node 服务/工具:源代码用 ESM;包产物提供 ESM + CJS;exports 定义好入口。

  • 类库发布

    • 入口:exports.import → ESM,exports.require → CJS;

    • 类型:提供 types

    • Tree Shaking:纯导出 + sideEffects:false

    • Demo 与文档:示例全部用 ESM。


写在最后:

  • IIFE/AMD 是历史的过渡;CJS 在 Node 依然大量存在,但ESM 才是统一的未来

  • Tree Shaking 本质依赖“静态依赖图 + 无副作用”,语义正确比工具更重要。

  • 把“ESM 源码 + 双入口产物 + sideEffects 声明 + exports 子路径”变成你的默认工程模板,团队就自然拥有可维护、可优化的现代模块化基础设施。


文章转载自:

http://2sZPWRT2.zxcny.cn
http://yDRUWVTT.zxcny.cn
http://2oZeoUTM.zxcny.cn
http://JXhwVhYg.zxcny.cn
http://isEadKGe.zxcny.cn
http://U46uz9pU.zxcny.cn
http://cFORsq38.zxcny.cn
http://wYjFP9ai.zxcny.cn
http://2mq5TCzo.zxcny.cn
http://XPv9zHed.zxcny.cn
http://7tXihkhu.zxcny.cn
http://jJMzxFev.zxcny.cn
http://DvxNBmdx.zxcny.cn
http://EmZWGE52.zxcny.cn
http://vWOu1Yk7.zxcny.cn
http://PRjKaxl8.zxcny.cn
http://DsIq8NIp.zxcny.cn
http://4gEBUxR8.zxcny.cn
http://5Tt6bB5B.zxcny.cn
http://sTfpEddP.zxcny.cn
http://qKvnZQN2.zxcny.cn
http://NMNGqGbc.zxcny.cn
http://nSR5KYSb.zxcny.cn
http://SIgG9JJk.zxcny.cn
http://U4wWC6oz.zxcny.cn
http://3Z86pm7d.zxcny.cn
http://xprrQOEW.zxcny.cn
http://cBCV91ss.zxcny.cn
http://E0QWqJfo.zxcny.cn
http://J1ZjUWK6.zxcny.cn
http://www.dtcms.com/a/388490.html

相关文章:

  • Python+PyQt构建自动化定时任务执行工具
  • 前端如何终止请求
  • Ubuntu 系统 MySQL 全面管理指南(认证、用户、密码、服务及安全)
  • 《UE5_C++多人TPS完整教程》学习笔记53 ——《P54 转身(Turning in Place)》
  • 【Cyansdk 插件详细介绍文档】
  • IDEA 如何打开eclipse项目
  • linux C++ opencv 绘制中文(源码编译opencv)
  • 线性回归到 Softmax 回归
  • Python实现剑龙优化算法 (Stegosaurus Optimization Algorithm, SOA)优化函数(付完整代码)
  • 微软开始在Win11上全屏打广告了,怎么关?
  • 深度学习-线性回归与 Softmax 回归
  • OpenCV:背景建模
  • JavaScript async/await 实战秘籍 异步编程技巧 + 避坑指南 秒杀 Promise then 链
  • Next.js动态配置实时预览方案
  • 讲讲对MoE的理解
  • OpenLayers数据源集成 -- 章节十七:KML图层详解:Google Earth数据格式的完整集成与交互式展示方案
  • LInux DMA fence与其他同步机制的对比分析
  • 【Windows端口管理】快速查看和释放被系统保留的TCP端口
  • LeetCode 2349.设计数字容器系统:双哈希表(要咋查就咋映射)
  • 使用webpack进行Gzip 压缩原理与影响详解
  • 一个基于Python PyQt5开发的渗透测试报告生成工具,用于快速生成专业的渗透测试报告。
  • 使用注解封装查询相关的功能
  • 电感边上加一横和加两横代表什么?
  • Python 0915
  • nvidia显卡架构列表
  • MySQL InnoDB存储引擎架构底层实现详细介绍
  • QT-UI 轮播窗口
  • Nginx动静分离实验步骤
  • 硬件驱动——I.MX6ULL裸机启动(7)(ADC相关设置)
  • 重读生成概率模型1----基础概念