跳到主要内容

模块系统

模块系统是 Node.js 的核心特性之一,它允许将代码组织成可重用的模块。Node.js 支持 CommonJS 和 ES Modules 两种模块系统。

模块系统概述

为什么需要模块

在没有模块系统之前,JavaScript 代码存在以下问题:

  • 全局污染:所有变量都在全局作用域
  • 依赖管理困难:脚本加载顺序必须手动管理
  • 代码难以复用:无法方便地导入导出功能

模块系统解决了这些问题:

两种模块系统

特性CommonJSES Modules
引入方式require()import
导出方式module.exportsexport
加载时机运行时同步加载编译时静态分析
文件扩展名.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 / httpsHTTP 服务器和客户端
path路径处理
os操作系统信息
crypto加密功能
events事件处理
stream流处理
util实用工具
urlURL 解析
querystring查询字符串解析
child_process子进程
cluster多进程集群
netTCP/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');

小结

本章我们学习了:

  1. 模块系统作用:代码组织、依赖管理、避免全局污染
  2. CommonJSrequire()module.exports
  3. ES Modulesimportexport
  4. 模块查找规则:核心模块、相对路径、node_modules
  5. 模块缓存:避免重复加载
  6. 最佳实践:目录结构、入口文件、循环依赖处理

练习

  1. 创建一个数学工具模块,支持加减乘除和高级运算
  2. 将 CommonJS 模块转换为 ES Modules
  3. 实现一个配置模块,支持不同环境加载不同配置
  4. 解决一个循环依赖问题