Skip to content

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 基础 ​

  1. Node.js 是什么
  2. Node.js 和浏览器 JavaScript 的区别
  3. 版本选择与环境安装
  4. 运行脚本、REPL 与 npm scripts
  5. package.json 系统讲解
  6. CommonJS 与 ES Modules
  7. Node.js 中的全局对象
  8. 异步编程基础
  9. 错误处理基础

第二篇:Node.js 核心模块 ​

  1. path 与 URL
  2. fs 文件系统
  3. Buffer 与二进制数据
  4. Stream 流
  5. events 事件系统
  6. http、https 与 fetch
  7. crypto 加密与哈希
  8. child_process、worker_threads 与 cluster
  9. timers、AbortController 与取消控制

第三篇:Web 服务端开发 ​

  1. 从原生 HTTP 到框架
  2. Express、Koa、Fastify 如何选择
  3. 路由、中间件与请求生命周期
  4. 配置管理与环境变量
  5. 参数校验与错误响应
  6. 数据库访问与 ORM
  7. 认证、授权与会话
  8. 文件上传与静态资源

第四篇:工程化与质量保障 ​

  1. 项目结构设计
  2. TypeScript 在 Node.js 中的使用
  3. ESLint、Prettier 与代码规范
  4. 测试:node:test、Jest、Vitest
  5. 日志系统
  6. 接口文档与 OpenAPI
  7. 任务调度、队列与后台任务

第五篇:高级阶段 ​

  1. 事件循环深入理解
  2. Stream 背压与大文件处理
  3. 性能分析与诊断
  4. 安全最佳实践
  5. 进程管理、部署与 Docker
  6. 监控、链路追踪与可观测性
  7. 高级架构:Monorepo、微服务、Serverless
  8. 常见问题与排查思路
  9. 推荐学习顺序
  10. 学习资源

第一篇: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,但运行环境不同。

浏览器中有:

  • window
  • document
  • DOM API
  • BOM API
  • localStorage
  • sessionStorage
  • Web APIs

Node.js 中有:

  • globalThis
  • process
  • Buffer
  • fs
  • path
  • http
  • crypto
  • stream
  • child_process
  • worker_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 LTS
  • v26.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 24

package.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
node

REPL 适合:

  • 快速试验表达式。
  • 验证小段代码。
  • 查看内置对象。

执行一段命令:

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 3000
js
console.log(process.argv)

输出大致包含:

  • Node 可执行文件路径。
  • 当前脚本路径。
  • 用户传入的参数。

真实 CLI 项目通常使用:

  • commander
  • cac
  • yargs
  • minimist

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 的可读流为例,底层实际发生了这些事:

  1. createReadStream() 不会立刻读文件,只是登记了 path、start、end 和 highWaterMark。
  2. 事件循环进入 poll 阶段后,流才在 libuv 线程池中发起一次 read,把最多一个 chunk(默认大小见 highWaterMark)读进内核→用户态缓冲区,再作为 data 事件推给 JS。
  3. 每当一个 chunk 被消费掉,流会自动再发起下一块读取——永远只有有限块数据驻留在内存里,这就是它能处理 GB 级文件的原因。
  4. 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 池时官方建议配合 AsyncResource API,让诊断工具能把任务与回调关联起来(异步堆栈跟踪)。

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_threadsclusterchild_process.fork
隔离级别同进程内线程(独立 V8 Isolate/堆)独立 OS 进程独立 OS 进程
解决的问题单线程被 CPU 计算卡住单进程用不满多核运行外部命令/隔离崩溃域
通信成本postMessage 结构化克隆,可 transferList 零拷贝IPC(消息序列化)IPC(消息序列化)
共享状态不共享 JS 对象;可共享 SharedArrayBuffer + Atomics不共享,靠外部存储(Redis 等)不共享
一个单元崩溃的影响未处理错误可拖垮整个进程其余 worker 不受影响,primary 可 respawn不影响父进程
内存开销每 Worker 一个 V8 堆每进程一个完整运行时同 cluster
典型场景图像/压缩/加解密/大 JSONI/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 路由、中间件与请求生命周期 ​

一次请求通常经历:

  1. 进入服务器。
  2. 经过全局中间件。
  3. 匹配路由。
  4. 执行业务中间件。
  5. 执行控制器逻辑。
  6. 访问数据库或外部服务。
  7. 返回响应。
  8. 记录日志和指标。

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 -D

tsconfig.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 -D

ESLint 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/OOS 内核异步机制(Linux epoll、macOS kqueue、Windows IOCP),不占线程池http、net、tls、数据库查询
文件 I/O、DNS lookup、部分 crypto(如 scrypt/pbkdf2/randomBytes)、zliblibuv 线程池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单位是"对象个数"而非字节

一次完整的背压握手:

  1. 消费者向 Writable write(chunk);若内部缓冲达到高水位,write() 返回 false——这是建议暂停的信号,数据本身仍被接受。
  2. 实现方(如 pipe() / pipeline() / 你自己)收到 false 后停止继续写,等待 Writable 发出 'drain' 事件(缓冲降到高水位以下时触发)再继续。
  3. 对可读端:read() 返回 null 表示缓冲已抽空;流自动停止向 _read() 要数据,直到消费推进。
  4. 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-profCPU 采样性能剖析(找事件循环阻塞热点)生成 .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 的旧版本依赖。

线上内存持续增长 ​

排查:

  1. 看 process.memoryUsage()。
  2. 抓 heap snapshot。
  3. 检查全局缓存是否无限增长。
  4. 检查 EventEmitter 监听器是否不断增加。
  5. 检查日志、队列、连接是否未释放。

请求偶发超时 ​

排查:

  1. 数据库慢查询。
  2. 外部 API 无超时。
  3. 事件循环阻塞。
  4. 连接池耗尽。
  5. 队列积压。
  6. 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 学习资源 ​

建议不要只看文档。最有效的学习方式是:

  1. 写一个脚本。
  2. 写一个 HTTP 服务。
  3. 写一个带数据库的 API。
  4. 加上日志、测试、参数校验和错误处理。
  5. Docker 部署。
  6. 压测并排查性能问题。
  7. 回头再看事件循环、Stream、worker_threads 和安全最佳实践。

基于 VitePress 构建的个人学习文档知识库