CommonJS 与 ES Module 模块化
🎯 引言
学完这篇文章,你能理解为什么代码要拆分成模块,会用 CommonJS 和 ES Module 两种方式导出、导入代码,并清楚它们的区别,知道新项目该怎么选。
🧱 为什么需要模块化
刚学编程时,我们习惯把所有代码写在一个文件里。代码一多问题就来了:变量重名互相覆盖、找不到某个函数写在哪、改一处崩一片。
模块化就是把代码按功能拆到不同文件里,每个文件只管自己的事,需要谁就把它导入进来。 比如 utils.js 专门放工具函数,user.js 专门处理用户数据,app.js 负责把它们组织起来。
Node.js 历史上有两套模块化方案同时存在:传统的 CommonJS 和官方的 ES Module,两套都要看得懂。
🧱 CommonJS:Node.js 的传统方案
CommonJS 是 Node.js 诞生时就有的方案,用 module.exports 导出、require 导入。
function add(a, b) {
return a + b;
}
const user = {
name: '小明',
age: 18,
};
// 导出给其他文件使用
module.exports = {
add,
user,
};
// 导入自己的文件,要写相对路径
const { add, user } = require('./utils.js');
console.log(add(1, 2)); // 3
console.log(user.name); // 小明
如果只想导出单个值,也可以直接赋值:module.exports = add。另外 exports.add = add 是 module.exports 的简写形式,两者指向同一个对象。
exports = add,这会切断 exports 和 module.exports 的关联,导致导出失败。单个导出请用 module.exports = add。🧱 ES Module:官方标准方案
ES Module(简称 ESM)是 JavaScript 语言的官方模块化标准,你在 Vite 项目里写的 import / export 就是它。Node.js 中使用 ESM 有两种开启方式:
- 方式一:把文件后缀改成
.mjs。 - 方式二(推荐):在
package.json中加上"type": "module",之后项目里的.js文件都按 ESM 处理。
{
"name": "my-app",
"type": "module"
}
export function add(a, b) {
return a + b;
}
export const user = {
name: '小明',
age: 18,
};
import { add, user } from './utils.js';
console.log(add(1, 2)); // 3
console.log(user.name); // 小明
.js,写 import { add } from './utils' 会直接报错找不到模块。这和 Vite 项目里的习惯不一样,是初学者的高频踩坑点。⚡ 两种方案的区别
| 对比项 | CommonJS | ES Module |
|---|---|---|
| 导出 / 导入 | module.exports / require | export / import |
| 加载方式 | 运行时同步加载 | 编译期静态分析,异步加载 |
| 开启方式 | 默认就是 | .mjs 后缀或 "type": "module" |
| 本地文件后缀 | 可省略 .js | 必须写 .js |
__dirname | 可以直接用 | 不可用,需用 import.meta.dirname |
| 互相引用 | 不能 require ESM 模块 | 可以 import CommonJS 模块 |
node: 前缀,例如 require('node:fs')、import fs from 'node:fs'。一眼就能看出这是 Node.js 内置模块,而不是 npm 上同名的第三方包。💡 新手该怎么选
新项目直接用 ES Module。 它和前端 Vite、Vue 项目里的写法完全一致,一套语法前后端通用,也是官方明确的发展方向。
老项目遇到 CommonJS 能看懂即可。 大量存量项目和 npm 包还在用 CommonJS,维护时不需要强行改写。
🧾 小节总结
- 模块化把代码按功能拆到不同文件,解决命名冲突和依赖混乱问题。
- CommonJS 用
module.exports导出、require导入,是 Node.js 的传统默认方案。 - ES Module 用
export/import,通过.mjs后缀或"type": "module"开启。 - ESM 导入本地文件必须写完整
.js后缀,且没有__dirname。 - 新项目建议用 ES Module,和前端工程化写法保持一致。
❓ 知识问答
Q1:module.exports 和 exports 有什么区别?
exports 是 module.exports 的引用简写,给它加属性(exports.add = add)等价于给 module.exports 加属性。但直接重新赋值 exports = xxx 会失效,因为模块最终导出的是 module.exports。
Q2:为什么 ESM 里用 __dirname 会报错?
__dirname 是 CommonJS 包装出来的变量,ESM 的设计里没有它。Node.js 20.11 之后可以直接用 import.meta.dirname 代替,后面的 path 模块一篇会详细讲。
Q3:两种方案能在同一个项目里混用吗?
可以,但有规则:ESM 可以 import CommonJS 模块,CommonJS 不能 require ESM 模块。混用容易把自己绕晕,建议一个项目统一用一种。
Q4:.mjs 和 "type": "module" 选哪个?
做项目用 "type": "module" 更方便,所有 .js 文件统一按 ESM 处理。.mjs 适合写单个独立脚本,不依赖 package.json 配置。
🧪 小练习
用 ES Module 方式实现一个计算器模块:
- 创建
package.json,加入"type": "module"。 - 创建
calc.js,导出add(加法)和multiply(乘法)两个函数。 - 创建
app.js,导入这两个函数,分别计算add(2, 3)和multiply(4, 5)并打印。
// 请在这里编写代码
// 请在这里编写代码
🎉 恭喜你已经掌握 Node.js 的两种模块化方案!下一篇我们学习 fs 文件系统模块,让程序具备读写文件的能力。
