Skip to content

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_threadschild_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()             // 恢复读取
      })
    }
  })

追问延伸

  • pipepipeline 有什么区别?(pipeline 自动销毁 stream + 错误传播)
  • Web Streams API 和 Node.js Streams 的关系?(Web 标准,Deno/浏览器原生支持)
  • 如何用 Stream 实现大文件分片上传?

3. Express 和 Koa 的区别?中间件原理? ⭐⭐

对比 Node.js 两大 Web 框架。

考察点:Web 框架、中间件

Express vs Koa

维度ExpressKoa
作者TJ HolowaychukTJ Holowaychuk(Express 原班人马)
设计理念大而全(内置路由/模板/静态文件)小而精(核心只有中间件)
中间件回调函数 (req, res, next)async/await (ctx, next)
错误处理回调中 next(err)try/catch(async 天然支持)
Contextreq + 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=20

HTTP 状态码

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关系型复杂查询、JSONpg / postgres.js
MongoDB文档型灵活 Schemamongoose / 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.js
javascript
// 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 的 reloadrestart 有什么区别?(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.allocBuffer.allocUnsafe 的区别?为什么有安全风险?
  • ArrayBufferSharedArrayBufferBuffer 三者的关系?
  • 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.exportsimport / export
加载时机运行时加载编译时静态分析
值类型值的拷贝(修改不影响原模块)值的引用(实时绑定)
顶层 thismodule.exportsundefined
循环依赖返回已执行部分的 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.jsonexports 字段怎么同时支持 CJS 和 ESM?(条件导出)
  • Top-Level Await 的限制和应用场景?
  • 为什么 Node.js 不能直接 require ESM 模块?(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

维度BFFAPI Gateway
关注点前端体验流量管理
职责数据聚合/裁剪/SSR路由/限流/认证/日志
数量每个端一个 BFF通常全局一个
开发者前端团队平台/后端团队
技术栈Node.js / TypeScriptKong / 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 跨实例广播)
  • 如何实现可靠的消息送达?(消息确认 + 离线消息队列)

用心学习,用代码说话 💻