Appearance
Node.js
导航目录
一、基础篇
二、异步核心篇
三、模块与 I/O 篇
四、进程与并发篇
五、工程实践篇
Node.js 核心特性
非阻塞式 I/O 模型
核心概念
Node.js 的非阻塞式 I/O 模型是其高性能的关键,基于事件驱动的异步架构,能够高效处理大量并发连接。
- 基于事件驱动的异步 I/O 架构
- 单线程处理高并发连接(避免多线程开销)
- 适合 I/O 密集型应用:API 服务、实时聊天、数据流处理
- 示例:同时处理 10,000 个并发连接仅需约 30MB 内存
为什么 Node.js 适合处理高并发
1. 传统的多线程/多进程模型(对比模型)
想象一个银行,每个客户(请求)都需要一个柜员(线程)来专门服务。
- 工作方式:每当一个新客户到来(一个新请求),银行就开辟一个新的服务窗口,并分配一个专门的柜员(创建一个新线程或从线程池中取一个)。这个柜员会全程服务这位客户,包括处理他的业务(CPU 计算)和等待他填写表格(I/O 等待,如查询数据库、读写文件、调用外部 API)。
- 问题:当客户在慢吞吞地填表时(I/O 等待),这个柜员只能闲着,什么也做不了,但他却占着一个窗口(占用着系统内存和 CPU 调度资源)。如果同时来了成千上万个客户,就会导致巨大的开销:
- 内存开销:每个线程都需要分配独立的内存空间(如堆栈)。
- CPU 上下文切换开销:CPU 需要在成千上万个线程之间不断切换,这个过程本身就很耗时。
2. Node.js 的单线程事件驱动模型(解决方案)
现在,想象一个完全不同的银行,它只有一个"超级柜员"(单线程),但这个柜员效率极高。
- 事件循环 (Event Loop):这个超级柜员有一个永不停止的循环,不断检查任务队列里有没有事情要做。
- 接收请求:客户(请求)到来时快速接待。若只需简单查询(CPU 计算),当场处理完并返回。
- 处理异步 I/O:若需求耗时(如查询数据库),柜员不会傻等,而是说"您先去填个表(发起异步 I/O),填好叫我(注册回调)",然后立刻接待下一位客户。
- 回调函数 (Callback):当数据库返回数据,该"完成事件"被放入任务队列,超级柜员在循环中检查到后执行对应的回调函数。
一句话总结
Node.js 用"单线程 + 事件循环 + 异步 I/O",把线程等待 I/O 的时间省下来处理更多请求,从而以极小的资源开销支撑高并发。
适用场景与选型
核心概念
Node.js 并非万能,它在 I/O 密集型场景表现卓越,但在 CPU 密集型场景需要额外手段(子进程 / 工作线程)来规避事件循环阻塞。
适合的场景
| 场景 | 说明 |
|---|---|
| RESTful API / BFF | 高并发、I/O 为主,天然契合 |
| 实时通信 | WebSocket、聊天室、协同编辑(长连接) |
| 数据流处理 | 大文件上传下载、日志管道(Stream) |
| 微服务网关 | 请求转发、聚合,I/O 密集 |
| SSR 服务端渲染 | 与前端同构,生态统一 |
不擅长的场景
| 场景 | 原因 | 应对方案 |
|---|---|---|
| 大量 CPU 计算 | 单线程会阻塞事件循环 | worker_threads / child_process |
| 图像/视频转码 | 长时间占用 CPU | 交给专门服务或子进程 |
| 复杂科学计算 | 非其强项 | 调用 C++ 插件或其他语言服务 |
关键认知
"Node.js 单线程"指的是执行 JavaScript 的主线程是单线程,但底层 I/O 由 libuv 的线程池异步完成。因此 I/O 不阻塞主线程,而 JS 层的密集计算会阻塞。
模块系统:CommonJS vs ES Module
核心概念
Node.js 同时支持 CommonJS(CJS) 和 ES Module(ESM) 两套模块系统,理解二者差异是避免踩坑的基础。
基本用法对比
js
// CommonJS:Node.js 传统模块规范
const fs = require("fs");
module.exports = { foo };
exports.bar = bar;
// ES Module:ECMAScript 标准(需 .mjs 或 package.json 声明 "type": "module")
import fs from "fs";
export { foo };
export default bar;核心差异
| 维度 | CommonJS | ES Module |
|---|---|---|
| 加载时机 | 运行时同步加载 | 编译时静态分析 |
| 加载方式 | 值的拷贝 | 值的引用(动态绑定) |
| 顶层 await | 不支持 | 支持 |
| Tree-shaking | 不友好 | 友好(静态结构) |
this 指向 | module.exports | undefined |
| 循环依赖 | 返回未完成的部分导出 | 通过引用绑定处理更优雅 |
选型建议
新项目优先使用 ESM(面向未来、可 Tree-shaking);维护旧项目或需要动态 require 时用 CJS。两者可通过 import() 动态导入或 createRequire 互操作。
事件循环机制
核心概念
事件循环(Event Loop)是 Node.js 处理异步操作的核心机制,它通过不断循环检查各阶段的任务队列来处理各种异步事件。底层由 libuv 实现。
js
┌───────────────────────────┐
┌─>│ timers │ 执行 setTimeout/setInterval 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 执行系统操作回调(如 TCP 错误)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ Node 内部使用
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ I/O 事件: │
│ │ poll │<─────┤ 文件/网络操作 │
│ └─────────────┬─────────────┘ └───────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ 执行 setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ 关闭事件回调(如 socket.close)
└───────────────────────────┘六大阶段职责
| 阶段 | 职责 |
|---|---|
| timers | 执行到期的 setTimeout / setInterval 回调 |
| pending callbacks | 执行延迟到下一循环的 I/O 回调(如某些系统错误) |
| idle, prepare | 仅 Node 内部使用 |
| poll | 检索新的 I/O 事件、执行 I/O 回调(事件循环在此等待) |
| check | 执行 setImmediate 回调 |
| close callbacks | 执行关闭事件回调(如 socket.on('close')) |
宏任务、微任务与执行顺序
执行顺序
事件循环的执行顺序决定了不同类型任务的优先级,理解这一点对于编写高性能且行为可预测的 Node.js 应用至关重要。
整体优先级
- 同步代码:优先执行所有同步代码
process.nextTick队列:优先级最高的异步任务- 微任务队列(Promise):优先级高于宏任务
- 进入事件循环各阶段:timers → pending → poll → check → close,每个阶段结束后都会清空 nextTick 与微任务队列
process.nextTick 机制
核心概念
process.nextTick 是 Node.js 特有的异步 API,其回调在当前操作完成后、事件循环继续之前执行,优先级高于 Promise 微任务。
js
process.nextTick(() => console.log("NextTick 1"));
Promise.resolve().then(() => console.log("Promise 1"));
console.log("同步代码");
// 输出:
// 同步代码
// NextTick 1
// Promise 1关键点:
- 执行时机:当前执行栈结束后立即执行
- 优先级:高于微任务(Promise)
- 注意事项:nextTick 队列过长会"饿死"事件循环,阻塞后续阶段
setTimeout vs setImmediate
核心概念
setTimeout(fn, 0) 与 setImmediate(fn) 的执行顺序取决于当前所处的事件循环阶段。
主模块中:顺序不确定
js
// 受进程启动耗时影响,输出顺序可能不同
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));I/O 回调中:顺序确定
js
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate")); // 总是先执行
});原因:I/O 回调在 poll 阶段执行,完成后紧接着进入 check 阶段,因此 setImmediate 总是先于下一轮 timers 的 setTimeout 执行。
Node.js vs 浏览器事件循环
| 特性 | Node.js | 浏览器 |
|---|---|---|
| 宏任务队列 | 多个阶段(timers, poll, check 等) | 单个宏任务队列 |
| 微任务执行时机 | 每个阶段之间执行 | 每个宏任务之后执行 |
setImmediate | 支持 | 不支持 |
process.nextTick | 支持 | 不支持 |
requestAnimationFrame | 不支持 | 支持 |
核心区别
浏览器是"一个宏任务 → 清空微任务 → 渲染"的简单循环;Node.js 是"多阶段循环,每阶段之间清空 nextTick 与微任务"的复杂模型。这也是同一段异步代码在两端输出顺序可能不同的根源。
异步编程演进:回调 → Promise → async/await
核心概念
Node.js 的异步编程经历了三代演进:回调函数 → Promise → async/await。每一代都在解决前一代的痛点,最终让异步代码拥有接近同步代码的可读性。
第一代:回调函数(Callback)
Node.js 早期约定俗成的 错误优先回调(Error-First Callback) 风格:第一个参数永远是错误对象。
js
const fs = require("fs");
fs.readFile("a.txt", "utf8", (err, data) => {
if (err) return console.error(err);
console.log(data);
});痛点:回调地狱(Callback Hell)——多层嵌套导致代码横向膨胀、难以维护、错误处理分散。
js
fs.readFile("a.txt", "utf8", (err, a) => {
if (err) return handle(err);
fs.readFile("b.txt", "utf8", (err, b) => {
if (err) return handle(err);
fs.readFile("c.txt", "utf8", (err, c) => {
if (err) return handle(err);
console.log(a, b, c); // 层层嵌套,难以阅读
});
});
});第二代:Promise
Promise 将回调转化为链式调用,把嵌套结构"拉平",并统一了错误处理入口 .catch()。
js
const fs = require("fs").promises;
fs.readFile("a.txt", "utf8")
.then((a) => fs.readFile("b.txt", "utf8").then((b) => [a, b]))
.then(([a, b]) => console.log(a, b))
.catch((err) => console.error(err)); // 统一错误处理Promise 的三种状态:pending(进行中)→ fulfilled(已完成)/ rejected(已拒绝),状态一旦改变不可逆。
常用组合方法:
| 方法 | 说明 |
|---|---|
Promise.all | 全部成功才成功,任一失败即失败 |
Promise.race | 返回最先完成(成功或失败)的结果 |
Promise.allSettled | 等待全部完成,返回每个的状态(不会短路) |
Promise.any | 任一成功即成功,全部失败才失败 |
第三代:async/await
async/await 是基于 Promise 的语法糖,让异步代码写起来像同步代码,配合 try/catch 处理错误。
js
const fs = require("fs").promises;
async function readAll() {
try {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
console.log(a, b);
} catch (err) {
console.error(err); // 像同步代码一样捕获异常
}
}
readAll();注意:串行 vs 并行
连续 await 会串行执行。若任务之间没有依赖,应使用 Promise.all 并行执行以提升性能:
js
// ❌ 串行:耗时 = a + b
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
// ✅ 并行:耗时 = max(a, b)
const [a, b] = await Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
]);三代对比总结
| 方案 | 可读性 | 错误处理 | 组合能力 |
|---|---|---|---|
| 回调 | 差 | 分散、易遗漏 | 弱 |
| Promise | 中 | 统一 .catch() | 强 |
| async/await | 好 | try/catch 直观 | 强 |
EventEmitter 事件驱动
核心概念
EventEmitter 是 Node.js 事件驱动架构的基石。核心内置模块(Stream、HTTP、fs 等)几乎都继承自它,通过发布-订阅模式实现模块间的解耦通信。
基本用法
js
const EventEmitter = require("events");
const emitter = new EventEmitter();
// 订阅事件
emitter.on("data", (payload) => {
console.log("收到数据:", payload);
});
// 发布事件
emitter.emit("data", { id: 1, name: "node" });常用 API
| 方法 | 说明 |
|---|---|
on(event, listener) | 注册监听器(可重复触发) |
once(event, listener) | 只监听一次,触发后自动移除 |
emit(event, ...args) | 触发事件并传参 |
off / removeListener | 移除指定监听器 |
removeAllListeners | 移除某事件(或全部)的监听器 |
listenerCount(event) | 获取监听器数量 |
自定义类继承 EventEmitter
实际开发中常通过继承来赋予自定义对象事件能力:
js
const EventEmitter = require("events");
class OrderService extends EventEmitter {
createOrder(order) {
// 业务处理...
this.emit("created", order); // 通知订阅者
}
}
const service = new OrderService();
service.on("created", (order) => {
console.log("发送通知邮件:", order.id);
});
service.createOrder({ id: 1001 });常见问题与注意事项
- 内存泄漏警告:单个事件监听器超过 10 个会触发警告(
MaxListenersExceededWarning),可用setMaxListeners(n)调整,但更应排查是否忘记off。 - 错误事件:
error事件是特殊事件,若emit('error')时没有对应监听器,会直接抛出异常并使进程崩溃。务必监听error。 - 同步执行:
emit是同步调用所有监听器,若需异步应在监听器内使用setImmediate/process.nextTick。
Buffer 与二进制数据
核心概念
Buffer 是 Node.js 用于处理二进制数据的核心类。JavaScript 原生擅长处理字符串,但在文件读写、网络传输、图片/音视频处理等场景需要直接操作字节流,Buffer 就是为此而生。
Buffer 本质是一块固定长度的内存区域,位于 V8 堆外,不受垃圾回收的常规约束。
创建 Buffer
js
// 分配 10 字节并清零(安全)
const buf1 = Buffer.alloc(10);
// 从字符串创建
const buf2 = Buffer.from("hello", "utf8");
// 从数组创建
const buf3 = Buffer.from([0x68, 0x69]); // "hi"安全提示
避免使用已废弃的 new Buffer() 与 Buffer.allocUnsafe()(后者不会清零内存,可能残留敏感数据)。优先使用 Buffer.alloc / Buffer.from。
常用操作
js
const buf = Buffer.from("hello");
buf.toString("utf8"); // "hello",转回字符串
buf.length; // 5,字节长度
buf.toString("hex"); // "68656c6c6f",十六进制
buf.toString("base64"); // base64 编码
// 拼接多个 Buffer
const merged = Buffer.concat([Buffer.from("a"), Buffer.from("b")]);Stream 流与背压
核心概念
Stream(流)用于分块处理数据,无需一次性把全部数据读入内存。处理大文件、网络传输时,流能显著降低内存占用,是 Node.js 高效 I/O 的关键。
四种流类型
| 类型 | 说明 | 示例 |
|---|---|---|
| Readable(可读) | 数据来源 | fs.createReadStream |
| Writable(可写) | 数据目的地 | fs.createWriteStream |
| Duplex(双工) | 可读又可写 | TCP Socket |
| Transform(转换) | 读写并转换数据 | zlib.createGzip |
使用 pipe 处理大文件
对比一次性读取,流式拷贝内存占用几乎恒定:
js
const fs = require("fs");
// ❌ 一次性读入内存,大文件会 OOM
fs.readFile("big.mp4", (err, data) => {
fs.writeFile("copy.mp4", data, () => {});
});
// ✅ 流式处理,内存占用恒定
fs.createReadStream("big.mp4").pipe(fs.createWriteStream("copy.mp4"));背压(Backpressure)
什么是背压
当可读流生产数据的速度 > 可写流消费的速度时,数据会在缓冲区堆积,可能耗尽内存。这就是背压问题。
pipe() 会自动处理背压:当写入端缓冲区满时,它会暂停读取端,待缓冲区排空后再恢复。手动处理时可监听 drain 事件:
js
const canContinue = writable.write(chunk);
if (!canContinue) {
readable.pause(); // 暂停读取
writable.once("drain", () => readable.resume()); // 排空后恢复
}推荐使用 stream.pipeline,它能自动处理背压与错误、清理资源:
js
const { pipeline } = require("stream");
const fs = require("fs");
const zlib = require("zlib");
pipeline(
fs.createReadStream("input.txt"),
zlib.createGzip(),
fs.createWriteStream("input.txt.gz"),
(err) => {
if (err) console.error("处理失败", err);
else console.log("压缩完成");
}
);常用内置模块速览
核心概念
Node.js 提供了丰富的内置模块,无需安装即可使用。熟悉高频模块能大幅提升开发效率。
| 模块 | 用途 | 常用 API |
|---|---|---|
fs | 文件系统操作 | readFile / writeFile / createReadStream |
path | 跨平台路径处理 | join / resolve / basename / extname |
http | 创建 HTTP 服务与客户端 | createServer / request |
url | URL 解析 | new URL() / URLSearchParams |
os | 操作系统信息 | cpus / totalmem / platform |
crypto | 加密、哈希、随机数 | createHash / randomBytes |
util | 实用工具 | promisify / inspect |
events | 事件驱动 | EventEmitter |
stream | 流处理 | pipeline / Readable / Writable |
path:跨平台路径处理
为什么必须用 path
不同操作系统路径分隔符不同(Windows 用 \,Unix 用 /)。手动拼接字符串会导致跨平台 bug,务必使用 path 模块。
js
const path = require("path");
path.join("/foo", "bar", "baz"); // /foo/bar/baz
path.resolve("src", "index.js"); // 转为绝对路径
path.extname("app.config.js"); // ".js"
path.basename("/a/b/c.txt"); // "c.txt"
__dirname; // 当前文件所在目录(CommonJS)
__filename; // 当前文件绝对路径(CommonJS)util.promisify:回调转 Promise
将传统的错误优先回调 API 转换为 Promise,便于 async/await:
js
const util = require("util");
const fs = require("fs");
const readFile = util.promisify(fs.readFile);
const data = await readFile("a.txt", "utf8");进程与线程深度解析
核心概念
Node.js 主线程是单线程的(执行 JS 代码),但底层通过 libuv 线程池、多进程、worker_threads 实现真正的并行。理解进程与线程的区别,是设计高并发架构的前提。
进程 vs 线程
| 对比项 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源 | 独立的内存空间 | 共享所属进程的内存 |
| 通信 | IPC(较重,需序列化) | 共享内存(较轻,需加锁) |
| 开销 | 创建/切换开销大 | 创建/切换开销小 |
| 隔离性 | 强,一个崩溃不影响其他 | 弱,一个崩溃可能拖垮整个进程 |
| 适用 | CPU 隔离、多核利用(Cluster) | CPU 密集计算(worker_threads) |
Node.js 的并发模型
- 单线程执行 JS:所有 JS 代码在主线程运行,避免了线程安全问题。
- libuv 线程池:文件 I/O、DNS、
crypto等由后台线程池(默认 4 个)处理,完成后回调入队。 - 多进程(Cluster):利用多核 CPU,多个进程监听同一端口。
- worker_threads:在同一进程内创建工作线程,适合 CPU 密集任务。
子进程管理
核心概念
child_process 模块允许 Node.js 创建子进程,用于执行系统命令、运行其他脚本或分担 CPU 密集任务。
四种创建方式
| 方法 | 特点 | 适用场景 |
|---|---|---|
exec | 启动 shell 执行命令,结果缓存到回调 | 简单命令、输出量小 |
execFile | 直接执行可执行文件,不经过 shell(更安全) | 执行指定程序 |
spawn | 流式返回输出,不缓存 | 输出量大、长时间运行 |
fork | 专为创建 Node 子进程设计,内置 IPC 通道 | Node 进程间通信 |
fork 与 IPC 通信
fork 会在父子进程间建立 IPC 通道,可通过 send / on('message') 双向通信:
js
// parent.js
const { fork } = require("child_process");
const child = fork("./child.js");
child.send({ task: "compute", n: 40 });
child.on("message", (result) => {
console.log("子进程返回:", result);
});js
// child.js
process.on("message", (msg) => {
if (msg.task === "compute") {
const result = fibonacci(msg.n);
process.send({ result });
}
});
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}应用场景
将 CPU 密集计算(如大数据处理、图像转换)fork 到子进程,可避免阻塞主进程的事件循环,保证服务持续响应。
Cluster 集群实战
核心概念
Node.js 单进程无法利用多核 CPU。cluster 模块通过创建多个共享同一端口的工作进程,充分利用多核性能,是生产环境的标配。
工作原理
┌─────────────┐
│ Master 主进程 │ (负责调度,不处理请求)
└──────┬──────┘
┌────────────────┼────────────────┐
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Worker 1 │ │ Worker 2 │ │ Worker N │ (共享同一端口)
└───────────┘ └───────────┘ └───────────┘主进程负责创建并管理工作进程,请求由操作系统或主进程分发给各工作进程处理。
基本用法
js
const cluster = require("cluster");
const http = require("http");
const os = require("os");
if (cluster.isPrimary) {
const cpuCount = os.cpus().length;
console.log(`主进程启动,创建 ${cpuCount} 个工作进程`);
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
// 工作进程退出时自动重启,保证高可用
cluster.on("exit", (worker) => {
console.log(`工作进程 ${worker.process.pid} 退出,重启中...`);
cluster.fork();
});
} else {
http
.createServer((req, res) => {
res.end(`响应来自工作进程 ${process.pid}`);
})
.listen(3000);
}Cluster vs PM2
生产环境更推荐使用 PM2 进程管理器(pm2 start app.js -i max),它封装了 cluster 并提供负载均衡、日志管理、自动重启、零停机重载等能力,无需手写集群代码。
worker_threads 工作线程
核心概念
worker_threads 在同一进程内创建线程,共享内存(通过 SharedArrayBuffer),比多进程更轻量。它是解决 CPU 密集型任务阻塞事件循环的首选方案。
Cluster vs worker_threads
| 对比项 | Cluster(多进程) | worker_threads(多线程) |
|---|---|---|
| 隔离级别 | 进程级,内存独立 | 线程级,可共享内存 |
| 开销 | 较大 | 较小 |
| 通信 | IPC,需序列化 | 消息传递 + 共享内存 |
| 适用 | 多核扩展 Web 服务 | CPU 密集计算 |
基本用法
js
// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js", {
workerData: { n: 40 },
});
worker.on("message", (result) => {
console.log("计算结果:", result);
});
worker.on("error", (err) => console.error(err));js
// worker.js
const { parentPort, workerData } = require("worker_threads");
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(workerData.n);
parentPort.postMessage(result); // 计算完成后回传结果选型建议
- I/O 密集 → 直接用 Node 异步 API 即可,无需额外线程。
- CPU 密集(加密、压缩、图像处理、大数据计算)→ 用
worker_threads。 - 多核扩展 Web 服务 → 用
cluster或 PM2。
Node.js 框架对比
核心概念
Node.js 生态中主流的 Web 框架各有侧重。理解它们的设计哲学,有助于根据项目规模与团队习惯做出合理选型。
主流框架对比
| 框架 | 特点 | 适用场景 |
|---|---|---|
| Express | 老牌、生态成熟、中间件丰富 | 通用 Web 服务、快速开发 |
| Koa | 洋葱模型、基于 async/await、轻量 | 追求优雅异步流程控制 |
| Egg.js | 基于 Koa、约定优于配置、企业级 | 大型企业应用 |
| NestJS | TypeScript、模块化、依赖注入、类 Angular | 大型可维护的后端架构 |
| Fastify | 高性能、低开销、内置 schema 校验 | 高吞吐 API 服务 |
Koa 洋葱模型
核心思想
Koa 中间件的执行呈"洋葱圈"结构:请求由外向内穿过每一层中间件到达核心,响应再由内向外返回。await next() 是进入下一层的关键。
┌─────────────────────────┐
│ 中间件 1 (前) │
│ ┌───────────────────┐ │
│ │ 中间件 2 (前) │ │
│ │ ┌─────────────┐ │ │
请求 ──▶ │ 核心处理 │ │ │──▶ 响应
│ │ └─────────────┘ │ │
│ │ 中间件 2 (后) │ │
│ └───────────────────┘ │
│ 中间件 1 (后) │
└─────────────────────────┘js
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
console.log("1 前");
await next(); // 进入下一层
console.log("1 后");
});
app.use(async (ctx, next) => {
console.log("2 前");
ctx.body = "hello";
console.log("2 后");
});
app.listen(3000);
// 输出顺序:1 前 → 2 前 → 2 后 → 1 后Koa vs Express
| 对比项 | Express | Koa |
|---|---|---|
| 异步处理 | 回调为主 | async/await 原生支持 |
| 中间件模型 | 线性执行 | 洋葱模型(可控制前后) |
| 体积 | 较大(内置路由等) | 精简(核心极小,按需引入) |
| 错误处理 | 需 next(err) | try/catch 更直观 |
优雅退出与错误处理
核心概念
生产环境的 Node 服务必须处理好进程退出与异常捕获,否则可能导致请求丢失、资源泄漏、进程静默崩溃。
优雅退出(Graceful Shutdown)
收到退出信号时,应停止接收新请求、处理完存量请求、关闭数据库连接后再退出:
js
const server = app.listen(3000);
function gracefulShutdown() {
console.log("收到退出信号,开始优雅关闭...");
server.close(() => {
console.log("HTTP 服务已关闭");
// 关闭数据库连接、清理资源
db.close(() => process.exit(0));
});
// 兜底:10 秒内未关闭则强制退出
setTimeout(() => {
console.error("强制退出");
process.exit(1);
}, 10000);
}
process.on("SIGTERM", gracefulShutdown); // kill 信号
process.on("SIGINT", gracefulShutdown); // Ctrl+C全局异常兜底
js
// 未捕获的同步异常
process.on("uncaughtException", (err) => {
console.error("未捕获异常:", err);
// 记录日志后退出,交由进程管理器重启
process.exit(1);
});
// 未处理的 Promise 拒绝
process.on("unhandledRejection", (reason) => {
console.error("未处理的 Promise 拒绝:", reason);
});重要原则
uncaughtException 触发后进程已处于不确定状态,不应继续运行。正确做法是记录日志 → 优雅关闭 → 退出,再由 PM2/Cluster 拉起新进程,而非"捕获后继续跑"。
性能优化实践
核心概念
Node.js 性能优化围绕不阻塞事件循环与充分利用系统资源两大主线展开。
优化建议清单
- 避免阻塞事件循环:不要在主线程做大量同步计算(如
JSON.parse超大对象、同步加密),CPU 密集任务交给worker_threads。 - 善用流处理:大文件、大响应体使用 Stream,避免一次性载入内存。
- 多核扩展:使用
cluster/ PM2 充分利用多核 CPU。 - 合理缓存:热点数据用 Redis / 内存缓存,减少重复计算与 I/O。
- 连接池:数据库、HTTP 客户端使用连接池,避免频繁建连开销。
- 减少同步 API:优先使用异步版本(
fs.readFile而非fs.readFileSync)。 - 开启 gzip 压缩:减小传输体积。
- 监控与诊断:使用
--prof、clinic.js、0x等工具定位性能瓶颈。
定位阻塞
可通过监控事件循环延迟(perf_hooks 的 monitorEventLoopDelay)来发现是否存在长任务阻塞事件循环。