Node.js Design Patterns
打牢 Node.js 底层心智模型,为 Electron 主进程开发提供理论基础。
前四章 = 平台原理 → 模块系统 → 异步基础 → 回调控制流,是全书的地基。
Chapter 1 — The Node.js Platform
1.1 Node.js 哲学(The Node.js Philosophy)
- Small core:核心库极小,其余交给 npm 生态
- Small modules:每个模块只做一件事(Unix 哲学)
- Small surface area:暴露最少的 API,功能扩展由使用者组合
- Simplicity and pragmatism:务实优先于完美
Electron 主进程 = Node.js 环境,这套哲学决定了为什么你不应该在主进程里堆砌逻辑,而是通过 IPC 把职责分散到各模块。
1.2 Node.js 工作原理
I/O 的本质问题
| I/O 类型 | 延迟量级 |
|---|---|
| 内存访问 | 纳秒 |
| 磁盘读写 | 毫秒 |
| 网络请求 | 数十~数百毫秒 |
传统服务器(Apache):每请求一个线程 → 大量线程阻塞等 I/O → 资源浪费
非阻塞 I/O & 事件多路复用(Event Demultiplexing)
┌──────────────────────────────────────────────────┐
│ 应用层发起多个 I/O 请求(非阻塞) │
│ ↓ │
│ 交给操作系统 I/O 多路复用器(epoll/kqueue/IOCP) │
│ ↓(等待事件就绪) │
│ 就绪事件放入 Event Queue │
│ ↓ │
│ Event Loop 逐一取出并执行回调 │
└──────────────────────────────────────────────────┘
Reactor 模式
- 应用程序提交 I/O 操作,同时提供 handler(回调)
- libuv 把请求传给操作系统
- I/O 完成后,事件放入队列
- Event Loop 调用对应 handler
- handler 执行完毕,控制权归还 Event Loop
Node.js 是单线程事件循环,但 I/O 是由 libuv 的线程池(默认 4 线程)在后台处理的。「单线程」只指 JavaScript 执行环境。
1.3 Node.js 技术栈
JavaScript 代码
↓
V8(JavaScript 引擎)
↓
Node.js Bindings(C++ 层)
↓
libuv(跨平台异步 I/O)+ 核心 C 库
↓
操作系统
- V8:执行 JS,JIT 编译
- libuv:封装 epoll/kqueue/IOCP,提供线程池、定时器、信号处理
- 核心模块:
fsnethttppathcrypto等
Chapter 2 — The Module System
2.1 为什么需要模块
- 避免全局作用域污染
- 明确依赖关系
- 封装实现细节,暴露接口
2.2 CommonJS(CJS)
基本用法
// math.js — 导出
function add(a, b) { return a + b }
module.exports = { add }
// main.js — 导入
const { add } = require('./math')
解析算法(Resolving Algorithm)
require('X') 的查找顺序:
- 核心模块(
fs、path等)→ 直接返回 - 以
./或../开头 → 文件路径解析- 尝试
X、X.js、X.json、X.node - 尝试
X/index.js
- 尝试
- 否则 → 逐级向上查找
node_modules/X
模块缓存(Module Cache)
require('./foo') === require('./foo') // true — 同一对象引用
- 第一次
require执行并缓存,后续返回缓存 - 单例效果:导出的对象在整个进程中共享
循环依赖(Circular Dependencies)
不会报错——Node.js 用「部分导出快照」打破循环,静默返回对方当前已有的 exports:
1. 开始加载 a.js,注册空 exports {}
2. a.js require('./b') → 开始加载 b.js
3. b.js require('./a') → 检测到循环,返回当前 a 的 exports:{}
4. b.js 执行完毕,exports = { name: 'b' }
5. 回到 a.js,b = { name: 'b' },继续执行
6. a.js 执行完毕,exports = { name: 'a' }
// a.js
const b = require('./b')
console.log('a 拿到 b.name:', b.name) // 'b' ← 正常
exports.name = 'a'
// b.js
const a = require('./a')
console.log('b 拿到 a.name:', a.name) // undefined ← 此时 a 还没 export
exports.name = 'b'
循环依赖时,被 require 的模块拿到的是对方「还没导出完」的版本。不报错只报 undefined——这是最难查的 bug 之一。
ESM 的循环依赖
ESM 有静态链接阶段,导出的是活绑定(live binding)而非值拷贝。若被引用的变量在使用前已初始化则正常,否则仍是 undefined。同样不报错,行为更难预测。
如何发现循环依赖
工具检测(推荐):
# madge — 可视化依赖图,专门查循环
npx madge --circular src/index.js
# 输出示例:
# Found 2 circular dependencies!
# 1) src/a.js -> src/b.js -> src/a.js
# 生成 SVG 依赖图
npx madge --image graph.svg src/index.js
# dpdm — 支持 TypeScript,速度更快
npx dpdm --no-warning --no-tree src/index.ts
ESLint 实时提示:
{ "rules": { "import/no-cycle": ["error", { "maxDepth": 10 }] } }
运行时快速排查——看到莫名 undefined 时:
const a = require('./a')
console.log('[debug] a loaded:', Object.keys(a)) // 如果是 {} 说明循环了
根治方法
| 方案 | 适用场景 |
|---|---|
| 提取公共模块 | A 和 B 互相依赖 → 把共享部分移到 C |
| 依赖注入 | A 不直接 require B,而是接收 B 作为参数 |
| 懒加载 | 在函数内部 require,而非模块顶层 |
| 重新设计分层 | 根本原因是两个模块职责边界不清 |
2.3 ES Modules(ESM)
// 命名导出
export function add(a, b) { return a + b }
export const PI = 3.14
// 默认导出
export default class Calculator { ... }
// 导入
import { add, PI } from './math.js'
import Calculator from './calculator.js'
// 动态导入(异步)
const { add } = await import('./math.js')
ESM vs CJS 关键差异
| CJS | ESM | |
|---|---|---|
| 加载时机 | 运行时(动态) | 静态分析(编译时) |
this 顶层 |
module.exports |
undefined |
__dirname / __filename |
✅ | ❌(用 import.meta.url) |
| Tree-shaking | ❌ | ✅ |
| 互操作 | CJS 可 require CJS |
ESM 可 import CJS(反之有限制) |
2.4 常用模块模式
// 1. 命名导出 — 多个工具函数
export const add = (a, b) => a + b
export const sub = (a, b) => a - b
// 2. 导出类 — 需要多实例
export class Database { constructor(url) {...} }
// 3. 导出实例(单例)— 共享状态
class Logger { log(msg) {...} }
export default new Logger() // 整个进程共一个
// 4. Monkey Patching(慎用)
import fs from 'fs'
const originalReadFile = fs.readFile
fs.readFile = (...args) => { /* 包装逻辑 */ originalReadFile(...args) }
Chapter 3 — Callbacks and Events
3.1 Callback 模式
延续传递风格(Continuation-Passing Style,CPS)
// 直接风格(Direct Style)
function add(a, b) { return a + b }
// CPS 风格 — 结果通过回调传递
function addCPS(a, b, callback) { callback(a + b) }
// 异步 CPS
function readFileCPS(path, callback) {
fs.readFile(path, (err, data) => callback(err, data))
}
Node.js 错误优先约定(Error-First Callback)
fs.readFile('file.txt', (err, data) => {
if (err) {
// 处理错误,return 避免继续执行
return console.error(err)
}
console.log(data)
})
callback的第一个参数永远是err(无错误则为null)- 回调要么同步执行,要么永远异步执行——不能混用
- 不能在回调内部抛出异常(
throw会逃逸 Event Loop,导致进程崩溃)
未捕获异常
process.on('uncaughtException', (err) => {
console.error('Uncaught:', err)
process.exit(1) // 必须退出,状态已不可预测
})
3.2 Observer 模式 — EventEmitter
import { EventEmitter } from 'events'
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter()
// 注册监听
emitter.on('data', (chunk) => console.log('收到:', chunk))
emitter.once('end', () => console.log('结束')) // 只触发一次
// 触发事件
emitter.emit('data', 'hello')
emitter.emit('end')
错误处理
// 必须监听 error 事件,否则会抛出异常
emitter.on('error', (err) => console.error('错误:', err))
同步 vs 异步事件
// ❌ 危险:同步 emit 在 on() 注册前触发
const emitter = new EventEmitter()
emitter.emit('ready') // 此时还没有监听器
emitter.on('ready', () => console.log('ready')) // 永远不会触发
// ✅ 正确:用 process.nextTick 推迟到下一个 tick
class MyStream extends EventEmitter {
constructor() {
super()
process.nextTick(() => this.emit('ready')) // 确保监听器已注册
}
}
Callback vs EventEmitter 选择指南
| 场景 | 选择 |
|---|---|
| 结果只有一次(成功/失败) | Callback / Promise |
| 事件可能触发多次 | EventEmitter |
| 多个监听者 | EventEmitter |
| 需要解耦生产者与消费者 | EventEmitter |
Chapter 4 — Asynchronous Control Flow with Callbacks
4.1 Callback Hell(回调地狱)
// ❌ 典型的回调地狱 — 难以阅读、难以维护
fs.readFile('a.txt', (err, a) => {
if (err) return handleErr(err)
fs.readFile('b.txt', (err, b) => {
if (err) return handleErr(err)
fs.writeFile('out.txt', a + b, (err) => {
if (err) return handleErr(err)
console.log('done')
})
})
})
4.2 最佳实践(逃离回调地狱)
- 提前返回:
if (err) return callback(err)— 减少嵌套 - 命名函数:把匿名回调抽成有名字的函数
- 模块化:拆分成小函数,每个只做一件事
- 使用 async/await(现代 Node.js 首选,见第5章)
// ✅ 拆分命名函数
function onWriteDone(err) {
if (err) return handleErr(err)
console.log('done')
}
function onReadB(err, a, b) {
if (err) return handleErr(err)
fs.writeFile('out.txt', a + b, onWriteDone)
}
function onReadA(err, a) {
if (err) return handleErr(err)
fs.readFile('b.txt', (err, b) => onReadB(err, a, b))
}
fs.readFile('a.txt', onReadA)
4.3 顺序执行(Sequential Execution)
任务一个接一个,前一个完成才开始下一个:
function executeSequentially(tasks, callback) {
function iterate(index) {
if (index === tasks.length) return callback()
tasks[index](() => iterate(index + 1))
}
iterate(0)
}
适用场景:任务间有依赖关系,或需要控制执行顺序。
4.4 顺序迭代(Sequential Iteration)
对集合中每个元素依次异步处理:
function processItems(items, asyncTask, callback) {
function next(index) {
if (index === items.length) return callback()
asyncTask(items[index], (err) => {
if (err) return callback(err)
next(index + 1)
})
}
next(0)
}
4.5 并行执行(Parallel Execution)
所有任务同时启动,全部完成后回调:
function executeParallel(tasks, callback) {
let completed = 0
let hasErrors = false
tasks.forEach(task => {
task((err) => {
if (hasErrors) return
if (err) { hasErrors = true; return callback(err) }
if (++completed === tasks.length) callback()
})
})
}
并行任务共享变量时要注意写冲突。Node.js 单线程不存在真正的并发写,但异步交错仍可导致逻辑错误。
适用场景:任务间无依赖,追求最短完成时间。
4.6 有限并行(Limited Parallel Execution)
防止同时发起过多请求(如文件句柄、HTTP 连接耗尽):
function limitedParallel(tasks, concurrency, callback) {
let running = 0, index = 0, completed = 0
function next() {
while (running < concurrency && index < tasks.length) {
const task = tasks[index++]
running++
task(() => {
running--
completed++
if (completed === tasks.length) return callback()
next()
})
}
}
next()
}
生产代码中直接用 p-limit(npm)或 Promise.all + 信号量,不需要手写。
四章总结 & 知识地图
平台原理 (Ch.1)
└─ 单线程事件循环 + libuv 线程池
└─ Reactor 模式:所有异步 I/O 的根基
模块系统 (Ch.2)
├─ CJS:运行时加载、缓存单例、循环依赖陷阱
└─ ESM:静态分析、tree-shaking、动态 import
异步原语 (Ch.3)
├─ Callback + 错误优先约定
└─ EventEmitter + Observer 模式
控制流 (Ch.4)
├─ 顺序:任务串联,保序
├─ 并行:全部并发,最快完成
└─ 限流并行:并发上限,保护资源
Ch.5 开始进入 Promise & async/await,Ch.6 是 Stream——这两章和前四章同等重要,建议连续阅读。
相关笔记
- Electron — 本书 Ch.1 的事件循环是 Electron 主进程的底层
- Electron - 内建浏览器(Built-In Browser) — WebContentsView 的异步架构即基于 Ch.3-4 原理