外观
Node.js 系统学习文档
适合人群:已经学过 JavaScript,想系统进入服务端开发、全栈开发、前端工程化底层或后端基础设施的学习者。
学习目标:从 Node.js 运行脚本、模块系统、异步模型和内置 API 开始,逐步掌握 Web 服务开发、数据库、认证、测试、性能、安全、部署和高级架构。
核对时间:2026-06-24。Node.js 官网当前显示
v24.18.0 Latest LTS、v26.3.1 Latest Release。生产项目优先选择 Active LTS 或 Maintenance LTS 版本。
目录
第一篇:Node.js 基础
- Node.js 是什么
- Node.js 和浏览器 JavaScript 的区别
- 版本选择与环境安装
- 运行脚本、REPL 与 npm scripts
- package.json 系统讲解
- CommonJS 与 ES Modules
- Node.js 中的全局对象
- 异步编程基础
- 错误处理基础
第二篇:Node.js 核心模块
- path 与 URL
- fs 文件系统
- Buffer 与二进制数据
- Stream 流
- events 事件系统
- http、https 与 fetch
- crypto 加密与哈希
- child_process、worker_threads 与 cluster
- timers、AbortController 与取消控制
第三篇:Web 服务端开发
第四篇:工程化与质量保障
- 项目结构设计
- TypeScript 在 Node.js 中的使用
- ESLint、Prettier 与代码规范
- 测试:node:test、Jest、Vitest
- 日志系统
- 接口文档与 OpenAPI
- 任务调度、队列与后台任务
第五篇:高级阶段
- 事件循环深入理解
- Stream 背压与大文件处理
- 性能分析与诊断
- 安全最佳实践
- 进程管理、部署与 Docker
- 监控、链路追踪与可观测性
- 高级架构:Monorepo、微服务、Serverless
- 常见问题与排查思路
- 推荐学习顺序
- 学习资源
第一篇:Node.js 基础
1.1 Node.js 是什么
Node.js 是一个基于 V8 JavaScript 引擎的 JavaScript 运行时。它让 JavaScript 可以脱离浏览器,运行在服务器、本地命令行、构建工具、脚本任务、桌面工具和云函数等场景中。
一句话理解:
浏览器让 JavaScript 操作页面,Node.js 让 JavaScript 操作系统资源和网络服务。
Node.js 常见用途:
- 写后端 API 服务。
- 写 BFF 层,也就是前端和后端之间的适配层。
- 写命令行工具,例如脚手架、构建工具、自动化脚本。
- 写前端工程化工具,例如 Vite、Webpack、ESLint、Prettier、Babel 的运行环境都依赖 Node.js。
- 写实时通信服务,例如 WebSocket、Socket.IO。
- 写 SSR 服务,例如 Next.js、Nuxt、NestJS、Express。
- 写数据处理、文件处理、定时任务和后台任务。
Node.js 的核心特点:
- 单线程 JavaScript 执行模型。
- 非阻塞 I/O。
- 事件驱动。
- npm 生态庞大。
- 适合高并发 I/O 密集型服务。
- 不适合在主线程里长时间执行 CPU 密集型任务。
需要注意:
Node.js 不是一门新语言。它运行的是 JavaScript,但提供了一套浏览器没有的服务端能力,例如文件系统、进程、网络、加密、流、操作系统信息等。
1.2 Node.js 和浏览器 JavaScript 的区别
两者都能执行 JavaScript,但运行环境不同。
浏览器中有:
windowdocument- DOM API
- BOM API
- localStorage
- sessionStorage
- Web APIs
Node.js 中有:
globalThisprocessBufferfspathhttpcryptostreamchild_processworker_threads
浏览器关注:
- 页面渲染。
- 用户交互。
- DOM 操作。
- 前端资源加载。
- 跨域安全策略。
Node.js 关注:
- 文件读写。
- 网络通信。
- 服务端请求处理。
- 进程管理。
- 数据库连接。
- 后台任务。
- 安全与性能。
一个简单对比:
js
// 浏览器中常见
document.querySelector('#app').textContent = 'Hello'
// Node.js 中常见
import { readFile } from 'node:fs/promises'
const content = await readFile('./README.md', 'utf8')
console.log(content)学习 Node.js 时要建立一个边界意识:
document、window是浏览器对象,不属于 Node.js。fs、process、Buffer是 Node.js 能力,不属于普通浏览器环境。fetch、URL、AbortController等 API 现在在 Node.js 和浏览器中都可用,但具体细节仍要以运行环境为准。
1.3 版本选择与环境安装
Node.js 版本分为 Current、Active LTS、Maintenance LTS、EOL 等状态。
生产项目建议:
- 优先使用 LTS。
- 不要使用 EOL 版本。
- 团队项目固定 Node.js 大版本。
- 使用
.nvmrc、.node-version或package.json engines约束版本。
当前核对时间为 2026-06-24,Node.js 官网显示:
v24.18.0 Latest LTSv26.3.1 Latest Release
一般建议:
- 学习和生产项目:优先安装 Node.js 24 LTS。
- 尝鲜和实验:可以试 Node.js 26 Current。
- 老项目维护:按项目要求安装对应大版本。
查看版本:
bash
node -v
npm -v推荐使用版本管理工具:
bash
# macOS/Linux 常见
nvm install 24
nvm use 24
# Windows 可以使用 nvm-windows 或 fnm
fnm install 24
fnm use 24package.json 中约束版本:
json
{
"engines": {
"node": ">=24 <25"
}
}这个配置的作用:
engines.node:告诉团队或部署平台项目期望的 Node.js 版本范围。>=24 <25:表示允许 Node.js 24 的所有小版本,不允许自动跳到 25 或 26。- 它不一定会强制阻止安装,是否强制取决于包管理器和配置。
1.4 运行脚本、REPL 与 npm scripts
运行单个文件:
bash
node index.js进入 REPL:
bash
nodeREPL 适合:
- 快速试验表达式。
- 验证小段代码。
- 查看内置对象。
执行一段命令:
bash
node -e "console.log(process.version)"npm scripts 示例:
json
{
"scripts": {
"dev": "node --watch src/index.js",
"start": "node dist/index.js",
"test": "node --test",
"lint": "eslint ."
}
}配置项作用:
dev:开发环境启动命令。node --watch:文件变化后自动重启进程,适合简单项目开发。start:生产环境启动命令,通常运行构建后的产物。test:运行 Node.js 内置测试运行器。lint:运行代码规范检查。
脚本执行:
bash
npm run dev
npm run start
npm test常见命令区别:
node file.js:直接用 Node.js 执行文件。npm run xxx:执行package.json里的脚本。npx xxx:临时执行本地或远程包里的命令。pnpm dlx xxx:pnpm 生态中类似npx的用法。
1.5 package.json 系统讲解
package.json 是 Node.js 项目的核心配置文件。
常见字段:
json
{
"name": "node-learning-demo",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "node --watch src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"fastify": "^5.0.0"
},
"devDependencies": {
"eslint": "^9.0.0"
},
"engines": {
"node": ">=24 <25"
}
}字段说明:
name:包名。发布到 npm 时必须唯一。version:包版本,通常遵循语义化版本。type:模块类型。module表示.js默认按 ESM 解析;commonjs表示.js默认按 CommonJS 解析。private:设为true可以防止误发布到 npm。scripts:项目命令集合。dependencies:生产运行需要的依赖。devDependencies:开发、构建、测试需要的依赖。engines:声明运行环境版本。exports:控制包对外暴露的入口。main:CommonJS 时代常见的默认入口。types:TypeScript 类型声明入口。
依赖版本符号:
^1.2.3:允许升级 minor 和 patch,例如1.x.x。~1.2.3:允许升级 patch,例如1.2.x。1.2.3:锁定精确版本。latest:永远取最新版本,不建议业务项目使用。
锁文件:
- npm:
package-lock.json - pnpm:
pnpm-lock.yaml - yarn:
yarn.lock
锁文件的作用:
- 固定依赖树。
- 保证团队和 CI 安装结果一致。
- 降低“我本地能跑,别人不能跑”的概率。
1.6 CommonJS 与 ES Modules
Node.js 里有两套模块系统:
- CommonJS,简称 CJS。
- ES Modules,简称 ESM。
CommonJS 写法:
js
const path = require('node:path')
module.exports = {
joinRoot(...parts) {
return path.join(process.cwd(), ...parts)
},
}ESM 写法:
js
import path from 'node:path'
export function joinRoot(...parts) {
return path.join(process.cwd(), ...parts)
}如何判断文件按哪种模块解析:
package.json中"type": "module":.js默认是 ESM。package.json中"type": "commonjs"或没有设置:.js默认是 CJS。.mjs:始终按 ESM 解析。.cjs:始终按 CommonJS 解析。
常见差异:
- CJS 可以使用
require、module.exports、__dirname、__filename。 - ESM 使用
import、export、import.meta.url。 - ESM 支持顶层
await。 - CJS 的
require是同步加载。 - ESM 的模块加载和解析更适合现代工具链。
ESM 中模拟 __dirname:
js
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)包导出配置:
json
{
"name": "my-utils",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./logger": {
"import": "./dist/logger.js",
"types": "./dist/logger.d.ts"
}
}
}配置项作用:
".":包根入口,例如import x from 'my-utils'。"./logger":子路径入口,例如import { logger } from 'my-utils/logger'。"import":ESM 使用的入口。"types":TypeScript 类型入口。exports:限制外部只能访问声明过的入口,有利于维护包边界。
学习建议:
- 新项目优先学 ESM。
- 维护老项目必须理解 CJS。
- 写库时要理解 ESM/CJS 双格式、
exports、types。 - 遇到
ERR_REQUIRE_ESM,通常是 CJS 项目尝试require一个只支持 ESM 的包。
1.7 Node.js 中的全局对象
常见全局对象:
globalThis:标准全局对象。process:当前进程信息和控制。Buffer:二进制数据处理。console:控制台输出。setTimeout、setInterval、setImmediate:定时器。queueMicrotask:加入微任务队列。URL、URLSearchParams:URL 处理。AbortController:取消异步任务。fetch:发起 HTTP 请求。
process 常见用法:
js
console.log(process.version)
console.log(process.platform)
console.log(process.cwd())
console.log(process.env.NODE_ENV)
console.log(process.argv)含义:
process.version:当前 Node.js 版本。process.platform:运行平台,例如win32、darwin、linux。process.cwd():当前工作目录。process.env:环境变量。process.argv:命令行参数。
命令行参数示例:
bash
node cli.js --port 3000js
console.log(process.argv)输出大致包含:
- Node 可执行文件路径。
- 当前脚本路径。
- 用户传入的参数。
真实 CLI 项目通常使用:
commandercacyargsminimist
1.8 异步编程基础
Node.js 以非阻塞 I/O 著称。学习 Node.js 必须理解异步。
三种常见异步写法:
回调函数
js
import { readFile } from 'node:fs'
readFile('./package.json', 'utf8', (err, data) => {
if (err) {
console.error(err)
return
}
console.log(data)
})特点:
- 老 API 常见。
- 错误优先回调,通常第一个参数是
err。 - 嵌套多了会产生回调地狱。
Promise
js
import { readFile } from 'node:fs/promises'
readFile('./package.json', 'utf8')
.then((data) => {
console.log(data)
})
.catch((err) => {
console.error(err)
})特点:
- 链式处理。
- 错误用
.catch捕获。 - 比回调更容易组合。
async / await
js
import { readFile } from 'node:fs/promises'
async function main() {
try {
const data = await readFile('./package.json', 'utf8')
console.log(data)
} catch (err) {
console.error(err)
}
}
main()特点:
- 写法像同步代码。
- 本质仍然是 Promise。
- 服务端项目中最常用。
并发处理:
js
const [user, orders, profile] = await Promise.all([
getUser(userId),
getOrders(userId),
getProfile(userId),
])注意:
await放在循环中可能导致串行执行。- 无依赖的异步任务应该用
Promise.all并发。 - 有失败容忍需求时可以用
Promise.allSettled。
1.9 错误处理基础
Node.js 中错误来源很多:
- 同步异常。
- Promise rejection。
- 回调错误。
- EventEmitter 的
error事件。 - HTTP 请求错误。
- 数据库连接错误。
- 进程级异常。
同步错误:
js
try {
JSON.parse('{bad json}')
} catch (err) {
console.error('JSON 解析失败', err)
}异步错误:
js
try {
const data = await readFile('./missing.txt', 'utf8')
} catch (err) {
console.error('读取文件失败', err)
}错误优先回调:
js
readFile('./missing.txt', 'utf8', (err, data) => {
if (err) {
console.error(err)
return
}
console.log(data)
})EventEmitter 错误:
js
stream.on('error', (err) => {
console.error('流处理失败', err)
})进程级兜底:
js
process.on('uncaughtException', (err) => {
console.error('未捕获异常', err)
process.exit(1)
})
process.on('unhandledRejection', (reason) => {
console.error('未处理 Promise rejection', reason)
process.exit(1)
})注意:
- 进程级错误处理是最后兜底,不应该当成普通业务错误处理方式。
- 服务端遇到未知异常后,通常应该记录日志、停止接收新请求、优雅退出,让进程管理器重启。
- 不要吞掉错误。吞掉错误会让系统处于未知状态。
第二篇:Node.js 核心模块
2.1 path 与 URL
path 用于处理文件系统路径。
js
import path from 'node:path'
console.log(path.join('src', 'utils', 'index.js'))
console.log(path.resolve('src', 'index.js'))
console.log(path.extname('app.tar.gz'))
console.log(path.basename('/user/project/index.js'))
console.log(path.dirname('/user/project/index.js'))常用 API:
path.join():拼接路径。path.resolve():解析成绝对路径。path.dirname():获取目录名。path.basename():获取文件名。path.extname():获取扩展名。path.normalize():规范化路径。
注意:
- Windows 路径分隔符是
\。 - POSIX 路径分隔符是
/。 - 不要手写字符串拼接路径,优先使用
path.join或path.resolve。
URL 处理:
js
const url = new URL('https://example.com/users?page=1&size=20')
console.log(url.origin)
console.log(url.pathname)
console.log(url.searchParams.get('page'))常用能力:
new URL():解析 URL。url.searchParams:处理查询参数。URLSearchParams:构造查询字符串。
js
const params = new URLSearchParams({
page: '1',
size: '20',
})
console.log(params.toString())2.2 fs 文件系统
Node.js 文件系统 API 有三种风格:
- 回调版:
node:fs - Promise 版:
node:fs/promises - 同步版:
readFileSync等
推荐优先使用 Promise 版:
js
import { readFile, writeFile, mkdir } from 'node:fs/promises'
await mkdir('./logs', { recursive: true })
const content = await readFile('./package.json', 'utf8')
await writeFile('./logs/package-copy.json', content)配置项说明:
recursive: true:递归创建目录,目录已存在时不报错。'utf8':按字符串读取;不传编码时返回Buffer。
判断文件是否存在:
js
import { access } from 'node:fs/promises'
async function exists(file) {
try {
await access(file)
return true
} catch {
return false
}
}读取目录:
js
import { readdir } from 'node:fs/promises'
const files = await readdir('./src', { withFileTypes: true })
for (const file of files) {
console.log(file.name, file.isDirectory())
}同步 API:
js
import { readFileSync } from 'node:fs'
const pkg = readFileSync('./package.json', 'utf8')同步 API 适合:
- CLI 启动时读取配置。
- 构建脚本。
- 初始化阶段。
不适合:
- Web 请求处理过程中频繁调用。
- 高并发服务主路径。
2.3 Buffer 与二进制数据
Buffer 用于处理二进制数据。
常见场景:
- 文件读写。
- 网络传输。
- 图片、音频、视频处理。
- 加密和哈希。
- Base64 编解码。
示例:
js
const buf = Buffer.from('你好 Node.js', 'utf8')
console.log(buf)
console.log(buf.toString('utf8'))
console.log(buf.toString('base64'))Base64 转回字符串:
js
const encoded = Buffer.from('hello').toString('base64')
const decoded = Buffer.from(encoded, 'base64').toString('utf8')
console.log(decoded)注意:
Buffer是字节序列,不是普通字符串。- 中文字符在 UTF-8 中通常占多个字节。
- 处理文件上传、下载、加密时经常会遇到 Buffer。
- 大文件不要一次性读成 Buffer,应该使用 Stream。
2.4 Stream 流
Stream 用于分块处理数据。
为什么需要流:
- 大文件一次性读入内存会占用大量内存。
- 网络传输天然是分块到达的。
- 流可以边读边处理边写。
常见流类型:
- Readable:可读流。
- Writable:可写流。
- Duplex:双工流。
- Transform:转换流。
复制文件:
js
import { createReadStream, createWriteStream } from 'node:fs'
const reader = createReadStream('./big.log')
const writer = createWriteStream('./big-copy.log')
reader.pipe(writer)更推荐使用 pipeline:
js
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
await pipeline(
createReadStream('./big.log'),
createWriteStream('./big-copy.log'),
)pipeline 的优势:
- 自动处理错误。
- 自动关闭流。
- 支持 Promise。
- 比手动
.pipe()更适合生产代码。
Transform 示例:
js
import { Transform } from 'node:stream'
const upper = new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase())
},
})流学习重点:
- chunk:每次读取的数据块。
- backpressure:背压,写入方处理不过来时要暂停读取。
- highWaterMark:内部缓冲区阈值。
- pipeline:生产代码中组合流的推荐方式。
Stream 的内部机制(为什么它能省内存)
以 fs 的可读流为例,底层实际发生了这些事:
createReadStream()不会立刻读文件,只是登记了path、start、end和highWaterMark。- 事件循环进入 poll 阶段后,流才在 libuv 线程池中发起一次
read,把最多一个 chunk(默认大小见highWaterMark)读进内核→用户态缓冲区,再作为data事件推给 JS。 - 每当一个 chunk 被消费掉,流会自动再发起下一块读取——永远只有有限块数据驻留在内存里,这就是它能处理 GB 级文件的原因。
fs.read()等同步 API 则会阻塞整个事件循环;一次性读入的readFile()会把整个文件放进内存。三者的内存占用差距是数量级的。
.pipe() 与手写 .on('data') 的本质区别(内部机制层面):
| 写法 | 背压处理 | 错误处理 |
|---|---|---|
reader.on('data', c => writer.write(c)) | ❌ 不处理。write() 返回 false 时继续写,数据在 writer 内部缓冲堆积,内存暴涨 | ❌ 需要手动监听两端 error |
reader.pipe(writer) | ✅ write() 返回 false 时自动 reader.pause(),等 drain 后再 resume() | ❌ 默认不转发错误,需手动加 error 监听 |
pipeline(a, b, c) | ✅ 同 pipe | ✅ 任一端出错即销毁所有流,并把错误抛给 Promise |
💡 write() 返回 false 不是错误,它是一个"你该慢下来了"的信号:表示 writer 的内部缓冲已达 highWaterMark。read() 返回 null 同理,表示可读缓冲已空(也可能是文件结束,用 readable / end 事件区分)。
2.5 events 事件系统
Node.js 很多对象都基于 EventEmitter。
基础示例:
js
import { EventEmitter } from 'node:events'
const emitter = new EventEmitter()
emitter.on('login', (user) => {
console.log(`${user.name} 登录了`)
})
emitter.emit('login', { name: 'Alice' })常用方法:
on:监听事件。once:只监听一次。emit:触发事件。off:移除监听。listenerCount:查看监听器数量。
错误事件:
js
emitter.on('error', (err) => {
console.error(err)
})注意:
- 如果 EventEmitter 触发
error事件但没有监听器,进程可能抛出异常。 - 不要无限增加监听器,否则可能产生内存泄漏警告。
- 事件适合解耦,但不要把核心业务流程全部藏在事件里,调试会变困难。
2.6 http、https 与 fetch
原生 HTTP 服务:
js
import http from 'node:http'
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ ok: true }))
return
}
res.writeHead(404)
res.end('Not Found')
})
server.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})核心对象:
req:请求对象。res:响应对象。req.method:请求方法。req.url:请求路径。req.headers:请求头。res.writeHead():写响应状态码和响应头。res.end():结束响应。
发起请求:
js
const response = await fetch('https://api.github.com')
const data = await response.json()
console.log(data)带超时控制:
js
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch('https://example.com/api', {
signal: controller.signal,
})
console.log(await response.text())
} finally {
clearTimeout(timer)
}学习重点:
- HTTP 是请求响应模型。
- Node 原生 HTTP API 偏底层。
- 真实项目通常使用 Express、Koa、Fastify、NestJS。
- 服务端必须处理超时、错误、请求体大小、跨域、日志、鉴权。
2.7 crypto 加密与哈希
crypto 用于加密、哈希、签名、随机数等安全相关能力。
哈希:
js
import { createHash } from 'node:crypto'
const hash = createHash('sha256')
.update('hello')
.digest('hex')
console.log(hash)随机 token:
js
import { randomBytes } from 'node:crypto'
const token = randomBytes(32).toString('hex')
console.log(token)密码存储注意:
- 不要用普通 hash 直接存密码。
- 不要用 MD5 存密码。
- 应使用 bcrypt、argon2、scrypt 这类适合密码哈希的算法。
- 每个密码都要有 salt。
示例:
js
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'
import { promisify } from 'node:util'
const scryptAsync = promisify(scrypt)
async function hashPassword(password) {
const salt = randomBytes(16).toString('hex')
const derivedKey = await scryptAsync(password, salt, 64)
return `${salt}:${derivedKey.toString('hex')}`
}
async function verifyPassword(password, stored) {
const [salt, key] = stored.split(':')
const derivedKey = await scryptAsync(password, salt, 64)
return timingSafeEqual(Buffer.from(key, 'hex'), derivedKey)
}安全原则:
- 密码不可逆。
- token 要有足够随机性。
- 密钥不要写死在源码里。
- 敏感配置放环境变量或密钥管理服务。
2.8 child_process、worker_threads 与 cluster
Node.js 主线程适合 I/O,不适合长时间 CPU 计算。
child_process
用于启动子进程。
js
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
const execFileAsync = promisify(execFile)
const { stdout } = await execFileAsync('node', ['-v'])
console.log(stdout)常见 API:
exec:执行 shell 命令,容易受 shell 注入影响。execFile:直接执行文件,更安全。spawn:适合长时间运行或大量输出的进程。fork:启动新的 Node.js 子进程,带 IPC 通道。
安全注意:
- 不要把用户输入直接拼进 shell 命令。
- 优先使用
execFile或spawn参数数组。 - 对命令参数做白名单校验。
worker_threads
用于 CPU 密集型任务。
js
import { Worker } from 'node:worker_threads'
const worker = new Worker(new URL('./worker.js', import.meta.url), {
workerData: { count: 1000000 },
})
worker.on('message', (result) => {
console.log(result)
})
worker.on('error', (err) => {
console.error(err)
})内部机制:每个 Worker 是一个独立的 OS 线程 + 独立的 V8 Isolate——有自己的事件循环、自己的堆内存,彼此不共享 JS 对象。跨线程通信靠 postMessage(默认结构化克隆,大数据有复制开销),想零拷贝要显式传 transferList(可转移 ArrayBuffer、MessagePort 等)。workerData 同样是一次克隆。worker 内可用 isMainThread === false、parentPort、threadId 判断身份。
适合:
- 图片处理。
- 加密计算。
- 大量 JSON 转换。
- 压缩解压。
- CPU 密集型算法。
不适合:
- 普通数据库查询。
- 普通 HTTP 请求。
- 简单业务逻辑。
⚠️ 注意事项:
- Worker 数量不是越多越好——每个 Worker 都有独立 V8 堆(默认上限与主线程同级),常见做法是 CPU 核数上下,且用连接池/任务队列复用 Worker,而不是每个任务 new 一个。
- ⚠️ 所有 Worker 共享同一个 libuv 线程池(见 5.1 的 libuv 小节):Worker 里跑大量
fs或crypto.scrypt时记得调大UV_THREADPOOL_SIZE。 - 构建 Worker 池时官方建议配合
AsyncResourceAPI,让诊断工具能把任务与回调关联起来(异步堆栈跟踪)。
cluster
cluster 用于多进程利用多核 CPU。现在很多生产环境更常用容器、进程管理器或平台层扩容,但理解 cluster 仍然有价值。
内部机制(nodejs.org cluster 文档):
- worker 进程通过
child_process.fork()启动,因此能与 primary 通过 IPC 通信并传递 server handle。 - 连接分发有两种方式:默认(除 Windows 外)是 round-robin(SCHED_RR)——primary 监听端口、接受连接后轮转分发给各 worker,并内置避免单 worker 过载的逻辑;另一种是
SCHED_NONE把 listen socket 交给感兴趣的内核,由 OS 调度(官方文档指出实践中分布可能严重不均)。可通过cluster.schedulingPolicy或环境变量NODE_CLUSTER_SCHED_POLICY调整,且在第一个 worker fork 前设置才生效。 - 每个 worker 是完整的独立进程:独立 V8 实例、独立堆、独立事件循环,崩溃互不影响;
process.send/IPC 用于通信。
适合场景:
- 单机多核部署。
- 老项目多进程模型。
- 理解 Node.js 水平扩展。
现代实践:
- 容器中通常一个进程一个容器,再由 Kubernetes、PM2、systemd 或云平台管理。
- 不要在完全不了解部署平台的情况下盲目 cluster。
三者对比与选型(Worker Threads vs Cluster vs child_process)
| 维度 | worker_threads | cluster | child_process.fork |
|---|---|---|---|
| 隔离级别 | 同进程内线程(独立 V8 Isolate/堆) | 独立 OS 进程 | 独立 OS 进程 |
| 解决的问题 | 单线程被 CPU 计算卡住 | 单进程用不满多核 | 运行外部命令/隔离崩溃域 |
| 通信成本 | postMessage 结构化克隆,可 transferList 零拷贝 | IPC(消息序列化) | IPC(消息序列化) |
| 共享状态 | 不共享 JS 对象;可共享 SharedArrayBuffer + Atomics | 不共享,靠外部存储(Redis 等) | 不共享 |
| 一个单元崩溃的影响 | 未处理错误可拖垮整个进程 | 其余 worker 不受影响,primary 可 respawn | 不影响父进程 |
| 内存开销 | 每 Worker 一个 V8 堆 | 每进程一个完整运行时 | 同 cluster |
| 典型场景 | 图像/压缩/加解密/大 JSON | I/O 密集的 Web 服务单机多核 | 调用系统命令、跑不受信任脚本 |
💡 一句话决策:CPU 密集 → worker_threads;请求吞吐 → cluster 或平台层多副本;执行外部程序 → child_process(优先 execFile/spawn 参数数组形式)。三者可以组合:每核一个 cluster worker,worker 内再挂小型 Worker 池做重计算。
2.9 timers、AbortController 与取消控制
定时器:
js
setTimeout(() => {
console.log('1 秒后执行')
}, 1000)
const timer = setInterval(() => {
console.log('每秒执行')
}, 1000)
clearInterval(timer)setImmediate:
js
setImmediate(() => {
console.log('当前事件循环轮次结束后执行')
})Promise 版定时器:
js
import { setTimeout } from 'node:timers/promises'
await setTimeout(1000)
console.log('等待 1 秒')取消控制:
js
const controller = new AbortController()
setTimeout(() => {
controller.abort()
}, 3000)
await fetch('https://example.com', {
signal: controller.signal,
})适用场景:
- HTTP 请求超时。
- 后台任务取消。
- 用户主动取消操作。
- 服务关闭时中断未完成任务。
第三篇:Web 服务端开发
3.1 从原生 HTTP 到框架
原生 HTTP 能帮助你理解底层,但真实项目一般不会全部手写。
原生 HTTP 要自己处理:
- 路由匹配。
- 请求体解析。
- 查询参数解析。
- Cookie。
- Session。
- CORS。
- 静态资源。
- 错误处理。
- 日志。
- 参数校验。
框架的作用:
- 提供路由系统。
- 提供中间件机制。
- 提供请求和响应封装。
- 提供插件生态。
- 降低重复样板代码。
一个最小 Express 示例:
js
import express from 'express'
const app = express()
app.use(express.json())
app.get('/health', (req, res) => {
res.json({ ok: true })
})
app.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})一个最小 Fastify 示例:
js
import Fastify from 'fastify'
const app = Fastify({
logger: true,
})
app.get('/health', async () => {
return { ok: true }
})
await app.listen({ port: 3000, host: '0.0.0.0' })3.2 Express、Koa、Fastify 如何选择
Express
特点:
- 生态成熟。
- 教程多。
- 中间件丰富。
- 老项目多。
- 默认能力比较轻,需要自己组合。
适合:
- 学习服务端基础。
- 中小型 API。
- 老项目维护。
- 快速写原型。
Koa
特点:
- 中间件模型优雅。
- 洋葱模型清晰。
- 比 Express 更轻。
- 很多能力要自己选插件。
适合:
- 想理解中间件模型。
- 对框架控制度要求高。
- 团队有较强工程化能力。
Fastify
特点:
- 性能较好。
- 插件体系清晰。
- Schema 驱动能力强。
- 日志、校验、序列化体验更现代。
适合:
- 高性能 API。
- 中大型服务。
- 希望用 schema 管理请求和响应。
NestJS
特点:
- 基于 TypeScript。
- 架构完整。
- 依赖注入、模块、控制器、服务、管道、守卫、拦截器体系成熟。
- 适合企业级项目。
适合:
- 中大型后端项目。
- 团队协作。
- 需要清晰分层和强约束的项目。
选择建议:
- 初学 Node.js:先学原生 HTTP,再学 Express 或 Fastify。
- 做企业级项目:学 NestJS。
- 做高性能 API:关注 Fastify。
- 维护老项目:Express 和 Koa 必须能看懂。
3.3 路由、中间件与请求生命周期
一次请求通常经历:
- 进入服务器。
- 经过全局中间件。
- 匹配路由。
- 执行业务中间件。
- 执行控制器逻辑。
- 访问数据库或外部服务。
- 返回响应。
- 记录日志和指标。
Express 中间件:
js
app.use((req, res, next) => {
console.log(req.method, req.url)
next()
})错误中间件:
js
app.use((err, req, res, next) => {
console.error(err)
res.status(500).json({
code: 'INTERNAL_ERROR',
message: '服务器内部错误',
})
})中间件常见用途:
- 日志。
- CORS。
- 请求体解析。
- 鉴权。
- 限流。
- 参数校验。
- 错误处理。
- 静态资源。
注意:
- Express 中必须调用
next()才会进入下一个中间件。 - 如果已经
res.send()或res.json(),不要继续写响应。 - 错误处理中间件有四个参数:
err, req, res, next。
3.4 配置管理与环境变量
环境变量常用于:
- 端口。
- 数据库地址。
- Redis 地址。
- JWT 密钥。
- 第三方 API Key。
- 日志等级。
- 当前环境。
示例:
bash
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://user:pass@localhost:5432/app
JWT_SECRET=change-me读取:
js
const port = Number(process.env.PORT || 3000)
const databaseUrl = process.env.DATABASE_URL更稳妥的方式是做配置校验:
js
import { z } from 'zod'
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
})
export const env = envSchema.parse(process.env)好处:
- 启动时就发现配置缺失。
- 避免运行一半才发现数据库地址为空。
- 给配置项提供类型。
- 明确哪些配置是必须的。
注意:
- 不要把
.env中的密钥提交到 Git。 .env.example可以提交,用于说明需要哪些变量。- 生产环境更推荐由部署平台或密钥管理服务注入环境变量。
3.5 参数校验与错误响应
不要相信任何客户端输入。
需要校验:
- 路由参数。
- 查询参数。
- 请求体。
- Header。
- Cookie。
- 文件类型和大小。
使用 Zod 示例:
js
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(1).max(50),
email: z.string().email(),
age: z.number().int().min(0).optional(),
})
app.post('/users', (req, res, next) => {
try {
const body = createUserSchema.parse(req.body)
res.json({ data: body })
} catch (err) {
next(err)
}
})统一错误响应:
json
{
"code": "VALIDATION_ERROR",
"message": "参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
}
]
}常见错误类型:
VALIDATION_ERROR:参数错误。UNAUTHORIZED:未登录。FORBIDDEN:无权限。NOT_FOUND:资源不存在。CONFLICT:资源冲突。RATE_LIMITED:请求过快。INTERNAL_ERROR:服务端未知错误。
3.6 数据库访问与 ORM
Node.js 常见数据库:
- PostgreSQL。
- MySQL。
- MongoDB。
- Redis。
- SQLite。
常见 ORM / 查询工具:
- Prisma。
- TypeORM。
- Sequelize。
- Drizzle。
- Knex。
- Mongoose。
Prisma 示例:
js
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const users = await prisma.user.findMany({
where: {
status: 'ACTIVE',
},
take: 20,
})数据库连接注意:
- 不要每个请求都创建一个新连接池。
- 应该复用连接池。
- 服务关闭时要优雅断开连接。
- 查询要有超时。
- 复杂查询要关注索引。
- 事务要尽量短。
事务示例:
js
await prisma.$transaction(async (tx) => {
const order = await tx.order.create({ data: orderData })
await tx.orderItem.createMany({ data: items })
return order
})高级关注点:
- N+1 查询问题。
- 连接池耗尽。
- 慢查询。
- 数据库迁移。
- 读写分离。
- 事务隔离级别。
- 幂等性。
3.7 认证、授权与会话
认证回答:
你是谁?
授权回答:
你能做什么?
常见认证方式:
- Cookie + Session。
- JWT。
- OAuth 2.0。
- API Key。
- 单点登录 SSO。
JWT 基本结构:
- Header。
- Payload。
- Signature。
JWT 使用注意:
- 不要把敏感信息放进 payload。
- JWT 默认只是编码,不是加密。
- 设置过期时间。
- 密钥要足够安全。
- 需要考虑刷新 token 和退出登录。
授权模型:
- RBAC:基于角色的访问控制。
- ABAC:基于属性的访问控制。
- ACL:访问控制列表。
RBAC 示例:
js
function requireRole(role) {
return (req, res, next) => {
if (!req.user?.roles?.includes(role)) {
res.status(403).json({ code: 'FORBIDDEN' })
return
}
next()
}
}
app.delete('/users/:id', requireRole('admin'), deleteUser)安全注意:
- 密码必须哈希存储。
- 登录接口要限流。
- 管理后台要做权限校验。
- 服务端不能只相信前端隐藏按钮。
- 敏感操作要记录审计日志。
3.8 文件上传与静态资源
文件上传要关注:
- 文件大小。
- 文件类型。
- 文件名安全。
- 存储位置。
- 病毒扫描。
- 权限控制。
- CDN 分发。
常见库:
- multer。
- busboy。
- formidable。
上传后不要直接信任原始文件名:
js
import { randomUUID } from 'node:crypto'
import path from 'node:path'
function safeFileName(originalName) {
const ext = path.extname(originalName).toLowerCase()
return `${randomUUID()}${ext}`
}静态资源:
js
app.use('/public', express.static('public', {
maxAge: '1d',
}))注意:
- 用户上传文件不要和服务代码放在一起。
- 不要允许上传可执行脚本。
- 图片类型要做真实内容检查,不要只看后缀。
- 大文件上传要考虑分片、断点续传、对象存储。
第四篇:工程化与质量保障
4.1 项目结构设计
小项目结构:
text
src/
├── index.js
├── routes/
├── services/
├── repositories/
├── middlewares/
├── config/
└── utils/中大型项目结构:
text
src/
├── app.js
├── server.js
├── config/
├── modules/
│ ├── user/
│ │ ├── user.routes.js
│ │ ├── user.controller.js
│ │ ├── user.service.js
│ │ └── user.repository.js
│ └── order/
├── middlewares/
├── common/
├── jobs/
├── database/
└── tests/分层建议:
- route:定义 URL 和 HTTP 方法。
- controller:处理请求和响应。
- service:业务逻辑。
- repository:数据库访问。
- middleware:横切逻辑。
- config:配置读取和校验。
- common:通用错误、工具、类型。
原则:
- controller 不直接写复杂业务。
- service 不直接依赖 HTTP 框架对象。
- repository 不处理业务规则。
- config 启动时校验。
- 错误响应统一出口。
4.2 TypeScript 在 Node.js 中的使用
Node.js 项目越来越多使用 TypeScript。
常见方案:
- 开发时用
tsx运行 TS。 - 构建时用
tsc输出 JS。 - 或使用 swc、esbuild、tsup 构建库。
安装:
bash
npm install typescript tsx @types/node -Dtsconfig.json 示例:
json
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src/**/*.ts"]
}配置项作用:
target:输出 JS 的语法目标。module:模块输出方式,Node ESM 项目常用NodeNext。moduleResolution:模块解析策略。strict:开启严格类型检查。esModuleInterop:改善 CommonJS 默认导入兼容。skipLibCheck:跳过依赖类型检查,提高速度。outDir:输出目录。rootDir:源码根目录。types:引入 Node.js 类型。
package.json:
json
{
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}注意:
- TypeScript 类型检查不等于运行时校验。
- 外部输入仍然需要 Zod、Valibot、Joi 等运行时校验。
- ESM + TypeScript 的路径后缀、模块解析要特别注意。
4.3 ESLint、Prettier 与代码规范
安装:
bash
npm install eslint prettier typescript-eslint -DESLint flat config 示例:
js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['src/**/*.ts'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},
},
]Prettier 配置:
json
{
"singleQuote": true,
"semi": false,
"printWidth": 100
}脚本:
json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier . --write",
"format:check": "prettier . --check"
}
}作用:
- ESLint:检查代码质量和潜在错误。
- Prettier:统一代码格式。
- TypeScript:提供类型检查。
团队建议:
- CI 中运行
lint、format:check、test。 - 提交前可以配合 lint-staged。
- 不要把所有规则都设为 error,容易降低开发效率。
- 对安全、未处理 Promise、未使用变量等问题要严格。
4.4 测试:node:test、Jest、Vitest
Node.js 内置测试运行器:
js
import test from 'node:test'
import assert from 'node:assert/strict'
function add(a, b) {
return a + b
}
test('add', () => {
assert.equal(add(1, 2), 3)
})运行:
bash
node --test常见测试类型:
- 单元测试:测试单个函数或类。
- 集成测试:测试多个模块协作。
- E2E 测试:从外部请求真实服务。
- 契约测试:验证服务之间的 API 合约。
服务测试示例:
js
import test from 'node:test'
import assert from 'node:assert/strict'
import { buildApp } from '../src/app.js'
test('GET /health', async () => {
const app = buildApp()
const res = await app.inject({
method: 'GET',
url: '/health',
})
assert.equal(res.statusCode, 200)
})测试重点:
- 不要依赖测试执行顺序。
- 每个测试要能独立运行。
- 数据库测试要隔离数据。
- 外部 API 要 mock 或使用测试环境。
- 关键业务要覆盖成功路径、失败路径、边界情况。
4.5 日志系统
不要只用 console.log 管理生产日志。
日志应该包含:
- 时间。
- 等级。
- 请求 ID。
- 用户 ID。
- 路由。
- 状态码。
- 耗时。
- 错误堆栈。
常见库:
- pino。
- winston。
- bunyan。
Pino 示例:
js
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization', 'password'],
})配置项作用:
level:日志级别,例如debug、info、warn、error。redact:脱敏字段,避免日志泄露密码、token。
日志原则:
- 错误日志要有堆栈。
- 业务关键动作要有审计日志。
- 不要记录明文密码、token、身份证、银行卡等敏感信息。
- 请求日志要带 requestId,方便串联一次请求的所有日志。
4.6 接口文档与 OpenAPI
接口文档要描述:
- 路径。
- 方法。
- 请求参数。
- 请求体。
- 响应结构。
- 状态码。
- 鉴权方式。
- 错误码。
OpenAPI 的价值:
- 前后端对齐接口。
- 自动生成 Swagger UI。
- 自动生成客户端 SDK。
- 支持接口 mock。
- 支持契约测试。
文档示例:
yaml
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
summary: 获取用户列表
responses:
'200':
description: 成功实践建议:
- 接口文档要随代码一起更新。
- 错误响应也要文档化。
- 不要只写成功响应。
- 参数校验规则要和文档保持一致。
4.7 任务调度、队列与后台任务
后台任务场景:
- 发邮件。
- 图片压缩。
- 数据导出。
- 定时清理。
- 消息推送。
- 订单超时取消。
- 第三方回调重试。
简单定时任务:
js
setInterval(async () => {
await cleanupExpiredSessions()
}, 60 * 1000)注意:
- 多实例部署时,简单
setInterval可能每个实例都执行一次。 - 重要任务要使用队列或分布式锁。
- 定时任务要可观测、可重试、可告警。
常见队列:
- BullMQ。
- RabbitMQ。
- Kafka。
- Redis Streams。
队列适合:
- 削峰填谷。
- 异步处理。
- 失败重试。
- 解耦服务。
- 限制并发。
任务设计要点:
- 幂等性。
- 重试次数。
- 死信队列。
- 超时时间。
- 任务状态。
- 监控告警。
第五篇:高级阶段
5.1 事件循环深入理解
Node.js 的 JavaScript 执行是单线程的,但 I/O 可以由底层系统和线程池协作完成。
你需要理解:
- 调用栈。
- 微任务队列。
- 宏任务。
- timer。
- I/O callback。
- check 阶段。
- close callback。
process.nextTick。Promise.then。setTimeout。setImmediate。
示例:
js
console.log('start')
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
Promise.resolve().then(() => {
console.log('promise')
})
process.nextTick(() => {
console.log('nextTick')
})
console.log('end')通常会先看到:
text
start
end
nextTick
promise事件循环各阶段的内部机制
事件循环由 libuv 实现,一轮 tick 依次经过以下阶段,每个阶段有一个 FIFO 回调队列(依据 nodejs.org 官方 "The Node.js Event Loop" 文档):
text
┌───────────────────────────┐
┌─>│ timers │ setTimeout / setInterval 到期回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 系统操作回调(如 TCP ECONNREFUSED)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ libuv 内部使用
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │ 网络 I/O 回调的主要战场
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │ setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ socket.on('close') 等
└───────────────────────────┘关键规则:
- 阶段之间与每个回调之后,Node 都会先清空
process.nextTick队列,再清空微任务(Promise)队列——两个队列都排空前不会进入下一个阶段。这就是上面示例中nextTick先于promise的原因,也是"递归nextTick会饿死事件循环"的原因(官方文档明确警告:I/O 密集场景下递归 nextTick 可能让目标永远无法达成)。 - timers 的阈值是"之后最早何时执行",不是"精确何时执行":
setTimeout(fn, 100)只保证不早于 100ms,实际受 OS 调度和当前正在执行的其他回调影响。 - poll 阶段的两个职责:计算需要阻塞等待 I/O 多久;处理 poll 队列里的事件。poll 队列为空且有
setImmediate任务时直接进入 check 阶段,否则阻塞等待。 - 从 libuv 1.45.0(Node.js 20)起,每轮迭代中 timers 在 poll 之后运行(早期版本在 poll 之前),这会影响某些场景下
setImmediate与 timer 的相对时序。 - 顶层作用域中
setTimeout(fn, 0)与setImmediate(fn)的顺序不确定(取决于进入循环前的准备耗时);但在 I/O 回调(poll 阶段)内,setImmediate稳定先于到期 timer。官方建议:日常一律优先setImmediate,语义更可推理。
libuv 与线程池:单线程模型的边界
"Node.js 是单线程"只指 JS 执行线程。I/O 的实际完成分两条路:
| 路径 | 由谁完成 | 典型 API |
|---|---|---|
| 网络 I/O | OS 内核异步机制(Linux epoll、macOS kqueue、Windows IOCP),不占线程池 | http、net、tls、数据库查询 |
文件 I/O、DNS lookup、部分 crypto(如 scrypt/pbkdf2/randomBytes)、zlib | libuv 线程池 | fs.readFile、crypto.scrypt |
| 同步 API | 直接阻塞 JS 主线程 | fs.readFileSync(请求路径上禁用) |
线程池要点:
- 默认 4 线程,由环境变量
UV_THREADPOOL_SIZE在进程启动前设置,最大 1024;线程池在首次需要时初始化,之后无法调整。 - 64 个并发
fs.readFile在默认配置下同一时刻仍只有 4 个在执行,其余排队——文件 I/O 密集型服务调大此值是常见优化。 - ⚠️ 线程池是全进程共享的:所有
worker_threads共用同一个池。如果每个 worker 都频繁做crypto.scrypt,它们会排在同一个池后面,这是 Worker 池方案里最隐蔽的瓶颈。 - 💡 网络 I/O 并发能力与线程池无关,所以"单机数万并发 HTTP 长连接"完全可行;真正怕的是慢数据库查询占满连接池、而不是占满线程池。
学习重点:
- 同步代码先执行。
process.nextTick优先级非常高,不要递归滥用。- Promise 微任务会在当前调用栈清空后执行。
setTimeout(fn, 0)不代表立即执行。setImmediate和setTimeout的先后在不同上下文中可能不同。
高并发服务的关键:
- 不要阻塞事件循环。
- 不要在请求路径中执行大量同步计算。
- 避免大 JSON 同步序列化和解析阻塞。
- CPU 密集任务交给 worker_threads 或外部服务。
5.2 Stream 背压与大文件处理
背压是指写入方处理速度跟不上读取方,系统需要让读取方慢下来。
错误写法:
js
reader.on('data', (chunk) => {
writer.write(chunk)
})问题:
- 如果写入速度慢,内存可能持续增长。
- 没有正确处理错误。
- 没有正确处理结束和关闭。
推荐写法:
js
import { pipeline } from 'node:stream/promises'
import { createReadStream, createWriteStream } from 'node:fs'
import { createGzip } from 'node:zlib'
await pipeline(
createReadStream('./access.log'),
createGzip(),
createWriteStream('./access.log.gz'),
)大文件下载:
js
app.get('/download', async (req, res, next) => {
try {
res.setHeader('Content-Type', 'application/octet-stream')
res.setHeader('Content-Disposition', 'attachment; filename="data.zip"')
await pipeline(
createReadStream('./data.zip'),
res,
)
} catch (err) {
next(err)
}
})高级点:
highWaterMark决定内部缓冲区大小。- 背压不是错误,是流控机制。
- 大文件不要用
readFile一次性读取。 pipeline比裸.pipe()更适合生产代码。- HTTP 响应本身也是 Writable Stream。
背压机制的内部实现
highWaterMark(高水位线)是触发流控的阈值,官方默认值:
| 流类型 | 默认 highWaterMark | 说明 |
|---|---|---|
| 字节流(Buffer 模式) | 64 KiB(65536 字节) | Node.js 22.0.0 起统一上调(此前可读流多为 16 KiB);fs.createReadStream 更早已是 64 KiB |
objectMode 流 | 16 | 单位是"对象个数"而非字节 |
一次完整的背压握手:
- 消费者向 Writable
write(chunk);若内部缓冲达到高水位,write()返回false——这是建议暂停的信号,数据本身仍被接受。 - 实现方(如
pipe()/pipeline()/ 你自己)收到false后停止继续写,等待 Writable 发出'drain'事件(缓冲降到高水位以下时触发)再继续。 - 对可读端:
read()返回null表示缓冲已抽空;流自动停止向_read()要数据,直到消费推进。 - Transform 处于中间:它的可读缓冲满了以后,
write()会返回false,把压力逐级反向传播回源头,整条链路自然减速到最慢一环的速率。
js
// 不用 pipeline 时,正确的背压手写姿势
let flowings = true
writer.on('drain', () => {
flowings = true
pump()
})
function pump() {
while (flowings) {
const chunk = reader.read()
if (chunk === null) {
reader.once('readable', pump)
return
}
flowings = writer.write(chunk)
}
}⚠️ 常见错误与后果:
- 忽略
write()返回值持续写入 → 数据堆积在 writer 内存缓冲 → RSS 无上限增长,最终 OOM。 - 在
data事件里await异步操作(如逐条写库)→data事件不会等待,读取端仍全速推送;应改用 async 迭代器for await (const chunk of reader),它天然按背压节奏取数。 - 高水位设太大 → 吞吐略升但内存与延迟变差;设太小 → 频繁 pause/drain 开销。多数场景保持默认,用压测调整。
- 💡 zlib 这类 Transform 的输入输出块大小不成比例(压缩率高时输出远小于输入),链路中出现压缩/解压环节时适当给 Transform 更高的高水位。
5.3 性能分析与诊断
性能问题常见来源:
- 事件循环阻塞。
- 慢数据库查询。
- N+1 查询。
- 大对象 JSON 序列化。
- 内存泄漏。
- 日志过多。
- 同步文件 I/O。
- 过多正则回溯。
- 依赖库体积和启动慢。
基础观察:
js
import { performance } from 'node:perf_hooks'
const start = performance.now()
await doSomething()
const end = performance.now()
console.log(`cost: ${end - start}ms`)查看内存:
js
console.log(process.memoryUsage())常见字段:
rss:进程占用的常驻内存。heapTotal:V8 堆总大小。heapUsed:V8 堆已使用大小。external:V8 管理之外的内存,例如 Buffer。
调试工具:
- Chrome DevTools Inspector。
node --inspect。node --inspect-brk。- flamegraph。
- heap snapshot。
- clinic.js。
- autocannon。
启动调试:
bash
node --inspect src/server.js压测:
bash
npx autocannon http://localhost:3000/health优化原则:
- 先测量,再优化。
- 先定位瓶颈,再改代码。
- 不要凭感觉优化。
- 优先优化高频路径。
- 给关键接口加超时、日志和指标。
内存管理与泄漏诊断
V8 的堆内存分代回收是理解 Node 内存行为的底座:
text
process.memoryUsage() 各字段与内存布局的对应关系:
heapUsed / heapTotal → V8 托管堆(new space 新生代 + old space 老生代)
external → V8 管理之外的内存(如 Buffer 底层内存由内核 API 分配)
rss → 进程占用的物理内存总和(含代码段、栈、native 层,永远 ≥ heapUsed)
arrayBuffers → ArrayBuffer/Buffer 的字节数(计入 external)- 新生代(new space):短命对象,Scavenge 算法(复制式回收),廉价高频。
- 老生代(old space):熬过多次新生代回收的对象晋升至此,Mark-Sweep + Mark-Compact(标记清除/整理)。
- 关键推论:把本应短命的对象长期挂在模块级 Map/数组/闭包里 → 晋升老生代 → Full GC 变频繁、停顿变长 → 表现为"P99 毛刺 + 内存缓慢上涨"。
诊断工具链(按侵入性从低到高):
| 手段 | 用途 | 示例 |
|---|---|---|
process.memoryUsage() 定时采样 | 看趋势:heapUsed 是否只涨不跌 | 每 30s 打点,接指标系统 |
v8.getHeapStatistics() / --trace-gc | 看 GC 频率、堆上限、晋升情况 | node --trace-gc app.js |
node --heap-profile / DevTools heap snapshot | 抓两三次快照做 diff,找增长对象保留链 | node --inspect 后 Chrome devtools:// Memory 面板 |
worker.getHeapSnapshot() / v8.writeHeapSnapshot() | 生产环境按需落盘快照 | 触发阈值时自动 dump |
node --cpu-prof | CPU 采样性能剖析(找事件循环阻塞热点) | 生成 .cpuprofile 用 DevTools 打开 |
| diagnostic report | 崩溃/卡死时的进程级现场快照 | process.report.writeReport(),含堆栈、资源、水位 |
js
// 主动触发一次堆快照文件(配合 --heapsnapshot-near-heap-limit 可自动在逼近上限时输出)
import v8 from 'node:v8'
v8.writeHeapSnapshot() // 生成 isolate-*.heapsnapshot,拖进 DevTools 分析
// 不落地快照、直接在堆上统计某构造函数的存活对象数(适合线上周期采样做泄漏回归测试)
import { queryObjects } from 'node:v8'
console.log(queryObjects(MySession)) // 存活对象数量
console.log(queryObjects(MySession, { format: 'summary' })) // 每个对象的简要字符串表示⚠️ 泄漏高发点清单:
- 模块级
Map/Array当缓存用但没有 TTL/上限 → 用lru-cache或WeakMap(键可被回收)。 EventEmitter监听器只加不减(每个请求都on())→ 官方 10 个监听器告警就是提示;长生命周期对象上注册匿名监听是最隐蔽的一种。- 闭包意外捕获大对象(错误对象带
req/res上下文被长期持有)。 - 未
destroy()的流、未unref()的定时器/句柄让进程和内存都"下不了班"。 - 全局 try/catch 里把 request 上下文塞进全局数组"稍后上报",上报失败就一直攒。
--max-old-space-size(单位 MB)只是上限不是调优手段:容器里先确保限制 ≥ 该值 + Buffer/native 开销,否则表现为 OOM Killer 杀进程而 heapUsed 看起来正常(rss ≠ heapUsed 的坑)。
💡 判别"泄漏 vs 正常占用":让服务稳定承载恒定流量 30–60 分钟,每次 Full GC 后的 heap 低点若持续抬升,才叫泄漏;低点平稳但高点冲高,通常是分配速率问题(优化对象复用/减少解析),不是泄漏。
5.4 安全最佳实践
Node.js 服务端常见风险:
- 注入攻击。
- XSS。
- CSRF。
- SSRF。
- 路径穿越。
- 反序列化风险。
- 依赖供应链风险。
- 明文密钥泄露。
- 请求体过大导致内存压力。
- 日志泄露敏感信息。
输入校验:
js
const schema = z.object({
id: z.string().uuid(),
})路径穿越防护:
js
import path from 'node:path'
const root = path.resolve('./uploads')
const target = path.resolve(root, userInput)
if (!target.startsWith(root)) {
throw new Error('非法路径')
}HTTP 安全头:
js
import helmet from 'helmet'
app.use(helmet())限流:
js
import rateLimit from 'express-rate-limit'
app.use(rateLimit({
windowMs: 60 * 1000,
max: 100,
}))依赖安全:
bash
npm audit安全原则:
- 不信任用户输入。
- 不拼接 SQL。
- 不拼接 shell 命令。
- 不把密钥写进源码。
- 不把敏感信息写进日志。
- 上传文件要限制大小和类型。
- 管理接口要鉴权和审计。
- 生产环境不要暴露详细错误堆栈给用户。
5.5 进程管理、部署与 Docker
生产服务必须考虑:
- 如何启动。
- 如何重启。
- 如何优雅关闭。
- 如何读取配置。
- 如何记录日志。
- 如何健康检查。
- 如何水平扩容。
优雅关闭:
js
const server = app.listen(3000)
async function shutdown(signal) {
console.log(`received ${signal}`)
server.close(async () => {
await closeDatabase()
process.exit(0)
})
setTimeout(() => {
process.exit(1)
}, 10_000).unref()
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)为什么需要优雅关闭:
- 停止接收新请求。
- 等待正在处理的请求结束。
- 关闭数据库连接。
- 关闭队列消费者。
- 避免数据写一半进程退出。
Dockerfile 示例:
dockerfile
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]生产建议:
- 使用 LTS 基础镜像。
- 不要在镜像中写死密钥。
- 使用
npm ci保证依赖一致。 - 容器中尽量单进程。
- 通过平台做重启和扩容。
- 提供
/health健康检查。
进程管理方式:
- systemd。
- PM2。
- Docker。
- Kubernetes。
- 云平台进程管理。
5.6 监控、链路追踪与可观测性
可观测性包括:
- Logs:日志。
- Metrics:指标。
- Traces:链路追踪。
关键指标:
- QPS。
- 响应时间。
- 错误率。
- 事件循环延迟。
- CPU。
- 内存。
- GC。
- 数据库连接数。
- 队列积压。
请求 ID:
js
import { randomUUID } from 'node:crypto'
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || randomUUID()
res.setHeader('x-request-id', req.id)
next()
})链路追踪常见方案:
- OpenTelemetry。
- Jaeger。
- Zipkin。
- 云厂商 APM。
告警建议:
- 错误率超过阈值告警。
- P95/P99 响应时间异常告警。
- 内存持续增长告警。
- 队列积压告警。
- 数据库连接池耗尽告警。
5.7 高级架构:Monorepo、微服务、Serverless
Monorepo
适合:
- 多个服务共享类型。
- 前后端共享工具。
- 多包协作开发。
- 统一版本和规范。
常见工具:
- pnpm workspace。
- npm workspace。
- Turborepo。
- Nx。
注意:
- 包边界要清晰。
- 构建缓存要配置好。
- 共享包不要反向依赖业务包。
- TypeScript paths 和 package exports 要统一。
微服务
适合:
- 团队规模较大。
- 业务边界清晰。
- 服务需要独立扩展。
- 发布节奏不同。
代价:
- 网络调用复杂。
- 数据一致性复杂。
- 链路追踪必不可少。
- 部署和监控成本上升。
不要为了“高级”拆微服务。单体结构清晰、模块边界合理,很多时候更适合早期项目。
Serverless
适合:
- 事件驱动任务。
- 低频 API。
- 图片处理。
- Webhook。
- 定时任务。
注意:
- 冷启动。
- 运行时间限制。
- 本地调试复杂。
- 数据库连接管理。
- 平台绑定。
5.8 常见问题与排查思路
端口被占用
现象:
text
EADDRINUSE: address already in use :::3000处理:
- 换端口。
- 找到并停止占用端口的进程。
- 开发环境读取
PORT环境变量。
Cannot use import statement outside a module
原因:
- 当前文件被当成 CommonJS 执行。
- 但代码使用了 ESM
import。
解决:
- 在
package.json设置"type": "module"。 - 或把文件改成
.mjs。 - 或改用
require。
ERR_REQUIRE_ESM
原因:
- CommonJS 项目使用
require加载了只支持 ESM 的包。
解决:
- 改成 ESM 项目。
- 使用动态
import()。 - 换兼容 CJS 的旧版本依赖。
线上内存持续增长
排查:
- 看
process.memoryUsage()。 - 抓 heap snapshot。
- 检查全局缓存是否无限增长。
- 检查 EventEmitter 监听器是否不断增加。
- 检查日志、队列、连接是否未释放。
请求偶发超时
排查:
- 数据库慢查询。
- 外部 API 无超时。
- 事件循环阻塞。
- 连接池耗尽。
- 队列积压。
- CPU 使用率过高。
文件路径在 Windows 正常,Linux 报错
原因:
- 路径大小写问题。
- 手写路径分隔符。
- 部署环境文件不存在。
建议:
- 使用
path.join。 - 保持文件名大小写一致。
- CI 中在 Linux 环境跑测试。
5.9 推荐学习顺序
第一阶段:会运行
目标:
- 会安装 Node.js LTS。
- 会运行 JS 文件。
- 会使用 npm scripts。
- 理解 package.json。
- 理解 CJS 和 ESM 基本差异。
练习:
- 写一个读取文件的脚本。
- 写一个命令行参数解析小工具。
- 写一个最小 HTTP 服务。
第二阶段:会使用核心模块
目标:
- 掌握
fs、path、url。 - 理解 Buffer。
- 会使用 Stream 处理大文件。
- 会使用
http和fetch。
练习:
- 写一个日志压缩脚本。
- 写一个静态文件服务器。
- 写一个调用第三方 API 的脚本。
第三阶段:会写 Web API
目标:
- 会使用 Express 或 Fastify。
- 理解中间件。
- 会做参数校验。
- 会连接数据库。
- 会做统一错误处理。
练习:
- 写一个用户 CRUD API。
- 加入登录认证。
- 加入分页、搜索、错误码。
第四阶段:会工程化
目标:
- 接入 TypeScript。
- 接入 ESLint 和 Prettier。
- 编写测试。
- 设计项目结构。
- 接入日志。
练习:
- 用 TypeScript 重构 API。
- 给核心 service 写测试。
- 加入 requestId 和结构化日志。
第五阶段:会生产化
目标:
- 理解优雅关闭。
- 会 Docker 部署。
- 会配置环境变量。
- 会处理安全问题。
- 会做监控和性能排查。
练习:
- 写 Dockerfile。
- 加健康检查。
- 加限流和安全头。
- 用压测工具测试接口。
第六阶段:会高级架构
目标:
- 理解事件循环和背压。
- 会定位内存泄漏和性能瓶颈。
- 理解队列、任务、微服务。
- 理解 Monorepo 和 Serverless 场景。
练习:
- 写一个队列任务系统。
- 写一个大文件上传/下载服务。
- 做一次 heap snapshot 分析。
- 设计一个模块化后端项目。
5.10 学习资源
- Node.js 官方文档:https://nodejs.org/docs/latest/api/
- Node.js Learn:https://nodejs.org/learn
- Node.js Releases:https://nodejs.org/en/about/previous-releases
- Node.js Event Loop:https://nodejs.org/learn/asynchronous-work/event-loop-timers-and-nexttick
- Node.js Test Runner:https://nodejs.org/docs/latest/api/test.html
- Node.js Streams:https://nodejs.org/docs/latest/api/stream.html
- Node.js Worker Threads:https://nodejs.org/docs/latest/api/worker_threads.html
- Node.js Security Best Practices:https://nodejs.org/learn/getting-started/security-best-practices
- Express:https://expressjs.com/
- Fastify:https://fastify.dev/
- NestJS:https://nestjs.com/
- Prisma:https://www.prisma.io/
- TypeScript:https://www.typescriptlang.org/
- OpenTelemetry:https://opentelemetry.io/
建议不要只看文档。最有效的学习方式是:
- 写一个脚本。
- 写一个 HTTP 服务。
- 写一个带数据库的 API。
- 加上日志、测试、参数校验和错误处理。
- Docker 部署。
- 压测并排查性能问题。
- 回头再看事件循环、Stream、worker_threads 和安全最佳实践。