Node.js 开发指南 / 第 5 章 · 模块系统
第 5 章 · 模块系统
5.1 模块系统概述
Node.js 的模块系统是其最重要的特性之一。它允许我们将代码拆分为独立、可复用的单元。
两种模块系统对比
| 特性 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 加载方式 | 同步加载 | 异步加载 |
| 语法 | require() / module.exports | import / export |
| 运行时解析 | ✅ | ❌(编译时解析) |
| 动态导入 | ✅ 天然支持 | import() 函数 |
| 循环依赖 | 部分支持 | 支持(活绑定) |
this 指向 | module.exports | undefined |
| 文件扩展名 | .js(默认) | .mjs 或 .js("type": "module") |
__dirname | ✅ 可用 | ❌ 需要手动构造 |
| 浏览器兼容 | ❌ | ✅ 原生支持 |
| Tree Shaking | ❌ | ✅ 静态分析 |
| Node.js 默认 | ✅(当前) | 趋势方向 |
5.2 CommonJS 详解
基本导出
// math.js — 导出方式 1:module.exports
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
const PI = 3.14159;
module.exports = { add, multiply, PI };
// math.js — 导出方式 2:逐个挂载 exports
exports.add = function (a, b) {
return a + b;
};
exports.multiply = function (a, b) {
return a * b;
};
// ⚠️ 不能直接赋值 exports,会断开引用
// exports = { add, multiply }; // 错误!
// module.exports = { add, multiply }; // 正确
基本导入
// app.js
// 方式 1:解构导入(推荐)
const { add, multiply, PI } = require('./math');
console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12
// 方式 2:整体导入
const math = require('./math');
console.log(math.add(1, 2)); // 3
// 方式 3:只执行模块(无导出)
require('./init');
// 导入核心模块
const fs = require('fs');
const path = require('path');
// 导入第三方模块
const express = require('express');
// 导入 JSON 文件
const config = require('./config.json');
CommonJS 的加载机制
require('./math')
│
▼
1. 路径解析
├── 核心模块 → 直接返回
├── 相对路径 → 解析为绝对路径
├── 绝对路径 → 直接使用
└── 第三方模块 → node_modules 查找
│
▼
2. 文件定位(按顺序尝试)
├── math.js
├── math.json
├── math.node (C++ 插件)
└── math/ → math/index.js
│
▼
3. 编译执行
├── 读取文件内容
├── 包装为函数
│ (function(exports, require, module, __filename, __dirname) {
│ // 你的代码
│ })
├── 编译执行
└── 缓存到 require.cache
模块缓存
// module-a.js
console.log('module-a 被加载');
module.exports = { loaded: true };
// app.js
const a1 = require('./module-a'); // 输出: module-a 被加载
const a2 = require('./module-a'); // 不会再次输出(已缓存)
console.log(a1 === a2); // true
// 查看缓存
console.log(Object.keys(require.cache));
// 清除缓存(谨慎使用)
delete require.cache[require.resolve('./module-a')];
循环依赖
// a.js
console.log('a 开始');
exports.done = false;
const b = require('./b');
console.log('在 a 中, b.done =', b.done);
exports.done = true;
console.log('a 结束');
// b.js
console.log('b 开始');
exports.done = false;
const a = require('./a');
console.log('在 b 中, a.done =', a.done); // false(a 还没执行完)
exports.done = true;
console.log('b 结束');
// main.js
const a = require('./a');
console.log('在 main 中, a.done =', a.done, ', b.done =', require('./b').done);
// 输出:
// a 开始
// b 开始
// 在 b 中, a.done = false
// b 结束
// 在 a 中, b.done = true
// a 结束
// 在 main 中, a.done = true, b.done = true
5.3 ES Modules 详解
启用 ESM
方式 1:在 package.json 中设置 type:
{
"name": "my-project",
"type": "module"
}
方式 2:使用 .mjs 扩展名:
# .mjs 文件自动使用 ESM
math.mjs
app.mjs
方式 3:通过 --input-type 标志:
node --input-type=module -e "import os from 'os'; console.log(os.platform())"
命名导出(Named Exports)
// math.mjs — 方式 1:逐个导出
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export const PI = 3.14159;
// math.mjs — 方式 2:统一导出
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
const PI = 3.14159;
export { add, multiply, PI };
// math.mjs — 方式 3:重命名导出
export { add as sum, multiply as product };
默认导出(Default Export)
// logger.mjs
export default class Logger {
constructor(prefix) {
this.prefix = prefix;
}
log(message) {
console.log(`[${this.prefix}] ${message}`);
}
}
// 也可以同时有默认导出和命名导出
export const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
导入方式
// app.mjs
// 命名导入
import { add, multiply, PI } from './math.mjs';
console.log(add(1, 2));
// 重命名导入
import { add as sum } from './math.mjs';
console.log(sum(1, 2));
// 默认导入
import Logger from './logger.mjs';
const logger = new Logger('APP');
logger.log('Hello');
// 混合导入
import Logger, { LOG_LEVELS } from './logger.mjs';
// 命名空间导入(导入所有)
import * as math from './math.mjs';
console.log(math.add(1, 2));
console.log(math.PI);
// 仅执行副作用
import './init.mjs';
// 动态导入
const module = await import('./math.mjs');
console.log(module.add(1, 2));
ESM 的活绑定
// counter.mjs
export let count = 0;
export function increment() {
count++;
}
// app.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1(活绑定,自动更新!)
5.4 动态导入
// 动态导入返回 Promise
async function loadModule(condition) {
if (condition) {
const { add } = await import('./math.mjs');
return add(1, 2);
} else {
const { multiply } = await import('./math.mjs');
return multiply(3, 4);
}
}
// 按需加载(代码分割)
async function handleRequest(type) {
switch (type) {
case 'json': {
const { parseJSON } = await import('./handlers/json.mjs');
return parseJSON();
}
case 'xml': {
const { parseXML } = await import('./handlers/xml.mjs');
return parseXML();
}
}
}
// CJS 中使用动态导入
async function main() {
const esm = await import('./esm-module.mjs');
console.log(esm.default);
}
main();
5.5 CJS 与 ESM 互操作
从 ESM 中导入 CJS
// ESM 可以导入 CJS 模块
import fs from 'fs'; // 核心模块
import express from 'express'; // 第三方 CJS 模块
import lodash from 'lodash';
// CJS 模块的 default export 是 module.exports
import myCjsModule from './my-cjs-module.cjs';
从 CJS 中导入 ESM
// CJS 不能直接 require ESM,但可以使用动态 import
async function main() {
const { add } = await import('./math.mjs');
console.log(add(1, 2));
}
main();
// 或者使用顶层 await(Node.js 14.8+,仅 ESM)
// CJS 不支持顶层 await
互操作注意事项
| 场景 | 结果 |
|---|---|
ESM import CJS | ✅ 可以,module.exports 变为默认导出 |
ESM import ESM | ✅ 正常工作 |
CJS require() ESM | ❌ 不可以,报错 |
CJS await import() ESM | ✅ 可以,使用动态导入 |
CJS require() CJS | ✅ 正常工作 |
文件扩展名与互操作
| 扩展名 | 包含 CJS 时 | 包含 ESM 时 |
|---|---|---|
.js | "type": "commonjs"(默认) | "type": "module" |
.mjs | ❌ 不允许 | ✅ 始终 ESM |
.cjs | ✅ 始终 CJS | ❌ 不允许 |
5.6 模块解析策略
node_modules 查找
require('lodash')
│
▼
从当前目录向上查找 node_modules
/home/user/project/node_modules/lodash
/home/user/node_modules/lodash
/home/node_modules/lodash
/node_modules/lodash
│
▼
找到后读取 package.json 的 "main" 字段
(ESM 优先读取 "exports" 和 "module" 字段)
package.json 的模块字段
{
"name": "my-package",
"version": "1.0.0",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./package.json": "./package.json"
},
"type": "module"
}
exports 字段详解
{
"exports": {
// 条件导出
".": {
"import": "./esm/index.mjs",
"require": "./cjs/index.cjs",
"types": "./types/index.d.ts",
"default": "./esm/index.mjs"
},
// 子路径导出
"./utils": "./utils/index.mjs",
"./config": "./config/index.mjs",
// 通配符导出
"./features/*": "./features/*.mjs"
}
}
5.7 实战:创建可复用模块
项目结构
my-utils/
├── package.json
├── src/
│ ├── index.mjs
│ ├── string.mjs
│ ├── array.mjs
│ └── validation.mjs
└── test/
└── index.test.mjs
模块实现
// src/string.mjs
export function capitalize(str) {
if (typeof str !== 'string') throw new TypeError('Expected a string');
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function camelCase(str) {
return str
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
.replace(/^[A-Z]/, (c) => c.toLowerCase());
}
export function truncate(str, length = 100, suffix = '...') {
if (str.length <= length) return str;
return str.slice(0, length) + suffix;
}
// src/array.mjs
export function unique(arr) {
return [...new Set(arr)];
}
export function chunk(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
export function groupBy(arr, keyFn) {
return arr.reduce((groups, item) => {
const key = keyFn(item);
(groups[key] ??= []).push(item);
return groups;
}, {});
}
// src/index.mjs — 统一导出
export { capitalize, camelCase, truncate } from './string.mjs';
export { unique, chunk, groupBy } from './array.mjs';
export { isEmail, isURL, isUUID } from './validation.mjs';
package.json 配置
{
"name": "@my/utils",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.mjs",
"./string": "./src/string.mjs",
"./array": "./src/array.mjs",
"./validation": "./src/validation.mjs"
},
"engines": {
"node": ">=20.0.0"
}
}
使用模块
// 使用完整导入
import { capitalize, unique } from '@my/utils';
// 使用子路径导入(只加载需要的部分)
import { capitalize } from '@my/utils/string';
import { unique, chunk } from '@my/utils/array';
注意事项
⚠️ 不要混用 CJS 和 ESM:在同一个包中尽量统一使用一种模块系统。如果需要同时支持,使用
exports字段分别指定入口。
⚠️ ESM 中没有
__dirname和__filename:需要手动构造:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
⚠️ CJS 是同步加载的:
require()会阻塞主线程,在启动时加载大量模块可能影响启动性能。
⚠️ 循环依赖的陷阱:CJS 循环依赖时,获取到的可能是未完成的导出对象。尽量避免循环依赖,使用依赖注入或提取公共模块。
业务场景
- 单体仓库(Monorepo):使用
exports字段管理多个子包入口 - 渐进式迁移:新代码用 ESM,旧代码保持 CJS,通过动态
import()桥接 - 插件系统:使用动态
import()按需加载插件 - 构建工具配置:Webpack/Vite 的代码分割基于 ESM 的
import()实现
扩展阅读
上一章:第 4 章 · 变量与数据类型 下一章:第 6 章 · 异步编程基础 — 回调、Promise 和 async/await。