Day07_Node.js 深度解析:从模块系统到文件操作全指南
本文是一篇面向实战的Node.js进阶指南,重点讲解核心内置模块、异常处理、JSON数据与模块化规范。文章结构清晰,包含以下主要内容: 内置模块详解:重点解析path、fs、url、querystring模块的实战应用,提供跨平台路径处理、文件读写、URL解析等核心能力。 异常与JSON处理:系统介绍错误对象、try-catch机制和JSON数据的序列化/反序列化方法。 模块化规范:深度对比Com
一篇面向实战的 Node 进阶博客:内置模块(path / fs / url / querystring)、异常与 JSON、CommonJS 与 ESM 的完整链路。示例可独立运行,不依赖外部讲义路径。
目录
- 导读:知识架构与权威参考
- 1. Node.js 模块系统概述
- 2. 内置模块详解
- 3. 异常处理机制
- 4. JSON 数据处理
- 5. 模块化规范深度解析
- 6. 实战应用场景
- 7. 核心案例速查与知识点归纳
- 总结
导读:知识架构与权威参考
本文解决什么问题
| 板块 | 核心能力 | 典型场景 |
|---|---|---|
| path / fs | 路径拼接、读写、目录、流式复制 | 日志、配置、静态资源、大文件 |
| url / querystring | 解析 URL、序列化查询串 | 路由参数、表单、爬虫 |
| 异常处理 | Error、throw、try/catch |
稳健 CLI、服务启动 |
| JSON | stringify / parse |
配置、API 数据交换 |
| 模块化 | CJS require、ESM import |
工具库拆分、工程化 |
知识脉络(Mermaid)
权威文档
| 主题 | 链接 |
|---|---|
| Node.js API 总览 | nodejs.org/api |
| fs | nodejs.org/api/fs.html |
| path | nodejs.org/api/path.html |
| url | nodejs.org/api/url.html |
| Modules: CJS | nodejs.org/api/modules.html |
| Modules: ESM | nodejs.org/api/esm.html |
| JSON | MDN — JSON |
与工程化的关系
- Webpack / Vite:用
path.resolve定位入口,fs读源码写产物。 - Express 雏形:静态文件服务即本章
fs+path+Content-Type。 - 前后端分离:接口返回 JSON 字符串,前端
fetch+JSON.parse。
1. Node.js 模块系统概述
Node.js 中的模块分为三种类型:
模块系统核心概念
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
模块化特点:
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了
- 模块加载顺序按照代码书写顺序执行
模块化带来的好处:
- 提高代码的复用性
- 提高代码的可维护性
- 实现按需加载
【代码注释】
- 内置模块(
fs、path):require('fs')即可,无需 npm 安装。 - 自定义模块:项目内
.js文件,通过module.exports暴露、require('./x')引入。 - 第三方模块:
npm install后从node_modules加载,如express。 - 每个文件是一个独立模块作用域;顶层
var不会挂到global,避免污染。 - 同一文件多次
require只执行一次顶层代码(缓存),与 §7.3 速查一致。
2. 内置模块详解
2.1 Path 路径操作模块
Path 模块提供了处理文件路径和目录路径的实用工具,是跨平台路径操作的核心模块。
核心方法详解
1. path.join([path1][, path2][, …])
功能描述:使用系统特定的分隔符将路径片段连接在一起,规范化生成的路径。
名词解释:
- 路径分隔符:Unix系统使用"/“,Windows系统使用”"
- 路径规范化:将不同格式的路径转换为系统标准格式
实际应用示例:
const path = require('path');
const fullPath = path.join('user', 'documents', 'file.txt');
console.log(fullPath);
const mixedPath = path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
console.log(mixedPath);
const resolvedPath = path.join('a', 'b', '..', 'c');
console.log(resolvedPath);
【代码注释】
path.join用当前系统的分隔符拼接片段(Windows 为\,Unix 为/),并去掉多余斜杠;不保证结果是绝对路径。join('/foo', 'bar', ..., 'quux', '..')中..会吃掉quux,得到/foo/bar/baz/asdf;若片段里已含/(如baz/asdf),join 仍会正确拼接。join('a', 'b', '..', 'c')→a/c,适合在__dirname后接data、logs等相对片段。- 不要用字符串
__dirname + '/data/a.txt',跨平台与规范化都交给path模块。
经典使用场景:
- 构建配置文件路径(Webpack、Vite配置)
- 日志文件路径管理
- 静态资源路径处理
业界应用案例:
- Webpack:在模块解析时使用path.join构建绝对路径
- VSCode:插件系统路径拼接
- Create React App:构建工具中的路径处理
2. path.resolve([…paths])
功能描述:将路径或路径片段序列解析为绝对路径。
核心原理:
实际应用示例:
const path = require('path');
const absolutePath = path.resolve('static', 'assets', 'image.png');
console.log(absolutePath);
const multiPath = path.resolve('/foo', 'bar', 'baz');
console.log(multiPath);
const currentFileDir = path.resolve(__dirname, 'data.json');
console.log(currentFileDir);
const parentPath = path.resolve('/a/b', '../c');
console.log(parentPath);
【代码注释】
resolve从右向左处理:遇到绝对路径片段(如/foo)则丢弃其左侧所有相对片段,最终得到规范化的绝对路径。resolve('static', 'assets', 'image.png')基于当前 cwd(执行node时所在目录),与脚本文件位置无关——这是与join(__dirname, ...)的核心区别。resolve(__dirname, 'data.json')推荐用于「无论从哪里启动 node,都找到当前模块旁的 data.json」。- 记忆:
join= 拼路径;resolve= 拼完后钉在系统地图上的绝对坐标。
经典使用场景:
- 配置文件路径解析
- 文件上传目录处理
- 日志文件路径构建
3. path.basename(path[, ext])
功能描述:返回路径的最后一部分,通常用于获取文件名。
实际应用示例:
const path = require('path');
const fileName = path.basename('/path/to/example.txt');
const baseName = path.basename('/path/to/example.txt', '.txt');
const multiExt = path.basename('/path/to/file.tar.gz', '.gz');
const dirName = path.basename('/path/to/directory/');
【代码注释】
basename(path)取路径最后一段;第二参数ext若传入且匹配末尾扩展名,则去掉该扩展(如.txt→ 得到example)。basename('file.tar.gz', '.gz')只去掉.gz,剩余file.tar;双扩展名需业务层自行判断。- 路径以
/结尾时,最后一段视为目录名directory,常用于区分「目录路径」与「文件路径」。
4. path.dirname(path)
功能描述:返回路径中目录的部分。
实际应用示例:
const path = require('path');
const dirPath = path.dirname('/path/to/example.txt');
const nestedDir = path.dirname('/path/to/nested/directory/file.js');
const currentDir = path.dirname('/path/to/');
【代码注释】
dirname返回路径中去掉文件名后的目录部分;与 CommonJS 的__dirname常量概念一致(dirname(__filename) === __dirname)。- 嵌套越深,
dirname只剥最后一层,不会递归到根目录。 - 尾部带分隔符的路径,
dirname('/path/to/')为/path,写日志或拼配置时常与basename配合使用。
5. path.extname(path)
功能描述:返回路径中文件的后缀名,从最后一个 . 开始到字符串结束。
实际应用示例:
const path = require('path');
console.log(path.extname('index.html'));
console.log(path.extname('package.json'));
console.log(path.extname('README'));
console.log(path.extname('.gitignore'));
【代码注释】
- 返回值包含点,如
'.html'、'.json',便于与白名单数组includes比较。 - 只认最后一个点:
app.min.js→'.js';.tar.gz需自行解析。 README、.gitignore返回'';静态服务里常据此设置Content-Type(.html→text/html)。
经典使用场景:
- 文件类型验证
- 静态资源处理(图片、CSS、JS)
- 文件上传时的类型检查
6. path.isAbsolute(path)
功能描述:判断路径是否为绝对路径。
实际应用示例:
const path = require('path');
console.log(path.isAbsolute('/foo/bar'));
console.log(path.isAbsolute('foo/bar'));
console.log(path.isAbsolute('C:\\foo\\bar'));
console.log(path.isAbsolute('\\\\server\\share'));
【代码注释】
- Unix/macOS:以
/开头为绝对路径;foo/bar为相对路径。 - Windows:
C:\...盘符路径、\\server\shareUNC 路径为绝对;构建工具在解析 entry 时先用isAbsolute再决定join还是直接使用。
Path 模块综合应用案例
const path = require('path');
// 【实战案例】构建静态资源服务器路径处理
class StaticPathManager {
constructor(rootDir) {
this.rootDir = path.resolve(rootDir);
}
buildPath(relativePath) {
const fullPath = path.join(this.rootDir, relativePath);
const normalizedPath = path.normalize(fullPath);
if (!normalizedPath.startsWith(this.rootDir)) {
throw new Error('非法路径访问');
}
return normalizedPath;
}
getFileInfo(filePath) {
return {
name: path.basename(filePath),
dirname: path.dirname(filePath),
extname: path.extname(filePath),
isAbsolute: path.isAbsolute(filePath)
};
}
}
const pathManager = new StaticPathManager('./public');
const imagePath = pathManager.buildPath('images/logo.png');
console.log('完整路径:', imagePath);
console.log('文件信息:', pathManager.getFileInfo(imagePath));
【代码注释】
- 构造函数
path.resolve(rootDir)把./public钉成绝对根目录,后续所有请求都相对该根解析。 buildPath先join再normalize(处理..、重复斜杠),然后用startsWith(this.rootDir)拦截../../../etc/passwd类路径遍历攻击。- 注意:Windows 上应用
path.resolve后的根路径与normalize结果比较时,大小写/长路径格式要一致,生产环境可再加path.relative判断是否含..。 getFileInfo聚合 basename/extname,供静态服务器设置响应头或日志;与 §6.1 文件服务器思路一致。
2.2 FS 文件系统模块
FS(File System)模块是 Node.js 中最重要的内置模块之一,提供了文件读写、目录操作等完整的文件系统 API。
FS 模块架构
名词解释
- 同步 vs 异步:同步操作会阻塞代码执行直到操作完成,异步操作不会阻塞,通过回调函数处理结果
- Buffer:Node.js 中用于处理二进制数据的类,类似于数组但存储的是原始内存数据
- 文件描述符:操作系统用于访问文件的数字标识符
- 流(Stream):处理流式数据的抽象接口,可以逐块处理数据而不需要全部加载到内存
2.2.1 文件读取操作
1. 异步文件读取 (fs.readFile)
基础语法:fs.readFile(path[, options], callback)
完整示例代码:
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'data', 'example.txt');
fs.readFile(filePath, (err, data) => {
if (err) {
console.error('文件读取失败:', err.errno, err.code);
return;
}
console.log('Buffer 数据:', data);
console.log('字符串内容:', data.toString());
});
fs.readFile(filePath, 'utf-8', (err, content) => {
if (err) {
console.error('文件读取失败:', err.message);
return;
}
console.log('文件内容:', content);
});
console.log('开始读取文件...');
【代码注释】
path.join(__dirname, 'data', 'example.txt')保证无论从哪个目录执行node,都读取脚本旁边的 data 目录(课堂案例datas/a.txt同理)。- 不传编码时回调的
data是 Buffer,文本需data.toString('utf-8');图片/二进制应保持 Buffer 直接写入或处理。 - 传
'utf-8'时 Node 在读完文件后自动解码为字符串,适合.txt、.json、.md。 console.log('开始读取...')会先于两个回调执行,说明readFile不阻塞事件循环;Web 服务中必须用异步,否则一个慢磁盘会卡住所有请求。err.code常见ENOENT(路径错)、EACCES(无权限);务必if (err) return,避免对undefined调toString。
回调函数参数说明:
err:错误对象,如果操作成功则为nulldata:文件内容(Buffer 或字符串,取决于是否指定编码)
2. 同步文件读取 (fs.readFileSync)
基础语法:fs.readFileSync(path[, options])
完整示例代码:
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'data', 'example.txt');
try {
const content = fs.readFileSync(filePath, 'utf-8');
console.log('文件内容:', content);
const buffer = fs.readFileSync(filePath);
console.log('Buffer 长度:', buffer.length);
console.log('前100字节:', buffer.toString('utf-8', 0, 100));
} catch (error) {
console.error('文件读取失败:', error.message);
console.error('错误代码:', error.code);
}
try {
const configPath = path.join(__dirname, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
console.log('配置加载成功:', config);
} catch (error) {
console.error('配置文件加载失败,使用默认配置');
}
【代码注释】
readFileSync在读完前阻塞主线程;同一文件连续读两次会访问磁盘两遍,仅作演示,实际选一种即可。buffer.toString('utf-8', 0, 100)的第三、四参数是字节起止偏移,不是字符下标;含中文时勿按字符数截取。- 同步 API 失败时抛异常,用
try/catch;error.code与异步回调里的err.code含义相同。 JSON.parse(readFileSync(...))是启动时加载config.json的经典写法;parse失败会抛SyntaxError,应单独 catch 或合并到同一try。- 下方
config分支演示:文件缺失时用默认配置,避免进程退出。
同步 vs 异步选择指南:
- 使用异步:大文件操作、Web 服务、避免阻塞主线程
- 使用同步:配置文件读取、启动脚本、小文件快速读取
文件路径:cwd 与 __dirname(易错点)
fs.readFile('./datas/a.txt') 中的 ./ 相对的是启动 node 时的工作目录,不是脚本所在目录。
const fs = require('fs');
const path = require('path');
// const filename = './datas/a.txt';
const filename = path.join(__dirname, './datas/a.txt');
console.log('读取路径:', filename);
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) {
console.log('文件读取失败:', err.message);
return;
}
console.log(data);
});
【代码注释】
./datas/a.txt相对的是启动node时的工作目录 cwd:在项目根执行node src/app.js会找「项目根/datas」,在src里执行则找「src/datas」,极易 ENOENT。path.join(__dirname, './datas/a.txt')相对当前模块文件所在目录,与从哪里执行命令无关,是课堂案例与生产部署的推荐写法。path.resolve('./datas/a.txt')同样相对 cwd,只是结果会变成绝对路径字符串;需要绝对路径且锚定脚本目录时应写path.resolve(__dirname, 'datas/a.txt')。- 打印
filename便于调试:复制终端里的路径到资源管理器核对文件是否真实存在。
2.2.2 文件写入操作
1. 基础文件写入 (fs.writeFile)
基础语法:fs.writeFile(file, data[, options], callback)
完整示例代码:
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'output', 'log.txt');
const content = `日志记录 - ${new Date().toISOString()}\n`;
fs.writeFile(filePath, content, { encoding: 'utf-8' }, (err) => {
if (err) {
console.error('文件写入失败:', err.message);
return;
}
console.log('文件写入成功!');
fs.readFile(filePath, 'utf-8', (err, data) => {
if (!err) {
console.log('验证内容:', data);
}
});
});
try {
fs.writeFileSync(filePath, content, 'utf-8');
console.log('同步写入成功!');
} catch (error) {
console.error('同步写入失败:', error.message);
}
【代码注释】
writeFile默认 覆盖已有文件;output目录不存在时会ENOENT,需先mkdirSync(..., { recursive: true })。- 异步写入完成后才适合
readFile验证;若两次writeFile几乎同时发起,后完成的会覆盖先完成的。 encoding: 'utf-8'可把字符串按 UTF-8 编码写入;也可直接写 Buffer(二进制文件)。- 同步
writeFileSync会阻塞到落盘结束,CLI 小文件可以,高并发服务应改用异步或 Stream。
写入模式选项:
const fs = require('fs');
const writeOptions = {
'w': '写入模式,默认值,覆盖文件内容',
'wx': '独占写入,文件存在则失败',
'a': '追加模式,在文件末尾添加内容',
'ax': '独占追加,文件不存在则创建'
};
fs.writeFile('example.txt', '内容', { flag: 'w' }, (err) => {
if (err) console.error(err);
});
【代码注释】
flag: 'w'截断并覆盖;wx用于「原子创建」:文件已存在则报错,避免误覆盖重要配置。a/ax与appendFile类似,在文件尾追加;日志场景常用a,新文件用ax可防止重复初始化时覆盖旧日志。flag也可用于createWriteStream({ flags: 'a' }),与writeFile语义一致。
2. 文件追加内容 (fs.appendFile)
完整示例代码:
const fs = require('fs');
const path = require('path');
const logFile = path.join(__dirname, 'logs', 'app.log');
const logMessage = `[${new Date().toISOString()}] 用户登录成功\n`;
fs.appendFile(logFile, logMessage, (err) => {
if (err) {
console.error('日志写入失败:', err.message);
} else {
console.log('日志追加成功');
}
});
try {
const logEntries = [];
for (let i = 0; i < 1000; i++) {
logEntries.push(`[Log ${i}] ${new Date().toISOString()}\n`);
}
fs.appendFileSync(logFile, logEntries.join(''));
console.log('批量日志写入成功');
} catch (error) {
console.error('批量写入失败:', error.message);
}
【代码注释】
appendFile在文件末尾追加,不截断原内容;文件不存在时会创建新文件(目录仍须存在)。- 单行异步追加适合访问日志;循环 1000 次
appendFileSync会触发 1000 次磁盘操作,性能差。 - 示例先在内存
join再一次appendFileSync,与 Day06「批量再写」优化思路相同;更高吞吐用createWriteStream({ flags: 'a' })保持句柄打开。
1. 文件重命名和移动 (fs.rename)
完整示例代码:
const fs = require('fs');
const path = require('path');
const oldName = path.join(__dirname, 'data.txt');
const newName = path.join(__dirname, 'data_backup.txt');
fs.rename(oldName, newName, (err) => {
if (err) {
console.error('重命名失败:', err.message);
} else {
console.log('文件重命名成功!');
}
});
const sourceFile = path.join(__dirname, 'uploads', 'image.png');
const targetFile = path.join(__dirname, 'public', 'images', 'logo.png');
const targetDir = path.dirname(targetFile);
fs.mkdir(targetDir, { recursive: true }, (err) => {
if (!err) {
fs.rename(sourceFile, targetFile, (err) => {
if (err) {
console.error('文件移动失败:', err.message);
} else {
console.log('文件移动成功!');
}
});
}
});
【代码注释】
rename(old, new)在同一磁盘分区内通常只改目录项,不复制数据,故既可改扩展名(a.txt→a.md)也可跨文件夹「移动」。- 目标文件已存在时多数系统会覆盖;重要文件应先备份。
- 移动前先
mkdir(targetDir, { recursive: true }),否则目标路径父目录不存在会ENOENT。 - 跨设备移动可能报
EXDEV,需copyFile+unlink组合。
2. 文件删除 (fs.unlink)
完整示例代码:
const fs = require('fs');
const path = require('path');
const fileToDelete = path.join(__dirname, 'temp.txt');
fs.unlink(fileToDelete, (err) => {
if (err) {
if (err.code === 'ENOENT') {
console.log('文件不存在,无需删除');
} else {
console.error('文件删除失败:', err.message);
}
} else {
console.log('文件删除成功!');
}
});
const filesToDelete = ['temp1.txt', 'temp2.txt', 'temp3.txt'];
const deletePromises = filesToDelete.map(file => {
return new Promise((resolve, reject) => {
fs.unlink(file, (err) => {
if (err) reject(err);
else resolve();
});
});
});
Promise.all(deletePromises)
.then(() => console.log('所有文件删除成功'))
.catch(err => console.error('批量删除失败:', err.message));
【代码注释】
unlink只删文件,不能删非空目录;课堂流程「改名 → 再删」对应rename后unlink。ENOENT表示路径不存在,幂等删除场景可视为成功,不必抛错给用户。Promise.all并行删除:任一失败则整个catch;若需「尽量删完」应改用Promise.allSettled。- 批量路径示例为相对 cwd 的文件名,生产环境应
path.join(__dirname, file)。
3. 目录操作 (mkdir, rmdir, readdir)
完整示例代码:
const fs = require('fs');
const path = require('path');
const dirPath = path.join(__dirname, 'projects', 'web', 'src', 'components');
fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) {
console.error('目录创建失败:', err.message);
} else {
console.log('目录结构创建成功!');
}
});
const readDir = path.join(__dirname, 'projects');
fs.readdir(readDir, (err, files) => {
if (err) {
console.error('目录读取失败:', err.message);
return;
}
console.log('目录内容:');
files.forEach((file, index) => {
console.log(`${index + 1}. ${file}`);
});
});
const dirToDelete = path.join(__dirname, 'old_projects');
fs.rmdir(dirToDelete, { recursive: true }, (err) => {
if (err) {
if (err.code === 'ENOENT') {
console.log('目录不存在,无需删除');
} else {
console.error('目录删除失败:', err.message);
}
} else {
console.log('目录删除成功!');
}
});
【代码注释】
mkdir(..., { recursive: true })等价mkdir -p,一次创建projects/web/src/components多级目录。readdir只列当前层文件名,不含子目录内部;要区分文件/目录需对每个条目再stat或使用readdir的withFileTypes选项(Node 10+)。rmdir(..., { recursive: true })递归删除整棵树;Node 14.14+ 也可用fs.rm(path, { recursive: true, force: true })。- 删除非空目录若不用
recursive会报ENOTEMPTY。
4. 文件状态和判断 (fs.stat, fs.access)
完整示例代码:
const fs = require('fs');
const path = require('path');
const targetPath = path.join(__dirname, 'example.txt');
fs.stat(targetPath, (err, stats) => {
if (err) {
console.error('获取文件信息失败:', err.message);
return;
}
console.log('文件信息:');
console.log('- 是否为文件:', stats.isFile());
console.log('- 是否为目录:', stats.isDirectory());
console.log('- 文件大小:', stats.size, '字节');
console.log('- 创建时间:', stats.birthtime);
console.log('- 修改时间:', stats.mtime);
});
fs.access(targetPath, fs.constants.F_OK, (err) => {
console.log(err ? '文件或目录不存在' : '文件或目录存在');
});
fs.access(targetPath, fs.constants.R_OK | fs.constants.W_OK, (err) => {
console.log(err ? '没有读写权限' : '有读写权限');
});
【代码注释】
stat返回fs.Stats:isFile()/isDirectory()决定用readFile还是readdir;对目录readFile会EISDIR。size为字节数;mtime常用于缓存失效、日志轮转判断。access(F_OK)仅判断存在;R_OK | W_OK用位或组合检测读+写权限,失败不一定是「不存在」,也可能是权限不足。existsSync简单但同步阻塞;高并发服务优先异步access/stat。
2.2.4 流式文件操作
流式操作优势:
- 内存效率:不需要将整个文件加载到内存
- 时间效率:开始处理数据更快
- 管道机制:可以连接多个流操作
1. 创建读取流 (createReadStream)
完整示例代码:
const fs = require('fs');
const path = require('path');
// 【代码注释】创建大文件读取流
const sourceFile = path.join(__dirname, 'large-file.txt');
const readStream = fs.createReadStream(sourceFile, {
// 【代码注释】设置编码
encoding: 'utf-8',
// 【代码注释】设置缓冲区大小(64KB)
highWaterMark: 64 * 1024,
// 【代码注释】设置起始位置
start: 0,
// 【代码注释】设置结束位置
end: undefined
});
// 【代码注释】监听数据事件
readStream.on('data', (chunk) => {
console.log('接收到数据块,大小:', chunk.length);
console.log('数据块内容:', chunk.substring(0, 100) + '...');
});
// 【代码注释】监听结束事件
readStream.on('end', () => {
console.log('文件读取完毕!');
});
// 【代码注释】监听错误事件
readStream.on('error', (err) => {
console.error('文件读取错误:', err.message);
});
// 【代码注释】监听打开事件
readStream.on('open', (fd) => {
console.log('文件已打开,文件描述符:', fd);
});
【代码注释】
createReadStream按highWaterMark(默认 64KB)分块触发data事件,内存占用近似常量,适合大文件。encoding: 'utf-8'时chunk为字符串,否则为 Buffer;start/end可只读文件片段(断点续传、分片下载)。- 必须监听
error,否则磁盘错误可能导致进程未处理异常;end表示数据读完,close表示描述符关闭。
2. 创建写入流 (createWriteStream)
完整示例代码:
const fs = require('fs');
const path = require('path');
// 【代码注释】创建文件写入流
const targetFile = path.join(__dirname, 'output.txt');
const writeStream = fs.createWriteStream(targetFile, {
// 【代码注释】设置编码
encoding: 'utf-8',
// 【代码注释】设置标志位
flags: 'w',
// 【代码注释】设置权限
mode: 0o666,
// 【代码注释】设置缓冲区大小
highWaterMark: 16 * 1024
});
// 【代码注释】监听准备就绪事件
writeStream.on('ready', () => {
console.log('写入流已准备就绪');
// 【代码注释】写入数据
for (let i = 0; i < 100; i++) {
const canWrite = writeStream.write(`行 ${i}: ${Date.now()}\n`);
// 【代码注释】检查缓冲区是否已满
if (!canWrite) {
console.log('缓冲区已满,等待 drain 事件');
}
}
// 【代码注释】结束写入
writeStream.end();
});
// 【代码注释】监听 drain 事件(缓冲区清空)
writeStream.on('drain', () => {
console.log('缓冲区已清空,可以继续写入');
});
// 【代码注释】监听完成事件
writeStream.on('finish', () => {
console.log('所有数据已写入完成!');
});
// 【代码注释】监听关闭事件
writeStream.on('close', () => {
console.log('文件已关闭');
});
// 【代码注释】监听错误事件
writeStream.on('error', (err) => {
console.error('写入错误:', err.message);
});
【代码注释】
write()返回false表示内部缓冲已满,应停止写入并等待drain再继续,否则内存暴涨(背压机制)。end()表示不再写入新数据,冲刷缓冲后触发finish;close表示 fd 已关闭。flags: 'w'覆盖写;日志追加用flags: 'a'。mode: 0o666控制新建文件权限(受 umask 影响)。
3. 管道操作 (pipe)
完整示例代码:
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
// 【代码注释】文件复制
const sourceFile = path.join(__dirname, 'source.txt');
const targetFile = path.join(__dirname, 'target.txt');
const readStream = fs.createReadStream(sourceFile);
const writeStream = fs.createWriteStream(targetFile);
// 【代码注释】使用管道进行文件复制
readStream.pipe(writeStream);
console.log('开始文件复制...');
// 【代码注释】监听复制完成
writeStream.on('finish', () => {
console.log('文件复制完成!');
});
// 【代码注释】文件压缩示例
const inputFile = path.join(__dirname, 'large-file.txt');
const compressedFile = path.join(__dirname, 'large-file.txt.gz');
const input = fs.createReadStream(inputFile);
const gzip = zlib.createGzip();
const output = fs.createWriteStream(compressedFile);
// 【代码注释】管道链:读取 -> 压缩 -> 写入
input.pipe(gzip).pipe(output);
output.on('finish', () => {
console.log('文件压缩完成!');
});
// 【代码注释】文件解压示例
const decompressedFile = path.join(__dirname, 'decompressed.txt');
const gzInput = fs.createReadStream(compressedFile);
const gunzip = zlib.createGunzip();
const decompressedOutput = fs.createWriteStream(decompressedFile);
gzInput.pipe(gunzip).pipe(decompressedOutput);
decompressedOutput.on('finish', () => {
console.log('文件解压完成!');
});
【代码注释】
readStream.pipe(writeStream)自动处理背压,是文件复制的标准写法;finish在数据全部写入后触发。input.pipe(gzip).pipe(output)形成管道链:读 → 压缩 → 写,全程不落完整文件到内存。- 解压链
gunzip顺序相反;任一流error应销毁整条链(pipelineAPI 更安全,Node 10+)。 - 课堂「流式案例」:复制大文件、打包日志、上传下载代理都基于同一模型。
4. 流式写入大批量行(createWriteStream + close)
const fs = require('fs');
const path = require('path');
const file = path.join(__dirname, 'output', 'big.log');
const ws = fs.createWriteStream(file);
ws.on('close', () => {
console.log('写入完毕!');
});
for (let i = 0; i < 100000; i++) {
ws.write(`${i} ${Math.random()} ${Date.now()}\n`);
}
ws.close();
【代码注释】
createWriteStream内部有缓冲区;write返回false表示应暂停写入并监听drain,示例中 10 万次循环在极高吞吐下需处理背压。- 必须调用
ws.close()(或end())才会刷新剩余缓冲并关闭文件描述符,触发close事件;只write不close可能导致数据未完全落盘。 - 课堂案例对比:
appendFileSync万次同步 I/O 极慢;流式 + 单次打开文件句柄是写大日志的正确方向。 - 复制文件优先
readStream.pipe(writeStream),不必把整文件读入内存。
2.2.5 Promise API 和 fs.promises
从 Node.js 10 开始,fs 模块提供了基于 Promise 的 API,使异步操作更加优雅。
完整示例代码:
const fs = require('fs').promises;
const path = require('path');
// 【代码注释】使用 async/await 读取文件
async function readFileAsync(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
console.log('文件内容:', content);
return content;
} catch (error) {
console.error('文件读取失败:', error.message);
throw error;
}
}
// 【代码注释】使用 async/await 写入文件
async function writeFileAsync(filePath, content) {
try {
await fs.writeFile(filePath, content, 'utf-8');
console.log('文件写入成功');
} catch (error) {
console.error('文件写入失败:', error.message);
throw error;
}
}
// 【代码注释】使用 Promise.all 并行处理多个文件
async function processMultipleFiles() {
const files = [
'file1.txt',
'file2.txt',
'file3.txt'
];
try {
// 【代码注释】并行读取多个文件
const contents = await Promise.all(
files.map(file => fs.readFile(file, 'utf-8'))
);
console.log('所有文件读取完成');
return contents;
} catch (error) {
console.error('批量文件处理失败:', error.message);
}
}
// 【代码注释】综合示例:文件处理流程
async function processFileWorkflow() {
const inputPath = path.join(__dirname, 'input.txt');
const outputPath = path.join(__dirname, 'output.txt');
try {
// 【代码注释】1. 检查输入文件是否存在
await fs.access(inputPath);
// 【代码注释】2. 读取文件内容
const content = await fs.readFile(inputPath, 'utf-8');
// 【代码注释】3. 处理内容
const processedContent = content.toUpperCase();
// 【代码注释】4. 写入输出文件
await fs.writeFile(outputPath, processedContent);
console.log('文件处理流程完成');
} catch (error) {
if (error.code === 'ENOENT') {
console.log('输入文件不存在');
} else {
console.error('文件处理失败:', error.message);
}
}
}
【代码注释】
fs.promises与回调 API 等价,但返回 Promise,适合async/await写线性流程。Promise.all(files.map(...))并行读多个文件,任一失败则整体 reject;需「部分成功」用allSettled。processFileWorkflow:access检查存在 →readFile→ 处理 →writeFile,是 CLI/ETL 脚本的典型管道;ENOENT单独分支提示用户。throw error在工具函数里可把错误交给上层统一处理;勿在 Web 中间件里吞掉异常。
2.3 URL 模块
URL 模块用于处理和解析 URL 字符串,是 Web 开发中必不可少的工具。
核心功能
const url = require('url');
// 【代码注释】示例 URL
const siteAddress = 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash';
// 【代码注释】方法一:使用 url.parse()(传统方法)
const urlData = url.parse(siteAddress);
console.log('解析结果:', urlData);
/* 输出结构:
{
protocol: 'https:',
slashes: true,
auth: 'user:pass',
host: 'sub.example.com:8080',
port: '8080',
hostname: 'sub.example.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash'
}
*/
// 【代码注释】方法二:使用 URL 类(推荐,WHATWG 标准)
const urlInfo = new URL(siteAddress);
console.log('现代 URL 解析:');
console.log('- 协议:', urlInfo.protocol);
console.log('- 主机名:', urlInfo.hostname);
console.log('- 端口:', urlInfo.port);
console.log('- 路径:', urlInfo.pathname);
console.log('- 查询参数:', urlInfo.search);
console.log('- 哈希:', urlInfo.hash);
console.log('- 用户信息:', urlInfo.username, urlInfo.password);
// 【代码注释】处理查询参数
console.log('查询参数详情:');
urlInfo.searchParams.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
【代码注释】
url.parse(str)(传统 API)返回带protocol、pathname、query等字段的对象;query仍是字符串,需再querystring.parse。new URL(str)是 WHATWG 标准,与浏览器一致;searchParams提供get/set/forEach,推荐新项目使用。protocol含尾部的:(如https:);hostname不含端口,host含端口。- 解析相对 URL 需基址:
new URL('/api/users', 'https://example.com')。
URL 模块实战应用
const url = require('url');
// 【实战案例】构建 URL 处理工具类
class URLProcessor {
// 【代码注释】解析 URL 并提取关键信息
static parseURL(urlString) {
try {
const parsedUrl = new URL(urlString);
return {
protocol: parsedUrl.protocol.replace(':', ''),
hostname: parsedUrl.hostname,
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? '443' : '80'),
pathname: parsedUrl.pathname,
searchParams: this.getSearchParams(parsedUrl),
hash: parsedUrl.hash.replace('#', '')
};
} catch (error) {
throw new Error(`无效的 URL: ${urlString}`);
}
}
// 【代码注释】提取查询参数
static getSearchParams(urlObj) {
const params = {};
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
return params;
}
// 【代码注释】构建 URL
static buildURL(baseURL, path, params = {}) {
const urlObj = new URL(path, baseURL);
Object.entries(params).forEach(([key, value]) => {
urlObj.searchParams.set(key, value);
});
return urlObj.toString();
}
}
// 使用示例
const testURL = 'https://api.example.com/users?page=1&limit=10&sort=name';
const parsed = URLProcessor.parseURL(testURL);
console.log('解析结果:', parsed);
const builtURL = URLProcessor.buildURL(
'https://api.example.com',
'/search',
{ q: 'nodejs', lang: 'zh' }
);
console.log('构建的 URL:', builtURL);
【代码注释】
parseURL用new URL包在try/catch里,无效 URL 转业务错误,避免进程崩溃。searchParams.forEach把查询串转为普通对象,便于与 Express 的req.query对照。buildURL(baseURL, path, params)中new URL(path, baseURL)解析相对路径;searchParams.set自动编码特殊字符。- 默认端口:HTTPS 空 port 时示例补
443,HTTP 补80,与浏览器行为一致。
2.4 QueryString 模块
QueryString 模块用于解析和格式化 URL 查询字符串,虽然很多功能已被 URL 模块取代,但仍在维护旧项目时有价值。
核心方法
const querystring = require('querystring');
// 【代码注释】解析查询字符串为对象
const queryString = 'name=John&age=30&city=New%20York';
const parsed = querystring.parse(queryString);
console.log('解析结果:', parsed);
// 输出: { name: 'John', age: '30', city: 'New York' }
// 【代码注释】将对象序列化为查询字符串
const params = {
search: 'nodejs tutorial',
page: 1,
limit: 10,
filters: ['beginner', 'video']
};
const serialized = querystring.stringify(params);
console.log('序列化结果:', serialized);
// 输出: 'search=nodejs%20tutorial&page=1&limit=10&filters=beginner&filters=video'
// 【代码注释】自定义分隔符
const customParsed = querystring.parse('a=1;b=2;c=3', ';', '=');
console.log('自定义分隔符:', customParsed);
// 【代码注释】转义特殊字符
const escaped = querystring.escape('hello world');
console.log('转义结果:', escaped); // 'hello%20world'
const unescaped = querystring.unescape('hello%20world');
console.log('反转义结果:', unescaped);
【代码注释】
parse('name=John&age=30')得到对象,所有值为字符串(age为'30'而非数字),计算前需Number()或parseInt。stringify会对空格等编码为%20;数组字段filters会生成重复键filters=a&filters=b。- 第二、三个参数可自定义分隔符,如
';'与'=',用于解析非标准日志格式。 - 新代码优先
URLSearchParams;维护旧项目、阅读老 Express 代码时仍会见到querystring。
3. 异常处理机制
3.1 错误对象基础
Node.js 中的错误处理遵循 JavaScript 标准,提供了一套完整的错误处理机制。
Error 对象结构
错误对象创建
// 【代码注释】创建基础错误对象
const basicError = new Error('这是一个基本错误');
console.log('错误信息:', basicError.message);
console.log('错误名称:', basicError.name);
// 【代码注释】创建带有错误代码的错误
class FileSystemError extends Error {
constructor(message, code) {
super(message);
this.name = 'FileSystemError';
this.code = code;
}
}
const fsError = new FileSystemError('文件不存在', 'ENOENT');
console.log('自定义错误:', fsError);
// 【代码注释】常见内置错误类型
try {
// 【代码注释】语法错误
// const badSyntax = ; // SyntaxError
// 【代码注释】类型错误
const num = 42;
// num.toUpperCase(); // TypeError
// 【代码注释】引用错误
// nonexistentFunction(); // ReferenceError
// 【代码注释】范围错误
// new Array(-1); // RangeError
} catch (error) {
console.error('捕获到错误:', error.name, error.message);
}
3.2 throw 抛出错误
// 【代码注释】主动抛出错误示例
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('参数必须是数字');
}
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
// 【代码注释】测试除法函数
try {
console.log('10 ÷ 2 =', divide(10, 2));
console.log('10 ÷ 0 =', divide(10, 0));
} catch (error) {
console.error('计算错误:', error.message);
}
// 【代码注释】条件抛出错误
function processUserInput(input) {
if (!input) {
throw new Error('输入不能为空');
}
if (input.length < 3) {
throw new Error('输入长度不能少于3个字符');
}
return input.toUpperCase();
}
// 【代码注释】抛出不同类型的错误
function validateConfig(config) {
if (!config) {
throw new Error('配置对象不能为空');
}
if (typeof config !== 'object') {
throw new TypeError('配置必须是对象');
}
if (!config.port || isNaN(config.port)) {
throw new Error('端口号无效');
}
return true;
}
3.3 try-catch-finally 结构
// 【代码注释】完整的错误处理结构
function completeErrorHandling() {
try {
// 【代码注释】可能出错的代码
console.log('开始执行...');
// 模拟一个错误
throw new Error('模拟的错误');
console.log('这行代码不会执行');
} catch (error) {
// 【代码注释】错误处理代码
console.error('捕获到错误:', error.message);
console.error('错误类型:', error.name);
// 【代码注释】根据错误类型进行不同处理
if (error instanceof TypeError) {
console.log('类型错误,请检查参数类型');
} else if (error instanceof ReferenceError) {
console.log('引用错误,请检查变量名');
} else {
console.log('其他错误:', error.message);
}
} finally {
// 【代码注释】无论是否出错都会执行的代码
console.log('清理资源...');
console.log('执行完成');
}
}
completeErrorHandling();
// 【代码注释】嵌套 try-catch 示例
function nestedTryCatch() {
try {
console.log('外层 try 开始');
try {
console.log('内层 try 开始');
throw new Error('内层错误');
} catch (innerError) {
console.log('内层错误处理:', innerError.message);
// 【代码注释】可以重新抛出错误
throw innerError;
}
} catch (outerError) {
console.log('外层错误处理:', outerError.message);
}
}
nestedTryCatch();
try/catch 三条归纳(务必记住):
try内无论是运行时错误还是throw主动抛出,都会被catch捕获,由开发者处理,进程通常不会因此直接崩溃。- 有无异常,
try/catch之后的语句仍会执行(除非你在catch里process.exit)。 try内出错位置之后的代码不会执行;需要「出错也继续」时拆成多个try或提前return。
【代码注释】
fs.readFile(path, (err) => { throw new Error() })中外层try/catch捕不到回调里的异常,因为回调在之后的事件循环才执行。- 正确做法:在回调里
if (err)处理,或使用async/await+fs.promises,让错误以 Promise rejection 形式进入try/catch。 process.on('uncaughtException')不应代替业务错误处理;未捕获的异步错误会导致进程不稳定。
错误处理最佳实践
// 【实战案例】异步操作错误处理
class AsyncErrorHandler {
// 【代码注释】异步函数错误处理
static async readFileWithErrorHandling(filePath) {
try {
const fs = require('fs').promises;
const content = await fs.readFile(filePath, 'utf-8');
return { success: true, data: content };
} catch (error) {
// 【代码注释】根据错误代码返回不同的错误信息
if (error.code === 'ENOENT') {
return { success: false, error: '文件不存在', code: 'FILE_NOT_FOUND' };
} else if (error.code === 'EACCES') {
return { success: false, error: '没有访问权限', code: 'ACCESS_DENIED' };
} else {
return { success: false, error: error.message, code: 'UNKNOWN_ERROR' };
}
}
}
// 【代码注释】批量操作错误处理
static async processBatch(items) {
const results = [];
const errors = [];
for (const item of items) {
try {
const result = await this.processItem(item);
results.push(result);
} catch (error) {
errors.push({ item, error: error.message });
// 【代码注释】继续处理下一个项目,不中断整个流程
}
}
return { results, errors, totalCount: items.length };
}
static async processItem(item) {
// 【代码注释】模拟项目处理
if (Math.random() > 0.7) {
throw new Error('随机处理失败');
}
return `处理结果: ${item}`;
}
}
// 使用示例
async function testAsyncErrorHandler() {
const handler = AsyncErrorHandler;
// 测试文件读取
const fileResult = await handler.readFileWithErrorHandling('nonexistent.txt');
console.log('文件读取结果:', fileResult);
// 测试批量处理
const items = ['item1', 'item2', 'item3', 'item4', 'item5'];
const batchResult = await handler.processBatch(items);
console.log('批量处理结果:', batchResult);
}
testAsyncErrorHandler();
4. JSON 数据处理
4.1 JSON 格式详解
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。
JSON 与 JavaScript 对象的区别
// 【代码注释】JavaScript 对象(有效)
const jsObject = {
name: 'John',
age: 30,
isAdmin: true,
hobbies: ['reading', 'coding'],
address: {
city: 'New York',
country: 'USA'
},
greet: function() { // JavaScript 对象可以包含函数
return 'Hello!';
},
lastName: 'Doe', // 最后一个属性后面可以有逗号
};
// 【代码注释】JSON 格式(必须严格遵守规范)
const jsonString = `{
"name": "John", // 属性名必须用双引号
"age": 30, // 数字不需要引号
"isAdmin": true, // 布尔值不需要引号
"hobbies": [ // 数组
"reading",
"coding"
],
"address": { // 嵌套对象
"city": "New York",
"country": "USA"
}
// 注意:不能包含函数
// 注意:最后一个属性后面不能有逗号
}`;
console.log('JSON 字符串:', jsonString);
JSON 语法硬性规则(考试/联调常踩坑):
| 规则 | 说明 |
|---|---|
| 字符串必须双引号 | 'a' 在 JSON 文本中非法 |
| 属性名必须双引号 | { name: 1 } 不是合法 JSON |
| 最后一项无尾逗号 | [1,2,] 解析失败 |
| 不能有函数/表达式 | 值只能是 JSON 支持的六种类型 |
【代码注释】
- JSON 文本中属性名、字符串必须用双引号;不能有函数、注释、尾逗号(与 JS 对象字面量不同)。
JSON.parse(jsonString)失败抛SyntaxError(如少括号、单引号);读取data01.json等配置文件时必须try/catch或.catch。undefined、函数、Date对象不能直接出现在 JSON 文本里;日期常以 ISO 字符串存储,解析后用reviver转回Date。
JSON 数据类型
4.2 JSON 对象方法
JSON.stringify()
// 【代码注释】基础序列化
const person = {
name: 'Alice',
age: 25,
city: 'Boston',
skills: ['JavaScript', 'Node.js', 'React']
};
const jsonString = JSON.stringify(person);
console.log('序列化结果:', jsonString);
// 输出: '{"name":"Alice","age":25,"city":"Boston","skills":["JavaScript","Node.js","React"]}'
// 【代码注释】格式化输出(带缩进)
const formattedJson = JSON.stringify(person, null, 2);
console.log('格式化的 JSON:');
console.log(formattedJson);
/*
输出:
{
"name": "Alice",
"age": 25,
"city": "Boston",
"skills": [
"JavaScript",
"Node.js",
"React"
]
}
*/
// 【代码注释】使用 replacer 函数过滤属性
const sensitiveData = {
username: 'user123',
password: 'secret123',
email: 'user@example.com',
apiKey: 'abc123xyz'
};
const safeJson = JSON.stringify(sensitiveData, (key, value) => {
// 【代码注释】过滤敏感信息
if (key === 'password' || key === 'apiKey') {
return undefined;
}
return value;
}, 2);
console.log('安全的 JSON:', safeJson);
// 【代码注释】序列化日期对象
const data = {
timestamp: Date.now(),
date: new Date(),
customDate: new Date().toISOString()
};
const dataJson = JSON.stringify(data);
console.log('包含日期的 JSON:', dataJson);
// 【代码注释】处理循环引用
const obj = { name: 'Object' };
obj.self = obj; // 创建循环引用
try {
// 【代码注释】直接序列化会报错
// JSON.stringify(obj); // TypeError: Converting circular structure to JSON
// 【代码注释】使用自定义 replacer 处理循环引用
const safeStringify = (obj, indent = 2) => {
const cache = new Set();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
return '[Circular]';
}
cache.add(value);
}
return value;
}, indent);
};
console.log('处理循环引用:', safeStringify(obj));
} catch (error) {
console.error('序列化错误:', error.message);
}
JSON.parse()
// 【代码注释】基础解析
const jsonStr = '{"name":"Bob","age":35,"isActive":true}';
const parsedObj = JSON.parse(jsonStr);
console.log('解析结果:', parsedObj);
console.log('姓名:', parsedObj.name);
console.log('年龄:', parsedObj.age);
// 【代码注释】使用 reviver 函数转换数据
const jsonData = `{
"name": "Charlie",
"birthDate": "1990-05-15",
"score": "95",
"price": "99.99"
}`;
const transformed = JSON.parse(jsonData, (key, value) => {
// 【代码注释】转换特定字段
if (key === 'birthDate') {
return new Date(value);
}
if (key === 'score' || key === 'price') {
return Number(value);
}
return value;
});
console.log('转换后的数据:', transformed);
console.log('生日:', transformed.birthDate instanceof Date); // true
console.log('分数:', typeof transformed.score); // 'number'
// 【代码注释】错误处理
function safeJsonParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('JSON 解析失败:', error.message);
return defaultValue;
}
}
const invalidJson = '{invalid json}';
const result = safeJsonParse(invalidJson, {});
console.log('解析结果:', result); // {}
// 【代码注释】解析数组
const jsonArray = '[{"id":1,"name":"Item 1"},{"id":2,"name":"Item 2"}]';
const items = JSON.parse(jsonArray);
console.log('解析的数组:', items);
items.forEach(item => {
console.log(`- ${item.id}: ${item.name}`);
});
【代码注释】
JSON.stringify(obj, null, 2)第三参数为缩进,生成可读的配置文件;生产接口常省略缩进以减小体积。replacer可为函数:返回undefined的键会被省略,用于过滤密码字段;数组形式的 replacer 白名单指定要序列化的键。JSON.parse(text, reviver)的 reviver 在解析每个键值时调用,可把 ISO 日期字符串转回Date对象。- 循环引用对象直接
stringify会抛错,需自定义 replacer 或拆结构;safeJsonParse模式避免坏配置拖垮启动流程。
4.3 JSON 实战应用
配置文件管理
const fs = require('fs').promises;
const path = require('path');
// 【实战案例】配置文件管理器
class ConfigManager {
constructor(configPath) {
this.configPath = configPath;
this.config = null;
}
// 【代码注释】加载配置文件
async load() {
try {
const content = await fs.readFile(this.configPath, 'utf-8');
this.config = JSON.parse(content);
console.log('配置加载成功');
return this.config;
} catch (error) {
if (error.code === 'ENOENT') {
console.log('配置文件不存在,创建默认配置');
return this.createDefaultConfig();
} else if (error instanceof SyntaxError) {
throw new Error('配置文件格式错误');
} else {
throw error;
}
}
}
// 【代码注释】创建默认配置
async createDefaultConfig() {
this.config = {
app: {
name: 'My Application',
version: '1.0.0',
port: 3000
},
database: {
host: 'localhost',
port: 5432,
name: 'mydb',
username: 'user',
password: ''
},
logging: {
level: 'info',
file: 'app.log'
}
};
await this.save();
return this.config;
}
// 【代码注释】保存配置文件
async save() {
try {
const formatted = JSON.stringify(this.config, null, 2);
await fs.writeFile(this.configPath, formatted, 'utf-8');
console.log('配置保存成功');
} catch (error) {
console.error('配置保存失败:', error.message);
throw error;
}
}
// 【代码注释】获取配置项
get(key) {
const keys = key.split('.');
let value = this.config;
for (const k of keys) {
if (value && typeof value === 'object') {
value = value[k];
} else {
return undefined;
}
}
return value;
}
// 【代码注释】设置配置项
set(key, value) {
const keys = key.split('.');
let current = this.config;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in current) || typeof current[k] !== 'object') {
current[k] = {};
}
current = current[k];
}
current[keys[keys.length - 1]] = value;
}
}
// 使用示例
async function testConfigManager() {
const configPath = path.join(__dirname, 'config.json');
const manager = new ConfigManager(configPath);
// 加载或创建配置
await manager.load();
// 读取配置
console.log('应用名称:', manager.get('app.name'));
console.log('数据库端口:', manager.get('database.port'));
// 修改配置
manager.set('app.port', 8080);
manager.set('database.password', 'secret');
// 保存配置
await manager.save();
}
testConfigManager();
【代码注释】
load():ENOENT时走createDefaultConfig()写盘,适合首次部署;SyntaxError表示 JSON 损坏,应告警而非静默覆盖。get('app.name')用点分路径访问嵌套对象,与lodash.get思路相同;中间缺失键返回undefined。set自动创建中间对象,便于manager.set('database.port', 3306);save()再stringify写回,注意并发写需加锁或单进程。- 课堂
data01.json读写与该类相同,可对照 §4 的parse/stringify练习。
5. 模块化规范深度解析
5.0 主流模块化规范对照
| 规范 | 环境 | 加载方式 | 关键字 | 现状 |
|---|---|---|---|---|
| CommonJS | Node、Webpack 打包 | 同步(运行时) | require / module.exports |
Node 默认,生态最广 |
| AMD | 浏览器(RequireJS) | 异步 | define |
已少见 |
| CMD | 浏览器(SeaJS) | 异步,偏 CMD 写法 | define |
已少见 |
| ES Module | 浏览器原生 + Node | 静态分析 | import / export |
官方推荐方向 |
【代码注释】
- CommonJS:
require/module.exports,Node 默认,同步加载,适合服务端与课堂01-mod~08-mod案例。 - ESM:
import/export,浏览器同源语法;Node 通过.mjs或package.json的"type":"module"启用。 - 同一
.js文件内不要混写require与import(除非使用实验性互操作或拆成两个入口);第三方包需看其exports字段支持哪种格式。 - AMD/CMD 多见于历史前端(RequireJS、SeaJS),现代工程由 Webpack/Vite 在构建期打包,运行时仍是 CJS 或 ESM。
5.1 CommonJS 规范
CommonJS 是 Node.js 默认使用的模块化规范,采用同步加载方式,主要服务于服务端环境。
CommonJS 核心概念
模块导出方式
// 【方式1】导出空对象(默认情况)
// 文件: modules/01-mod.js
// 如果没有显式导出,require() 返回空对象 {}
// 【方式2】使用 module.exports 导出单个值
// 文件: modules/02-mod.js
const greeting = 'Hello, World!';
function sayHello() {
console.log(greeting);
}
// 【代码注释】导出一个包含多个方法的对象
module.exports = {
greeting,
sayHello,
name: 'CommonJS Module',
version: '1.0.0'
};
// 【方式3】使用 module.exports 添加属性
// 文件: modules/03-mod.js
module.exports.getMessage = function() {
return 'This is a message from module';
};
module.exports.config = {
debug: true,
version: '1.0.0'
};
module.exports.util = {
format: function(str) {
return str.toUpperCase();
}
};
// 【方式4】使用 exports 别名(注意限制)
// 文件: modules/04-mod.js
// 【代码注释】exports 是 module.exports 的引用,只能添加属性,不能重新赋值
exports.name = 'Module 4';
exports.version = '2.0.0';
exports.getData = function() {
return { id: 1, value: 'test' };
};
// 【代码注释】错误示例:不要这样使用
// exports = { name: 'Error' }; // 这样会断开与 module.exports 的连接
// 【方式5】导出 JSON 数据
// 文件: modules/05-mod.json
{
"name": "JSON Module",
"version": "1.0.0",
"description": "A JSON configuration file"
}
// 【方式6】导出函数或类
// 文件: modules/06-mod.js
class UserService {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
console.log(`用户 ${user.name} 已添加`);
}
getUsers() {
return this.users;
}
}
// 【代码注释】导出构造函数
module.exports = UserService;
// 【方式7】条件导出
// 文件: modules/07-mod.js
function getData() {
return process.env.NODE_ENV === 'production'
? { mode: 'production' }
: { mode: 'development' };
}
module.exports = getData();
模块导入方式
// 【代码注释】主文件:index.js
const path = require('path');
// 【导入1】基本导入
const mod01 = require('./modules/01-mod.js');
console.log('模块1:', mod01); // {}
// 【导入2】导入并解构
const mod02 = require('./modules/02-mod');
const { sayHello, name } = mod02;
sayHello();
console.log('模块名称:', name);
// 【导入3】直接解构导入
const { getMessage, config } = require('./modules/03-mod');
console.log('消息:', getMessage());
console.log('配置:', config);
// 【导入4】导入 JSON 文件
const jsonConfig = require('./modules/05-mod.json');
console.log('JSON 配置:', jsonConfig);
// 【导入5】导入类/构造函数
const UserService = require('./modules/06-mod');
const userService = new UserService();
userService.addUser({ name: 'Alice', age: 30 });
console.log('用户列表:', userService.getUsers());
// 【导入6】导入内置模块
const fs = require('fs');
const http = require('http');
// 【导入7】路径处理
const customModule = require(path.join(__dirname, 'modules', 'custom.js'));
// 【导入8】模块缓存验证
console.log('\n模块缓存测试:');
require('./modules/02-mod'); // 第一次执行
require('./modules/02-mod'); // 使用缓存
require('./modules/02-mod'); // 使用缓存
console.log('模块只加载一次,后续使用缓存');
模块加载机制
// 【代码注释】模块查找路径解析
console.log('模块查找路径:');
console.log('1. 相对路径模块: ./modules/custom.js');
console.log('2. 绝对路径模块: /path/to/module.js');
console.log('3. 内置模块: fs, path, http');
console.log('4. node_modules 中的模块');
// 【代码注释】查看模块查找路径
console.log('\n模块查找路径 (module.paths):');
module.paths.forEach((path, index) => {
console.log(`${index + 1}. ${path}`);
});
// 【代码注释】模块信息
console.log('\n当前模块信息:');
console.log('文件名:', __filename);
console.log('目录名:', __dirname);
console.log('模块ID:', module.id);
console.log('是否为主模块:', require.main === module);
// 【代码注释】动态模块加载
function dynamicRequire(moduleName) {
try {
const module = require(moduleName);
console.log(`模块 ${moduleName} 加载成功`);
return module;
} catch (error) {
console.error(`模块 ${moduleName} 加载失败:`, error.message);
return null;
}
}
// 使用动态加载
const fsExtra = dynamicRequire('fs-extra'); // 如果未安装会返回 null
const pathModule = dynamicRequire('path'); // 内置模块,加载成功
CommonJS 实战应用
// 【实战案例】工具函数模块
// 文件: utils/logger.js
const fs = require('fs');
const path = require('path');
class Logger {
constructor(logDir = './logs') {
this.logDir = logDir;
this.ensureLogDirectory();
}
ensureLogDirectory() {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
console.log(logMessage.trim());
const logFile = path.join(this.logDir, 'app.log');
fs.appendFileSync(logFile, logMessage);
}
info(message) {
this.log('INFO', message);
}
error(message) {
this.log('ERROR', message);
}
warn(message) {
this.log('WARN', message);
}
}
// 【代码注释】导出单例
module.exports = new Logger();
// 【实战案例】使用日志模块
// 文件: app.js
const logger = require('./utils/logger');
logger.info('应用程序启动');
logger.warn('配置文件缺失,使用默认配置');
logger.error('数据库连接失败');
// 【实战案例】配置管理模块
// 文件: config/database.js
module.exports = {
development: {
host: 'localhost',
port: 5432,
database: 'dev_db',
username: 'dev_user',
password: 'dev_pass'
},
production: {
host: process.env.DB_HOST || 'prod-db.example.com',
port: parseInt(process.env.DB_PORT) || 5432,
database: process.env.DB_NAME || 'prod_db',
username: process.env.DB_USER || 'prod_user',
password: process.env.DB_PASSWORD || ''
},
test: {
host: 'localhost',
port: 5432,
database: 'test_db',
username: 'test_user',
password: 'test_pass'
}
};
// 【代码注释】根据环境获取配置
// 文件: config/index.js
const dbConfig = require('./database');
function getConfig() {
const env = process.env.NODE_ENV || 'development';
return dbConfig[env] || dbConfig.development;
}
module.exports = { getConfig };
5.2 ES6 模块规范
ES6 模块(ESM)是 JavaScript 官方的模块化标准,采用静态导入/导出,支持 tree-shaking,是现代前端开发的主流选择。
ES6 vs CommonJS 对比
ES6 模块导出语法
// 【方式1】默认导出 (export default)
// 文件: esm-modules/default-export.mjs
const defaultMessage = 'This is the default export';
function defaultFunction() {
console.log('This is a default function');
}
class DefaultClass {
constructor() {
this.name = 'Default Class';
}
greet() {
console.log(`Hello from ${this.name}`);
}
}
// 【代码注释】默认导出,一个模块只能有一个默认导出
export default defaultMessage;
// 或导出函数
// export default defaultFunction;
// 或导出类
// export default DefaultClass;
// 【方式2】命名导出 (export)
// 文件: esm-modules/named-export.mjs
// 【代码注释】导出变量
export const PI = 3.14159;
export const appName = 'ES6 Module Demo';
// 【代码注释】导出函数
export function calculateArea(radius) {
return PI * radius * radius;
}
export function calculateCircumference(radius) {
return 2 * PI * radius;
}
// 【代码注释】导出类
export class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return calculateArea(this.radius);
}
get circumference() {
return calculateCircumference(this.radius);
}
}
// 【方式3】混合导出
// 文件: esm-modules/mixed-export.mjs
// 【代码注释】默认导出和命名导出可以共存
export default function main() {
console.log('这是主要功能');
}
export const version = '1.0.0';
export const author = 'Developer';
export function helperFunction() {
console.log('这是辅助功能');
}
// 【方式4】导出列表(推荐)
// 文件: esm-modules/export-list.mjs
// 【代码注释】先声明,后导出,结构更清晰
const config = {
debug: true,
version: '2.0.0'
};
function init() {
console.log('初始化应用');
}
function cleanup() {
console.log('清理资源');
}
class Application {
constructor() {
this.config = config;
}
start() {
init();
}
stop() {
cleanup();
}
}
// 【代码注释】统一导出声明
export { config, init, cleanup, Application };
// 【方式5】导出时重命名
export { config as appConfig, Application as App };
// 【方式6】重新导出其他模块
// 文件: esm-modules/re-export.mjs
// 【代码注释】重新导出其他模块的内容
export { default } from './default-export.mjs';
export { PI, calculateArea, Circle } from './named-export.mjs';
// 【代码注释】重新导出并重命名
export { calculateCircumference as getCircumference } from './named-export.mjs';
ES6 模块导入语法
// 【方式1】导入默认导出
// 文件: esm-modules/import-default.mjs
import defaultMessage from './default-export.mjs';
import defaultFunction from './default-export.mjs';
console.log('默认导入:', defaultMessage);
defaultFunction();
// 【方式2】导入命名导出
// 文件: esm-modules/import-named.mjs
import { PI, appName, calculateArea } from './named-export.mjs';
import { Circle as CircleShape } from './named-export.mjs'; // 导入时重命名
console.log('圆周率:', PI);
console.log('应用名称:', appName);
console.log('面积计算:', calculateArea(5));
const circle = new CircleShape(10);
console.log('圆的面积:', circle.area);
// 【方式3】导入全部命名导出
// 文件: esm-modules/import-all.mjs
import * as mathUtils from './named-export.mjs';
console.log('所有数学工具:', mathUtils);
console.log('使用导入的工具:', mathUtils.calculateArea(3));
// 【方式4】混合导入
// 文件: esm-modules/import-mixed.mjs
import mainFunction, { version, author, helperFunction } from './mixed-export.mjs';
mainFunction();
console.log('版本:', version, '作者:', author);
helperFunction();
// 【方式5】只导入副作用
// 文件: esm-modules/import-side-effect.mjs
// 【代码注释】只执行模块代码,不导入任何内容
import './polyfills.mjs';
// 【方式6】动态导入
// 文件: esm-modules/dynamic-import.mjs
async function loadModule() {
try {
// 【代码注释】动态导入返回 Promise
const module = await import('./named-export.mjs');
console.log('动态导入成功:', module);
const result = module.calculateArea(10);
console.log('计算结果:', result);
} catch (error) {
console.error('模块加载失败:', error.message);
}
}
loadModule();
在 Node.js 中使用 ES6 模块
// 【方式1】使用 .mjs 扩展名
// 文件: app.mjs
import { readFile } from 'fs/promises';
import path from 'path';
async function main() {
const filePath = path.join(process.cwd(), 'data.txt');
const content = await readFile(filePath, 'utf-8');
console.log('文件内容:', content);
}
main();
// 【方式2】在 package.json 中设置 "type": "module"
// 文件: package.json
/*
{
"name": "es6-module-demo",
"version": "1.0.0",
"type": "module",
"main": "app.js"
}
*/
// 【方式3】使用 .js 扩展名但配置为 ESM
// 文件: app.js (在 "type": "module" 项目中)
import express from 'express';
import { router } from './routes/index.js';
const app = express();
app.use(router);
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
ES6 模块实战项目
// 【实战案例】工具函数库
// 文件: utils/string-utils.mjs
// 【代码注释】字符串处理工具函数
export function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
export function truncate(str, length = 50) {
if (!str || str.length <= length) return str;
return str.substring(0, length) + '...';
}
export function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function generateRandomString(length = 10) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// 【实战案例】日期工具函数
// 文件: utils/date-utils.mjs
export function formatDate(date, format = 'YYYY-MM-DD') {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
export function addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
export function isWeekend(date) {
const day = new Date(date).getDay();
return day === 0 || day === 6;
}
// 【实战案例】主应用文件
// 文件: app.mjs
import { capitalize, slugify, generateRandomString } from './utils/string-utils.mjs';
import { formatDate, addDays, isWeekend } from './utils/date-utils.mjs';
class BlogPostGenerator {
constructor(title) {
this.title = title;
this.slug = slugify(title);
this.createdAt = new Date();
this.id = generateRandomString(8);
}
generate() {
return {
id: this.id,
title: capitalize(this.title),
slug: this.slug,
createdAt: formatDate(this.createdAt, 'YYYY-MM-DD HH:mm:ss'),
publishDate: formatDate(addDays(this.createdAt, 7), 'YYYY-MM-DD'),
isWeekendPublish: isWeekend(addDays(this.createdAt, 7))
};
}
}
// 【代码注释】使用示例
const post = new BlogPostGenerator('Introduction to ES6 Modules');
console.log('生成的博客文章:', post.generate());
// 【实战案例】API 客户端
// 文件: services/api-client.mjs
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.headers = {
'Content-Type': 'application/json'
};
}
async get(endpoint) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'GET',
headers: this.headers
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
// 【代码注释】导出 API 客户端实例
export const apiClient = new APIClient('https://api.example.com');
export { APIClient };
6. 实战应用场景
6.1 文件服务器实现
// 【实战案例】静态文件服务器
const http = require('http');
const fs = require('fs');
const path = require('path');
class StaticFileServer {
constructor(rootDir, port = 3000) {
this.rootDir = path.resolve(rootDir);
this.port = port;
this.mimeTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon'
};
}
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return this.mimeTypes[ext] || 'application/octet-stream';
}
serveFile(req, res) {
// 【代码注释】构建文件路径,防止路径遍历攻击
const requestedPath = path.normalize(req.url === '/' ? '/index.html' : req.url);
const filePath = path.join(this.rootDir, requestedPath);
// 【代码注释】安全检查:确保文件在根目录内
if (!filePath.startsWith(this.rootDir)) {
this.sendError(res, 403, '禁止访问');
return;
}
// 【代码注释】检查文件是否存在
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
this.sendError(res, 404, '文件不存在');
return;
}
// 【代码注释】获取文件状态
fs.stat(filePath, (err, stats) => {
if (err) {
this.sendError(res, 500, '服务器错误');
return;
}
if (stats.isDirectory()) {
// 【代码注释】如果是目录,尝试提供 index.html
const indexPath = path.join(filePath, 'index.html');
fs.access(indexPath, fs.constants.F_OK, (err) => {
if (err) {
this.sendError(res, 404, '找不到索引文件');
} else {
this.sendFile(indexPath, res);
}
});
} else {
// 【代码注释】发送文件
this.sendFile(filePath, res);
}
});
});
}
sendFile(filePath, res) {
const stream = fs.createReadStream(filePath);
const mimeType = this.getMimeType(filePath);
res.writeHead(200, {
'Content-Type': mimeType,
'Cache-Control': 'max-age=86400' // 缓存一天
});
stream.pipe(res);
stream.on('error', (err) => {
this.sendError(res, 500, '文件读取错误');
});
}
sendError(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(message);
}
start() {
const server = http.createServer((req, res) => {
this.serveFile(req, res);
});
server.listen(this.port, () => {
console.log(`文件服务器启动在 http://localhost:${this.port}`);
console.log(`根目录: ${this.rootDir}`);
});
}
}
// 使用示例
const server = new StaticFileServer('./public', 8080);
server.start();
6.2 日志系统实现
// 【实战案例】高级日志系统
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
class Logger {
constructor(options = {}) {
this.logDir = options.logDir || './logs';
this.logFile = options.logFile || 'app.log';
this.level = options.level || 'info';
this.maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB
this.maxFiles = options.maxFiles || 5;
this.levels = {
error: 0,
warn: 1,
info: 2,
debug: 3
};
this.init();
}
init() {
// 【代码注释】确保日志目录存在
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
shouldLog(level) {
return this.levels[level] <= this.levels[this.level];
}
formatMessage(level, message, meta = {}) {
const timestamp = new Date().toISOString();
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr}\n`;
}
async writeLog(message) {
const logPath = path.join(this.logDir, this.logFile);
try {
// 【代码注释】检查文件大小,如果超过限制则轮换
if (fs.existsSync(logPath)) {
const stats = fs.statSync(logPath);
if (stats.size >= this.maxSize) {
await this.rotateLogs();
}
}
// 【代码注释】追加日志
await fs.promises.appendFile(logPath, message);
} catch (error) {
console.error('日志写入失败:', error.message);
}
}
async rotateLogs() {
const logPath = path.join(this.logDir, this.logFile);
// 【代码注释】删除最老的日志文件
const oldestLog = path.join(this.logDir, `${this.logFile}.${this.maxFiles}`);
if (fs.existsSync(oldestLog)) {
fs.unlinkSync(oldestLog);
}
// 【代码注释】重命名现有日志文件
for (let i = this.maxFiles - 1; i >= 1; i--) {
const oldLog = path.join(this.logDir, `${this.logFile}.${i}`);
const newLog = path.join(this.logDir, `${this.logFile}.${i + 1}`);
if (fs.existsSync(oldLog)) {
fs.renameSync(oldLog, newLog);
}
}
// 【代码注释】重命名当前日志文件
const rotatedLog = path.join(this.logDir, `${this.logFile}.1`);
if (fs.existsSync(logPath)) {
fs.renameSync(logPath, rotatedLog);
}
}
log(level, message, meta) {
if (!this.shouldLog(level)) {
return;
}
const formattedMessage = this.formatMessage(level, message, meta);
// 【代码注释】输出到控制台
console.log(formattedMessage.trim());
// 【代码注释】写入文件
this.writeLog(formattedMessage);
}
error(message, meta) {
this.log('error', message, meta);
}
warn(message, meta) {
this.log('warn', message, meta);
}
info(message, meta) {
this.log('info', message, meta);
}
debug(message, meta) {
this.log('debug', message, meta);
}
}
// 使用示例
const logger = new Logger({
logDir: './logs',
logFile: 'app.log',
level: 'debug',
maxSize: 1024 * 1024, // 1MB
maxFiles: 3
});
logger.info('应用程序启动');
logger.debug('调试信息', { userId: 123, action: 'login' });
logger.warn('警告信息', { memory: '80%' });
logger.error('错误信息', { error: 'Connection timeout', code: 'ETIMEDOUT' });
6.3 配置管理系统
// 【实战案例】企业级配置管理
const fs = require('fs');
const path = require('path');
class ConfigManager {
constructor(configDir = './config') {
this.configDir = configDir;
this.config = {};
this.watchers = [];
}
load() {
// 【代码注释】加载环境配置
const env = process.env.NODE_ENV || 'development';
const configFile = path.join(this.configDir, `${env}.json`);
try {
if (fs.existsSync(configFile)) {
const content = fs.readFileSync(configFile, 'utf-8');
this.config = JSON.parse(content);
console.log(`配置加载成功: ${env}`);
} else {
console.warn(`配置文件不存在: ${configFile}`);
this.config = this.getDefaultConfig();
}
} catch (error) {
console.error('配置加载失败:', error.message);
this.config = this.getDefaultConfig();
}
return this.config;
}
getDefaultConfig() {
return {
app: {
name: 'Application',
version: '1.0.0',
port: 3000
},
database: {
host: 'localhost',
port: 5432,
name: 'mydb'
},
logging: {
level: 'info'
}
};
}
get(key, defaultValue = null) {
const keys = key.split('.');
let value = this.config;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return defaultValue;
}
}
return value;
}
set(key, value) {
const keys = key.split('.');
let current = this.config;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in current) || typeof current[k] !== 'object') {
current[k] = {};
}
current = current[k];
}
current[keys[keys.length - 1]] = value;
}
watch(key, callback) {
// 【代码注释】监听配置变化
this.watchers.push({ key, callback });
}
notify(key, value) {
// 【代码注释】通知监听器
this.watchers.forEach(({ key: watchedKey, callback }) => {
if (key.startsWith(watchedKey)) {
callback(key, value);
}
});
}
save(env = 'development') {
const configFile = path.join(this.configDir, `${env}.json`);
const content = JSON.stringify(this.config, null, 2);
try {
fs.writeFileSync(configFile, content, 'utf-8');
console.log('配置保存成功');
} catch (error) {
console.error('配置保存失败:', error.message);
}
}
}
// 使用示例
const configManager = new ConfigManager('./config');
configManager.load();
console.log('应用名称:', configManager.get('app.name'));
console.log('数据库端口:', configManager.get('database.port'));
// 设置新值
configManager.set('app.port', 8080);
configManager.set('features.newFeature', true);
// 监听变化
configManager.watch('app.port', (key, value) => {
console.log(`配置 ${key} 变更为 ${value}`);
});
// 保存配置
configManager.save('development');
7. 核心案例速查与知识点归纳
7.1 案例学习路线
| 步骤 | 目录主题 | 验证要点 |
|---|---|---|
| ① | 内置模块 fs | join(__dirname) 读 datas/a.txt |
| ② | 其他内置模块 | url.parse / new URL、qs.parse |
| ③ | 异常处理 | new Error、throw、try/catch |
| ④ | JSON | parse / stringify 读写 data01.json |
| ⑤ | CommonJS | 01-mod~08-mod 暴露与 require |
| ⑥ | ES6 模块 | index.mjs + package.json "type":"module" |
| ⑦ | 流式 | createReadStream → pipe → 复制 |
| ⑧ | 目录 | mkdir / rmdir / readdir / access / stat |
7.2 fs API 速查
| 需求 | API | 备注 |
|---|---|---|
| 读文本 | readFile(p, 'utf-8', cb) |
不指定编码得 Buffer |
| 写覆盖 | writeFile |
文件不存在会创建 |
| 追加 | appendFile |
日志场景 |
| 改名/移动 | rename |
可跨目录 |
| 删文件 | unlink |
不能删非空目录 |
| 建目录 | mkdirSync(dir, { recursive: true }) |
多级一次创建 |
| 删目录 | rmdir(dir, { recursive: true }) |
Node 14+ 递归删 |
| 列目录 | readdir |
返回文件名数组 |
| 是否存在 | access / existsSync |
高并发慎用同步 |
| 大文件 | createReadStream.pipe(ws) |
省内存 |
7.3 CommonJS 暴露/import 速查
| 写法 | 结果 |
|---|---|
| 无导出 | require 得 {} |
module.exports = fn |
得该函数/对象 |
exports.x = 1 |
得 { x: 1 } |
exports = {} |
❌ 与 module.exports 脱钩 |
require('./x.json') |
直接解析为对象 |
require('./dir') |
读 package.json 的 main 或 index.js |
【代码注释】
- 第一次
require('./02-mod')会执行模块顶层代码并把module.exports放进require.cache;之后同一绝对路径的require直接返回缓存,不再执行顶层。 - 开发时若改了模块需重启进程,或
delete require.cache[require.resolve('./02-mod')](仅调试用,生产勿依赖)。 require('./05-mod.json')无需.json以外的解析,返回深拷贝前的对象;require('./目录')解析package.json的main或index.js。exports = {}会断开与module.exports的引用,导致require拿到空对象——课堂04-mod常见考点。
7.4 ESM 启用方式
| 方式 | 配置 |
|---|---|
| 后缀 | 使用 .mjs |
| 项目级 | package.json → "type": "module",则 .js 按 ESM 解析 |
【代码注释】
.mjs文件始终按 ESM 解析;或在package.json写"type": "module"使.js也按 ESM 解析(课堂index.mjs+ 配置案例)。export default每个模块只能有一个;import xxx from './m.mjs'导入默认导出。- 命名导出可多个:
export const a = 1;或export { a, b };导入需import { a, b } from '...'且名称一致(或用as重命名)。 - ESM 的
import提升且静态分析,利于 Tree Shaking;动态加载用import('./mod.mjs')返回 Promise。
7.5 实战:容量单位转换模块(CJS + ESM)
CommonJS 版:
// convert-byte.js
const convertByte = (bytes, type = 0) => bytes / 1024 ** type;
module.exports = convertByte;
// main.js
const convertByte = require('./convert-byte');
console.log('MB:', convertByte(1048576, 2));
ESM 版:
// convert-byte.mjs
export default function convertByte(bytes, type = 0) {
return bytes / 1024 ** type;
}
// main.mjs
import convertByte from './convert-byte.mjs';
console.log('GB:', convertByte(1073741824, 3));
【代码注释】
convertByte(bytes, type)用bytes / 1024 ** type做进制换算:type=0字节,1KB,2MB,3GB,4TB(基于 1024,非 1000)。- CJS:
module.exports = fn+require('./convert-byte');ESM:export default+import convertByte from './convert-byte.mjs'。 1048576字节 = 1MB(1024²);1073741824= 1GB,可用控制台验证与课堂main.js/main.mjs输出一致。- 该模块可发布为 npm 包供他人
require,与 Day08 包管理章节衔接。
7.6 可运行 HTML:JSON 与 querystring 对照
保存为 json-query-demo.html,在浏览器打开(理解服务端 JSON.parse / qs.parse 的输入从哪来)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JSON 与查询串演示</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
textarea { width: 100%; height: 120px; font-family: monospace; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 8px; overflow: auto; }
button { margin: 8px 8px 8px 0; padding: 8px 16px; cursor: pointer; }
</style>
</head>
<body>
<h1>JSON.parse 演示</h1>
<textarea id="jsonIn">{ "name": "Node", "skills": ["fs", "path"] }</textarea>
<button type="button" id="btnParse">解析 JSON</button>
<pre id="jsonOut">点击解析…</pre>
<h1>查询串(对应 qs.parse)</h1>
<p>当前 URL 查询部分:<code id="search"></code></p>
<pre id="qsOut"></pre>
<script>
document.getElementById('btnParse').onclick = () => {
const raw = document.getElementById('jsonIn').value;
try {
const obj = JSON.parse(raw);
document.getElementById('jsonOut').textContent = JSON.stringify(obj, null, 2);
} catch (e) {
document.getElementById('jsonOut').textContent = '解析失败: ' + e.message;
}
};
const search = location.search.slice(1);
document.getElementById('search').textContent = search || '(无)';
const params = Object.fromEntries(new URLSearchParams(search));
document.getElementById('qsOut').textContent = JSON.stringify(params, null, 2);
</script>
</body>
</html>
【代码注释】
JSON.parse解析请求体或配置文件;页面里try/catch演示非法 JSON 时的SyntaxError.message。location.search.slice(1)去掉?得到查询串;URLSearchParams与 Node 的querystring.parse一样把a=1&b=2转为对象(值均为字符串)。- 浏览器地址栏加
?wd=node&type=api刷新,观察qsOut输出,对应服务端url.parse(req.url).query或new URL(req.url, 'http://x').searchParams。 - 表单
application/x-www-form-urlencoded与查询串格式相同,Express 中由中间件解析为req.query/req.body。
7.7 常见错误码
| code | 场景 |
|---|---|
| ENOENT | 路径不存在 |
| EACCES | 无读写权限 |
| EISDIR | 对目录执行了读文件 |
| EEXIST | 创建已存在目录(未设 recursive) |
总结
本教程深入解析了 Node.js 的核心模块和模块化系统,涵盖了从基础概念到高级应用的完整知识体系。通过学习本教程,您应该能够:
- 熟练掌握 Node.js 内置模块:path、fs、url、querystring 等模块的使用方法
- 理解并应用模块化规范:CommonJS 和 ES6 模块系统的区别和使用场景
- 实现文件操作功能:读写、复制、移动、删除等文件系统操作
- 构建企业级应用:日志系统、配置管理、文件服务器等实战项目
- 遵循最佳实践:错误处理、安全考虑、性能优化等方面的专业实践
学习建议
- 实践为主:每个知识点都要亲自编写代码进行验证
- 项目驱动:通过实际项目来巩固所学知识
- 阅读源码:研究优秀的开源项目,学习设计模式和最佳实践
- 持续更新:Node.js 发展迅速,要关注新版本的新特性和改进
参考资源
希望本教程能帮助您在 Node.js 开发之路上走得更远!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)