【JS】【前端】JavaScript 模块化演进之路(CJS/AMD/UMD/CMD/ESM)

【JS】【前端】JavaScript 模块化演进之路(CJS/AMD/UMD/CMD/ESM)

CommonJS

CommonJS 是 JavaScript 在服务器端(Node.js)的一种 模块规范

在浏览器之外的环境(比如服务器)中,JavaScript 需要一种方式来组织代码、管理依赖和导出功能。CommonJS 就是为了解决这个问题而诞生的。

核心概念

CommonJS 的精髓可以概括为三个关键词:requiremoduleexports

  • 导入(require): 使用 require() 函数来加载模块
  • 模块(module): 每个文件都是一个独立的模块,拥有自己的作用域
  • 导出(exports): 使用 module.exportsexports 对象来决定模块中哪些内容可以被外部访问

代码示例

假设我们有两个文件:

  1. math.js (定义模块)
constadd=(a, b)=> a + b;constPI=3.14;// 将需要公开的内容导出 module.exports ={ add,PI};
  1. app.js (使用模块)
// 引入 math.js 模块const math =require('./math.js'); console.log(math.add(2,3));// 输出 5 console.log(math.PI);// 输出 3.14

特点

  • 同步加载: 模块是按顺序加载的。这对服务器端(文件存在磁盘上)没问题,但由于网络延迟,这种方式不适合浏览器,这也是后来出现 ESM(ES Modules)的原因。
  • CJS 无法在浏览器中工作
  • 运行时执行: 只有代码运行到 require 这一行时,模块才会被加载和执行。
  • 值拷贝: 当你导出一个变量时,外部获取的是这个值的一个快照(副本)。即使原模块里的变量后来变了,外部拿到的值也不会跟着变。
  • 缓存: 模块第一次加载后会被缓存。再次 require 同一个模块时,会直接从缓存读,不会重新运行代码。

AMD (Asynchronous Module Definition)

在 CommonJS 统治服务器端的同时,浏览器端遇到了挑战:由于网络环境限制,脚本不能像在本地磁盘那样同步加载。AMD 应运而生,它采取了“异步加载”策略。

特点

  • 核心实现: 最著名的库是 RequireJS
  • 语法特征: 使用 define 函数定义模块,使用 require 加载模块
  • AMD 异步导入模块(因此得名)
  • AMD 最初是为前端设计的(而 CJS 则用于后端)
  • 我认为 AMD 和 CJS 正好相反。

示例

// math.js - 定义模块define([],function(){constadd=(a, b)=> a + b;return{ add };});// app.js - 使用模块require(['./math.js'],function(math){ console.log(math.add(2,3));// 输出 5});

CMD (Common Module Definition)

CMD 全称 Common Module Definition(通用模块定义),是国内开发者主导的规范,依托 SeaJS 实现,同样面向浏览器端。它和 AMD 的目标一致,但风格更灵活,主打 “按需加载”

核心特点:就近依赖

CMD 最大的亮点是 “就近依赖”—— 不用在模块开头就声明所有依赖,而是用到哪个模块,再在哪个位置用require加载,就像走到半路需要什么再临时取一样

比如这样定义模块:

define(function(require, exports, module){// 用到时再加载依赖const dataService =require('./services/dataService');const uiUtils =require('./utils/uiUtils');functiondisplayUserInfo(){const userData = dataService.fetchUserData(); uiUtils.showMessage(userData.username);} exports.displayUserInfo = displayUserInfo;});

这种方式的好处很明显:不用提前加载用不上的模块,减少初始加载资源;代码逻辑和依赖引入就近,读起来更顺,后续修改时改动范围也小,维护起来更轻松

UMD (Universal Module Definition)

随着模块化方案的割裂,开发者面临一个难题:我写的库是给 Node.js 用的(CommonJS),还是给浏览器用的?

UMD 的核心是 “环境判断”—— 加载模块时,先判断当前运行环境,再选择对应的模块化规范:

  • 如果是 Node.js 环境,就用 CommonJS 的module.exportsrequire
  • 如果是支持 AMD 的浏览器环境,就用define异步加载;
  • 如果是普通浏览器,就把模块挂载到window全局对象上

示例

(function(root, factory){if(typeof define ==='function'&& define.amd){// AMD 环境define([], factory);}elseif(typeof module ==='object'&& module.exports){// CommonJS 环境 module.exports =factory();}else{// 浏览器全局环境 root.myLibrary =factory();}}(typeof self !=='undefined'? self :this,function(){return{add:(a, b)=> a + b };}));

现在开发者很少手动写 UMD,但 Webpack 或 Rollup 等打包工具常生成 UMD 代码以确保最大的兼容性

ES Modules

ESMES Modules(ECMAScript Modules)的缩写。它是 JavaScript 官方在 2015 年(ES6)正式推出的模块化标准

在 ESM 出现之前,JavaScript 社区一直使用 CommonJS(Node.js)或 AMD 等非官方方案。ESM 的诞生是为了让 JavaScript 拥有原生的、统一的模块系统,使其既能在浏览器中运行,也能在服务器端运行

它不仅统一了标准,还带来了性能上的飞跃

  • 语法特征:importexport

示例

// math.jsexportconstadd=(a, b)=> a + b;exportconstPI=3.14;// app.jsimport{ add,PI}from'./math.js'; console.log(add(2,3));

ESM 与 CommonJS 的区别对比

这是面试和实践中最常被提及的部分:

  1. 静态 vs 动态:
    • ESM 是静态的import 必须在文件顶层。这让编译器可以在运行代码前知道模块结构,从而实现 Tree Shaking(剔除没用到的代码)。
    • CommonJS 是动态的:可以在 if 语句里写 require()
  2. 符号引用 (Live Bindings) vs 值拷贝:
    • CommonJS 导出的是值的副本
    • ESM 导出的是值的引用。如果原模块里的变量变了,外部获取的值也会实时更新。
  3. 加载方式:
    • ESM 原生支持异步加载,甚至可以支持 Top-level await

对比

规范加载方式依赖处理适用场景核心语法示例
CommonJS同步加载运行时确定,require同步获取Node.js 服务器端开发const mod = require('./mod'); module.exports = {}
AMD异步加载依赖前置,定义时声明依赖浏览器端开发,需兼容复杂依赖场景define('mod', ['dep'], (dep) => {}); require(['mod'], (mod) => {})
CMD异步加载就近依赖,用到时require加载浏览器端开发,追求代码灵活简洁define((require) => { const mod = require('./mod'); })
UMD环境判断,按需同步 / 异步 / 挂载全局融合 CommonJS、AMD 特点跨平台库 / 框架(浏览器、Node.js 通用)见 示例的环境判断逻辑
ESM静态加载(编译阶段确定)import/export静态声明,只读现代浏览器、Node.js 新项目,未来主流import { mod } from './mod.js'; export const x = 1;

站在 2026 年的节点看,ES Modules 已经是绝对的赢家。无论是现代浏览器、最新的 Node.js 版本,还是 Deno/Bun 等新兴运行时,都将 ESM 作为一等公民。AMD 已成往事,而UMD 隐身于工具链后

参考

  1. https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm
  2. https://juejin.cn/post/7454030739781910537
  3. 《JavaScript 高级程序设计》第5版

Read more

Spring Boot 安全认证与授权

Spring Boot 安全认证与授权

Spring Boot 安全认证与授权 22.1 学习目标与重点提示 学习目标:掌握Spring Boot安全认证与授权的核心概念与使用方法,包括Spring Security的定义与特点、Spring Boot与Spring Security的集成、Spring Boot与Spring Security的配置、Spring Boot与Spring Security的认证、Spring Boot与Spring Security的授权、Spring Boot与Spring Security的实际应用场景,学会在实际开发中处理安全认证与授权问题。 重点:Spring Security的定义与特点、Spring Boot与Spring Security的集成、Spring Boot与Spring Security的配置、Spring Boot与Spring Security的认证、Spring Boot与Spring Security的授权、Spring Boot与Spring Security的实际应用场景。 22.2 Spring Security概述 Spring

By Ne0inhk
openclaw 对接完飞书群机器人配置踩坑记:消息不回、Gateway 断开问题排查

openclaw 对接完飞书群机器人配置踩坑记:消息不回、Gateway 断开问题排查

前言 用 OpenClaw 配飞书机器人,踩了两个坑:群消息不回、Gateway 总是断开。排查了好一阵子,总算搞定了,记录一下希望能帮到遇到同样问题的朋友。 发现问题 飞书消息不回复 在飞书群里 @ 了机器人,完全没反应。一开始以为是网络不好或者机器人没上线,但状态显示明明是连接着的,这就奇怪了。 Gateway 频繁断开 每次改完配置跑 openclaw gateway restart,或者根本什么都没干,Gateway 说断就断。再想启动就报错,必须跑一遍 openclaw doctor --fix 重新安装才能用。太影响使用了。 查看原因 飞书机器人 ID 搞错了 翻日志看到这么一句: receive events or callbacks through persistent connection only available in

By Ne0inhk
Python开发从入门到精通:网络爬虫高级应用与Scrapy框架

Python开发从入门到精通:网络爬虫高级应用与Scrapy框架

《Python开发从入门到精通》设计指南第三十九篇:网络爬虫高级应用与Scrapy框架 一、学习目标与重点 💡 学习目标:掌握Python网络爬虫的高级技巧,包括Scrapy框架、分布式爬虫、动态网页爬取、反爬虫策略等;学习Scrapy、Selenium、BeautifulSoup等库的使用;通过实战案例实现网络爬虫应用。 ⚠️ 学习重点:Scrapy框架、分布式爬虫、动态网页爬取、反爬虫策略、Selenium库、BeautifulSoup库、网络爬虫实战。 39.1 网络爬虫概述 39.1.1 什么是网络爬虫 网络爬虫(Web Crawler)是一种程序,用于自动访问网页并提取信息。网络爬虫的应用场景包括数据分析、搜索引擎、内容聚合等。 39.1.2 网络爬虫的流程 * 发送请求:向网页发送HTTP请求。 * 获取响应:获取网页的HTML内容。 * 解析内容:提取网页中的信息。 * 存储数据:将提取的信息存储到数据库或文件中。

By Ne0inhk
从小项目到大型鸿蒙 App 的架构变化

从小项目到大型鸿蒙 App 的架构变化

子玥酱(掘金 / 知乎 / ZEEKLOG / 简书 同名) 大家好,我是子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。 我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括前端工程化、小程序、React / RN、Flutter、跨端方案, 在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。 技术方向:前端 / 跨端 / 小程序 / 移动端工程化 内容平台:掘金、知乎、ZEEKLOG、简书 创作特点:实战导向、源码拆解、少空谈多落地 文章状态:长期稳定更新,大量原创输出 我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、

By Ne0inhk