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

CommonJS 与 ES Module 完全入门指南:从基础概念到项目实战

面向刚刚接触模块化的同学,本文会循序渐进地带你理解 CommonJS(CJS)ES Module(ESM) 两套主流模块系统的来龙去脉、语法差异以及在真实项目中的落地方式。阅读完后,你应该能够:

  • 说清楚 CommonJS 与 ES Module 的历史背景与核心理念
  • 熟练书写两种模块语法,并知道默认导出、命名导出等概念
  • 分辨二者在运行时行为上的关键差别(例如同步/异步加载、值复制与值引用)
  • 在 Node.js、浏览器与打包工具中正确配置和混用两种模块系统
  • 避开常见坑并能制定升级/迁移策略

1. 为什么需要模块化?

  • 代码拆分:把复杂项目拆成可维护的小文件。
  • 作用域隔离:避免变量全局污染。
  • 依赖管理:显式声明需要的函数、对象或配置。
  • 重用与共享:复用公共逻辑,方便团队协作。

这里的“模块”可以理解为“一个文件 + 它愿意分享出去的接口”。文件内部的变量默认只在本文件可见,想给别人用,就通过导出(CommonJS 的 module.exports 或 ESM 的 export)把它公开出来;想使用别人提供的功能,就通过导入(requireimport)拿到。这是整个模块化体系的核心。

早期 JavaScript 没有官方模块方案,只能使用 <script> 标签串联文件或借助 IIFE(立即执行函数)模拟。随着项目体量增长,工程化需求催生了社区与官方的两套标准:

时间模块方案应用场景
2009CommonJSNode.js 服务器端
2015ES ModuleECMAScript 标准,浏览器与 Node.js

2. CommonJS(CJS)快速上手

2.1 核心概念

  • 同步加载:在 require() 时立即执行并返回导出的对象。
  • 单文件单模块module 对象对应当前文件,exports 是最终暴露的接口。
  • 运行在 Node.js 中:浏览器原生不支持,需要打包工具处理。

换句话说:当你在某个文件里写下 require('./math'),Node 会立刻跳到 math.js 把里面的代码跑完,再把需要导出的内容丢回给你。这个过程像“打电话让朋友帮忙查询结果”,挂断电话前必须等对方查完才能继续往下执行。

2.2 最常用的写法(一步步)

// math.js —— 先写一个模块
const PI = 3.14;
function area(r) {return PI * r * r;
}module.exports = { PI, area }; // 将需要的内容导出去
// app.js —— 再在另一个文件里导入使用
const math = require('./math'); // require 返回导出的对象
console.log(math.area(2));

2.3 扩展导入方式

// 1. 解构直接拿需要的函数
const { area } = require('./math');// 2. 给导入的对象起别名
const mathLib = require('./math');// 3. 导入 JSON / Node 内置模块 / 第三方包
const pkg = require('./package.json');
const fs = require('node:fs');
const express = require('express');// 4. 根据条件再 require(同步执行,但可以放在 if/函数中)
if (process.env.NODE_ENV === 'development') {const devTools = require('./dev-tools');devTools.init();
}
  • 路径小贴士:以 ./../ 开头表示相对路径,不加前缀默认去 node_modules 中寻找;省略扩展名时会按 .js → .json → .node 顺序尝试。
  • JSON 支持require('./data.json') 会自动解析成对象,同样只会在第一次 require 时读取磁盘并缓存。
  • 执行时机require() 总是同步执行,放在函数里也一样会阻塞当前调用,适合 Node.js 服务器端但不适合浏览器。

小白提示:CommonJS 中 require() 调用时机就是模块执行时机,每次 require() 整个模块都会先跑一遍,然后把 module.exports 的内容返回回来。

2.4 模块缓存与单例

  • 首次 require() 会执行模块并缓存结果。
  • 后续重复 require() 会直接读取缓存(单例模式)。
  • 如果希望重新执行模块,需要删除 require.cache 中的记录,但不常见。

