模块系统
模块系统是 Node.js 的核心特性之一,它允许将代码组织成可重用的模块。Node.js 支持 CommonJS 和 ES Modules 两种模块系统。
模块系统概述
为什么需要模块
在没有模块系统之前,JavaScript 代码存在以下问题:
- 全局污染:所有变量都在全局作用域
- 依赖管理困难:脚本加载顺序必须手动管理
- 代码难以复用:无法方便地导入导出功能
模块系统解决了这些问题:
两种模块系统
| 特性 | CommonJS | ES Modules |
|---|---|---|
| 引入方式 | require() | import |
| 导出方式 | module.exports | export |
| 加载时机 | 运行时同步加载 | 编译时静态分析 |
| 文件扩展名 | .js, .cjs | .mjs, .js(需配置) |
| 使用环境 | Node.js 原生 | 现代浏览器和 Node.js |
| 动态导入 | 直接支持 | 需使用 import() |
CommonJS 模块
基本导出
// math.js
// 方式1:导出单个值
module.exports = function add(a, b) {
return a + b;
};
// 方式2:导出对象
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
// 方式3:逐个添加属性
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
重要提示:不要直接给 exports 赋值,因为它只是 module.exports 的引用。
// ❌ 错误:会断开与 module.exports 的连接
exports = function add(a, b) { ... };
// ✅ 正确:添加属性到 exports
exports.add = (a, b) => a + b;
// ✅ 正确:直接给 module.exports 赋值
module.exports = function add(a, b) { ... };
基本导入
// main.js
// 导入整个模块
const math = require('./math');
console.log(math.add(1, 2)); // 3
// 解构导入
const { add, subtract } = require('./math');
console.log(add(1, 2)); // 3
console.log(subtract(5, 3)); // 2
模块查找规则
require() 的查找顺序:
// 1. 核心模块
const http = require('http'); // Node.js 内置模块
const fs = require('fs');
// 2. 相对路径
const myModule = require('./myModule'); // 同目录
const utils = require('../utils/helper'); // 上级目录
// 3. 绝对路径
const config = require('/etc/app/config');
// 4. node_modules
const express = require('express'); // 从 node_modules 查找
查找 node_modules 的顺序:
模块缓存
Node.js 会缓存已加载的模块:
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count,
};
// main.js
const counter1 = require('./counter');
const counter2 = require('./counter'); // 返回缓存的模块
counter1.increment();
console.log(counter1.getCount()); // 1
console.log(counter2.getCount()); // 1 - 同一个实例
模块包装
每个 CommonJS 模块都被包装在一个函数中:
// 实际执行的代码
(function(exports, require, module, __filename, __dirname) {
// 你的模块代码
});
// 因此可以访问这些变量
console.log(__filename); // 当前文件的绝对路径
console.log(__dirname); // 当前文件所在目录的绝对路径
console.log(module); // 模块对象
console.log(exports); // 导出对象
ES Modules
ES Modules 是 ECMAScript 标准的模块系统,在现代 JavaScript 中广泛使用。
启用 ES Modules
有两种方式启用:
方式1:使用 .mjs 扩展名
node app.mjs
方式2:在 package.json 中配置
{
"type": "module"
}
导出
// utils.mjs
// 命名导出
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export class Calculator {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
}
// 默认导出(每个模块只能有一个)
export default class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
// 导出列表
export { add, subtract };
// 重命名导出
export { add as sum, subtract as minus };
// 重新导出
export { PI } from './constants.mjs';
export * from './math.mjs';
导入
// main.mjs
// 命名导入
import { add, subtract } from './utils.mjs';
console.log(add(1, 2)); // 3
// 重命名导入
import { add as sum } from './utils.mjs';
console.log(sum(1, 2)); // 3
// 默认导入
import Logger from './utils.mjs';
const logger = new Logger();
logger.log('Hello');
// 混合导入
import Logger, { add, subtract } from './utils.mjs';
// 导入所有
import * as utils from './utils.mjs';
console.log(utils.add(1, 2));
console.log(utils.default);
// 仅导入(用于副作用)
import './setup.mjs';
// 动态导入
const moduleName = './utils.mjs';
import(moduleName).then(module => {
console.log(module.add(1, 2));
});
// 或使用 async/await
async function loadModule() {
const module = await import('./utils.mjs');
return module.add(1, 2);
}
ES Modules 与 CommonJS 的区别
// CommonJS - 运行时加载
const config = require('./config.json'); // 可以是变量路径
const moduleName = './utils.js';
const utils = require(moduleName); // 动态路径
// ES Modules - 编译时分析
import config from './config.json'; // 必须是静态字符串
// import utils from moduleName; // ❌ 错误:必须是静态路径
// ES Modules 动态导入
const utils = await import(moduleName); // ✅ 使用动态 import()
模块最佳实践
目录结构
my-project/
├── package.json
├── src/
│ ├── index.js # 主入口
│ ├── utils/
│ │ ├── index.js # 工具模块入口
│ │ ├── string.js # 字符串工具
│ │ └── number.js # 数字工具
│ └── services/
│ ├── index.js
│ └── user.js
└── tests/
└── utils.test.js
入口文件模式
// utils/index.js - 聚合导出
const string = require('./string');
const number = require('./number');
module.exports = {
...string,
...number,
};
// 使用时
const utils = require('./utils');
utils.formatString('hello');
utils.roundNumber(3.14);
条件导出
// config.js
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
db: 'mongodb://localhost:27017/dev',
debug: true,
},
production: {
db: process.env.DB_URL,
debug: false,
},
};
module.exports = configs[env];
循环依赖处理
// a.js
const b = require('./b');
console.log('a.js: b.loaded =', b.loaded);
exports.loaded = true;
// b.js
const a = require('./a');
console.log('b.js: a.loaded =', a.loaded);
exports.loaded = true;
// main.js
const a = require('./a');
// 输出:
// b.js: a.loaded = undefined
// a.js: b.loaded = true
解决方案:重构代码避免循环依赖,或延迟导入:
// a.js
exports.loaded = false;
exports.init = function() {
const b = require('./b'); // 延迟导入
exports.loaded = true;
};
内置模块
Node.js 提供了丰富的内置模块,无需安装即可使用:
常用内置模块
| 模块 | 用途 |
|---|---|
fs | 文件系统操作 |
http / https | HTTP 服务器和客户端 |
path | 路径处理 |
os | 操作系统信息 |
crypto | 加密功能 |
events | 事件处理 |
stream | 流处理 |
util | 实用工具 |
url | URL 解析 |
querystring | 查询字符串解析 |
child_process | 子进程 |
cluster | 多进程集群 |
net | TCP/UDP 网络 |
使用内置模块
// Node.js 16+ 推荐使用 node: 前缀
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
// 或 CommonJS
const fs = require('node:fs');
const http = require('node:http');
const path = require('node:path');
小结
本章我们学习了:
- 模块系统作用:代码组织、依赖管理、避免全局污染
- CommonJS:
require()和module.exports - ES Modules:
import和export - 模块查找规则:核心模块、相对路径、node_modules
- 模块缓存:避免重复加载
- 最佳实践:目录结构、入口文件、循环依赖处理
练习
- 创建一个数学工具模块,支持加减乘除和高级运算
- 将 CommonJS 模块转换为 ES Modules
- 实现一个配置模块,支持不同环境加载不同配置
- 解决一个循环依赖问题