主题
Node.js / 后端基础
说明
共 15 题,难度 ⭐ ~ ⭐⭐⭐,覆盖 Node.js 事件循环、Stream、中间件、数据库、进程管理、BFF 等后端基础知识,面向 5 年前端进阶全栈方向。
1. Node.js 的事件循环和浏览器有什么区别? ⭐⭐⭐
对比 Node.js 和浏览器的事件循环差异。
考察点:Node.js 事件循环
浏览器事件循环
浏览器 Event Loop(简化版):
┌──────────────────────┐
│ 执行同步代码 │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 清空微任务队列 │ ← Promise.then / queueMicrotask
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 执行一个宏任务 │ ← setTimeout / setInterval / I/O
└──────────┬───────────┘
↓
回到微任务队列 ↑
规则: 每执行一个宏任务 → 就清空所有微任务 → 再执行下一个宏任务Node.js 事件循环
Node.js Event Loop(libuv 实现,6 个阶段):
┌───────────────────────┐
│ timers │ setTimeout / setInterval 回调
└──────────┬────────────┘
↓
┌───────────────────────┐
│ pending callbacks │ 系统操作回调(如 TCP 错误)
└──────────┬────────────┘
↓
┌───────────────────────┐
│ idle, prepare │ 内部使用
└──────────┬────────────┘
↓
┌───────────────────────┐
│ poll │ I/O 回调(文件读写、网络请求)
│ │ ← 大部分回调在这里执行
└──────────┬────────────┘
↓
┌───────────────────────┐
│ check │ setImmediate 回调
└──────────┬────────────┘
↓
┌───────────────────────┐
│ close callbacks │ socket.on('close') 等
└───────────────────────┘
每个阶段之间: 执行所有微任务(nextTick 优先于 Promise)关键区别
① process.nextTick vs Promise.then
Node.js 中 nextTick 优先级高于 Promise:
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('promise'))
// 输出: nextTick → promise
浏览器中没有 nextTick,只有 Promise(微任务)
② setImmediate vs setTimeout(fn, 0)
setImmediate: 在 check 阶段执行
setTimeout(fn, 0): 在 timers 阶段执行
// 在主模块中,顺序不确定(取决于系统调度)
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
// 可能: timeout → immediate 或 immediate → timeout
// 在 I/O 回调中,setImmediate 一定先执行
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
// 一定: immediate → timeout(poll → check → timers)
③ 微任务执行时机
浏览器: 每个宏任务后执行所有微任务
Node.js: 每个阶段切换时执行所有微任务(v11+ 行为与浏览器趋同)经典面试题
javascript
console.log('start')
setTimeout(() => {
console.log('timeout 1')
Promise.resolve().then(() => console.log('promise inside timeout'))
}, 0)
setImmediate(() => {
console.log('immediate 1')
})
Promise.resolve().then(() => {
console.log('promise 1')
})
process.nextTick(() => {
console.log('nextTick 1')
})
console.log('end')
// 输出:
// start
// end
// nextTick 1 ← nextTick 最高优先级微任务
// promise 1 ← Promise 微任务
// timeout 1 ← timers 阶段(或 immediate 1,取决于调度)
// promise inside timeout ← timeout 回调中的微任务
// immediate 1 ← check 阶段追问延伸
- Node.js 的
worker_threads和child_process有什么区别? libuv的线程池默认多大?什么操作会使用线程池?(默认 4 个线程,文件 I/O 使用)- Node.js v11 之后事件循环行为有什么变化?
2. Node.js 的 Stream 是什么?有哪些类型?实际应用场景? ⭐⭐⭐
理解 Stream 的核心概念和应用。
考察点:Stream、内存优化
为什么需要 Stream
问题: 处理大文件
// ❌ 不用 Stream — 一次性读入内存
const data = fs.readFileSync('video.mp4') // 2GB → 内存爆掉
res.end(data)
// ✅ 用 Stream — 分块处理
fs.createReadStream('video.mp4').pipe(res)
// 每次只读 64KB → 内存占用极低
Stream = 数据像水流一样,一块一块地处理
→ 不需要把所有数据都加载到内存中
→ 适合大文件、实时数据、网络传输四种 Stream
| 类型 | 描述 | 示例 |
|---|---|---|
| Readable | 可读流(数据源) | fs.createReadStream / http.IncomingMessage |
| Writable | 可写流(数据目标) | fs.createWriteStream / http.ServerResponse |
| Duplex | 双工流(可读 + 可写) | net.Socket / WebSocket |
| Transform | 转换流(读入 → 处理 → 输出) | zlib.createGzip() / crypto.createCipher() |
基本用法
javascript
const fs = require('fs')
const zlib = require('zlib')
// 读取 → 压缩 → 写入(管道链)
fs.createReadStream('input.log') // Readable
.pipe(zlib.createGzip()) // Transform
.pipe(fs.createWriteStream('input.gz')) // Writable
.on('finish', () => console.log('压缩完成'))
// 等价于:
const readable = fs.createReadStream('input.log')
const gzip = zlib.createGzip()
const writable = fs.createWriteStream('input.gz')
readable.on('data', (chunk) => {
gzip.write(chunk)
})
readable.on('end', () => {
gzip.end()
})
gzip.on('data', (chunk) => {
writable.write(chunk)
})
gzip.on('end', () => {
writable.end()
})实际应用场景
javascript
// ① HTTP 文件下载(大文件流式传输)
app.get('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream')
res.setHeader('Content-Disposition', 'attachment; filename="big.zip"')
fs.createReadStream('/path/to/big.zip').pipe(res)
})
// ② CSV 逐行处理(不占用大量内存)
const readline = require('readline')
const rl = readline.createInterface({
input: fs.createReadStream('data.csv'),
})
let lineCount = 0
rl.on('line', (line) => {
lineCount++
// 逐行处理,内存占用始终很低
})
// ③ 自定义 Transform Stream
const { Transform } = require('stream')
const upperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase())
callback()
}
})
process.stdin.pipe(upperCase).pipe(process.stdout)
// 输入 hello → 输出 HELLO
// ④ pipeline(推荐,自动错误处理)
const { pipeline } = require('stream/promises')
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
)背压 (Backpressure)
问题: 读取速度 > 写入速度 → 数据堆积在内存中
例: 从 SSD 读取(500MB/s)→ 写入网络(10MB/s)
如果不处理背压 → 数据全堆在内存 → OOM
pipe() 自动处理背压:
writable 缓冲区满 → 返回 false
→ readable 暂停读取(pause)
→ writable 缓冲区排空 → 触发 drain 事件
→ readable 恢复读取(resume)
手动处理:
const readable = fs.createReadStream('big.file')
const writable = fs.createWriteStream('output.file')
readable.on('data', (chunk) => {
const canContinue = writable.write(chunk)
if (!canContinue) {
readable.pause() // 暂停读取
writable.once('drain', () => {
readable.resume() // 恢复读取
})
}
})追问延伸
pipe和pipeline有什么区别?(pipeline 自动销毁 stream + 错误传播)- Web Streams API 和 Node.js Streams 的关系?(Web 标准,Deno/浏览器原生支持)
- 如何用 Stream 实现大文件分片上传?
3. Express 和 Koa 的区别?中间件原理? ⭐⭐
对比 Node.js 两大 Web 框架。
考察点:Web 框架、中间件
Express vs Koa
| 维度 | Express | Koa |
|---|---|---|
| 作者 | TJ Holowaychuk | TJ Holowaychuk(Express 原班人马) |
| 设计理念 | 大而全(内置路由/模板/静态文件) | 小而精(核心只有中间件) |
| 中间件 | 回调函数 (req, res, next) | async/await (ctx, next) |
| 错误处理 | 回调中 next(err) | try/catch(async 天然支持) |
| Context | req + res 分离 | ctx 封装了 req + res |
| 生态 | 最大(大量中间件) | 较小(需要自己组合) |
Express 中间件(洋葱模型 — 单向)
javascript
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('A - before')
next()
console.log('A - after') // 注意: Express 中这里不可靠
})
app.use((req, res, next) => {
console.log('B - before')
next()
console.log('B - after')
})
app.get('/', (req, res) => {
console.log('handler')
res.send('OK')
})
// Express 的 next() 是回调式的
// 如果中间件是异步的,"after" 部分不会等待异步完成
// → 实际上是单向流水线,不是真正的洋葱模型Koa 中间件(洋葱模型 — 真正的双向)
javascript
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log('A - before')
await next()
console.log('A - after') // ✅ 一定在 B 完成后执行
})
app.use(async (ctx, next) => {
console.log('B - before')
await next()
console.log('B - after')
})
app.use(async (ctx) => {
console.log('handler')
ctx.body = 'OK'
})
// 输出: A-before → B-before → handler → B-after → A-after
// 像洋葱一样: 请求从外到内,响应从内到外Koa 中间件核心实现
javascript
// koa-compose 简化版
function compose(middlewares) {
return function(ctx) {
let index = -1
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
const fn = middlewares[i]
if (!fn) return Promise.resolve()
try {
return Promise.resolve(
fn(ctx, () => dispatch(i + 1))
)
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}常见中间件设计
javascript
// 计时中间件(Koa)
app.use(async (ctx, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
ctx.set('X-Response-Time', `${duration}ms`)
})
// 错误处理中间件(Koa — 放在最外层)
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = err.status || 500
ctx.body = { error: err.message }
ctx.app.emit('error', err, ctx)
}
})
// JWT 认证中间件
app.use(async (ctx, next) => {
const token = ctx.headers.authorization?.replace('Bearer ', '')
if (!token) { ctx.throw(401, 'Token required') }
try {
ctx.state.user = jwt.verify(token, SECRET)
await next()
} catch {
ctx.throw(401, 'Invalid token')
}
})追问延伸
- Koa 的
ctx.throw()和直接throw有什么区别? - Fastify / Hono / Elysia 等新框架和 Express/Koa 比有什么优势?
- 如何设计一个可取消的中间件链?
4. Node.js 如何处理高并发?单线程为什么不是瓶颈? ⭐⭐
理解 Node.js 的并发模型。
考察点:并发模型
为什么单线程能处理高并发
传统多线程模型 (Java / PHP):
每个请求 → 分配一个线程
1000 个并发 → 1000 个线程
线程切换开销大 / 内存占用高(每个线程 ~2MB)
Node.js 事件驱动模型:
单线程 + 事件循环 + 非阻塞 I/O
1000 个并发 → 还是 1 个线程
I/O 操作(文件/网络/数据库)→ 委托给 libuv 线程池或 OS
→ 回调排入事件队列 → 主线程逐个处理
关键: Web 服务大部分时间在等 I/O(数据库查询、文件读写)
→ 单线程不是瓶颈,I/O 等待时间才是
→ Node.js 不等 I/O → 去处理其他请求 → 高效利用 CPU哪些场景不适合
✅ Node.js 适合:
- I/O 密集型(API 服务、文件处理、实时通信)
- 高并发短请求(SSR、BFF、微服务网关)
❌ Node.js 不太适合:
- CPU 密集型计算(图片处理、视频编解码、机器学习)
→ 单线程会被阻塞 → 所有请求都卡住
解决 CPU 密集型:
① worker_threads — 多线程(共享内存)
② child_process — 多进程
③ 委托给其他服务(Go / Rust / Python)worker_threads
javascript
// main.js
const { Worker } = require('worker_threads')
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data })
worker.on('message', resolve)
worker.on('error', reject)
})
}
const result = await runWorker({ numbers: [1, 2, 3, 4, 5] })
// worker.js
const { parentPort, workerData } = require('worker_threads')
const sum = workerData.numbers.reduce((a, b) => a + b, 0)
parentPort.postMessage(sum)cluster 模式
javascript
const cluster = require('cluster')
const os = require('os')
const http = require('http')
if (cluster.isPrimary) {
const cpuCount = os.cpus().length
for (let i = 0; i < cpuCount; i++) {
cluster.fork()
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died, restarting...`)
cluster.fork()
})
} else {
http.createServer((req, res) => {
res.writeHead(200)
res.end(`Handled by PID: ${process.pid}`)
}).listen(3000)
}
// 8 核 CPU → 启动 8 个工作进程 → 利用全部 CPU
// 主进程负责管理 → 工作进程挂了自动重启追问延伸
- PM2 的 cluster 模式和原生 cluster 有什么区别?
- Node.js 的
SharedArrayBuffer怎么在 worker_threads 间共享数据? - 如何用 Node.js 构建一个任务队列系统?(Bull / BullMQ)
5. RESTful API 设计规范?HTTP 状态码的正确使用? ⭐⭐
设计规范的 RESTful API。
考察点:API 设计
RESTful 核心原则
REST = Representational State Transfer
核心: 用 URL 定位资源,用 HTTP 方法操作资源
GET /users → 获取用户列表
GET /users/123 → 获取单个用户
POST /users → 创建用户
PUT /users/123 → 全量更新用户
PATCH /users/123 → 部分更新用户
DELETE /users/123 → 删除用户
URL 设计原则:
✅ 名词复数: /users, /articles, /comments
❌ 动词: /getUser, /createArticle
✅ 嵌套关系: /users/123/posts (用户 123 的文章)
✅ 筛选排序: /users?role=admin&sort=created_at&page=1&limit=20HTTP 状态码
2xx 成功:
200 OK → GET/PUT/PATCH 成功
201 Created → POST 创建成功
204 No Content → DELETE 成功(无返回体)
3xx 重定向:
301 Moved Permanently → 永久重定向
302 Found → 临时重定向
304 Not Modified → 缓存命中
4xx 客户端错误:
400 Bad Request → 请求参数错误
401 Unauthorized → 未认证(没有 token)
403 Forbidden → 已认证但没有权限
404 Not Found → 资源不存在
409 Conflict → 资源冲突(如重复创建)
422 Unprocessable → 参数格式正确但语义错误
429 Too Many Requests → 限流
5xx 服务端错误:
500 Internal Error → 服务器内部错误
502 Bad Gateway → 上游服务不可用
503 Service Unavailable → 服务暂时不可用
504 Gateway Timeout → 上游服务超时API 响应格式
typescript
// 统一响应结构
interface ApiResponse<T> {
code: number
message: string
data: T
}
// 分页
interface PaginatedResponse<T> {
code: number
message: string
data: {
items: T[]
total: number
page: number
pageSize: number
totalPages: number
}
}
// 错误
interface ErrorResponse {
code: number
message: string
errors?: Array<{
field: string
message: string
}>
}API 版本管理
方案 1: URL 路径 (最常用)
/api/v1/users
/api/v2/users
方案 2: Header
Accept: application/vnd.api+json;version=2
方案 3: 查询参数
/api/users?version=2追问延伸
- GraphQL 和 RESTful 的优缺点对比?什么时候选 GraphQL?
- API 限流(Rate Limiting)怎么实现?令牌桶和漏桶算法?
- 如何设计幂等的 API?
Idempotency-Key怎么用?
6. Node.js 如何连接和操作数据库?ORM vs 原生查询? ⭐⭐
理解 Node.js 中的数据库操作。
考察点:数据库
常用数据库对比
| 数据库 | 类型 | 适用场景 | Node.js 驱动 |
|---|---|---|---|
| MySQL | 关系型 | 传统业务、事务 | mysql2 |
| PostgreSQL | 关系型 | 复杂查询、JSON | pg / postgres.js |
| MongoDB | 文档型 | 灵活 Schema | mongoose / mongodb |
| Redis | 键值对 | 缓存、会话、队列 | ioredis |
| SQLite | 嵌入式 | 小型应用、本地存储 | better-sqlite3 |
原生查询
javascript
// MySQL (mysql2)
import mysql from 'mysql2/promise'
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'myapp',
waitForConnections: true,
connectionLimit: 10,
})
const [rows] = await pool.execute(
'SELECT * FROM users WHERE role = ? AND age > ?',
['admin', 18]
)
// PostgreSQL (postgres.js) — 推荐
import postgres from 'postgres'
const sql = postgres('postgres://localhost/myapp')
const users = await sql`
SELECT * FROM users WHERE role = ${'admin'} AND age > ${18}
`
// 模板字符串自动参数化 → 防 SQL 注入 ✅ORM 对比
ORM (Object-Relational Mapping):
数据库表 ↔ JS 对象
SQL 查询 ↔ 方法调用
常用 ORM:
Prisma → 类型安全、自动生成类型、迁移管理 ← 推荐
Drizzle → 轻量、类 SQL 语法、性能好
TypeORM → 装饰器风格、类似 Java Hibernate
Sequelize → 老牌、功能全typescript
// Prisma 示例
// schema.prisma
// model User {
// id Int @id @default(autoincrement())
// name String
// email String @unique
// posts Post[]
// }
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const user = await prisma.user.create({
data: { name: 'Alice', email: 'alice@example.com' }
})
const users = await prisma.user.findMany({
where: { name: { contains: 'Ali' } },
include: { posts: true },
orderBy: { createdAt: 'desc' },
take: 10,
})
// Drizzle 示例
import { eq, gt, and } from 'drizzle-orm'
const users = await db
.select()
.from(usersTable)
.where(and(eq(usersTable.role, 'admin'), gt(usersTable.age, 18)))
.limit(10)ORM vs 原生查询
| 维度 | ORM | 原生查询 |
|---|---|---|
| 开发效率 | ✅ 高(自动类型推导) | ❌ 低(手写 SQL) |
| 性能 | 🟡 有一定开销 | ✅ 最佳性能 |
| 复杂查询 | ❌ 复杂查询难表达 | ✅ SQL 灵活 |
| 可维护性 | ✅ Schema 即文档 | 🟡 SQL 分散在代码中 |
| 迁移管理 | ✅ 内置(Prisma Migrate) | ❌ 需要额外工具 |
追问延伸
- 连接池的原理?为什么需要连接池?
- N+1 查询问题是什么?ORM 怎么解决?(eager loading / dataloader)
- 数据库事务在 Node.js 中怎么使用?
7. Node.js 的进程管理?PM2 的核心功能? ⭐⭐
理解 Node.js 的进程管理和部署。
考察点:进程管理
Node.js 进程模型
单进程的问题:
① 无法利用多核 CPU → 只用到 1 个核心
② 进程崩溃 → 整个服务挂掉
③ 无法平滑重启 → 更新代码需要停服
解决方案:
cluster 模块 → 多进程 (前面第 4 题已介绍)
PM2 → 进程管理器(生产环境标配)PM2 核心功能
bash
# 基本使用
pm2 start app.js # 启动
pm2 start app.js -i max # cluster 模式,自动启动 CPU 核心数个进程
pm2 start app.js --name my-app # 命名
pm2 start app.js --watch # 监听文件变化自动重启
# 管理
pm2 list # 查看所有进程
pm2 logs my-app # 查看日志
pm2 monit # 实时监控面板
pm2 restart my-app # 重启
pm2 reload my-app # 平滑重启(零停机)
pm2 stop my-app # 停止
pm2 delete my-app # 删除
# 配置文件
pm2 start ecosystem.config.jsjavascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: './dist/index.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '500M',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: './logs/error.log',
out_file: './logs/out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss',
}]
}PM2 关键特性
① 零停机重启 (Graceful Reload)
pm2 reload my-app
→ 逐个重启工作进程
→ 新进程就绪后才终止旧进程
→ 用户完全无感知
② 自动重启
进程崩溃 → PM2 自动重新启动
max_memory_restart → 内存超限自动重启
cron_restart → 定时重启
③ 日志管理
pm2 logs → 实时查看日志
pm2 flush → 清空日志
pm2-logrotate → 日志切割
④ 开机自启
pm2 startup → 生成系统自启动脚本
pm2 save → 保存当前进程列表
→ 系统重启后自动恢复所有进程
⑤ 部署
pm2 deploy ecosystem.config.js production
→ 自动 SSH 到服务器 → git pull → npm install → pm2 reload追问延伸
- PM2 的
reload和restart有什么区别?(reload 逐个重启,restart 全部杀掉再启动) - 如何实现 Node.js 的优雅退出?(
SIGTERM信号处理、关闭数据库连接、拒绝新请求) - Docker 环境下还需要 PM2 吗?(通常不需要,K8s 已有健康检查和自动重启)
8. Node.js 的 Buffer 和二进制数据处理? ⭐⭐
理解 Buffer 的原理和使用场景。
考察点:Buffer、二进制
Buffer 是什么
Buffer = Node.js 中处理二进制数据的类
- 类似于整数数组,但大小固定,分配在 V8 堆外内存
- 用于处理文件 I/O、网络传输、加密等二进制操作
- JavaScript 的字符串是 UTF-16 编码 → 不适合处理原始二进制
Buffer vs TypedArray:
Buffer 是 Uint8Array 的子类
→ Buffer 有额外的便利方法(toString, concat 等)
→ 在 Node.js 中处理二进制首选 Buffer基本操作
javascript
// 创建
const buf1 = Buffer.alloc(10) // 10 字节,填充 0
const buf2 = Buffer.alloc(10, 0xff) // 10 字节,填充 0xff
const buf3 = Buffer.from('Hello', 'utf-8') // 从字符串
const buf4 = Buffer.from([72, 101, 108, 108, 111]) // 从数组
const buf5 = Buffer.allocUnsafe(10) // 未初始化(快但不安全)
// 读写
buf3[0] // 72 (H 的 ASCII)
buf3.toString('utf-8') // 'Hello'
buf3.toString('base64') // 'SGVsbG8='
buf3.toString('hex') // '48656c6c6f'
// 拼接
const combined = Buffer.concat([buf3, Buffer.from(' World')])
combined.toString() // 'Hello World'
// 比较
Buffer.from('abc').compare(Buffer.from('abc')) // 0 (相等)
Buffer.from('abc').equals(Buffer.from('abc')) // true
// 查找
buf3.indexOf('ll') // 2
buf3.includes('He') // true实际应用
javascript
// ① 文件读取返回 Buffer
const data = fs.readFileSync('image.png')
console.log(data) // <Buffer 89 50 4e 47 ...>
console.log(data.length) // 文件字节数
// ② 网络请求收到 Buffer
http.createServer((req, res) => {
const chunks = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
const body = Buffer.concat(chunks).toString()
const json = JSON.parse(body)
})
})
// ③ 加密
const crypto = require('crypto')
const hash = crypto.createHash('sha256')
.update(Buffer.from('password'))
.digest('hex')
// ④ 判断文件类型(Magic Number)
const buf = fs.readFileSync('file')
if (buf[0] === 0x89 && buf[1] === 0x50) {
console.log('PNG 文件')
}
if (buf[0] === 0xFF && buf[1] === 0xD8) {
console.log('JPEG 文件')
}
// ⑤ Base64 编解码
const base64 = Buffer.from('Hello World').toString('base64')
const original = Buffer.from(base64, 'base64').toString('utf-8')追问延伸
Buffer.alloc和Buffer.allocUnsafe的区别?为什么有安全风险?ArrayBuffer、SharedArrayBuffer、Buffer三者的关系?- Node.js 中字符串编码有哪些?如何处理中文乱码?
9. Node.js 的错误处理最佳实践? ⭐⭐
设计健壮的错误处理机制。
考察点:错误处理
错误分类
① 可预期错误 (Operational Errors):
- 数据库连接失败
- 用户输入验证失败
- API 请求超时
- 文件不存在
→ 需要优雅处理,给用户友好提示
② 程序错误 (Programmer Errors):
- TypeError: Cannot read property 'x' of undefined
- 数组越界
- 内存泄漏
→ Bug,需要修复代码
③ 未捕获异常:
- 没有 try/catch 的同步错误
- 没有 .catch() 的 Promise 拒绝
→ 可能导致进程崩溃错误处理模式
javascript
// ① async/await + try/catch
async function getUser(id) {
try {
const user = await db.user.findUnique({ where: { id } })
if (!user) throw new NotFoundError('User not found')
return user
} catch (error) {
if (error instanceof NotFoundError) throw error
throw new InternalError('Failed to fetch user', { cause: error })
}
}
// ② 自定义错误类
class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message)
this.name = this.constructor.name
this.statusCode = statusCode
this.code = code
Error.captureStackTrace(this, this.constructor)
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404, 'NOT_FOUND')
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 422, 'VALIDATION_ERROR')
this.errors = errors
}
}
// ③ 全局错误处理(Express)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500
const response = {
code: err.code || 'INTERNAL_ERROR',
message: err.message,
}
if (err.errors) response.errors = err.errors
if (process.env.NODE_ENV === 'development') {
response.stack = err.stack
}
res.status(statusCode).json(response)
})进程级错误处理
javascript
// 未捕获的同步异常
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error)
// 记录错误 → 上报监控 → 优雅退出
process.exit(1)
})
// 未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason)
// Node.js 15+ 默认会终止进程
})
// 优雅退出
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully')
server.close() // 停止接收新请求
await db.$disconnect() // 关闭数据库连接
process.exit(0)
})错误处理 Checklist
□ 所有 async 函数都有错误处理
□ 自定义错误类(区分业务错误和系统错误)
□ 全局错误中间件(Express/Koa)
□ uncaughtException / unhandledRejection 处理
□ SIGTERM 优雅退出
□ 生产环境不暴露 stack trace
□ 错误日志记录(winston / pino)
□ 错误上报(Sentry)
□ 健康检查端点(/health)追问延伸
Error.cause(ES2022)怎么用?为什么需要错误链?- 如何防止错误消息泄露敏感信息?
- 如何实现 Circuit Breaker(熔断器)模式?
10. CommonJS 和 ES Modules 在 Node.js 中的区别? ⭐⭐
理解 Node.js 中两种模块系统的差异。
考察点:模块系统
核心区别
| 维度 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 语法 | require() / module.exports | import / export |
| 加载时机 | 运行时加载 | 编译时静态分析 |
| 值类型 | 值的拷贝(修改不影响原模块) | 值的引用(实时绑定) |
| 顶层 this | module.exports | undefined |
| 循环依赖 | 返回已执行部分的 exports | 引用绑定(可能获取未初始化的值) |
| 异步 | 同步加载 | 支持 await(Top-Level Await) |
| Tree Shaking | ❌ 不支持 | ✅ 支持 |
运行时 vs 编译时
javascript
// CJS — 运行时加载(可以动态决定导入什么)
const module = condition ? require('./a') : require('./b')
// ESM — 编译时静态分析(必须在顶层,不能在 if 里)
import { a } from './a' // ✅ 必须在文件顶层
// if (condition) { import { b } from './b' } ❌ 语法错误
// ESM 动态导入(运行时)
const module = await import(condition ? './a' : './b')值拷贝 vs 引用绑定
javascript
// CJS — 值拷贝
// counter.js
let count = 0
module.exports = { count, increment: () => ++count }
// main.js
const counter = require('./counter')
console.log(counter.count) // 0
counter.increment()
console.log(counter.count) // 0 ← 还是 0!拷贝的值不变
// ESM — 引用绑定
// counter.mjs
export let count = 0
export function increment() { count++ }
// main.mjs
import { count, increment } from './counter.mjs'
console.log(count) // 0
increment()
console.log(count) // 1 ← 实时反映变化!Node.js 中使用 ESM
json
// 方式 1: package.json 中设置 type
{ "type": "module" }
// → 所有 .js 文件都当作 ESM
// → CJS 文件需要改用 .cjs 后缀
// 方式 2: 使用 .mjs 后缀
// 文件后缀为 .mjs → 当作 ESM
// 文件后缀为 .cjs → 当作 CJS
// ESM 中使用 CJS
import cjsModule from './lib.cjs' // 默认导出
// CJS 中使用 ESM
const esmModule = await import('./lib.mjs') // 必须用动态 import注意事项
ESM 中没有的 CJS 变量:
__dirname → import.meta.dirname (Node.js 21+)
或 path.dirname(fileURLToPath(import.meta.url))
__filename → import.meta.filename (Node.js 21+)
或 fileURLToPath(import.meta.url)
require → createRequire(import.meta.url)
module → 不存在
exports → 使用 export
Node.js 生态现状:
- 大部分包已支持 ESM(通过 package.json 的 exports 字段)
- 新项目推荐直接用 ESM
- 旧项目迁移需要注意 CJS/ESM 互操作性追问延伸
package.json的exports字段怎么同时支持 CJS 和 ESM?(条件导出)- Top-Level Await 的限制和应用场景?
- 为什么 Node.js 不能直接
requireESM 模块?(ESM 是异步的,CJS 是同步的)
11. Node.js 的安全防护?常见攻击和防御手段? ⭐⭐
理解 Node.js 后端服务的安全实践。
考察点:Web 安全
常见安全威胁
┌───────────────────────────────────────────┐
│ 注入攻击 │
│ SQL 注入 / NoSQL 注入 / 命令注入 │
├───────────────────────────────────────────┤
│ 认证与授权 │
│ JWT 安全 / 会话劫持 / 密码存储 │
├───────────────────────────────────────────┤
│ 输入验证 │
│ XSS / 路径遍历 / 原型链污染 │
├───────────────────────────────────────────┤
│ 服务层 │
│ DoS / 依赖投毒 / 信息泄露 │
└───────────────────────────────────────────┘SQL 注入防御
javascript
// ❌ 拼接 SQL(危险!)
const query = `SELECT * FROM users WHERE name = '${userInput}'`
// 攻击者输入: ' OR 1=1 --
// → SELECT * FROM users WHERE name = '' OR 1=1 --' → 返回所有用户
// ✅ 参数化查询
const [rows] = await pool.execute(
'SELECT * FROM users WHERE name = ?',
[userInput]
)
// ✅ ORM 自动处理
const user = await prisma.user.findMany({
where: { name: userInput }
})密码存储
javascript
import bcrypt from 'bcrypt'
// ✅ 注册: 哈希存储
const saltRounds = 12
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds)
// 存入数据库的是 $2b$12$xxxx... 不是明文
// ✅ 登录: 比较哈希
const isMatch = await bcrypt.compare(inputPassword, storedHash)
// ❌ 绝对不要:
// 明文存储密码
// 使用 MD5 / SHA256 直接哈希(没有 salt,容易彩虹表破解)
// 自己发明加密算法JWT 安全
javascript
import jwt from 'jsonwebtoken'
// 签发
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '15m',
issuer: 'my-app',
audience: 'my-app-client',
}
)
// 验证
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'my-app',
audience: 'my-app-client',
})
// JWT 安全要点:
// ✅ 使用强密钥(256 位以上随机字符串)
// ✅ 设置过期时间(Access Token 15min + Refresh Token 7d)
// ✅ 验证 issuer / audience 防止 Token 混用
// ✅ HTTPS 传输
// ❌ 不要在 JWT 中存敏感信息(payload 是 Base64,不是加密)
// ❌ 不要用 "none" 算法请求安全
javascript
import helmet from 'helmet'
import rateLimit from 'express-rate-limit'
import cors from 'cors'
const app = express()
// ① 安全 HTTP 头
app.use(helmet())
// → X-Content-Type-Options: nosniff
// → X-Frame-Options: DENY
// → Content-Security-Policy: ...
// → Strict-Transport-Security: ...
// ② 限流
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
standardHeaders: true,
legacyHeaders: false,
}))
// ③ CORS 白名单
app.use(cors({
origin: ['https://my-app.com', 'https://admin.my-app.com'],
credentials: true,
}))
// ④ 请求体大小限制
app.use(express.json({ limit: '10kb' }))
// ⑤ 输入验证 (zod)
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
})
app.post('/users', (req, res) => {
const result = createUserSchema.safeParse(req.body)
if (!result.success) {
return res.status(422).json({ errors: result.error.issues })
}
// result.data 是类型安全的 ✅
})原型链污染防御
javascript
// 攻击: 通过 JSON 修改 Object.prototype
// { "__proto__": { "isAdmin": true } }
// ✅ 使用 Object.create(null) 创建无原型对象
const safeObj = Object.create(null)
// ✅ 冻结原型
Object.freeze(Object.prototype)
// ✅ 使用 Map 代替普通对象存储用户数据
const userSettings = new Map()追问延伸
- OWASP Top 10 都有哪些?前端后端各应该关注哪些?
- Refresh Token 的旋转(Rotation)策略?如何检测 Token 被盗?
- 如何防止 ReDoS(正则表达式拒绝服务)攻击?
12. Node.js 性能优化?如何排查性能问题? ⭐⭐⭐
系统性优化 Node.js 服务性能。
考察点:性能优化
性能优化维度
┌─────────────────────────────────────────┐
│ 代码层 │
│ 避免同步 API / 减少内存分配 / 缓存 │
├─────────────────────────────────────────┤
│ 数据库层 │
│ 索引优化 / 连接池 / 查询优化 / 缓存 │
├─────────────────────────────────────────┤
│ 架构层 │
│ 集群 / 负载均衡 / CDN / 微服务 │
├─────────────────────────────────────────┤
│ 运行时 │
│ V8 内存调优 / GC 优化 / Worker 线程 │
└─────────────────────────────────────────┘代码层优化
javascript
// ❌ 同步文件操作(阻塞事件循环)
const data = fs.readFileSync('big.json')
// ✅ 异步文件操作
const data = await fs.promises.readFile('big.json')
// ❌ 大量字符串拼接
let result = ''
for (const item of items) {
result += item.name + ','
}
// ✅ 数组 join
const result = items.map(item => item.name).join(',')
// ❌ 频繁创建对象
function handleRequest(req) {
const config = { timeout: 5000, retries: 3 } // 每次请求都创建
}
// ✅ 复用对象
const CONFIG = Object.freeze({ timeout: 5000, retries: 3 })
function handleRequest(req) {
// 复用 CONFIG
}缓存策略
javascript
// ① 内存缓存 (LRU Cache)
import { LRUCache } from 'lru-cache'
const cache = new LRUCache({
max: 1000,
ttl: 5 * 60 * 1000, // 5 分钟过期
})
async function getUser(id) {
const cached = cache.get(`user:${id}`)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(`user:${id}`, user)
return user
}
// ② Redis 缓存(分布式)
import Redis from 'ioredis'
const redis = new Redis()
async function getUserWithRedis(id) {
const cached = await redis.get(`user:${id}`)
if (cached) return JSON.parse(cached)
const user = await db.user.findUnique({ where: { id } })
await redis.setex(`user:${id}`, 300, JSON.stringify(user)) // 300s TTL
return user
}
// ③ HTTP 缓存头
app.get('/api/config', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600')
res.json(appConfig)
})性能排查工具
bash
# ① Node.js 内置性能分析
node --prof app.js
# 运行一段时间后生成 isolate-*.log
node --prof-process isolate-*.log > profile.txt
# 查看 CPU 时间分布
# ② 火焰图
node --inspect app.js
# Chrome DevTools → Performance → Record
# 或使用 0x:
npx 0x app.js
# 生成交互式火焰图
# ③ Clinic.js(推荐)
npx clinic doctor -- node app.js
# 自动分析 CPU / 内存 / 事件循环延迟 → 生成报告
npx clinic flame -- node app.js
# 生成火焰图
npx clinic bubbleprof -- node app.js
# 分析异步操作瓶颈内存排查
javascript
// 查看内存使用
console.log(process.memoryUsage())
// {
// rss: 30 * 1024 * 1024, // 常驻内存
// heapTotal: 10 * 1024 * 1024, // V8 堆总量
// heapUsed: 8 * 1024 * 1024, // V8 堆已用
// external: 1 * 1024 * 1024, // C++ 对象内存
// arrayBuffers: 500 * 1024, // ArrayBuffer 内存
// }
// 检测内存泄漏: 对比两次 heapUsed
// 如果持续增长不回落 → 内存泄漏
// Chrome DevTools 远程调试
// node --inspect app.js
// Chrome → chrome://inspect → Memory → Heap Snapshot追问延伸
- Node.js 的
--max-old-space-size怎么设置?什么时候需要调整? - 如何监控 Event Loop 延迟?(
perf_hooks.monitorEventLoopDelay) - APM 工具(Datadog / New Relic / Elastic APM)怎么集成?
13. 什么是 BFF(Backend For Frontend)?前端为什么需要 BFF? ⭐⭐
理解 BFF 架构模式及其应用。
考察点:架构设计
BFF 是什么
BFF = Backend For Frontend(服务于前端的后端)
传统架构:
前端 → 后端微服务(多个)
问题:
① 前端需要调用多个微服务 → 接口聚合复杂
② 不同端(Web / App / 小程序)需要不同数据格式
③ 后端 API 粒度太细 → 前端请求太多
④ 前端无法控制后端 API 的变更节奏
BFF 架构:
前端 → BFF(Node.js)→ 后端微服务
┌────────┐ ┌──────────┐ ┌──────────────┐
│ Web │ → │ Web BFF │ → │ 用户服务 │
└────────┘ └──────────┘ │ 订单服务 │
┌────────┐ ┌──────────┐ │ 商品服务 │
│ App │ → │ App BFF │ → │ 支付服务 │
└────────┘ └──────────┘ └──────────────┘
BFF 的职责:
① 接口聚合: 多个微服务的数据合成一个接口返回
② 数据裁剪: 按前端需要返回字段(减少传输量)
③ 协议转换: 后端 gRPC → 前端 REST / GraphQL
④ 鉴权网关: 统一处理认证和权限
⑤ SSR 渲染: 服务端渲染页面BFF 示例
typescript
// BFF: 聚合多个微服务数据
app.get('/api/homepage', async (req, res) => {
const [user, orders, recommendations] = await Promise.all([
userService.getProfile(req.userId),
orderService.getRecent(req.userId, { limit: 5 }),
productService.getRecommendations(req.userId),
])
res.json({
user: {
name: user.name,
avatar: user.avatar,
},
recentOrders: orders.map(o => ({
id: o.id,
status: o.status,
total: o.totalAmount,
})),
recommendations: recommendations.slice(0, 10),
})
})
// 没有 BFF 时前端需要:
// fetch('/user-service/profile')
// fetch('/order-service/recent?limit=5')
// fetch('/product-service/recommendations')
// → 3 个请求 → 前端聚合 → 体验差BFF 技术选型
Node.js BFF 框架:
Next.js / Nuxt.js → SSR + API Routes(最常用)
NestJS → 企业级、装饰器、DI
Fastify → 高性能、schema 验证
tRPC → 端到端类型安全(TypeScript 全栈)
Hono → 轻量、边缘计算友好
GraphQL as BFF:
Apollo Server → 让前端自定义查询字段
→ 天然解决"不同端需要不同数据"的问题BFF vs API Gateway
| 维度 | BFF | API Gateway |
|---|---|---|
| 关注点 | 前端体验 | 流量管理 |
| 职责 | 数据聚合/裁剪/SSR | 路由/限流/认证/日志 |
| 数量 | 每个端一个 BFF | 通常全局一个 |
| 开发者 | 前端团队 | 平台/后端团队 |
| 技术栈 | Node.js / TypeScript | Kong / Envoy / Nginx |
实际部署:
前端 → API Gateway → BFF → 微服务
API Gateway 负责: 限流、认证、日志、路由
BFF 负责: 数据聚合、SSR、协议转换追问延伸
- BFF 的缺点?什么时候不需要 BFF?(增加一层维护成本、小团队不需要)
- tRPC 的原理?如何实现端到端类型安全?
- 如何处理 BFF 的缓存策略?(HTTP 缓存 + Redis + CDN 多层缓存)
14. Node.js 的日志体系怎么设计?日志级别和最佳实践? ⭐⭐
设计生产级别的日志系统。
考察点:日志体系
为什么需要日志体系
console.log 的问题:
❌ 无法区分日志级别(info / warn / error)
❌ 无法持久化(进程重启就丢失)
❌ 无法结构化(难以搜索和分析)
❌ 无法控制输出(生产环境不想看 debug)
❌ 性能差(同步输出,阻塞事件循环)日志级别
日志级别(从低到高):
trace → 最详细的调试信息(循环内部、变量值)
debug → 调试信息(函数入参、SQL 语句)
info → 业务信息(用户登录、订单创建)
warn → 警告(即将过期的 token、慢查询)
error → 错误(API 失败、数据库断连)
fatal → 致命(进程即将崩溃)
生产环境通常设为 info:
→ 只记录 info + warn + error + fatal
→ 不记录 trace 和 debug(太多噪音)Pino(推荐)
javascript
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
base: {
service: 'my-api',
version: '1.0.0',
},
serializers: {
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
err: pino.stdSerializers.err,
},
})
// 使用
logger.info({ userId: 123 }, 'User logged in')
logger.warn({ duration: 5200 }, 'Slow query detected')
logger.error({ err, requestId }, 'Payment failed')
// 输出 (JSON 格式):
// {"level":30,"time":1711234567,"service":"my-api","userId":123,"msg":"User logged in"}
// → 结构化日志 → 方便 ELK / Loki 搜索和分析请求日志中间件
javascript
// Express + Pino
import pinoHttp from 'pino-http'
app.use(pinoHttp({
logger,
autoLogging: true,
customProps: (req) => ({
requestId: req.headers['x-request-id'] || crypto.randomUUID(),
userId: req.user?.id,
}),
customSuccessMessage: (req, res) => {
return `${req.method} ${req.url} ${res.statusCode}`
},
customErrorMessage: (req, res, err) => {
return `${req.method} ${req.url} failed: ${err.message}`
},
}))
// 每个请求自动记录:
// {"method":"GET","url":"/api/users","statusCode":200,"responseTime":45,"requestId":"xxx"}Request ID 链路追踪
javascript
import { AsyncLocalStorage } from 'async_hooks'
const als = new AsyncLocalStorage()
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || crypto.randomUUID()
res.setHeader('x-request-id', requestId)
als.run({ requestId }, next)
})
function getLogger() {
const store = als.getStore()
return logger.child({ requestId: store?.requestId })
}
// 在任何地方使用
async function createOrder(data) {
const log = getLogger()
log.info({ data }, 'Creating order')
// ...
log.info({ orderId }, 'Order created')
}
// 同一个请求的所有日志都带有相同的 requestId → 可以串联查看日志架构
应用日志 → 文件 / stdout → 日志收集 → 存储 → 查询/告警
方案 1: ELK Stack
App → Filebeat → Logstash → Elasticsearch → Kibana
适合: 大规模、复杂查询
方案 2: Loki + Grafana
App → Promtail → Loki → Grafana
适合: 轻量级、和 Prometheus 配合
方案 3: 云服务
App → stdout → CloudWatch / Datadog / 阿里云 SLS
适合: 云原生、免运维
Docker / K8s 环境:
→ 日志输出到 stdout/stderr(不写文件)
→ 容器运行时自动收集日志
→ 统一由日志平台管理追问延伸
AsyncLocalStorage的原理?和 Thread-Local Storage 有什么关系?- 如何处理敏感信息脱敏?(密码、手机号、身份证号不能写入日志)
- 日志采样策略?如何在高流量场景下控制日志量?
15. Node.js 如何实现实时通信?SSE vs WebSocket 的选择? ⭐⭐
在 Node.js 中实现实时数据推送。
考察点:实时通信
实时通信方案对比
| 方案 | 方向 | 协议 | 重连 | 适用场景 |
|---|---|---|---|---|
| 短轮询 | 单向 | HTTP | 无需 | 简单状态检查 |
| 长轮询 | 单向 | HTTP | 需要 | 兼容性要求高 |
| SSE | 单向(服务器→客户端) | HTTP | 自动 | AI 流式输出、通知推送 |
| WebSocket | 双向 | WS | 需自行实现 | 即时聊天、协作编辑 |
SSE 实现
javascript
// 服务端 (Express)
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
const sendEvent = (event, data) => {
res.write(`event: ${event}\n`)
res.write(`data: ${JSON.stringify(data)}\n`)
res.write(`id: ${Date.now()}\n`)
res.write('\n')
}
sendEvent('connected', { message: 'SSE connected' })
const interval = setInterval(() => {
sendEvent('heartbeat', { time: Date.now() })
}, 30000)
req.on('close', () => {
clearInterval(interval)
})
})
// 客户端
const eventSource = new EventSource('/api/events')
eventSource.addEventListener('connected', (e) => {
console.log('连接成功:', JSON.parse(e.data))
})
eventSource.addEventListener('notification', (e) => {
const data = JSON.parse(e.data)
showNotification(data)
})
eventSource.onerror = () => {
console.log('连接断开,浏览器会自动重连')
// SSE 会自动重连,并通过 Last-Event-ID 恢复
}AI 流式输出(SSE 最佳场景)
typescript
// 服务端: AI 聊天流式返回
app.post('/api/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: req.body.messages,
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || ''
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`)
}
}
res.write('data: [DONE]\n\n')
res.end()
})
// 客户端: 使用 fetch + ReadableStream
async function* streamChat(messages) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages }),
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
const lines = text.split('\n').filter(line => line.startsWith('data: '))
for (const line of lines) {
const data = line.replace('data: ', '')
if (data === '[DONE]') return
yield JSON.parse(data).content
}
}
}
// 使用
for await (const token of streamChat([{ role: 'user', content: 'Hello' }])) {
outputElement.textContent += token
}WebSocket 实现 (ws 库)
javascript
import { WebSocketServer } from 'ws'
import http from 'http'
const server = http.createServer(app)
const wss = new WebSocketServer({ server })
const clients = new Map()
wss.on('connection', (ws, req) => {
const userId = authenticateFromUrl(req.url)
clients.set(userId, ws)
ws.on('message', (data) => {
const message = JSON.parse(data)
switch (message.type) {
case 'chat':
broadcastToRoom(message.roomId, {
type: 'chat',
from: userId,
content: message.content,
timestamp: Date.now(),
})
break
case 'typing':
broadcastToRoom(message.roomId, {
type: 'typing',
from: userId,
}, [userId])
break
}
})
ws.on('close', () => {
clients.delete(userId)
})
ws.send(JSON.stringify({ type: 'connected', userId }))
})
function broadcastToRoom(roomId, data, excludeUsers = []) {
const message = JSON.stringify(data)
for (const [userId, ws] of clients) {
if (!excludeUsers.includes(userId) && ws.readyState === ws.OPEN) {
ws.send(message)
}
}
}
server.listen(3000)如何选择
选 SSE 当:
✅ 只需要服务端向客户端推送(单向)
✅ AI 流式输出 / 实时通知 / 进度更新
✅ 需要自动重连 + 断点续传
✅ 基于 HTTP → 不需要额外协议 → CDN 友好
选 WebSocket 当:
✅ 需要双向通信(客户端和服务端互发消息)
✅ 即时聊天 / 协作编辑 / 在线游戏
✅ 低延迟要求(WebSocket 头部更小)
✅ 需要自定义二进制协议
选 长轮询 当:
✅ 需要兼容极旧浏览器
✅ 基础设施不支持 WebSocket/SSE追问延伸
- Socket.IO 和原生 WebSocket 有什么区别?什么时候需要 Socket.IO?
- 如何处理 WebSocket 的扩容?(Redis Pub/Sub 跨实例广播)
- 如何实现可靠的消息送达?(消息确认 + 离线消息队列)