**把它想象成“第一次问朋友借工具”。**第一次 require('./math') 时,Node 会去仓库(磁盘)把工具(模块)拿出来并登记放到公共柜子(缓存)里;以后别人再来借同一件工具,就直接从柜子里取走,不会再次跑去仓库。这也是为什么在 CommonJS 中常说“模块是单例”,因为整个进程里只有这一份共享副本。

// demo-cache.js
const counterA = require('./counter');
const counterB = require('./counter'); // 不会重新执行 counter.jsconsole.log(counterA === counterB); // true,拿到的是缓存里同一个对象

2.5 exportsmodule.exports

  • 默认 module.exports = {}
  • exports 是对 module.exports 的引用;直接修改 exports = 会断开引用。
  • 建议始终使用 module.exports 或保持 exports.xxx = 写法的一致性。
// ✅ 推荐
exports.area = area;
exports.PI = PI;// ✅ 等价写法
module.exports = { area, PI };// 🚫 错误:重新赋值会丢失导出
exports = { area, PI }; // 外部 require 拿到的还是旧对象

2.6 常见坑

  • 只能在顶层同步 require(),无法在条件内动态导入(尽管可以写,但执行时是同步的)。
  • CJS 导出的对象是 值拷贝(浅拷贝),不像 ESM 那样保持实时引用。

记住:“同步”意味着会卡住当前线程,所以不要在浏览器端直接使用 require(),页面会因为等待文件加载而卡死;“值拷贝”则意味着要么导出函数、要么导出可变对象,才能让调用方看到更新后的状态。


3. ES Module(ESM)快速上手

3.1 核心理念

  • 静态分析:编译阶段即可确定依赖关系(利于 Tree Shaking)。
  • 异步加载:浏览器可并行加载多个模块。
  • 实时绑定(Live Binding):导出的变量是引用,导入方能感知到值的变化。

逐句理解:ES Module 的 import 会在代码运行前就被“读透”,编译器能提前知道哪些模块被引用、哪些函数被用到,从而把没用的代码剪掉(Tree Shaking)。浏览器加载时也可以一边解析 HTML,一边请求多个模块文件,不需要按顺序阻塞。最关键的是,ESM 不是把值拷贝给你,而是给你一个“指向真实变量的遥控器”,变量变了你这里立刻能看到。

3.2 语法速览

// math.mjs
export const PI = 3.14159;
export function area(r) {return PI * r * r;
}const SECRET = 'local';
export default function circumference(r) {return 2 * PI * r;
}
// app.mjs
import circumference, { area, PI } from './math.mjs';
console.log(area(2));
console.log(circumference(2));

3.3 导入导出完整速查

// 1. 命名导出
export const PI = 3.14159;
export function area(r) {}// 2. 默认导出(模块只想暴露一个主功能时使用)
export default function circumference(r) {}// 3. 统一导出已有变量
const version = '1.0.0';
function calc() {}
export { version, calc as calculate };// 4. 一次性导入
import circumference, { area, PI as PI_VALUE } from './math.mjs';// 5. 导入整个命名空间
import * as math from './math.mjs';
console.log(math.area(2));// 6. 仅执行模块的副作用(不获取任何变量)
import './init-logger.js';
  • 固定规则import / export 必须写在模块的顶层作用域,不能包在 if 或函数里;需要按需加载时请使用 import()
  • 路径解析:浏览器里 import './math.js' 会基于当前文件的 URL 解析;Node.js 支持文件系统路径,还可以写 import fs from 'node:fs' 使用内置模块。
  • 命名检查:命名导入必须和导出的名字对应不上就会在构建阶段报错,这也是它更易调试的原因。
  • 命名空间导入import * as math 会把模块里所有命名导出打包到一个对象里,便于查看完整 API,但无法获取默认导出。
  • 默认导出理解:可以把默认导出想成“模块的主角”,导入时名字随便起;命名导出则是“配角列表”,导入时名字必须严格对应。

3.4 动态导入(懒加载)

