主题
Node.js 核心模块
Node.js 架构概览
整体架构
Node.js 的运行时架构由三层组成:JavaScript 应用层、Node.js Bindings(C++ 桥接层)、底层引擎层(V8 + libuv + 其他 C/C++ 库)。理解这一架构是掌握 Node.js 行为的根基。
┌─────────────────────────────────────────────────────────────────┐
│ Node.js 运行时架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ JavaScript 应用代码 │ │
│ │ (你写的 js/ts 代码、npm 包) │ │
│ └───────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────┴───────────────────────────────┐ │
│ │ Node.js Core Modules (JS 层) │ │
│ │ fs / http / stream / path / events ... │ │
│ └───────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────┴───────────────────────────────┐ │
│ │ Node.js Bindings (C++ 桥接层) │ │
│ │ 将 JS 调用转换为底层 C/C++ 操作 │ │
│ └──────┬────────────────────┬───────────────────┬───────────┘ │
│ │ │ │ │
│ ┌──────┴──────┐ ┌─────────┴─────────┐ ┌──────┴──────────┐ │
│ │ V8 引擎 │ │ libuv │ │ 其他 C/C++ 库 │ │
│ │ │ │ │ │ │ │
│ │ JS 解析执行 │ │ 事件循环 │ │ c-ares (DNS) │ │
│ │ JIT 编译 │ │ 异步 I/O │ │ OpenSSL (加密) │ │
│ │ 内存管理/GC │ │ 线程池 │ │ zlib (压缩) │ │
│ │ 调用栈 │ │ 跨平台抽象 │ │ llhttp (解析) │ │
│ └─────────────┘ └───────────────────┘ └─────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 操作系统 (Linux/macOS/Windows) │ │
│ │ epoll / kqueue / IOCP / 文件系统 / 网络 │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘V8 引擎
V8 是 Google 开发的高性能 JavaScript 引擎,也是 Chrome 浏览器的 JS 引擎。在 Node.js 中,V8 负责将 JavaScript 源码解析、编译为机器码并执行。
V8 的关键机制:
- JIT 编译:V8 不是纯解释执行,而是将热点代码(频繁执行的代码)通过 TurboFan 优化编译器编译为高度优化的机器码
- 隐藏类(Hidden Class):V8 为每个对象动态创建隐藏类以实现快速属性访问,这就是为什么始终以相同顺序初始化对象属性能带来性能提升
- 内存管理:V8 使用分代垃圾回收,新生代采用 Scavenge 算法,老生代采用 Mark-Sweep + Mark-Compact 算法
- 堆内存限制:64 位系统下 V8 堆内存默认上限约 1.7GB,可通过
--max-old-space-size调整
libuv 与事件循环
libuv 是一个跨平台异步 I/O 库,它是 Node.js 实现非阻塞 I/O 的核心。libuv 封装了不同操作系统的异步机制:Linux 上用 epoll,macOS 上用 kqueue,Windows 上用 IOCP。
┌───────────────────────────────────────────────────────┐
│ libuv 事件循环 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ ┌─────────────────┐ │ │
│ │ ┌────→│ timers │ setTimeout/ │ │
│ │ │ │ │ setInterval │ │
│ │ │ └────────┬────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────────┴────────┐ │ │
│ │ │ │ pending callbacks│ 系统级回调 │ │
│ │ │ └────────┬────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────────┴────────┐ │ │
│ │ │ │ idle/prepare │ 内部使用 │ │
│ │ │ └────────┬────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────────┴────────┐ │ │
│ │ │ │ poll │ I/O 回调 │ │
│ │ │ │ │ (文件/网络) │ │
│ │ │ └────────┬────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────────┴────────┐ │ │
│ │ │ │ check │ setImmediate │ │
│ │ │ └────────┬────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────────┴────────┐ │ │
│ │ └─────│ close callbacks │ socket.close() │ │
│ │ └─────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 每个阶段之间都会执行: │
│ 1. process.nextTick 队列 (微任务,最高优先级) │
│ 2. Promise 微任务队列 │
│ │
│ libuv 线程池 (默认 4 个线程,UV_THREADPOOL_SIZE): │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Thread 1│ │Thread 2│ │Thread 3│ │Thread 4│ │
│ │ DNS │ │ fs │ │ crypto │ │ zlib │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
└───────────────────────────────────────────────────────┘单线程模型
Node.js 的"单线程"指的是 JavaScript 代码的执行在单一线程(主线程)上运行。但 Node.js 本身并非只有一个线程:
- 主线程:执行 JS 代码、事件循环
- libuv 线程池:处理文件 I/O、DNS 查询、部分加密操作(默认 4 个线程)
- V8 辅助线程:垃圾回收、JIT 编译等
js
const crypto = require('crypto')
const start = Date.now()
for (let i = 0; i < 4; i++) {
crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', () => {
console.log(`任务 ${i} 完成,耗时: ${Date.now() - start}ms`)
})
}在 4 核 CPU 上运行此代码,4 个加密任务几乎同时完成(约 80ms),因为 libuv 线程池默认有 4 个线程并行处理。如果增加到 5 个任务,第 5 个任务的耗时将接近前 4 个的两倍,因为它需要等待线程池中有空闲线程。
js
process.env.UV_THREADPOOL_SIZE = 8
const crypto = require('crypto')
const start = Date.now()
for (let i = 0; i < 8; i++) {
crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', () => {
console.log(`任务 ${i} 完成,耗时: ${Date.now() - start}ms`)
})
}将线程池大小调整为 8 后,8 个任务可以并行完成。UV_THREADPOOL_SIZE 最大值为 1024。
fs 模块
概述
fs(File System)模块是 Node.js 与文件系统交互的核心模块,提供了三套风格的 API:同步(Sync)、回调异步(Callback)和 Promise 异步。理解它们的差异是掌握 Node.js I/O 模型的基础。
┌─────────────────────────────────────────────────────────────┐
│ fs 模块 API 体系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ 同步 API │ │ 回调异步 API │ │ Promise API │ │
│ │ │ │ │ │ │ │
│ │ readFileSync│ │ readFile(cb) │ │ fs/promises │ │
│ │ 阻塞主线程 │ │ 回调地狱风险 │ │ async/await │ │
│ │ 适合启动阶段 │ │ 高性能场景 │ │ 现代推荐写法 │ │
│ └─────────────┘ └──────────────────┘ └───────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 流式 API(Stream) │ │
│ │ createReadStream / createWriteStream │ │
│ │ 处理大文件、内存友好、支持背压 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘三套 API 风格对比
| 特性 | 同步 API | 回调异步 API | Promise API |
|---|---|---|---|
| 引入方式 | require('fs') | require('fs') | require('fs/promises') |
| 是否阻塞 | 阻塞主线程 | 不阻塞 | 不阻塞 |
| 错误处理 | try/catch | 回调第一个参数 | try/catch + async/await |
| 适用场景 | 启动配置读取 | 高并发 I/O | 现代异步流程 |
| 函数后缀 | *Sync | 无 | 无 |
readFile 与 writeFile
三种方式读取文件:
js
const fs = require('fs')
const fsp = require('fs/promises')
const data1 = fs.readFileSync('./config.json', 'utf-8')
fs.readFile('./config.json', 'utf-8', (err, data) => {
if (err) throw err
console.log(data)
})
async function readConfig() {
const data = await fsp.readFile('./config.json', 'utf-8')
return JSON.parse(data)
}三种方式写入文件:
js
const fs = require('fs')
const fsp = require('fs/promises')
fs.writeFileSync('./output.txt', 'Hello Node.js', 'utf-8')
fs.writeFile('./output.txt', 'Hello Node.js', 'utf-8', (err) => {
if (err) throw err
})
async function writeData() {
await fsp.writeFile('./output.txt', 'Hello Node.js', 'utf-8')
}目录操作
js
const fsp = require('fs/promises')
const path = require('path')
async function ensureDir(dirPath) {
await fsp.mkdir(dirPath, { recursive: true })
}
async function listFiles(dirPath) {
const entries = await fsp.readdir(dirPath, { withFileTypes: true })
const result = { files: [], dirs: [] }
for (const entry of entries) {
if (entry.isFile()) {
result.files.push(entry.name)
} else if (entry.isDirectory()) {
result.dirs.push(entry.name)
}
}
return result
}
async function walkDir(dir) {
const files = []
const entries = await fsp.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...await walkDir(fullPath))
} else {
files.push(fullPath)
}
}
return files
}
async function getFileInfo(filePath) {
const stats = await fsp.stat(filePath)
return {
size: stats.size,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
created: stats.birthtime,
modified: stats.mtime,
permissions: stats.mode.toString(8)
}
}文件监听 fs.watch
fs.watch 利用操作系统原生的文件变化通知机制来监听文件或目录的变化:
js
const fs = require('fs')
const watcher = fs.watch('./src', { recursive: true }, (eventType, filename) => {
console.log(`事件类型: ${eventType}`)
console.log(`变化文件: ${filename}`)
})
watcher.on('error', (err) => {
console.error('监听错误:', err)
})
process.on('SIGINT', () => {
watcher.close()
process.exit(0)
})fs.watch 与 fs.watchFile 对比:
| 特性 | fs.watch | fs.watchFile |
|---|---|---|
| 底层实现 | 操作系统原生通知(inotify/kqueue/FSEvents) | 轮询(stat polling) |
| 性能 | 高效,O(1) 通知 | 消耗 CPU,定时 stat 调用 |
| 跨平台一致性 | 行为略有差异 | 一致但慢 |
| 递归监听 | macOS/Windows 支持 | 不支持 |
| 适用场景 | 开发工具、热更新 | 网络文件系统(NFS) |
fs.watch vs chokidar
fs.watch 是 Node.js 原生 API,但在实际项目中往往使用 chokidar 这个第三方库。两者的核心差异:
┌─────────────────────────────────────────────────────────┐
│ fs.watch 的已知问题 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. macOS 上文件名报告不准确 │
│ 2. 某些编辑器(Vim/Sublime)保存文件时触发两次事件 │
│ 3. Linux 上 recursive 选项不可用(Node < 19) │
│ 4. 不报告文件名(某些平台) │
│ 5. rename 事件语义在不同平台不一致 │
│ │
│ chokidar 在底层使用 fs.watch + fs.watchFile 的组合 │
│ 来解决这些跨平台问题,并提供更完善的 API │
└─────────────────────────────────────────────────────────┘| 特性 | fs.watch | chokidar |
|---|---|---|
| 依赖 | 内置模块,零依赖 | 第三方包 |
| 跨平台一致性 | 行为差异大 | 封装了平台差异 |
| 重复事件去抖 | 无 | 内置 |
| 初始扫描事件 | 无 | 支持 ready 事件 |
| glob 模式 | 不支持 | 支持 |
| 忽略规则 | 不支持 | 支持 ignored 选项 |
| 稳定性 | 原始 | 生产级 |
chokidar 的典型用法(Vite、Webpack 等构建工具的底层都在使用它):
js
const chokidar = require('chokidar')
const watcher = chokidar.watch('./src', {
ignored: /(^|[\/\\])\../,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50
}
})
watcher
.on('add', (filePath) => console.log(`新增: ${filePath}`))
.on('change', (filePath) => console.log(`修改: ${filePath}`))
.on('unlink', (filePath) => console.log(`删除: ${filePath}`))
.on('ready', () => console.log('初始扫描完成'))
.on('error', (err) => console.error('错误:', err))awaitWriteFinish 选项是 chokidar 的精髓之一——它会等待文件写入完成后再触发事件,避免了编辑器写入过程中的中间态触发。stabilityThreshold 表示文件大小保持不变多少毫秒后才认为写入完成。
大文件流式处理
当文件体积较大时,一次性读取整个文件会消耗大量内存。流式 API 可以分块读取和写入,内存占用恒定。
┌──────────┐ chunk ┌──────────┐ chunk ┌──────────┐
│ │ ──────────→ │ │ ──────────→ │ │
│ 磁盘文件 │ 64KB │ 内存缓冲 │ 64KB │ 目标文件 │
│ │ ──────────→ │ 区(Buffer)│ ──────────→ │ │
└──────────┘ └──────────┘ └──────────┘
ReadStream WriteStream使用流复制大文件:
js
const fs = require('fs')
const { pipeline } = require('stream/promises')
async function copyFile(src, dest) {
await pipeline(
fs.createReadStream(src, { highWaterMark: 64 * 1024 }),
fs.createWriteStream(dest)
)
}逐行读取大型日志文件:
js
const fs = require('fs')
const readline = require('readline')
async function processLogFile(filePath) {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity
})
let lineCount = 0
let errorCount = 0
for await (const line of rl) {
lineCount++
if (line.includes('ERROR')) {
errorCount++
}
}
return { lineCount, errorCount }
}path 模块
概述
path 模块用于处理文件路径和目录路径。不同操作系统的路径分隔符不同(Windows 用 \,POSIX 用 /),path 模块会自动处理这些差异,保证代码的跨平台兼容性。
┌─────────────────────────────────────────────┐
│ path 路径组成 │
│ │
│ /home/user/projects/app/src/index.js │
│ ├─────┤ │
│ root │
│ ├─────────────────────────┤ │
│ dir │
│ ├──────────┤ │
│ base │
│ ├────┤ │
│ name │
│ ├────┤ │
│ ext │
└─────────────────────────────────────────────┘path.join vs path.resolve 区别
这是面试中高频考点。两者都能拼接路径,但行为完全不同:
js
const path = require('path')
path.join('a', 'b', 'c')
path.resolve('a', 'b', 'c')
path.join('/a', 'b', 'c')
path.resolve('/a', 'b', 'c')
path.join('a', '/b', 'c')
path.resolve('a', '/b', 'c')| 方法 | 行为特点 | 返回值特点 |
|---|---|---|
path.join | 纯粹拼接路径片段,处理 .. 和 .,不关心绝对/相对 | 可能返回相对路径 |
path.resolve | 从右向左处理,遇到绝对路径即停止,未遇到则以 cwd 为基准 | 总是返回绝对路径 |
path.resolve 的本质是模拟在终端中 cd 到指定路径后执行 pwd 的结果:
js
const path = require('path')
path.resolve('/foo', 'bar', 'baz')
path.resolve('/foo', '/bar', 'baz')
path.resolve('foo', 'bar')path.resolve('foo', 'bar') 等价于先 cd foo,再 cd bar,最终得到 当前工作目录/foo/bar。path.resolve('/foo', '/bar', 'baz') 处理到 /bar 时遇到绝对路径,之前的 /foo 被忽略,最终返回 /bar/baz。
path.parse 与 path.format
js
const path = require('path')
const parsed = path.parse('/home/user/docs/letter.txt')parsed 的结构为:
{
root: '/',
dir: '/home/user/docs',
base: 'letter.txt',
name: 'letter',
ext: '.txt'
}js
const path = require('path')
const formatted = path.format({
root: '/',
dir: '/home/user/docs',
base: 'letter.txt'
})
path.basename('/foo/bar/baz.js')
path.basename('/foo/bar/baz.js', '.js')
path.extname('index.html')
path.extname('index.coffee.md')
path.extname('index.')
path.extname('.hidden')
path.dirname('/foo/bar/baz.js')
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')
path.normalize('/foo/bar//baz/asdf/quux/..')跨平台路径处理(posix vs win32)
path 模块会根据当前操作系统自动使用对应的实现。但你可以通过 path.posix 和 path.win32 显式指定平台:
js
const path = require('path')
console.log(path.sep)
console.log(path.delimiter)
path.win32.join('C:\\Users', 'docs', 'file.txt')
path.posix.join('/home/user', 'docs', 'file.txt')
path.win32.parse('C:\\Users\\docs\\file.txt')
path.posix.parse('/home/user/docs/file.txt')实际开发中的跨平台最佳实践:
js
const path = require('path')
function toUnixPath(p) {
return p.split(path.sep).join('/')
}
function resolveFromRoot(...segments) {
return path.resolve(__dirname, '..', ...segments)
}在构建工具中(如 Webpack、Vite),URL 路径始终使用 / 分隔符,因此需要用 path.posix 或手动转换。而文件系统操作则应始终使用 path.join / path.resolve,让 Node.js 自动处理平台差异。
stream 模块
四种流类型
Node.js 中的流(Stream)是处理数据的抽象接口,所有流都是 EventEmitter 的实例。
┌─────────────────────────────────────────────────────────────────┐
│ Node.js 四种流类型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Readable(可读流) Writable(可写流) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 数据源 │ ────→ │ 数据目标 │ │
│ │ fs.createReadStream │ fs.createWriteStream │
│ │ http req (客户端收到) │ http res (服务端发送) │
│ │ process.stdin │ process.stdout │
│ └──────────────┘ └──────────────┘ │
│ │
│ Duplex(双工流) Transform(转换流) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 读 ←──── │ │ 读 ←──── │ │
│ │ │ │ ↑ 转换 │ │
│ │ 写 ────→ │ │ 写 ────→ │ │
│ │ net.Socket │ │ zlib.createGzip │
│ │ TCP 连接 │ │ crypto.createCipher │
│ └──────────────┘ └──────────────┘ │
│ │
│ Duplex: 读写独立,两侧无关联 │
│ Transform: 写入数据经过转换后从读取侧输出 │
└─────────────────────────────────────────────────────────────────┘| 流类型 | 作用 | 典型实例 | 核心事件 |
|---|---|---|---|
| Readable | 数据的来源 | fs.createReadStream、http.IncomingMessage | data、end、error |
| Writable | 数据的目标 | fs.createWriteStream、http.ServerResponse | drain、finish、error |
| Duplex | 同时可读可写 | net.Socket、TCP 连接 | 兼具两者事件 |
| Transform | 读写之间有转换 | zlib.createGzip、crypto | data、end、error |
流无处不在
Node.js 中流不是一个孤立的模块,它贯穿了几乎所有核心模块。理解这一点是掌握 Node.js 架构的关键。
┌─────────────────────────────────────────────────────────────┐
│ Node.js 中流的分布 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 模块 Readable Writable │
│ ───────────────────────────────────────────────────────── │
│ fs createReadStream createWriteStream │
│ http IncomingMessage(req) ServerResponse(res) │
│ net Socket (读取侧) Socket (写入侧) │
│ zlib createGunzip createGzip │
│ crypto createDecipher createCipher │
│ process process.stdin process.stdout │
│ child_process child.stdout child.stdin │
│ readline Interface - │
│ │
│ 所有这些流都继承自 EventEmitter │
│ 所有这些流都支持 pipe / pipeline 互联 │
└─────────────────────────────────────────────────────────────┘这意味着你可以将 HTTP 请求体直接 pipe 到文件、将文件 pipe 到 gzip 压缩再 pipe 到 HTTP 响应、将子进程的输出 pipe 到父进程的日志——一切都是流的组合。
自定义可读流和可写流:
js
const { Readable, Writable } = require('stream')
class CounterStream extends Readable {
constructor(max) {
super()
this.max = max
this.current = 0
}
_read() {
if (this.current < this.max) {
this.current++
this.push(`${this.current}\n`)
} else {
this.push(null)
}
}
}
class UpperCaseWriter extends Writable {
_write(chunk, encoding, callback) {
process.stdout.write(chunk.toString().toUpperCase())
callback()
}
}
const counter = new CounterStream(5)
const writer = new UpperCaseWriter()
counter.pipe(writer)背压(Backpressure)机制
当可写流处理数据的速度跟不上可读流产生数据的速度时,就会产生背压。如果不处理背压,数据会堆积在内存中,最终导致内存溢出。
正常情况(生产速度 = 消费速度):
Readable ──chunk──→ ──chunk──→ ──chunk──→ Writable
均匀流动 及时消费
背压情况(生产速度 > 消费速度):
Readable ──chunk──→ chunk chunk chunk ←── Writable
持续生产 ↑ 堆积在缓冲区中 处理较慢
背压处理流程:
┌──────────┐ write() ┌──────────┐
│ Readable │ ──────────→ │ Writable │
│ │ │ │
│ │ 返回 false │ 缓冲区满 │
│ pause() │ ←────────── │ │
│ │ │ │
│ │ 'drain'事件 │ 缓冲区空 │
│ resume() │ ←────────── │ │
└──────────┘ └──────────┘背压的触发条件与 highWaterMark 直接相关。highWaterMark 是流内部缓冲区的水位线(默认 16KB),当缓冲区中的数据量达到或超过这个值时,writable.write() 返回 false,表示消费者已经"装不下了"。
手动处理背压:
js
const fs = require('fs')
const readable = fs.createReadStream('./huge-file.dat')
const writable = fs.createWriteStream('./output.dat')
readable.on('data', (chunk) => {
const canWrite = writable.write(chunk)
if (!canWrite) {
readable.pause()
}
})
writable.on('drain', () => {
readable.resume()
})
readable.on('end', () => {
writable.end()
})pipe() 内部自动处理背压,但它的错误处理不够完善——如果链路中某个流出错,其他流不会自动销毁,可能导致内存泄漏。这正是 pipeline 诞生的原因。
pipeline vs pipe
stream.pipeline 是 Node.js 10+ 推荐的流管道 API,它自动处理背压和错误传播,并在管道完成或出错时进行清理。
js
const { pipeline } = require('stream/promises')
const fs = require('fs')
const zlib = require('zlib')
async function compress(input, output) {
await pipeline(
fs.createReadStream(input),
zlib.createGzip(),
fs.createWriteStream(output)
)
}pipeline 内部工作流程:
┌──────┐ pipe ┌───────────┐ pipe ┌──────┐
│ Read │ ────────→ │ Transform │ ────────→ │Write │
│Stream│ │ (Gzip) │ │Stream│
└──┬───┘ └─────┬─────┘ └──┬───┘
│ │ │
│ 自动背压处理 │ 自动背压处理 │
│ 错误自动传播 │ 错误自动传播 │
└──────────────────────┴─────────────────────┘
统一的 Promise / callback
自动清理所有流(destroy)pipeline 相比 pipe 的优势:
| 特性 | pipe | pipeline |
|---|---|---|
| 错误传播 | 不传播,需手动监听每个流 | 自动传播到最终回调 |
| 流清理 | 出错后其他流不自动销毁 | 自动 destroy 所有流 |
| 背压 | 自动处理 | 自动处理 |
| Promise 支持 | 无 | stream/promises 提供 |
| AbortSignal | 无 | 支持取消 |
使用 AbortController 取消流管道:
js
const { pipeline } = require('stream/promises')
const fs = require('fs')
async function downloadWithTimeout(src, dest, timeoutMs) {
const ac = new AbortController()
const timer = setTimeout(() => ac.abort(), timeoutMs)
try {
await pipeline(
fs.createReadStream(src),
fs.createWriteStream(dest),
{ signal: ac.signal }
)
} finally {
clearTimeout(timer)
}
}手写 Transform 流实战
Transform 流是一种特殊的 Duplex 流,它接收输入数据进行转换后输出。实际应用场景包括数据加密、压缩、格式转换、日志过滤等。
CSV 转 JSON 的 Transform 流:
js
const { Transform, pipeline } = require('stream')
const fs = require('fs')
class CsvToJson extends Transform {
constructor() {
super({ readableObjectMode: true })
this.headers = null
this.buffer = ''
}
_transform(chunk, encoding, callback) {
this.buffer += chunk.toString()
const lines = this.buffer.split('\n')
this.buffer = lines.pop()
for (const line of lines) {
if (!line.trim()) continue
const values = line.split(',').map(v => v.trim())
if (!this.headers) {
this.headers = values
continue
}
const obj = {}
this.headers.forEach((header, i) => {
obj[header] = values[i] || ''
})
this.push(JSON.stringify(obj) + '\n')
}
callback()
}
_flush(callback) {
if (this.buffer.trim() && this.headers) {
const values = this.buffer.split(',').map(v => v.trim())
const obj = {}
this.headers.forEach((header, i) => {
obj[header] = values[i] || ''
})
this.push(JSON.stringify(obj) + '\n')
}
callback()
}
}
pipeline(
fs.createReadStream('./data.csv'),
new CsvToJson(),
fs.createWriteStream('./data.ndjson'),
(err) => {
if (err) console.error(err)
}
)数据过滤 Transform 流:
js
const { Transform } = require('stream')
class FilterStream extends Transform {
constructor(predicate) {
super({ objectMode: true })
this.predicate = predicate
}
_transform(chunk, encoding, callback) {
if (this.predicate(chunk)) {
this.push(chunk)
}
callback()
}
}
class MapStream extends Transform {
constructor(mapper) {
super({ objectMode: true })
this.mapper = mapper
}
_transform(chunk, encoding, callback) {
this.push(this.mapper(chunk))
callback()
}
}
const { Readable } = require('stream')
const source = Readable.from([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 17 },
{ name: 'Charlie', age: 25 },
{ name: 'Diana', age: 15 }
])
const filter = new FilterStream(user => user.age >= 18)
const mapper = new MapStream(user => ({ ...user, adult: true }))
source
.pipe(filter)
.pipe(mapper)
.on('data', (data) => console.log(data))http/https 模块
创建 HTTP 服务器
Node.js HTTP Server 内部工作流程:
┌─────────────────────────────────────────────┐
│ http.createServer() │
│ │ │
│ ▼ │
│ 监听端口 (listen) │
│ │ │
│ ▼ │
│ ┌───── 收到请求 ─────┐ │
│ │ │ │
│ ▼ ▼ │
│ IncomingMessage ServerResponse │
│ (req 可读流) (res 可写流) │
│ method / url / statusCode / │
│ headers / body setHeader / write / │
│ end │
│ │ │ │
│ └──────┬─────────────┘ │
│ ▼ │
│ 执行请求处理函数 │
│ │ │
│ ▼ │
│ res.end() 发送响应 │
└─────────────────────────────────────────────┘IncomingMessage 是一个 Readable 流,ServerResponse 是一个 Writable 流。这是理解 Node.js HTTP 模块的关键——请求和响应都是流。
js
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end('你好,Node.js')
})
server.listen(3000, '127.0.0.1', () => {
console.log('服务器运行在 http://127.0.0.1:3000/')
})请求/响应生命周期
一个完整的 HTTP 请求/响应在 Node.js 中经历以下阶段:
┌──────────────────────────────────────────────────────────┐
│ HTTP 请求/响应完整生命周期 │
├──────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务端 │
│ ────── ────── │
│ │
│ 1. TCP 三次握手 ──────────────→ connection 事件 │
│ │
│ 2. 发送请求头 ──────────────→ request 事件触发 │
│ GET /api HTTP/1.1 解析为 IncomingMessage │
│ Host: localhost req.method = 'GET' │
│ Accept: application/json req.url = '/api' │
│ req.headers = {...} │
│ │
│ 3. 发送请求体 ──────────────→ req 'data' 事件 │
│ (POST/PUT) req 'end' 事件 │
│ │
│ 4. 处理业务逻辑 │
│ res.writeHead(200) │
│ res.write(chunk) │
│ │
│ 5. 接收响应 ←────────────── res.end() │
│ response 'finish' 事件 │
│ │
│ 6. Keep-Alive 或 TCP 四次挥手 │
└──────────────────────────────────────────────────────────┘IncomingMessage 与 ServerResponse
请求体的读取必须通过流式方式,因为 IncomingMessage 是 Readable 流:
js
const http = require('http')
const server = http.createServer((req, res) => {
const { method, url, headers, httpVersion } = req
if (method === 'POST') {
const chunks = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
const body = Buffer.concat(chunks).toString()
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ received: JSON.parse(body) }))
})
return
}
res.writeHead(200, {
'Content-Type': 'application/json',
'X-Request-Id': Date.now().toString()
})
res.end(JSON.stringify({ method, url, httpVersion }))
})
server.listen(3000)Keep-Alive
HTTP/1.1 默认启用 Keep-Alive,即一个 TCP 连接可以复用多个 HTTP 请求/响应。Node.js 的 http.Server 默认支持 Keep-Alive,可以通过以下参数控制:
js
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('OK')
})
server.keepAliveTimeout = 5000
server.headersTimeout = 60000
server.maxHeadersCount = 2000
server.timeout = 120000
server.listen(3000)| 属性 | 默认值 | 说明 |
|---|---|---|
keepAliveTimeout | 5000ms | Keep-Alive 连接的空闲超时时间 |
headersTimeout | 60000ms | 接收完整请求头的超时时间 |
timeout | 0(无限) | 整个请求的超时时间 |
maxHeadersCount | 2000 | 最大请求头数量 |
在客户端使用 Keep-Alive:
js
const http = require('http')
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 10,
maxFreeSockets: 5
})
function request(url) {
return new Promise((resolve, reject) => {
const req = http.get(url, { agent }, (res) => {
const chunks = []
res.on('data', (chunk) => chunks.push(chunk))
res.on('end', () => resolve(Buffer.concat(chunks).toString()))
})
req.on('error', reject)
})
}http.Agent 管理连接池,keepAlive: true 使得连接在请求完成后不会被关闭,而是放回池中供下次请求复用,避免了频繁的 TCP 三次握手开销。
手写简易静态文件服务器
js
const http = require('http')
const fs = require('fs')
const path = require('path')
const { pipeline } = require('stream')
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.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',
'.txt': 'text/plain; charset=utf-8',
'.woff2': 'font/woff2',
'.mp4': 'video/mp4'
}
const STATIC_DIR = path.join(__dirname, 'public')
function getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase()
return MIME_TYPES[ext] || 'application/octet-stream'
}
const server = http.createServer((req, res) => {
if (req.method !== 'GET') {
res.writeHead(405)
res.end('Method Not Allowed')
return
}
const urlPath = new URL(req.url, `http://${req.headers.host}`).pathname
let filePath = path.join(STATIC_DIR, urlPath === '/' ? 'index.html' : urlPath)
filePath = path.normalize(filePath)
if (!filePath.startsWith(STATIC_DIR)) {
res.writeHead(403)
res.end('Forbidden')
return
}
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('<h1>404 - 文件未找到</h1>')
return
}
const mimeType = getMimeType(filePath)
const headers = {
'Content-Type': mimeType,
'Content-Length': stats.size,
'Cache-Control': 'public, max-age=3600',
'ETag': `"${stats.size}-${stats.mtime.getTime()}"`,
'Last-Modified': stats.mtime.toUTCString()
}
const ifNoneMatch = req.headers['if-none-match']
const ifModifiedSince = req.headers['if-modified-since']
if (ifNoneMatch === headers['ETag'] ||
(ifModifiedSince && new Date(ifModifiedSince) >= stats.mtime)) {
res.writeHead(304, headers)
res.end()
return
}
res.writeHead(200, headers)
pipeline(fs.createReadStream(filePath), res, (err) => {
if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
console.error(err)
}
})
})
})
server.listen(8080, () => {
console.log('静态文件服务器运行在 http://127.0.0.1:8080/')
})这个实现包含了:路径遍历攻击防护(检查 startsWith)、ETag/Last-Modified 协商缓存、流式响应(避免大文件内存溢出)、pipeline 自动错误处理。
child_process 模块
exec/execFile/spawn/fork 四种方式对比
┌─────────────────────────────────────────────────────────────┐
│ child_process 四种方式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ exec execFile │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 创建 shell │ │ 直接执行文件 │ │
│ │ 输出缓存到内存 │ │ 不创建 shell │ │
│ │ 适合简短命令 │ │ 更安全更高效 │ │
│ │ 有 maxBuffer 限制│ │ 有 maxBuffer 限制│ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ spawn fork │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 流式输出 │ │ 专门fork Node进程│ │
│ │ 不创建 shell │ │ 自带 IPC 通道 │ │
│ │ 适合大数据输出 │ │ 可发送消息和句柄 │ │
│ │ 最底层的 API │ │ 基于 spawn 实现 │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘| 方法 | shell | 输出方式 | IPC | 安全性 | 适用场景 |
|---|---|---|---|---|---|
exec | 创建 | 缓冲(Buffer/String) | 无 | 低(shell 注入) | 简单 shell 命令 |
execFile | 不创建 | 缓冲(Buffer/String) | 无 | 高 | 直接执行可执行文件 |
spawn | 可选 | 流(Stream) | 可选 | 高 | 长时间运行、大输出 |
fork | 不创建 | 流(Stream) | 自带 | 高 | Node.js 子进程通信 |
exec 与 execFile
js
const { exec, execFile } = require('child_process')
exec('ls -la /tmp | head -20', (error, stdout, stderr) => {
if (error) {
console.error(`执行错误: ${error.message}`)
return
}
console.log(stdout)
})
execFile('node', ['--version'], (error, stdout) => {
if (error) {
console.error(error)
return
}
console.log(`Node.js 版本: ${stdout.trim()}`)
})exec 创建 shell 来执行命令,因此支持管道 |、重定向 >、通配符 * 等 shell 特性。但也正因为经过 shell,存在命令注入风险:
js
const { exec } = require('child_process')
const userInput = 'test; rm -rf /'
exec(`ls ${userInput}`)上述代码中恶意输入会被 shell 解析为两条命令。使用 execFile 或 spawn 可以避免此问题,因为参数是作为数组传入而非拼接成字符串。
spawn
js
const { spawn } = require('child_process')
const child = spawn('find', ['.', '-name', '*.js', '-type', 'f'])
child.stdout.on('data', (data) => {
console.log(`stdout: ${data}`)
})
child.stderr.on('data', (data) => {
console.error(`stderr: ${data}`)
})
child.on('close', (code) => {
console.log(`子进程退出,退出码: ${code}`)
})spawn 的流式特性适合管道组合:
js
const { spawn } = require('child_process')
const grep = spawn('grep', ['-r', 'TODO', './src'])
const wc = spawn('wc', ['-l'])
grep.stdout.pipe(wc.stdin)
wc.stdout.on('data', (data) => {
console.log(`TODO 总数: ${data.toString().trim()}`)
})IPC 通信
fork 是专门用于创建 Node.js 子进程的方法,它自动建立 IPC(Inter-Process Communication)通道。
┌─────────────┐ IPC 通道 ┌─────────────┐
│ 父进程 │ ←─────────────→ │ 子进程 │
│ (main.js) │ │ (worker.js) │
│ │ send()/on() │ │
│ child.send │ ────────────→ │ process.on │
│ child.on │ ←──────────── │ process.send │
└─────────────┘ └─────────────┘父进程 main.js:
js
const { fork } = require('child_process')
const path = require('path')
const worker = fork(path.join(__dirname, 'worker.js'))
worker.send({ type: 'compute', data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] })
worker.on('message', (result) => {
console.log(`计算结果: ${result.sum}`)
worker.kill()
})
worker.on('exit', (code) => {
console.log(`子进程退出: ${code}`)
})子进程 worker.js:
js
process.on('message', (msg) => {
if (msg.type === 'compute') {
const sum = msg.data.reduce((acc, val) => acc + val, 0)
process.send({ sum })
}
})IPC 通信传递的数据会经过序列化/反序列化(类似 JSON),因此不能传递函数、Symbol 等不可序列化的值。对于大量数据传输,IPC 的开销不可忽略,可以考虑使用 SharedArrayBuffer。
cluster 模块与多进程
cluster 模块允许 Node.js 利用多核 CPU,通过 fork 多个工作进程来处理请求。它是构建在 child_process.fork 之上的高级抽象。
┌──────────────────────────────────────────────────────┐
│ cluster 工作模式 │
├──────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ Master 进程 │ │
│ │ (不处理请求) │ │
│ │ 管理 Worker │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ │ │ │ │
│ ┌──────┴──────┐ ┌───┴──────┐ ┌──┴──────────┐ │
│ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │
│ │ PID: 1001 │ │ PID: 1002│ │ PID: 1003 │ │
│ │ port: 3000 │ │ port:3000│ │ port: 3000 │ │
│ └─────────────┘ └──────────┘ └─────────────┘ │
│ ↑ │
│ 所有 Worker 共享同一端口 │
│ 由 Master 通过 round-robin 分发 │
│ (Windows 除外,由操作系统调度) │
└──────────────────────────────────────────────────────┘js
const cluster = require('cluster')
const http = require('http')
const os = require('os')
const numCPUs = os.cpus().length
if (cluster.isMaster) {
console.log(`Master ${process.pid} 启动`)
for (let i = 0; i < numCPUs; i++) {
cluster.fork()
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} 退出 (${signal || code})`)
console.log('启动新的 Worker...')
cluster.fork()
})
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} 上线`)
})
} else {
http.createServer((req, res) => {
res.writeHead(200)
res.end(`Worker ${process.pid} 处理请求\n`)
}).listen(3000)
console.log(`Worker ${process.pid} 启动`)
}cluster 的核心原理是端口共享:Master 进程创建 TCP 服务器并监听端口,当新连接到达时,Master 通过 IPC 将连接的文件描述符(fd)传递给某个 Worker,Worker 直接在该 fd 上进行读写。这避免了多进程监听同一端口的冲突。
生产环境中通常使用 PM2 来管理 cluster,它提供了零停机重启、日志管理、进程监控等功能:
┌─────────────────────────────────────────────────────┐
│ PM2 进程管理架构 │
│ │
│ ┌──────────┐ │
│ │ PM2 God │ ←── PM2 守护进程,管理所有应用 │
│ │ Daemon │ │
│ └────┬─────┘ │
│ │ │
│ ┌────┴─────────────────────────────────────┐ │
│ │ App: my-server │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ inst 0 │ │ inst 1 │ │ inst 2 │ ... │ │
│ │ │ fork │ │ fork │ │ fork │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ │ │
│ │ pm2 start app.js -i max │ │
│ │ pm2 reload app.js (零停机重启) │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘worker_threads 对比
child_process 创建的是独立进程,而 worker_threads 创建的是同一进程内的线程。两者适用于不同场景。
┌──────────────────────────────────────────────────────────┐
│ child_process vs worker_threads │
│ │
│ child_process (多进程) worker_threads (多线程) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 进程 A │ │ 进程 │ │
│ │ ┌──────────────┐ │ │ ┌──────┐┌──────┐ │ │
│ │ │ V8 实例 │ │ │ │主线程 ││Worker│ │ │
│ │ │ 独立堆内存 │ │ │ │V8实例 ││V8实例│ │ │
│ │ └──────────────┘ │ │ │ ││ │ │ │
│ └──────────────────┘ │ │ ││ │ │ │
│ ┌──────────────────┐ │ └──────┘└──────┘ │ │
│ │ 进程 B │ │ │ │
│ │ ┌──────────────┐ │ │ 共享进程内存空间 │ │
│ │ │ V8 实例 │ │ │ SharedArrayBuffer │ │
│ │ │ 独立堆内存 │ │ └──────────────────┘ │
│ │ └──────────────┘ │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────┘| 维度 | child_process | worker_threads |
|---|---|---|
| 隔离级别 | 进程级,完全隔离 | 线程级,同一进程 |
| 内存 | 各自独立堆内存 | 可通过 SharedArrayBuffer 共享 |
| 启动开销 | 高(创建新进程、新 V8 实例) | 较低(共享进程资源) |
| 通信方式 | IPC(序列化) | MessagePort + SharedArrayBuffer |
| 稳定性 | 子进程崩溃不影响主进程 | Worker 崩溃可能影响进程 |
| 适用场景 | CPU 密集任务、隔离第三方代码 | CPU 密集计算、共享数据处理 |
js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads')
if (isMainThread) {
const worker = new Worker(__filename, {
workerData: { start: 1, end: 1000000 }
})
worker.on('message', (result) => {
console.log(`素数个数: ${result}`)
})
worker.on('error', (err) => {
console.error('Worker 错误:', err)
})
worker.on('exit', (code) => {
console.log(`Worker 退出: ${code}`)
})
} else {
const { start, end } = workerData
function isPrime(n) {
if (n < 2) return false
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false
}
return true
}
let count = 0
for (let i = start; i <= end; i++) {
if (isPrime(i)) count++
}
parentPort.postMessage(count)
}使用 SharedArrayBuffer 实现零拷贝数据共享:
js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads')
if (isMainThread) {
const sharedBuffer = new SharedArrayBuffer(4 * 1024)
const sharedArray = new Int32Array(sharedBuffer)
for (let i = 0; i < sharedArray.length; i++) {
sharedArray[i] = i
}
const worker = new Worker(__filename, {
workerData: { sharedBuffer }
})
worker.on('message', () => {
console.log('前 10 个元素:', Array.from(sharedArray.slice(0, 10)))
})
} else {
const { sharedBuffer } = workerData
const sharedArray = new Int32Array(sharedBuffer)
for (let i = 0; i < sharedArray.length; i++) {
Atomics.add(sharedArray, i, 100)
}
parentPort.postMessage('done')
}events 模块
EventEmitter 原理
Node.js 中几乎所有的异步 I/O 操作都基于事件驱动模型。EventEmitter 是 events 模块提供的核心类,它实现了观察者模式(发布-订阅模式),是 Node.js 事件驱动架构的基石。
┌──────────────────────────────────────────────────────┐
│ EventEmitter 内部结构 │
│ │
│ 内部维护一个事件 → 监听器映射表: │
│ │
│ _events = { │
│ 'data': [handler1, handler2, handler3], │
│ 'error': [errorHandler], │
│ 'end': [endHandler1, endHandler2], │
│ 'close': [closeHandler] │
│ } │
│ │
│ emit('data', payload) │
│ │ │
│ ├──→ handler1(payload) 同步依次调用 │
│ ├──→ handler2(payload) │
│ └──→ handler3(payload) │
│ │
│ 关键特性: │
│ - 监听器按注册顺序同步执行 │
│ - emit 返回 boolean 表示是否有监听器 │
│ - error 事件无监听器时抛出异常 │
└──────────────────────────────────────────────────────┘核心 API
js
const EventEmitter = require('events')
class DataProcessor extends EventEmitter {
process(data) {
this.emit('start')
const result = data.map(item => item * 2)
this.emit('data', result)
this.emit('end', { count: result.length })
}
}
const processor = new DataProcessor()
processor.on('start', () => console.log('处理开始'))
processor.on('data', (result) => console.log('处理结果:', result))
processor.on('end', (info) => console.log(`处理完成,共 ${info.count} 条`))
processor.process([1, 2, 3, 4, 5])EventEmitter 完整 API 速查:
| 方法 | 作用 | 别名 |
|---|---|---|
on(event, fn) | 注册监听器 | addListener |
once(event, fn) | 注册一次性监听器 | - |
off(event, fn) | 移除指定监听器 | removeListener |
emit(event, ...args) | 触发事件 | - |
removeAllListeners([event]) | 移除所有监听器 | - |
listenerCount(event) | 获取监听器数量 | - |
eventNames() | 获取所有事件名 | - |
prependListener(event, fn) | 在头部插入监听器 | - |
setMaxListeners(n) | 设置最大监听器数量 | - |
错误处理
error 事件在 EventEmitter 中具有特殊地位:如果 emit('error') 时没有注册任何 error 监听器,Node.js 会直接抛出异常并退出进程。这是 EventEmitter 唯一一个有特殊语义的事件。
js
const EventEmitter = require('events')
const emitter = new EventEmitter()
emitter.emit('error', new Error('未处理的错误'))
emitter.on('error', (err) => {
console.error('捕获到错误:', err.message)
})
emitter.emit('error', new Error('已处理的错误'))在生产环境中,始终为 EventEmitter 实例注册 error 监听器是最佳实践。对于使用了 EventEmitter 的类(如 Stream、HTTP Server),都应该监听 error 事件。
内存泄漏排查(MaxListenersExceededWarning)
当一个事件注册的监听器数量超过默认上限(10 个)时,Node.js 会发出 MaxListenersExceededWarning 警告。这通常意味着存在内存泄漏——代码可能在循环或重复调用中不断添加监听器而未移除。
js
const EventEmitter = require('events')
const emitter = new EventEmitter()
for (let i = 0; i < 20; i++) {
emitter.on('data', () => {})
}运行上述代码会看到警告:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [EventEmitter].
Use emitter.setMaxListeners() to increase limit常见的内存泄漏场景:
js
const EventEmitter = require('events')
class Connection extends EventEmitter {}
const conn = new Connection()
function handleRequest(req) {
conn.on('data', (data) => {
req.respond(data)
})
}上面的代码在每次 handleRequest 被调用时都会新增一个 data 监听器,但从不移除。随着请求量增加,监听器不断累积,导致内存泄漏和性能下降。
修复方案:
js
const EventEmitter = require('events')
class Connection extends EventEmitter {}
const conn = new Connection()
function handleRequest(req) {
function onData(data) {
req.respond(data)
conn.removeListener('data', onData)
}
conn.on('data', onData)
}
function handleRequestOnce(req) {
conn.once('data', (data) => {
req.respond(data)
})
}排查工具与方法:
js
const EventEmitter = require('events')
EventEmitter.defaultMaxListeners = 15
const emitter = new EventEmitter()
emitter.setMaxListeners(20)
console.log(emitter.listenerCount('data'))
console.log(emitter.eventNames())
console.log(emitter.listeners('data'))
process.on('warning', (warning) => {
if (warning.name === 'MaxListenersExceededWarning') {
console.error('发现潜在内存泄漏:', warning.message)
console.trace()
}
})手写 EventEmitter 实现
js
class SimpleEventEmitter {
constructor() {
this._events = Object.create(null)
this._maxListeners = 10
}
on(eventName, listener) {
if (typeof listener !== 'function') {
throw new TypeError('监听器必须是函数')
}
if (!this._events[eventName]) {
this._events[eventName] = []
}
if (this._events[eventName].length >= this._maxListeners && this._maxListeners > 0) {
console.warn(
`MaxListenersExceededWarning: ${eventName} 事件已有 ` +
`${this._events[eventName].length} 个监听器`
)
}
this._events[eventName].push(listener)
return this
}
once(eventName, listener) {
const wrapper = (...args) => {
listener.apply(this, args)
this.off(eventName, wrapper)
}
wrapper._original = listener
return this.on(eventName, wrapper)
}
emit(eventName, ...args) {
const listeners = this._events[eventName]
if (!listeners || listeners.length === 0) {
if (eventName === 'error') {
throw args[0] instanceof Error ? args[0] : new Error('Unhandled error')
}
return false
}
const copied = [...listeners]
for (const fn of copied) {
fn.apply(this, args)
}
return true
}
off(eventName, listener) {
const listeners = this._events[eventName]
if (!listeners) return this
this._events[eventName] = listeners.filter(
(fn) => fn !== listener && fn._original !== listener
)
if (this._events[eventName].length === 0) {
delete this._events[eventName]
}
return this
}
removeAllListeners(eventName) {
if (eventName) {
delete this._events[eventName]
} else {
this._events = Object.create(null)
}
return this
}
listenerCount(eventName) {
const listeners = this._events[eventName]
return listeners ? listeners.length : 0
}
eventNames() {
return Object.keys(this._events)
}
setMaxListeners(n) {
this._maxListeners = n
return this
}
prependListener(eventName, listener) {
if (!this._events[eventName]) {
this._events[eventName] = []
}
this._events[eventName].unshift(listener)
return this
}
}实现要点分析:
_events使用Object.create(null)创建无原型的纯净对象,避免原型链上的属性干扰emit中先复制监听器数组再遍历,防止在回调中添加/移除监听器导致迭代异常once通过wrapper._original保存原始函数引用,使off时传入原始函数也能正确移除error事件无监听器时直接throw,与 Node.js 原生行为一致
Buffer 与二进制数据
Buffer 概述
Buffer 是 Node.js 中处理二进制数据的核心类。JavaScript 原生的字符串是以 UTF-16 编码存储的,无法直接处理原始二进制数据(如图片、音频、网络数据包),Buffer 正是为此而生。
┌─────────────────────────────────────────────────────┐
│ Buffer 在内存中的位置 │
├─────────────────────────────────────────────────────┤
│ │
│ V8 堆内存 │
│ ┌──────────────────────────────┐ │
│ │ String / Number / Object │ │
│ │ Array / Function │ │
│ │ 受 V8 GC 管理 │ │
│ └──────────────────────────────┘ │
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ V8 堆外内存(C++ 层分配) │
│ ┌──────────────────────────────┐ │
│ │ Buffer │ │
│ │ 固定大小的原始内存块 │ │
│ │ 底层是 ArrayBuffer │ │
│ │ 不受 V8 堆大小限制 │ │
│ │ 通过 GC + C++ Release 回收 │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────┘Buffer 存储在 V8 堆外内存中(通过 C++ 层的 ArrayBuffer 分配),这意味着 Buffer 的大小不受 --max-old-space-size 限制。但 Buffer 的 JS 对象壳(存储 length、byteOffset 等元信息的对象)仍在 V8 堆中,当 JS 壳被 GC 回收时,会触发 C++ 层释放对应的堆外内存。
Buffer 创建与转换
js
const buf1 = Buffer.alloc(10)
const buf2 = Buffer.alloc(10, 0xff)
const buf3 = Buffer.allocUnsafe(10)
const buf4 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f])
const buf5 = Buffer.from('你好世界', 'utf-8')
const buf6 = Buffer.from('SGVsbG8=', 'base64')
console.log(buf4.toString('utf-8'))
console.log(buf5.toString('hex'))
console.log(buf5.toString('base64'))
console.log(buf5.length)
console.log('你好世界'.length)buf5.length 是 12(UTF-8 编码下每个中文字符占 3 字节),而字符串的 length 是 4。这是 Buffer 和 String 的核心区别——Buffer 的 length 是字节数,String 的 length 是字符数。
alloc vs allocUnsafe 的区别:
| 方法 | 初始化 | 性能 | 安全性 |
|---|---|---|---|
Buffer.alloc(size) | 零填充 | 较慢 | 安全 |
Buffer.allocUnsafe(size) | 不初始化 | 较快 | 可能包含旧数据 |
allocUnsafe 更快是因为跳过了内存清零步骤,但分配的内存中可能残留之前的敏感数据。在不会立即填充的场景下应使用 alloc。
Buffer 操作
js
const buf = Buffer.from('Hello')
const slice = buf.subarray(0, 3)
slice[0] = 0x4a
console.log(buf.toString())
const combined = Buffer.concat([
Buffer.from('Hello '),
Buffer.from('World')
])
console.log(combined.toString())
console.log(Buffer.from('abc').equals(Buffer.from('abc')))
console.log(Buffer.compare(Buffer.from('a'), Buffer.from('b')))subarray 返回的是原 Buffer 的视图,共享同一块底层内存,修改 slice 会影响原 Buffer。如果需要独立副本,使用 Buffer.from(buf)。
二进制协议解析是 Buffer 的典型应用场景:
js
function parsePacket(buf) {
const version = buf.readUInt8(0)
const type = buf.readUInt8(1)
const length = buf.readUInt16BE(2)
const payload = buf.subarray(4, 4 + length)
return { version, type, length, payload }
}
function buildPacket(version, type, payload) {
const header = Buffer.alloc(4)
header.writeUInt8(version, 0)
header.writeUInt8(type, 1)
header.writeUInt16BE(payload.length, 2)
return Buffer.concat([header, payload])
}
const packet = buildPacket(1, 2, Buffer.from('Hello'))
console.log(parsePacket(packet))编码处理(UTF-8/Base64)
js
const str = 'Hello 你好'
const utf8Buf = Buffer.from(str, 'utf-8')
console.log(utf8Buf)
console.log(utf8Buf.length)
const base64Str = utf8Buf.toString('base64')
console.log(base64Str)
const fromBase64 = Buffer.from(base64Str, 'base64')
console.log(fromBase64.toString('utf-8'))
const hexStr = utf8Buf.toString('hex')
console.log(hexStr)Node.js 支持的编码格式:
| 编码 | 说明 | 用途 |
|---|---|---|
utf-8 | 变长编码,ASCII 用 1 字节,中文用 3 字节 | 默认编码,文本文件 |
ascii | 7 位编码,0-127 | 纯英文字符 |
base64 | 每 3 字节编码为 4 字符,体积增加 33% | 二进制数据文本化传输 |
base64url | URL 安全的 Base64(+/ 替换为 -_) | JWT、URL 参数 |
hex | 每字节编码为 2 个十六进制字符 | 哈希值、调试 |
binary/latin1 | 单字节编码 | 遗留系统兼容 |
流式读取中的编码陷阱——多字节字符被截断:
js
const { StringDecoder } = require('string_decoder')
const decoder = new StringDecoder('utf-8')
const buf1 = Buffer.from([0xe4, 0xbd])
const buf2 = Buffer.from([0xa0, 0xe5, 0xa5, 0xbd])
console.log(buf1.toString())
console.log(buf2.toString())
console.log(decoder.write(buf1))
console.log(decoder.write(buf2))直接 toString() 会因为 UTF-8 多字节字符被分割到两个 chunk 中而产生乱码。StringDecoder 会缓存不完整的字节序列,等到下一个 chunk 到来时拼接出完整字符。readline 模块和 Readable 流的 setEncoding() 内部都使用了 StringDecoder。
ArrayBuffer/TypedArray 关系
┌─────────────────────────────────────────────────────────┐
│ ArrayBuffer 体系 │
│ │
│ ┌─────────────┐ │
│ │ ArrayBuffer │ ←── 底层二进制数据容器(不可直接操作) │
│ └──────┬──────┘ │
│ │ │
│ │ 通过视图(View)操作数据 │
│ │ │
│ ┌────┴────────────────────────────┐ │
│ │ TypedArray │ │
│ │ Uint8Array / Int32Array / │ │
│ │ Float64Array / ... │ │
│ └────────────────┬────────────────┘ │
│ │ │
│ │ Buffer 是 Uint8Array 的子类 │
│ │ │
│ ┌────────────────┴────────────────┐ │
│ │ Buffer │ │
│ │ 继承 Uint8Array 全部能力 │ │
│ │ 额外方法: toString(encoding) │ │
│ │ write / copy / readInt32LE │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘js
const buf = Buffer.from([1, 2, 3, 4])
console.log(buf instanceof Uint8Array)
console.log(buf instanceof Buffer)
const uint8 = new Uint8Array([1, 2, 3, 4])
const sharedBuf = Buffer.from(uint8.buffer)
uint8[0] = 99
console.log(sharedBuf[0])
const copiedBuf = Buffer.from(uint8)
uint8[0] = 200
console.log(copiedBuf[0])Buffer.from(typedArray.buffer) 传入底层 ArrayBuffer,创建的 Buffer 与 TypedArray 共享内存;Buffer.from(typedArray) 传入 TypedArray 本身,会复制数据到新内存。
process 模块
概述
process 是一个全局对象(无需 require),它提供了当前 Node.js 进程的信息和控制能力。它是 EventEmitter 的实例,也是 Node.js 与操作系统交互的桥梁。
┌─────────────────────────────────────────────────────────┐
│ process 对象核心能力 │
├─────────────────────────────────────────────────────────┤
│ │
│ 进程信息 │
│ ├── process.pid 当前进程 PID │
│ ├── process.ppid 父进程 PID │
│ ├── process.title 进程标题(ps 命令可见) │
│ ├── process.argv 命令行参数数组 │
│ ├── process.execPath Node.js 可执行文件路径 │
│ ├── process.version Node.js 版本 │
│ ├── process.versions V8/libuv/OpenSSL 等版本 │
│ ├── process.platform 操作系统平台 │
│ └── process.arch CPU 架构 │
│ │
│ 运行时控制 │
│ ├── process.cwd() 当前工作目录 │
│ ├── process.chdir(dir) 切换工作目录 │
│ ├── process.exit([code]) 退出进程 │
│ ├── process.abort() 立即终止(生成 core dump) │
│ ├── process.kill(pid) 发送信号 │
│ └── process.uptime() 进程运行时长(秒) │
│ │
│ 内存与性能 │
│ ├── process.memoryUsage() 内存使用详情 │
│ ├── process.cpuUsage() CPU 使用时间 │
│ └── process.hrtime.bigint() 高精度时间戳 │
│ │
│ I/O 流 │
│ ├── process.stdin 标准输入(Readable 流) │
│ ├── process.stdout 标准输出(Writable 流) │
│ └── process.stderr 标准错误(Writable 流) │
└─────────────────────────────────────────────────────────┘环境变量
process.env 是一个包含所有环境变量的对象。需要注意的是,所有的值都是字符串类型。
js
console.log(process.env.NODE_ENV)
console.log(process.env.PORT)
console.log(process.env.HOME)
console.log(process.env.PATH)
process.env.MY_VAR = 'hello'
console.log(process.env.MY_VAR)
process.env.DEBUG_MODE = 'true'
console.log(process.env.DEBUG_MODE === true)
console.log(process.env.DEBUG_MODE === 'true')环境变量的值始终是字符串,process.env.DEBUG_MODE = true 会被隐式转换为 'true'。判断时必须与字符串比较。
生产环境中配置管理的最佳实践:
js
function getConfig() {
return {
port: parseInt(process.env.PORT, 10) || 3000,
host: process.env.HOST || '0.0.0.0',
nodeEnv: process.env.NODE_ENV || 'development',
dbUrl: process.env.DATABASE_URL,
isProduction: process.env.NODE_ENV === 'production',
logLevel: process.env.LOG_LEVEL || 'info'
}
}
function validateConfig(config) {
const required = ['dbUrl']
const missing = required.filter(key => !config[key])
if (missing.length > 0) {
throw new Error(`缺少必要的环境变量: ${missing.join(', ')}`)
}
return config
}退出码
process.exit(code) 终止进程并返回退出码。退出码 0 表示正常退出,非零值表示异常。
js
process.exit(0)
process.exit(1)
process.exitCode = 1常见退出码含义:
| 退出码 | 含义 |
|---|---|
0 | 正常退出 |
1 | 未捕获的致命异常(Uncaught Fatal Exception) |
2 | 未使用(Bash 保留) |
3 | 内部 JavaScript 解析错误 |
5 | V8 致命错误 |
9 | 无效参数 |
12 | 无效的调试参数 |
13 | 未完成的 Top-Level Await |
>128 | 信号退出(128 + 信号编号) |
推荐使用 process.exitCode 而非直接调用 process.exit(),因为后者会立即终止进程,可能导致 stdout 缓冲区中的数据丢失(stdout 是异步的):
js
process.stdout.write('重要日志数据')
process.exit(0)
process.stdout.write('重要日志数据', () => {
process.exitCode = 0
})信号处理
Node.js 进程可以接收操作系统发送的信号,常见信号:
┌──────────────────────────────────────────────────────┐
│ 常用 POSIX 信号 │
├──────────┬──────────┬────────────────────────────────┤
│ 信号 │ 编号 │ 说明 │
├──────────┼──────────┼────────────────────────────────┤
│ SIGTERM │ 15 │ 请求终止(可拦截),默认行为 │
│ SIGINT │ 2 │ Ctrl+C 中断 │
│ SIGHUP │ 1 │ 终端关闭 │
│ SIGUSR1 │ 10 │ 用户自定义(Node 用于调试器) │
│ SIGUSR2 │ 12 │ 用户自定义 │
│ SIGKILL │ 9 │ 强制终止(不可拦截) │
│ SIGSTOP │ 19 │ 暂停进程(不可拦截) │
└──────────┴──────────┴────────────────────────────────┘优雅关闭(Graceful Shutdown)是生产环境的必备能力:
js
const http = require('http')
const server = http.createServer((req, res) => {
res.end('OK')
})
server.listen(3000)
let isShuttingDown = false
async function gracefulShutdown(signal) {
if (isShuttingDown) return
isShuttingDown = true
console.log(`收到 ${signal} 信号,开始优雅关闭...`)
server.close(() => {
console.log('HTTP 服务器已关闭')
})
setTimeout(() => {
console.error('强制退出:关闭超时')
process.exit(1)
}, 10000).unref()
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT')).unref() 使定时器不会阻止进程退出——如果所有连接都已关闭,进程不需要等待 10 秒超时。
未捕获异常与未处理的 Promise 拒绝
js
process.on('uncaughtException', (err, origin) => {
console.error(`未捕获异常 [${origin}]:`, err)
process.exitCode = 1
})
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason)
})
process.on('warning', (warning) => {
console.warn('进程警告:', warning.name, warning.message)
})uncaughtException 是最后的安全网。但注意:在捕获到未知异常后,进程的状态可能已经不一致(文件句柄泄漏、内存损坏等),最佳实践是记录错误日志后尽快重启进程,而不是继续运行。
process.nextTick vs setImmediate
这两个调度函数在 Node.js 事件循环中有本质区别:
┌─────────────────────────────────────────────────────────┐
│ process.nextTick vs setImmediate │
├─────────────────────────────────────────────────────────┤
│ │
│ 事件循环一个 tick 的执行顺序: │
│ │
│ ┌─────────────┐ │
│ │ 当前同步代码 │ │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ nextTick │ ← 在当前阶段结束后、下一阶段开始前执行 │
│ │ 队列 │ 属于微任务,优先级最高 │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Promise │ ← 微任务队列,优先级次于 nextTick │
│ │ .then 队列 │ │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 事件循环 │ ← timers → poll → check(setImmediate) │
│ │ 下一阶段 │ │
│ └─────────────┘ │
│ │
│ nextTick: 插队机制,在两个阶段之间执行 │
│ setImmediate: 在 check 阶段排队执行 │
└─────────────────────────────────────────────────────────┘| 维度 | process.nextTick | setImmediate |
|---|---|---|
| 执行时机 | 当前操作完成后、事件循环下一阶段前 | 事件循环的 check 阶段 |
| 优先级 | 最高(高于 Promise.then) | 低于微任务 |
| 递归调用 | 会饿死 I/O(阻塞事件循环) | 不会饿死 I/O |
| 实现位置 | V8 微任务队列 | libuv check 阶段 |
js
setImmediate(() => console.log('setImmediate'))
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise.then'))
console.log('同步代码')输出顺序:同步代码 → nextTick → Promise.then → setImmediate
递归 nextTick 饿死 I/O 的示例:
js
const fs = require('fs')
fs.readFile(__filename, () => {
console.log('文件读取完成')
})
function recursiveNextTick() {
process.nextTick(recursiveNextTick)
}
recursiveNextTick()上述代码中,文件读取完成的回调永远不会执行,因为 nextTick 队列永远不为空,事件循环无法进入 poll 阶段。解决方案是用 setImmediate 替代递归 nextTick。
内存监控
js
function formatMemory(bytes) {
return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}
function printMemoryUsage() {
const mem = process.memoryUsage()
console.log({
rss: formatMemory(mem.rss),
heapTotal: formatMemory(mem.heapTotal),
heapUsed: formatMemory(mem.heapUsed),
external: formatMemory(mem.external),
arrayBuffers: formatMemory(mem.arrayBuffers)
})
}
printMemoryUsage()| 字段 | 含义 |
|---|---|
rss | 常驻内存集(Resident Set Size),进程占用的物理内存总量 |
heapTotal | V8 堆内存总量 |
heapUsed | V8 堆内存使用量 |
external | V8 管理的 C++ 对象绑定的内存(如 Buffer 的 JS 壳) |
arrayBuffers | ArrayBuffer 和 SharedArrayBuffer 占用的内存 |
高精度性能计时:
js
const start = process.hrtime.bigint()
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
const end = process.hrtime.bigint()
console.log(`耗时: ${Number(end - start) / 1e6} ms`)process.hrtime.bigint() 返回纳秒精度的时间戳,远比 Date.now() 精确,适合性能基准测试。
面试高频问题
题目一:fs 模块中 readFile 和 createReadStream 的核心区别
问:处理一个 2GB 的日志文件应该用哪个 API?为什么?
答:必须使用 createReadStream。readFile 将整个文件一次性读入内存,而 createReadStream 以流的方式分块读取,每次只在内存中保持一个 highWaterMark 大小的数据块(默认 64KB)。
V8 堆内存默认限制约 1.7GB,readFile 读取 2GB 文件会导致内存溢出。即使通过 --max-old-space-size 扩大堆内存,一次性分配 2GB 的连续内存也可能因内存碎片而失败。流式处理可以在恒定的内存占用(约 64KB + 写入缓冲区)下完成整个文件的处理。
js
const fs = require('fs')
const readline = require('readline')
async function analyzeLog(filePath) {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity
})
const stats = { total: 0, errors: 0, warnings: 0 }
for await (const line of rl) {
stats.total++
if (line.includes('ERROR')) stats.errors++
if (line.includes('WARN')) stats.warnings++
}
return stats
}题目二:path.resolve 和 path.join 的区别
问:path.resolve('a', '/b', 'c') 和 path.join('a', '/b', 'c') 的结果分别是什么?
答:
path.resolve('a', '/b', 'c')→/b/cpath.join('a', '/b', 'c')→a/b/c
path.resolve 从右向左处理参数,遇到绝对路径即停止拼接——处理到 /b 时发现是绝对路径,忽略左侧的 'a',然后拼接 'c',返回 /b/c。path.join 则不关心参数是否是绝对路径,纯粹做字符串拼接并规范化。
核心区别:resolve 总是返回绝对路径(未遇到绝对路径时以 process.cwd() 为基准),join 可能返回相对路径。
题目三:什么是背压(Backpressure)
问:在 Node.js 流中如何处理背压?不处理会怎样?
答:背压是当 Readable 产生数据的速度超过 Writable 处理速度时产生的压力。
处理方式:检测 writable.write(chunk) 的返回值——当返回 false 时,说明 Writable 的内部缓冲区已达到 highWaterMark,此时应调用 readable.pause() 暂停读取;监听 Writable 的 drain 事件,缓冲区排空后调用 readable.resume() 恢复读取。
不处理背压的后果:数据在 Writable 的内部缓冲区中无限堆积,内存持续增长直到 OOM。例如从快速 SSD 读取文件写入慢速网络 socket,不处理背压时数据会大量堆积在内存中。
推荐使用 stream.pipeline() 自动处理背压,它还会在出错时自动销毁所有流,防止内存泄漏。
题目四:process.nextTick 和 setImmediate 的区别
问:为什么递归调用 nextTick 会饿死 I/O?
答:process.nextTick 的回调在当前操作完成后、事件循环进入下一阶段之前立即执行,属于微任务且优先级最高(高于 Promise.then)。setImmediate 的回调在事件循环的 check 阶段执行。
递归调用 nextTick 会在每个阶段切换时不断插入新的微任务,而事件循环在进入下一阶段前必须清空所有微任务队列。这导致事件循环永远停留在微任务清理阶段,无法进入 poll 阶段处理 I/O 回调。
js
function recursive() {
process.nextTick(recursive)
}
recursive()
setTimeout(() => console.log('永远不会执行'), 0)在 I/O 回调中,setImmediate 确定先于 setTimeout(fn, 0) 执行:
js
const fs = require('fs')
fs.readFile(__filename, () => {
setImmediate(() => console.log('setImmediate'))
setTimeout(() => console.log('setTimeout'), 0)
})题目五:手写 EventEmitter 的核心考点
问:手写 EventEmitter 需要注意哪些边界情况?
答:
error事件特殊处理:emit('error')无监听器时必须抛出异常,否则错误会被静默吞掉once的实现:通过包装函数实现一次性监听,但off时传入原始函数也需要能正确移除——通过wrapper._original保存引用emit中的数组复制:遍历监听器前先复制数组[...listeners],防止回调中执行off导致数组长度变化跳过监听器MaxListenersExceededWarning:监听器数量超过阈值时发出警告,帮助排查内存泄漏- 返回
this:on、off、once等方法返回this以支持链式调用
js
const emitter = new SimpleEventEmitter()
emitter
.on('a', () => console.log(1))
.on('a', () => console.log(2))
.once('b', () => console.log(3))题目六:Buffer.alloc 和 Buffer.allocUnsafe 的区别
问:什么场景下应该用 allocUnsafe?有什么风险?
答:Buffer.alloc(size) 会将分配的内存初始化为零,Buffer.allocUnsafe(size) 跳过初始化步骤。allocUnsafe 更快(在分配大 Buffer 时差异明显),但分配的内存中可能残留之前释放的数据(包括密码、密钥等敏感信息)。
适合使用 allocUnsafe 的场景:分配后立即完全填充的情况,如 const buf = Buffer.allocUnsafe(size); file.read(buf, 0, size)——因为读取操作会覆盖所有字节。
不应使用 allocUnsafe 的场景:Buffer 可能只被部分填充,或者直接被传输出去(如发送到网络),此时未填充的区域可能泄露旧数据。
题目七:child_process.exec 的安全风险
问:为什么不应该用 exec 执行包含用户输入的命令?
答:exec 通过 shell 执行命令,用户输入会被 shell 解释。攻击者可以通过分号 ;、管道 |、反引号 ` 等 shell 元字符注入恶意命令:
js
const { exec, execFile } = require('child_process')
const filename = 'test.txt; cat /etc/passwd'
exec(`cat ${filename}`)
execFile('cat', [filename], (err, stdout) => {
if (err) console.error(err)
})exec 会将整个字符串交给 shell 解析,分号后的 cat /etc/passwd 会被作为第二条命令执行。execFile 将参数作为数组传递给可执行文件,不经过 shell 解释,因此 ; cat /etc/passwd 只是一个普通的文件名参数,会提示文件不存在。
规则:凡是涉及用户输入的命令执行,一律使用 execFile 或 spawn,避免 shell 注入。
题目八:Node.js 如何利用多核 CPU
问:Node.js 是单线程的,如何在多核服务器上充分利用 CPU 资源?
答:Node.js 提供了三种方式利用多核 CPU:
cluster 模块:Master 进程 fork 出多个 Worker 进程,所有 Worker 共享同一端口。Master 通过 round-robin(Linux/macOS)或操作系统调度(Windows)将连接分发给 Worker。每个 Worker 是独立的 Node.js 进程,拥有独立的 V8 实例和事件循环。
worker_threads 模块:在同一进程内创建多个 JS 执行线程,线程之间可以通过
SharedArrayBuffer共享内存,避免了进程间通信的序列化开销。适合 CPU 密集型计算任务。child_process 模块:fork 出子进程执行特定任务,通过 IPC 通信。适合需要进程级隔离的场景。
选型建议:Web 服务器优先使用 cluster(或 PM2 的 cluster mode);CPU 密集计算优先使用 worker_threads;需要执行外部程序或需要强隔离时使用 child_process。
选型决策树:
需要利用多核?
│
├── Web 服务器 ──→ cluster / PM2
│
├── CPU 密集计算 ──→ worker_threads
│ (共享内存,低开销)
│
└── 执行外部程序 / 强隔离 ──→ child_process
(独立进程,安全)延伸阅读
学习路径推荐
┌──────────────────────────────────────────────────────────┐
│ Node.js 核心模块学习路径 │
├──────────────────────────────────────────────────────────┤
│ │
│ 第一阶段:基础掌握 │
│ ├── fs 模块:文件读写、目录操作、fs/promises │
│ ├── path 模块:路径拼接、解析、跨平台 │
│ ├── events 模块:EventEmitter、on/emit/once │
│ └── Buffer 模块:创建、转换、编码 │
│ │
│ 第二阶段:核心进阶 │
│ ├── stream 模块:四种流类型、背压、pipeline │
│ ├── http 模块:创建服务器、请求生命周期 │
│ ├── process 模块:信号处理、环境变量 │
│ └── child_process 模块:spawn/fork/IPC │
│ │
│ 第三阶段:深入原理 │
│ ├── libuv 事件循环源码阅读 │
│ ├── V8 内存管理与 GC 机制 │
│ ├── cluster 与 worker_threads 多核利用 │
│ └── 手写核心模块简易实现(EventEmitter/Stream) │
│ │
│ 第四阶段:生产实践 │
│ ├── 性能调优(profiling、内存泄漏排查) │
│ ├── 优雅关闭与进程管理(PM2/systemd) │
│ ├── 日志、监控、APM 集成 │
│ └── 安全实践(输入校验、依赖审计) │
└──────────────────────────────────────────────────────────┘推荐资源
官方文档
- Node.js 官方文档(https://nodejs.org/docs/latest/api/)——最权威的 API 参考,每个模块都有详细的方法签名和示例
- Node.js 官方 Guides(https://nodejs.org/en/learn)——入门指南和核心概念解释
深入原理
- 《Node.js 设计模式》(Node.js Design Patterns)——系统讲解 Node.js 的设计哲学和最佳实践,涵盖流、事件驱动、进程管理等核心主题
- 《深入浅出 Node.js》(朴灵著)——国内经典,深入讲解 V8、libuv、Buffer、Stream 等底层原理
- libuv 官方文档(https://docs.libuv.org/)——理解事件循环和异步 I/O 的底层实现
源码阅读
- Node.js GitHub 仓库(https://github.com/nodejs/node)——`lib/` 目录是 JS 层核心模块实现,
src/目录是 C++ 层实现 - 重点阅读:
lib/internal/streams/目录(Stream 实现)、lib/fs.js(fs 模块 JS 层)、lib/events.js(EventEmitter 实现)
实战项目建议
- 手写一个支持路由、中间件的迷你 HTTP 框架(深入理解 http 模块和 Stream)
- 实现一个文件监听 + 自动重启的开发工具(深入理解 fs.watch 和 child_process)
- 构建一个多进程日志处理管道(深入理解 Stream、child_process、cluster)
- 实现一个简易的 RPC 框架(深入理解 net 模块、Buffer 协议解析、EventEmitter)