在 2.2 节中我们搭建了 Node.js 的开发环境并掌握了包管理器的基本操作,接下来需要理解 Node.js 组织代码的根本方式——模块。无论是使用内置功能、加载第三方包,还是拆分自己的业务逻辑,都离不开模块系统。这一节我们梳理 Node.js 中模块的分类、各自的加载方式,并从一个最简程序开始,体会从编码到运行的完整闭环。
Node.js 默认使用 CommonJS 规范作为模块系统,每个 .js 文件都被视为一个独立的模块。从 v12 开始,ES Modules(ESM)也得到实验性及稳定支持,我们将在第 5 章深入两者原理,本节以 CommonJS 为主线说明日常开发中最常见的用法。
2.3.1 三类模块的划分
任何 Node.js 应用中的代码,都可以归类为以下三种模块:
- 核心模块(内置模块):Node.js 二进制文件内置的模块,由 C++ 或 JavaScript 实现。例如
fs、path、http等。无需安装,直接通过require('模块名')加载即可。 - 第三方模块:来自 npm 注册表、安装到
node_modules目录下的包。例如express、lodash、mysql2等。需要使用包管理器(npm/yarn/pnpm)安装后,才可在代码中require('包名')加载。 - 自定义模块:开发者自己在项目目录中创建的文件。例如
./utils.js、../config/app.js等。加载时需使用相对路径或绝对路径,且可以省略.js后缀。
三者共同构成了 Node.js 项目的模块生态,下面分别说明。
2.3.2 核心模块:开箱即用的标准库
Node.js 提供了四十余个内置模块,覆盖文件操作、网络编程、进程管理、加密解密等基础领域。常用的核心模块包括:
| 模块名 | 主要能力 |
|-----------|----------------------------------------|
| fs | 文件系统读写、目录操作、文件监听等 |
| path | 路径拼接、解析、规范化,跨平台兼容 |
| http | 创建 HTTP 服务器与客户端 |
| https | 同上,增加 TLS/SSL 支持 |
| url | URL 解析与构造 |
| querystring | 查询字符串解析与序列化 |
| os | 获取操作系统信息、CPU 架构、内存等 |
| process | 进程信息、环境变量、命令行参数、标准 I/O|
| events | 事件发射与监听(EventEmitter 基类) |
| stream | 流式数据处理接口 |
| crypto | 加密、摘要、HMAC 等 |
| child_process | 创建并管理子进程 |
| cluster | 多进程负载均衡 |
加载核心模块非常简单,只需在代码中 require 模块名,Node.js 会优先从内置模块中查找:
const fs = require('fs'); // 文件系统
const path = require('path'); // 路径处理
const http = require('http'); // HTTP 服务
核心模块由 Node.js 编译进二进制包,因此加载速度极快,且无需担心版本冲突。在项目初期,可以多翻阅官方文档,熟悉哪些能力已经“自带”,避免重复造轮子。
2.3.3 第三方模块:npm 生态的力量
当核心模块不能满足需求时(例如需要一个 Web 框架,或更好的日期处理库),就可以引入第三方模块。典型的流程是:
- 在项目根目录下执行
npm install <包名>安装。 - 包会被下载到
node_modules目录,并在package.json中记录依赖。 - 在代码中通过
require('包名')引用,Node.js 会按规则搜索node_modules目录。
例如安装 Express:
npm install express
然后在 app.js 中使用:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Express');
});
app.listen(3000, () => {
console.log('服务已启动:http://localhost:3000');
});
第三方模块也可以按需安装为开发依赖(--save-dev),例如测试框架或代码校验工具,这些包只在本机开发阶段使用,不会打包进生产环境。
2.3.4 自定义模块:组织自己的代码
随着应用变大,将所有逻辑写在一个文件里显然不可维护。Node.js 允许我们将逻辑拆分成多个自定义模块,通过 require 互相导入。
导出模块的两种方式:
- 将需要暴露的成员挂载到
module.exports对象上。 - 直接重写
module.exports为一个新的对象、类或函数。
最常见的是导出对象、函数或类:
// math.js - 导出多个工具函数
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
// greet.js - 直接导出函数
module.exports = function greet(name) {
return `Hello, ${name}!`;
};
导入自定义模块:
// main.js
const math = require('./math'); // 相对路径,可省略 .js
const greet = require('./greet');
console.log(math.add(2, 3)); // 5
console.log(greet('Node')); // Hello, Node!
注意区分:exports 是 module.exports 的引用,如果只给 exports 添加属性,效果和给 module.exports 添加属性相同。但若直接对 exports 赋值(如 exports = function(){}),则会切断引用,导致模块导出失败。稳妥起见,始终使用 module.exports 导出。
加载自定义模块时,Node.js 按照以下顺序解析路径(假设 require('./mymod')):
- 查找
./mymod.js文件。 - 查找
./mymod.json文件。 - 查找
./mymod.node编译好的二进制扩展。 - 将
./mymod当作目录,查找目录下的index.js/index.json/index.node,或依据该目录中package.json的main字段决定入口文件。
了解这些查找规则有助于在模块路径出错时快速定位问题。
2.3.5 第一个 Node.js 程序:从代码到运行
我们已经了解了模块的分类和引入方式,现在动手写一个最简单但结构完整的 Node.js 程序,串联核心模块、自定义模块和第三方模块。
项目结构:
my-node-app/
├── package.json
├── main.js
└── lib/
└── helper.js
1. 初始化项目:
mkdir my-node-app && cd my-node-app
npm init -y # 生成默认 package.json
2. 安装第三方模块(演示用):
npm install dayjs # 轻量日期处理库
3. 编写自定义模块 lib/helper.js:
const os = require('os'); // 核心模块
function systemInfo() {
return {
platform: os.platform(),
hostname: os.hostname(),
cpus: os.cpus().length
};
}
module.exports = { systemInfo };
4. 编写主入口 main.js:
const fs = require('fs'); // 核心模块
const path = require('path'); // 核心模块
const dayjs = require('dayjs'); // 第三方模块
const { systemInfo } = require('./lib/helper'); // 自定义模块
// 使用核心模块获取系统信息
const info = systemInfo();
console.log('系统信息:', info);
// 使用第三方模块格式化时间
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
// 将结果写入文件
const outputPath = path.join(__dirname, 'output.txt');
const content = `记录时间:${now}\n平台:${info.platform}\nCPU 核心数:${info.cpus}\n`;
fs.writeFile(outputPath, content, (err) => {
if (err) {
console.error('写入文件失败:', err);
return;
}
console.log('结果已保存至 output.txt');
});
这个程序展现了三种模块如何分工协作:核心模块 fs、path 处理文件与路径;第三方模块 dayjs 简化日期格式化;自定义模块 helper.js 封装业务相关的系统信息获取。
5. 运行程序:
在项目根目录下执行:
node main.js
终端会输出类似信息,并生成 output.txt 文件:
系统信息: { platform: 'darwin', hostname: 'MacBook-Pro', cpus: 8 }
结果已保存至 output.txt
如果代码有错误,终端会堆栈追踪,可以快速定位。对于更复杂的调试需求,2.4 节将详细介绍 VS Code 断点调试与 Chrome DevTools 等工具,但仅靠 console.log 和终端输出,已经足够完成大部分模块功能的初步验证。
2.3.6 小结
模块化是 Node.js 工程的基石。通过将代码合理划分为核心模块、第三方模块和自定义模块,我们能充分复用 Node.js 内置能力、集成社区贡献的成熟方案,并保持自身业务逻辑的清晰与可维护性。本节中从模块分类到实际运行一个简单程序,覆盖了日常开发中最常用的模式:
- 核心模块用
require直接加载,无需额外安装。 - 第三方模块用 npm 安装后按包名加载,赋予应用无限的扩展能力。
- 自定义模块通过相对路径导入,使用
module.exports导出对外接口,注意不要混淆exports和module.exports。 - 第一个程序的运行验证了从源码到输出的完整流程,为后续框架开发、工程化实践打下基础。
接下来,我们会深入了解 Node.js 的模块解析与缓存原理,以及 ESM 与 CommonJS 的互操作细节,这将帮助你写出更可靠、更高效的模块代码。