// 可写在 if、函数或事件回调中
if (window.innerWidth > 1024) {const { createChart } = await import('./heavy-chart.mjs');createChart();
}
  • import() 返回 Promise,首次加载后自动缓存。
  • 浏览器和支持 ESM 的 Node.js 环境都可以使用。
  • import.meta.url 可以拿到当前模块的文件地址,配合 new URL('./file', import.meta.url) 读取同目录资源。
  • 适合场景:大体积图表库、仅在特定按钮点击后才需要的逻辑、根据用户权限动态开启的功能等,把不常用的代码拆开按需加载可以显著缩短首屏时间。

4. CommonJS vs ES Module 核心对比

维度CommonJSES Module
设计目标Node.js 服务器端ECMAScript 标准(前后端统一)
加载方式同步执行,返回导出对象静态解析,默认异步加载
导出类型module.exports 单对象命名导出 + 默认导出
导入写法const lib = require('lib')import lib from 'lib'
值传递导出时拷贝值副本导出变量的实时引用
动态加载require() 可放在代码块内import() 返回 Promise
顶层语法可写在任意位置import/export 必须处于顶层
Tree Shaking静态性弱,需工具分析静态结构,易于 Tree Shaking

如果一句话概括:CommonJS 像“立刻运行、拿到一个打包好的盒子”,写法更随意;ES Module 像“提前登记好清单、按需快递过来”,写法更严格但能让工具读懂、浏览器自带支持。

4.1 导入写法一览表

场景CommonJSES Module备注
默认导出/整体导出const math = require('./math')import math from './math.js'CJS 返回对象,ESM 得到默认导出
命名导出/解构const { area } = require('./math')import { area } from './math.js'ESM 支持别名 area as calcArea
整个命名空间手动写 const math = require(...)import * as math from './math.js'ESM 推荐方式
仅执行模块副作用require('./setup')import './setup.js'两者都会执行模块顶层代码
动态导入const math = require('./math')const math = await import('./math.js')ESM 需等待 Promise

4.2 深入理解:值拷贝 vs 实时引用

// counter.cjs
let count = 0;
function add() {count += 1;
}
module.exports = { count, add };
// counter-user.cjs
const counter = require('./counter.cjs');
counter.add();
console.log(counter.count); // => 0 !!! (拿到的是导出时的旧值)

原因:CommonJS 导出的是一个对象的副本,count 的值在导出时就被拷贝了。若想获取最新值,需要导出函数 getCount 或者导出整个对象并在对象属性上更新:

// counter-fixed.cjs
module.exports = {state: { count: 0 },add() {this.state.count += 1;}
};
const counter = require('./counter-fixed.cjs');
counter.add();
console.log(counter.state.count); // => 1

而在 ES Module 中,同样的代码会表现为“实时绑定”:

// counter.mjs
export let count = 0;
export function add() {count += 1;
}
// counter-user.mjs
import { add, count } from './counter.mjs';
add();
console.log(count); // => 1 (始终读取最新值)

浏览器和 Node.js 在编译阶段就知道 count 的引用位置,因此可以实现实时更新。

小结:遇到“为什么我导入的值不更新”的问题,先看你用的是哪种模块。如果是 CommonJS,就要导出函数或对象再访问;如果是 ES Module,记得不要把导入的值重新赋值(例如 count = 5 会报错),而是通过调用导出的函数来修改源头。


5. Node.js 中两种模块的配置与互操作

5.1 通过文件扩展名区分

  • .cjs:强制视为 CommonJS。
  • .mjs:强制视为 ES Module。
  • .js:根据 package.jsontype 字段决定。

规则记忆:文件后缀优先级最高,只要写了 .cjs/.mjs 就不会再看 type。只有普通 .js 文件才会参考 package.json。所以常见做法是——老代码先保持 .cjs,逐步把新文件写成 .mjs,最后再切换 type: "module"

{"name": "demo","type": "module", // 默认为 "commonjs""main": "./index.js"
}

5.2 在 CommonJS 中使用 ES Module

// index.cjs
(async () => {const { area } = await import('./math.mjs');console.log(area(2));
})();

5.3 在 ES Module 中引入 CommonJS

// index.mjs
import pkg from './legacy.cjs';
const { area } = pkg;

