一篇面向实战的 Node 进阶博客:内置模块(path / fs / url / querystring)异常与 JSONCommonJS 与 ESM 的完整链路。示例可独立运行,不依赖外部讲义路径。

目录


导读:知识架构与权威参考

本文解决什么问题

板块 核心能力 典型场景
path / fs 路径拼接、读写、目录、流式复制 日志、配置、静态资源、大文件
url / querystring 解析 URL、序列化查询串 路由参数、表单、爬虫
异常处理 Errorthrowtry/catch 稳健 CLI、服务启动
JSON stringify / parse 配置、API 数据交换
模块化 CJS require、ESM import 工具库拆分、工程化

知识脉络(Mermaid)

模块化

语言机制

内置模块

path

fs 含 Stream

url

querystring

异常处理

JSON

CommonJS

ES Modules

权威文档

主题 链接
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 中的模块分为三种类型:

Node.js 模块系统

内置模块

自定义模块

第三方模块

fs path http

用户创建的.js文件

npm express lodash

模块系统核心概念

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。

模块化特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了
  • 模块加载顺序按照代码书写顺序执行

模块化带来的好处:

  • 提高代码的复用性
  • 提高代码的可维护性
  • 实现按需加载

【代码注释】

  • 内置模块fspath):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 后接 datalogs 等相对片段。
  • 不要用字符串 __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.htmltext/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\share UNC 路径为绝对;构建工具在解析 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 钉成绝对根目录,后续所有请求都相对该根解析。
  • buildPathjoinnormalize(处理 ..、重复斜杠),然后用 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 模块架构

FS 文件系统模块

文件操作

目录操作

文件信息

流式操作

读取 readFile

写入 writeFile

追加 appendFile

删除 unlink

重命名 rename

创建 mkdir

删除 rmdir

读取 readdir

判断 exists/access

状态 stat

权限 chmod

读取流 createReadStream

写入流 createWriteStream

名词解释
  • 同步 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 同理)。
  • 不传编码时回调的 dataBuffer,文本需 data.toString('utf-8');图片/二进制应保持 Buffer 直接写入或处理。
  • 'utf-8' 时 Node 在读完文件后自动解码为字符串,适合 .txt.json.md
  • console.log('开始读取...')先于两个回调执行,说明 readFile 不阻塞事件循环;Web 服务中必须用异步,否则一个慢磁盘会卡住所有请求。
  • err.code 常见 ENOENT(路径错)、EACCES(无权限);务必 if (err) return,避免对 undefinedtoString

回调函数参数说明

  • err:错误对象,如果操作成功则为 null
  • data:文件内容(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/catcherror.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 / axappendFile 类似,在文件尾追加;日志场景常用 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.txta.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 只删文件,不能删非空目录;课堂流程「改名 → 再删」对应 renameunlink
  • 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 或使用 readdirwithFileTypes 选项(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.StatsisFile() / isDirectory() 决定用 readFile 还是 readdir;对目录 readFileEISDIR
  • 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);
});

【代码注释】

  • createReadStreamhighWaterMark(默认 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() 表示不再写入新数据,冲刷缓冲后触发 finishclose 表示 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 应销毁整条链(pipeline API 更安全,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 事件;只 writeclose 可能导致数据未完全落盘。
  • 课堂案例对比: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
  • processFileWorkflowaccess 检查存在 → 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)返回带 protocolpathnamequery 等字段的对象;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);

【代码注释】

  • parseURLnew 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 对象结构

Error 对象

message: 错误信息

name: 错误名称

stack: 堆栈信息

code: 错误代码

错误对象创建
// 【代码注释】创建基础错误对象
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 三条归纳(务必记住):

  1. try 内无论是运行时错误还是 throw 主动抛出,都会被 catch 捕获,由开发者处理,进程通常不会因此直接崩溃。
  2. 有无异常,try/catch 之后的语句仍会执行(除非你在 catchprocess.exit)。
  3. 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 数据类型

JSON 数据类型

字符串 string

数字 number

布尔值 boolean

数组 array

对象 object

null

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
Node 默认

Webpack 转换

ES Module
标准

Vite 原生 ESM

AMD

老项目

【代码注释】

  • CommonJSrequire / module.exports,Node 默认,同步加载,适合服务端与课堂 01-mod08-mod 案例。
  • ESMimport / export,浏览器同源语法;Node 通过 .mjspackage.json"type":"module" 启用。
  • 同一 .js 文件内不要混写 requireimport(除非使用实验性互操作或拆成两个入口);第三方包需看其 exports 字段支持哪种格式。
  • AMD/CMD 多见于历史前端(RequireJS、SeaJS),现代工程由 Webpack/Vite 在构建期打包,运行时仍是 CJS 或 ESM。

5.1 CommonJS 规范

CommonJS 是 Node.js 默认使用的模块化规范,采用同步加载方式,主要服务于服务端环境。

CommonJS 核心概念

CommonJS 模块系统

require 导入模块

module.exports 导出模块

__dirname 当前目录

__filename 当前文件路径

导出单个值

导出多个值

动态导出

相对路径导入

绝对路径导入

内置模块导入

模块导出方式
// 【方式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 对比

模块化规范选择

CommonJS

ES6 Modules

Node.js 原生支持

同步加载

动态导入

现代标准

静态分析

Tree-shaking 支持

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 URLqs.parse
异常处理 new Errorthrowtry/catch
JSON parse / stringify 读写 data01.json
CommonJS 01-mod~08-mod 暴露与 require
ES6 模块 index.mjs + package.json "type":"module"
流式 createReadStreampipe → 复制
目录 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.jsonmainindex.js

【代码注释】

  • 第一次 require('./02-mod') 会执行模块顶层代码并把 module.exports 放进 require.cache;之后同一绝对路径的 require 直接返回缓存,不再执行顶层。
  • 开发时若改了模块需重启进程,或 delete require.cache[require.resolve('./02-mod')](仅调试用,生产勿依赖)。
  • require('./05-mod.json') 无需 .json 以外的解析,返回深拷贝前的对象;require('./目录') 解析 package.jsonmainindex.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 字节,1 KB,2 MB,3 GB,4 TB(基于 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).querynew 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 的核心模块和模块化系统,涵盖了从基础概念到高级应用的完整知识体系。通过学习本教程,您应该能够:

  1. 熟练掌握 Node.js 内置模块:path、fs、url、querystring 等模块的使用方法
  2. 理解并应用模块化规范:CommonJS 和 ES6 模块系统的区别和使用场景
  3. 实现文件操作功能:读写、复制、移动、删除等文件系统操作
  4. 构建企业级应用:日志系统、配置管理、文件服务器等实战项目
  5. 遵循最佳实践:错误处理、安全考虑、性能优化等方面的专业实践

学习建议

  1. 实践为主:每个知识点都要亲自编写代码进行验证
  2. 项目驱动:通过实际项目来巩固所学知识
  3. 阅读源码:研究优秀的开源项目,学习设计模式和最佳实践
  4. 持续更新:Node.js 发展迅速,要关注新版本的新特性和改进

参考资源

希望本教程能帮助您在 Node.js 开发之路上走得更远!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