在 Node.js 中,模块化是组织代码的核心手段。一个 Node.js 程序通常由三类模块构成:内置模块(由 Node.js 官方提供)、第三方模块(从 npm 安装的社区模块)和自定义模块(开发者自己编写的模块)。清楚地区分这三者,理解它们的加载方式和使用场景,是编写可维护、可扩展 Node.js 应用的第一步。
内置模块:无需安装,开箱即用
内置模块是 Node.js 运行时自带的模块,它们在安装 Node.js 时就已经存在,无需通过 npm 额外下载。这些模块封装了最常用的系统能力,例如文件操作、网络通信、路径处理、操作系统信息等。使用内置模块时,只需通过 require('模块名') 直接引入即可。
常见的内置模块速览
fs:文件系统操作(读取、写入、删除、目录遍历等)http/https:创建 HTTP/HTTPS 服务器与客户端path:路径拼接、解析、规范化(跨平台兼容)os:获取操作系统信息(CPU 架构、内存大小、网络接口等)stream:处理流式数据(大文件读写、数据管道)crypto:加解密、哈希、证书等安全功能events:事件驱动的核心模块,提供EventEmitter类util:工具函数(类型检查、promisify 等)process:全局进程对象,提供环境变量、命令行参数等
内置模块的版本与 Node.js 版本绑定,随着 Node.js 的升级而更新。它们通常非常稳定,API 变更会遵循 Node.js 的 LTS 与版本兼容性政策。在 require 时不需要指定路径,Node.js 会直接从内部缓存中获取对应模块。
// 使用内置模块:读取文件
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'data.txt');
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) throw err;
console.log(data);
});
某些内置模块还带有实验性质的 API,使用时需要 Node.js 版本满足要求,并且 API 可能在未来版本中改变,生产环境建议优先选择稳定 API。
第三方模块:npm 生态的庞大工具箱
第三方模块是通过 npm(Node Package Manager)安装的外部包,托管在 npm 仓库中。从 Web 框架(Express、Koa、Fastify)到数据库驱动(mysql2、pg、mongodb),再到工具库(lodash、axios、dayjs),几乎涵盖了所有通用需求。
安装与使用第三方模块
- 在项目根目录下通过
npm init创建package.json(如果还未创建)。 - 使用
npm install 包名或简写npm i 包名安装模块。模块会被下载到node_modules目录,并在package.json中记录依赖。 - 在代码中通过
require('包名')引入模块。与内置模块类似,第三方模块也不需要路径前缀,Node.js 会自动在node_modules中查找。
// 使用第三方模块:搭建一个简易 HTTP 服务
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Express');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
依赖管理与版本控制
package.json的dependencies字段记录生产环境必需的模块。devDependencies字段记录仅在开发阶段使用的模块(如测试框架、构建工具)。- 通过
npm install会自动安装所有列出的依赖。 package-lock.json锁定了依赖的精确版本,确保团队环境一致。
第三方模块的查找规则
当执行 require('express') 时,Node.js 会按照以下顺序查找:
- 检查核心模块(内置模块)中是否有同名模块。
- 从当前文件的
node_modules目录开始,逐级向上查找,直到找到包含该包的node_modules目录或到达根目录。 - 如果仍找不到,则抛出
MODULE_NOT_FOUND错误。
这种机制使得项目可以依赖多个不同版本的同一包(例如 node_modules 中可能出现嵌套的依赖),但大型项目中也可能因此造成 node_modules 体积膨胀,如今主流包管理器(如 pnpm、yarn)已经对这个问题进行了优化。
自定义模块:项目内部的代码组织
自定义模块是指开发者自己在项目中编写的 .js 文件或目录,通过 require() 引用,从而实现代码复用和逻辑拆分。例如,将数据校验、工具函数、数据库操作等抽成单独的模块。
创建一个自定义模块
新建一个文件 math.js:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
在另一个文件中使用:
const math = require('./math');
console.log(math.add(2, 3)); // 5
console.log(math.multiply(2, 3)); // 6
exports 与 module.exports 的区别
- 每个模块内部,
module.exports是最终暴露的对象。 exports是module.exports的一个引用,刚开始它们指向同一个空对象。- 如果给
exports重新赋一个新对象,会切断它与module.exports的联系;而module.exports重新赋值则可以彻底改变导出内容。
常见的最佳实践是统一使用 module.exports,避免混淆。
require 路径规则
- 相对路径:以
./或../开头,表示相对于当前文件的路径。 - 绝对路径:以
/开头,表示从文件系统根目录开始查找(不常用)。 - 文件扩展名可以省略,Node.js 会按
.js、.json、.node的顺序自动补全。 - 当路径指向一个目录时,Node.js 会查找目录下的
package.json并读取main字段指定的文件;如果没有package.json或main字段,则尝试加载index.js或index.node。这使得目录被视为一个完整的模块。
模块缓存
Node.js 对已加载的模块会进行缓存。当 require 一个模块时,如果该模块已经被加载过,则会直接返回缓存的 module.exports 对象,而不会再次执行模块代码。因此:
- 同一个模块在多次
require后得到的是同一个对象实例(单例模式)。 - 但基于文件路径区分,不同路径下可能有独立副本。
- 缓存机制提升了性能,避免了重复解析和编译。
这种缓存特性对于共享状态(如数据库连接池)非常有用,但也可能导致一些意想不到的单例问题,后续在模块系统原理章节中会深入讨论。
三类模块的区分汇总
| 类型 | 来源 | 引入方式 | 是否需要安装 | 示例 |
|------------|--------------------|------------------------|--------------|-----------------------------------------|
| 内置模块 | Node.js 自带 | require('模块名') | 否 | fs, http, path, os |
| 第三方模块 | npm 仓库 | require('包名') | 是 | express, axios, lodash |
| 自定义模块 | 项目文件 | require('./文件路径') | 否 | ./utils/logger.js, ./config/db.js |
在实际开发中,一个典型的 Node.js 文件可能同时引入这三类模块:
// 内置模块
const fs = require('fs');
const path = require('path');
// 第三方模块
const express = require('express');
const morgan = require('morgan');
// 自定义模块
const userService = require('./services/userService');
const config = require('./config/app');
掌握这三种模块的划分和用法,就拥有了构建 Node.js 项目的基石。后面章节还将深入模块系统的底层原理、循环引用的处理以及 ES Modules 与 CommonJS 的互操作,帮助你在更复杂的场景中游刃有余。