注意:ESM 默认把 CJS 导出视为默认导出对象;若 CJS 使用 module.exports = function,ESM 中 import pkg from './legacy.cjs' 会得到函数本身。

两个世界互相调用的口诀:

  • CJS 调 ESM:一定要用 await import(),因为 ESM 是异步的。
  • ESM 调 CJS:用 import pkg from 'cjs',再从 pkg 对象里取需要的成员。
  • 想要双向兼容:在 npm 包里同时导出两份产物,在 package.jsonexports 字段上根据 import/require 指向不同文件(第 8 节有示例)。

6. 浏览器环境下的实践

6.1 原生支持

<script type="module" src="./main.js"></script>
<script type="module">import { area } from './math.js';console.log(area(2));
</script>
  • 模块脚本自动延迟执行(defer),不会阻塞 HTML 解析。
  • 同源策略限制:本地测试需通过 HTTP 服务器,而非直接打开文件。
  • 浏览器会把每个模块文件当成独立的作用域,顶层变量不会跑到 window 上,这也是模块隔离的一部分。

6.2 兼容旧浏览器

  • 打包工具(Webpack、Rollup、Vite)可将 ESM 转译为浏览器支持的格式。
  • 可使用 <script nomodule> 提供降级脚本。

小白提醒:直接双击打开 HTML 会看到 “Cannot use import statement outside a module” 或跨域报错,这是因为浏览器禁止文件协议去加载模块。最简单的做法是在项目根目录启动一个静态服务器,例如 npx servenpm run dev


7. 打包工具中的模块处理

  • Webpack:默认支持 CommonJS 与 ES Module,配置 module.rulesresolve.extensions 即可;package.json 中的 "module" 字段用于指向 ESM 版本,供 Tree Shaking 使用。
  • Rollup:以 ES Module 为第一公民,对 CommonJS 需使用 @rollup/plugin-commonjs
  • Vite:基于 ES Module 开发,内部使用 Rollup 构建发布产物;无需额外配置即可消费两种模块。
  • esbuild / SWC:这些新型打包器也都默认理解两种格式,并能非常快地互相转换。

小结:现代打包器通常会先把所有模块转换为统一格式(通常是 ESM),再进行优化和打包。

记忆技巧:只要你看到工具里有 “Tree Shaking”“按需加载”“产出多格式” 等配置,本质上都是在处理 ESM 的静态结构。CommonJS 进来后也会被转换成类似 ESM 的内部表示,以便做进一步优化。


8. 常见混用场景与解决方案

场景问题解决方式
老项目 CJS,想引入新库 ESMrequire() 无法直接加载使用 async import() 或将新库打包后引入
新项目 ESM,依赖老库 CJSimport 得到默认导出对象const { xxx } = await import('cjs-lib'); 或在打包阶段转换
同一仓库需要同时输出 CJS 和 ESM 版本npm 包需要兼容exports 字段区分入口,使用打包工具产出双版本
{"name": "pkg","exports": {".": {"import": "./dist/index.esm.js","require": "./dist/index.cjs"}}
}

实战建议:当你需要同时面对老旧脚手架(只识别 require)和现代工具链(喜欢 import)时,不要强行让对方改变。最稳妥的做法是自己用打包工具产出两份文件,一个 .cjs 给旧系统,一个 .esm.js 给新系统,再在 package.json 里用 exportsmain/module 指明入口。

9. 迁移策略:从 CommonJS 走向 ES Module

  1. 梳理依赖:确认有哪些内部模块与外部包仍是 CJS。
  2. 先局部再整体:从叶子模块(无下游依赖)开始改写为 ESM。
  3. 保持接口稳定:使用 exports 字段同时输出 CJS 与 ESM。
  4. 检查工具链:确保测试、打包、Lint 等工具支持 ESM。
  5. 逐步启用 "type": "module":先在子包或独立目录尝试,再推广到主包。

迁移口诀:**先清点 → 小范围试点 → 双格式并存 → 全面切换。**不要一口气改全仓库,尤其是服务器端项目,谨防线上环境 Node 版本过低或第三方库尚未提供 ESM。


10. 常见问题答疑(FAQ)

  • Q:ES Module 一定比 CommonJS 快吗?
    A:并非绝对。ESM 更适合静态分析与 Tree Shaking,但实际性能取决于打包策略与运行环境。

  • Q:浏览器中可以直接使用 CommonJS 吗?
    A:不行,需要通过打包或转译工具转换为浏览器支持的语法。

  • Q:require()import() 可以混用吗?
    A:在 Node.js 中可以互相调用,但需遵循前文的互操作规则;在浏览器中不建议使用 require()

  • Q:默认导出与命名导出怎么选?
    A:若模块只暴露一个主功能,可以使用默认导出;若需要导出多个 API,使用命名导出更利于提示与重构。

  • Q:为什么说 ESM 的导出是“实时绑定”?
    A:ESM 导出的变量是引用,导入方访问时会读取最新值;CommonJS 在导出时把值拷贝给调用方。


11. 总结与最佳实践

  • 理解目标环境:后端运行时优先考虑 Node.js 的支持;前端看浏览器兼容性。
  • 优先选择 ESM:新项目建议以 ES Module 为主,获得更好的工具链支持。
  • 保留兼容层:对外发布的 npm 包同时提供 importrequire 入口,兼容旧生态。
  • 慎用默认导出:团队内部统一规范,避免同时使用默认导出与命名导出导致混乱。
  • 善用动态导入:把大体积模块按需加载,提高首屏性能。

只要掌握上述知识点,你就能在任何 JavaScript 项目中自如地选择并配置适合的模块系统。建议结合实际项目多尝试、调试,逐步建立对模块加载流程的直觉。

最后再筛一遍:看到 module.exports/require 就想到 CommonJS,“同步、值拷贝、Node 侧”;看到 export/import 就想到 ES Module,“静态、实时引用、浏览器 Node 都支持”。脑子里放一张表,遇到问题对照“导入失败?看路径和顶层语法”“值没更新?看是不是 CJS 拷贝”。循序渐进地练习,你就能从小白快速成长为模块化的老司机。

http://www.dtcms.com/a/594656.html

相关文章:

  • dedecms 调用 另一个网站凡科网站怎样做
  • 建立自己的平台网站吗如何做网站小编
  • 一个数据库两个网站wordpress登陆重庆网站托管
  • 烟台专业做网站公司哪家好百度云虚拟主机搭建wordpress
  • 网站设计的公司皆选奇点网络招商网站建设
  • 使用Linux终端进行文件操作
  • 网上请人做软件的网站wordpress驳回评论
  • 高光谱成像实现石质文物劣化情况的评估,助力文物保护
  • Vue 项目实战《尚医通》,完成医院详情模块业务,笔记20
  • 怎样在网站做推广开贴纸网站要怎么做的
  • 可以做课程的网站wordpress更改图片上传路径
  • 华清远见25072班单片机基础学习day1
  • 「C++」vector的使用及接口模拟详解
  • 企业网站建设案例有哪些公司西峡微网站开发
  • 国外设计网站大全附近做广告招牌的
  • NLP入门——文本表示概述
  • HYPE分布式水文模型建模方法:基本输入文件制备、驱动数据制备、HYPE模型运行与手动调参、自动率参等
  • FreeBSD14.3中ZFS文件系统与samba设置仅指定用户可编辑的共享
  • 超酷个人网站商务网站建设考试题库
  • C++之内联变量(Inline Variables)
  • 学校网站下载零基础学it从哪方面学起
  • 自己建设淘宝客网站需要备案么东莞seo网络推广
  • 杭州广告公司网站建设wordpress 插件作用
  • 做微信用什么网站wordpress 去掉80previous与 next81
  • 做名片去哪个网站it行业公司排名
  • 合肥网站建设团队网站制作实例教程
  • 博达网站建设流程中国新闻社招聘公示
  • GEE SCL掩膜高精度 NDVI 提取教程(10 米分辨率 + SCL 掩膜)——免费提供完整代码
  • 网站群建设公司排行榜网站后端用什么语言
  • 网站域名是网站架构吗邯郸网站建设怎么开发