Node.js 高频面试题

Node.js 高频面试题

1. Node.js 是什么?有哪些核心特点?

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,让 JavaScript 可以运行在服务端。

特性 说明
单线程 主线程单线程,通过事件循环处理并发(避免多线程上下文切换开销)
非阻塞 I/O I/O 操作异步执行,不阻塞主线程(底层使用 libuv 线程池)
事件驱动 基于事件循环(Event Loop)机制
跨平台 Windows、Linux、macOS
npm 生态 全球最大的开源包生态系统

Node.js 适合/不适合的场景

✅ 适合 ❌ 不适合
I/O 密集型应用(API 服务器、实时聊天) CPU 密集型计算(视频编码、大量数学运算)
高并发连接处理 复杂的关系型数据处理
实时应用(WebSocket、SSE) 需要多线程并行计算
微服务架构 强类型要求的企业级应用
BFF(Backend For Frontend)层

💡 面试加分点 :虽然 Node.js 主线程是单线程,但底层 libuv 使用了线程池(默认 4 个线程)来处理 I/O 操作。CPU 密集型任务可以使用 worker_threads 模块来创建工作线程。


2. Node.js 的事件循环(Event Loop)机制?

这是 Node.js 最核心的概念,与浏览器的事件循环有显著区别。

六个阶段(从上到下循环执行)

arduino 复制代码
   ┌───────────────────────────┐
┌──▶│        timers             │ ← setTimeout / setInterval 回调
│  └───────────┬───────────────┘
│  ┌───────────▼───────────────┐
│  │     pending callbacks     │ ← 系统级回调(如 TCP 错误)
│  └───────────┬───────────────┘
│  ┌───────────▼───────────────┐
│  │       idle, prepare       │ ← 内部使用
│  └───────────┬───────────────┘
│  ┌───────────▼───────────────┐
│  │          poll              │ ← 获取新的 I/O 事件,执行 I/O 回调
│  └───────────┬───────────────┘
│  ┌───────────▼───────────────┐
│  │          check             │ ← setImmediate 回调
│  └───────────┬───────────────┘
│  ┌───────────▼───────────────┐
│  │     close callbacks       │ ← socket.on('close') 等回调
│  └───────────┬───────────────┘
└──────────────┘

微任务优先级

arduino 复制代码
每个阶段之间执行微任务:
process.nextTick(优先级最高)> Promise.then > queueMicrotask
javascript 复制代码
// ✅ 经典面试题:输出顺序
console.log('1: script start')

setTimeout(() => console.log('2: setTimeout'), 0)

setImmediate(() => console.log('3: setImmediate'))

process.nextTick(() => console.log('4: nextTick'))

Promise.resolve().then(() => console.log('5: Promise'))

console.log('6: script end')

// 输出:
// 1: script start
// 6: script end
// 4: nextTick        ← 微任务最高优先级
// 5: Promise          ← 微任务
// 2: setTimeout       ← timers 阶段
// 3: setImmediate     ← check 阶段
// ⚠️ setTimeout 和 setImmediate 在主模块中顺序不确定!
javascript 复制代码
// ✅ 在 I/O 回调中,setImmediate 一定先于 setTimeout
const fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => console.log('setTimeout'), 0)
  setImmediate(() => console.log('setImmediate'))
})
// 始终输出:
// setImmediate  ← 因为 I/O 回调在 poll 阶段,check 阶段紧跟其后
// setTimeout

Node.js vs 浏览器 事件循环对比

特性 Node.js 浏览器
微任务时机 每个阶段之间执行 每个宏任务之后执行
特有 API process.nextTicksetImmediate requestAnimationFramerequestIdleCallback
阶段 6 个明确阶段 宏任务队列 + 微任务队列
nextTick 优先级最高的微任务 不支持

💡 面试加分点process.nextTick 虽然好用,但滥用会导致 I/O 饿死(微任务一直执行,事件循环无法进入下一个阶段)。推荐使用 setImmediate 替代递归 nextTick


3. CommonJS 和 ES Module 的区别?

特性 CommonJS (CJS) ES Module (ESM)
语法 require / module.exports import / export
加载时机 运行时(动态加载) 编译时(静态分析)
同步/异步 同步加载 异步加载
值的引用 值的拷贝(导出后修改不影响引入方) 值的引用(导出后修改会同步)
this 指向 module 对象 undefined
循环引用 返回已执行的部分 引用绑定(动态引用)
Tree-shaking ❌ 不支持 ✅ 支持
javascript 复制代码
// === CommonJS ===
// math.js
let count = 0
const increment = () => ++count
const getCount = () => count
module.exports = { count, increment, getCount }

// main.js
const math = require('./math')
console.log(math.count)      // 0
math.increment()
console.log(math.count)      // 0 ← 值的拷贝,不会变!
console.log(math.getCount()) // 1 ← 通过函数可以获取最新值
javascript 复制代码
// === ES Module ===
// math.mjs
export let count = 0
export const increment = () => ++count

// main.mjs
import { count, increment } from './math.mjs'
console.log(count)   // 0
increment()
console.log(count)   // 1 ← 值的引用,会同步变化!
javascript 复制代码
// 动态导入(两者都支持)
const module = await import('./module.js')

// package.json 中指定模块类型
// "type": "module"    → .js 文件使用 ESM
// "type": "commonjs"  → .js 文件使用 CJS(默认)

// 混合使用
// CJS 中使用 ESM:必须用动态 import()
// ESM 中使用 CJS:可以直接 import(Node.js 自动转换)

💡 面试加分点:CJS 导出的是值的拷贝,ESM 导出的是值的引用(live bindings)------ 这是面试中最常问的区别。ESM 的静态分析特性使得打包工具可以进行 Tree-shaking(移除未使用的导出)。


4. Express 中间件机制是怎样的?

Express 中间件是按照洋葱模型的简化版执行的 ------ 请求从上到下依次经过每个中间件,每个中间件可以决定是否将请求传递给下一个。

复制代码
请求 → 中间件1 → 中间件2 → 中间件3 → 路由处理 → 响应
javascript 复制代码
const express = require('express')
const app = express()

// ========== 内置中间件 ==========
app.use(express.json())                          // 解析 JSON body
app.use(express.urlencoded({ extended: true }))  // 解析表单数据
app.use(express.static('public'))                // 静态文件

// ========== 自定义中间件 ==========
// ✅ 日志中间件
const logger = (req, res, next) => {
  const start = Date.now()
  console.log(`→ ${req.method} ${req.url}`)

  // 响应完成后记录耗时
  res.on('finish', () => {
    const duration = Date.now() - start
    console.log(`← ${req.method} ${req.url} ${res.statusCode} [${duration}ms]`)
  })

  next() // ⚠️ 必须调用 next(),否则请求会挂起!
}
app.use(logger)

// ✅ 认证中间件
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]
  if (!token) {
    return res.status(401).json({ message: '请先登录' })
    // ⚠️ 不调用 next(),请求到此为止
  }
  try {
    const decoded = jwt.verify(token, SECRET_KEY)
    req.user = decoded // 将用户信息挂载到 req 上
    next()
  } catch (err) {
    res.status(401).json({ message: 'Token 无效或已过期' })
  }
}

// ========== 路由 ==========
// 公开接口(不需要认证)
app.post('/api/login', async (req, res) => {
  // 登录逻辑...
})

// 需要认证的接口
app.get('/api/users', authenticate, async (req, res) => {
  const { page = 1, size = 10 } = req.query
  const users = await User.find()
    .skip((page - 1) * size)
    .limit(Number(size))
  res.json({ code: 0, data: users })
})

// ========== 错误处理中间件(必须 4 个参数) ==========
app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(err.status || 500).json({
    code: -1,
    message: err.message || '服务器内部错误',
  })
})

app.listen(3000, () => console.log('Server running on :3000'))

中间件类型

类型 说明 示例
应用级 app.use(fn) 日志、CORS
路由级 router.use(fn) 模块权限控制
错误处理 app.use((err, req, res, next) => {}) 全局错误捕获
内置 express.json() 请求解析
第三方 cors()helmet() 功能增强

💡 面试加分点:Express 中间件是线性执行的(不是洋葱模型),而 Koa 使用的是真正的洋葱模型(基于 async/await 的洋葱模型,请求和响应都会经过每个中间件)。


5. Express 和 Koa 有什么区别?

特性 Express Koa
开发者 TJ Holowaychuk 团队 同一团队(Express 原班人马)
中间件模型 线性(callback-based) 洋葱模型(async/await)
异步处理 回调 / 需手动包装 async 原生支持 async/await
错误处理 需要 4 参数中间件 try/catch 自然捕获
内置功能 路由、模板引擎、静态文件 极简,仅 HTTP 核心
包大小 较大 轻量
路由 内置 @koa/router
社区生态 更丰富、更成熟 精而少
javascript 复制代码
// ✅ Koa 洋葱模型示例
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
  console.log('中间件1 - 进入')  // 1️⃣
  await next()
  console.log('中间件1 - 退出')  // 6️⃣
})

app.use(async (ctx, next) => {
  console.log('中间件2 - 进入')  // 2️⃣
  await next()
  console.log('中间件2 - 退出')  // 5️⃣
})

app.use(async (ctx) => {
  console.log('中间件3 - 核心')  // 3️⃣
  ctx.body = 'Hello'             // 4️⃣
})

// 执行顺序:1 → 2 → 3 → 4 → 5 → 6(洋葱模型)
javascript 复制代码
// ✅ Koa 的错误处理更优雅
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = { message: err.message }
    ctx.app.emit('error', err, ctx) // 触发全局错误事件
  }
})

💡 面试加分点 :Koa 的洋葱模型让中间件可以在 await next() 之后执行逻辑(如计算请求耗时),这在 Express 中难以优雅实现。新项目推荐 Koa,稳定运维的大型项目通常用 Express(生态成熟)。


6. Node.js 的 Stream(流)有哪些类型?

Stream 是处理大量数据的高效方式,不需要将所有数据加载到内存。

流类型 说明 常见示例
Readable 可读流 fs.createReadStreamhttp.IncomingMessage
Writable 可写流 fs.createWriteStreamhttp.ServerResponse
Duplex 双工流(可读可写) net.Socketzlib
Transform 转换流(读写+转换) zlib.createGzip()crypto.createCipher()
javascript 复制代码
const fs = require('fs')
const { Transform, pipeline } = require('stream')
const zlib = require('zlib')

// ✅ 读取大文件(避免内存溢出)
const readStream = fs.createReadStream('large-file.txt', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024, // 每次读取 64KB
})

readStream.on('data', (chunk) => console.log(`读取了 ${chunk.length} 字节`))
readStream.on('end', () => console.log('读取完成'))
readStream.on('error', (err) => console.error(err))

// ✅ pipe:将读流连接到写流(自动处理背压)
fs.createReadStream('input.txt')
  .pipe(fs.createWriteStream('output.txt'))
  .on('finish', () => console.log('复制完成'))

// ✅ Transform 流:数据转换
const toUpperCase = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase())
    callback()
  },
})

// ✅ 链式 pipe(文件读取 → 转大写 → 压缩 → 写入)
fs.createReadStream('input.txt')
  .pipe(toUpperCase)
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('output.txt.gz'))

// ✅ 推荐使用 pipeline(自动处理错误和清理)
const { promisify } = require('util')
const pipelineAsync = promisify(pipeline)

async function compressFile(input, output) {
  await pipelineAsync(
    fs.createReadStream(input),
    zlib.createGzip(),
    fs.createWriteStream(output)
  )
  console.log('压缩完成')
}
javascript 复制代码
// ✅ 为什么要用 Stream?
// 假设读取 2GB 文件:

// ❌ 一次性读取(内存爆炸)
const data = fs.readFileSync('2GB-file.txt') // 占用 2GB+ 内存

// ✅ 使用 Stream(仅占用 64KB 缓冲区)
const stream = fs.createReadStream('2GB-file.txt', { highWaterMark: 64 * 1024 })
stream.pipe(res) // 直接输出给客户端,内存占用极小

💡 面试加分点 :Stream 的 背压(Backpressure) 是一个重要概念 ------ 当写入速度慢于读取速度时,pipe 会自动暂停读取流,防止内存溢出。pipeline() 相比手动 pipe 更安全,会自动处理错误传播和流的销毁。


7. Node.js 如何处理文件操作?

javascript 复制代码
const fs = require('fs')
const fsPromises = require('fs/promises')  // ✅ 推荐使用 Promise 版本
const path = require('path')

// ========== 文件读写 ==========
// ✅ 异步读取(推荐)
const readFile = async (filePath) => {
  try {
    const content = await fsPromises.readFile(filePath, 'utf8')
    return content
  } catch (err) {
    if (err.code === 'ENOENT') throw new Error('文件不存在')
    if (err.code === 'EACCES') throw new Error('没有读取权限')
    throw err
  }
}

// ✅ 写入文件(自动创建目录)
const writeFile = async (filePath, content) => {
  await fsPromises.mkdir(path.dirname(filePath), { recursive: true })
  await fsPromises.writeFile(filePath, content, 'utf8')
}

// ✅ 追加内容
await fsPromises.appendFile('log.txt', `${new Date().toISOString()} - 操作日志\n`)

// ========== 目录操作 ==========
// 读取目录(含文件类型)
const listDir = async (dirPath) => {
  const entries = await fsPromises.readdir(dirPath, { withFileTypes: true })
  return entries.map(entry => ({
    name: entry.name,
    isDirectory: entry.isDirectory(),
    isFile: entry.isFile(),
    path: path.join(dirPath, entry.name),
  }))
}

// ✅ 递归遍历目录
async function walkDir(dir) {
  const files = []
  const entries = await fsPromises.readdir(dir, { withFileTypes: true })
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)
    if (entry.isDirectory()) {
      files.push(...await walkDir(fullPath))
    } else {
      files.push(fullPath)
    }
  }
  return files
}

// ========== 文件信息 ==========
const stat = await fsPromises.stat('file.txt')
console.log({
  size: stat.size,             // 文件大小(字节)
  isFile: stat.isFile(),
  isDir: stat.isDirectory(),
  created: stat.birthtime,     // 创建时间
  modified: stat.mtime,        // 修改时间
})

// ========== 文件存在检查 ==========
const exists = async (filePath) => {
  try {
    await fsPromises.access(filePath)
    return true
  } catch {
    return false
  }
}

// ========== 监听文件变化 ==========
fs.watch('config.json', (eventType, filename) => {
  console.log(`${filename} 发生了 ${eventType} 事件`)
})
javascript 复制代码
// ✅ path 模块常用方法
const p = '/home/user/docs/report.pdf'

path.dirname(p)     // '/home/user/docs'    ← 目录名
path.basename(p)    // 'report.pdf'         ← 文件名
path.extname(p)     // '.pdf'               ← 扩展名
path.parse(p)       // { root, dir, base, ext, name }
path.join('a', 'b', 'c')       // 'a/b/c'(跨平台)
path.resolve('a', 'b')         // 绝对路径
path.relative('/a/b', '/a/c')  // '../c'(相对路径)

💡 面试加分点 :始终使用 fs/promises 而不是回调版的 fspath.joinpath.resolve 的区别:join 只是简单拼接,resolve 会解析为绝对路径。


8. Node.js 如何处理跨域(CORS)?

javascript 复制代码
const express = require('express')
const app = express()

// ✅ 方式1:cors 中间件(推荐)
const cors = require('cors')
app.use(cors({
  origin: (origin, callback) => {
    const allowList = ['http://localhost:3000', 'https://example.com']
    if (!origin || allowList.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('CORS 不允许'))
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
  exposedHeaders: ['X-Total-Count'],  // 允许前端读取的自定义响应头
  credentials: true,     // 允许携带 Cookie
  maxAge: 86400,         // 预检请求缓存 24 小时
}))

// ✅ 方式2:手动设置响应头(灵活控制)
app.use((req, res, next) => {
  const origin = req.headers.origin
  const allowList = ['http://localhost:3000', 'https://example.com']

  if (allowList.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  res.setHeader('Access-Control-Allow-Credentials', 'true')
  res.setHeader('Access-Control-Max-Age', '86400')

  // 预检请求直接返回
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204)
  }
  next()
})

CORS 请求分类

类型 条件 预检请求
简单请求 GET/POST/HEAD + 简单头部 不需要
预检请求 PUT/DELETE/PATCH 或自定义头部 需要 OPTIONS 预检

💡 面试加分点Access-Control-Max-Age 可以缓存预检请求结果,避免每次都发 OPTIONS 请求。当 credentials: true 时,Allow-Origin 不能为 *,必须指定具体域名。


9. Node.js 的 cluster 模块如何利用多核 CPU?

Node.js 是单线程的,无法直接利用多核 CPU。cluster 模块通过 fork 多个子进程来实现多核利用。

markdown 复制代码
             ┌─────────────────┐
             │   Master 进程    │  ← 管理子进程
             │  (不处理请求)     │
             └──────┬──────────┘
        ┌───────────┼───────────┐
        ▼           ▼           ▼
   ┌─────────┐ ┌─────────┐ ┌─────────┐
   │ Worker1 │ │ Worker2 │ │ Worker3 │  ← 处理请求
   │  :3000  │ │  :3000  │ │  :3000  │  ← 共享同一端口
   └─────────┘ └─────────┘ └─────────┘
javascript 复制代码
const cluster = require('cluster')
const http = require('http')
const os = require('os')

const numCPUs = os.cpus().length

if (cluster.isPrimary) {
  console.log(`主进程 ${process.pid} 运行中,CPU 核心数: ${numCPUs}`)

  // 为每个 CPU 核心创建工作进程
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }

  // 监听工作进程退出 → 自动重启(守护进程)
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 退出 (code: ${code}, signal: ${signal})`)
    if (code !== 0) {
      console.log('异常退出,正在重启...')
      cluster.fork()
    }
  })

  // 主进程与工作进程通信
  for (const id in cluster.workers) {
    cluster.workers[id].on('message', (msg) => {
      console.log(`收到 Worker ${id} 的消息:`, msg)
    })
  }

} else {
  // 工作进程创建 HTTP 服务器
  http.createServer((req, res) => {
    res.writeHead(200)
    res.end(`Worker ${process.pid} 处理了请求\n`)
  }).listen(3000)

  console.log(`工作进程 ${process.pid} 启动`)
}

cluster vs worker_threads

特性 cluster worker_threads
隔离级别 进程级(独立内存) 线程级(可共享内存)
通信方式 IPC(进程间通信) MessagePort / SharedArrayBuffer
适用场景 多核 HTTP 服务 CPU 密集型计算
开销 较大(完整 V8 实例) 较小
javascript 复制代码
// ✅ 生产环境推荐使用 PM2(比手写 cluster 更强大)
// pm2 start app.js -i max        ← 启动 CPU 核心数个进程
// pm2 start app.js -i 4          ← 启动 4 个进程
// pm2 reload app.js              ← 零停机重启
// pm2 monit                      ← 监控面板

💡 面试加分点 :生产环境通常不直接使用 cluster 模块,而是使用 PM2 进程管理工具。PM2 提供了进程守护、负载均衡、日志管理、零停机重启等完整功能。


10. Node.js 常用的内置模块有哪些?

javascript 复制代码
// ========== http/https:创建服务器 ==========
const http = require('http')
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ message: 'Hello' }))
})
server.listen(3000)

// ========== os:操作系统信息 ==========
const os = require('os')
os.cpus()          // CPU 信息
os.cpus().length   // CPU 核心数
os.totalmem()      // 总内存(字节)
os.freemem()       // 空闲内存
os.platform()      // 'win32' | 'linux' | 'darwin'
os.hostname()      // 主机名
os.homedir()       // 用户主目录
os.tmpdir()        // 临时目录

// ========== crypto:加密 ==========
const crypto = require('crypto')

// 哈希
const hash = crypto.createHash('sha256').update('password').digest('hex')

// HMAC 签名
const hmac = crypto.createHmac('sha256', 'secret').update('data').digest('hex')

// 随机字节
const randomBytes = crypto.randomBytes(16).toString('hex') // 32字符随机字符串

// UUID
const uuid = crypto.randomUUID() // 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

// ========== events:事件发射器 ==========
const EventEmitter = require('events')
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter()

emitter.on('data', (data) => console.log('收到:', data))
emitter.once('connect', () => console.log('只触发一次'))
emitter.emit('data', { id: 1 })
emitter.emit('connect')

// ========== child_process:子进程 ==========
const { exec, execFile, spawn, fork } = require('child_process')

// exec:执行 shell 命令(缓冲输出,适合小数据)
exec('ls -la', (err, stdout, stderr) => console.log(stdout))

// spawn:流式输出(适合大数据,如日志流)
const child = spawn('node', ['script.js'])
child.stdout.on('data', (data) => console.log(data.toString()))

// fork:创建 Node.js 子进程(可通过 IPC 通信)
const worker = fork('worker.js')
worker.send({ type: 'start', data: bigArray })
worker.on('message', (result) => console.log(result))

// ========== url / querystring ==========
const url = new URL('https://example.com/path?name=张三&age=25')
url.hostname       // 'example.com'
url.pathname       // '/path'
url.searchParams.get('name') // '张三'

💡 面试加分点exec vs spawn 的核心区别 ------ exec 缓冲所有输出到内存再返回(有最大缓冲限制),spawn 通过流实时输出(适合大量数据)。fork 专门用于创建 Node.js 子进程,自带 IPC 通信通道。


11. Node.js 如何处理错误?

javascript 复制代码
// ========== 1. 同步错误:try/catch ==========
try {
  const data = JSON.parse(invalidJson)
} catch (err) {
  console.error('JSON 解析失败:', err.message)
}

// ========== 2. 异步错误:async/await + try/catch ==========
async function fetchData() {
  try {
    const data = await fsPromises.readFile('config.json', 'utf8')
    return JSON.parse(data)
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.error('配置文件不存在')
      return getDefaultConfig()
    }
    throw err // 不能处理的错误继续抛出
  }
}

// ========== 3. 回调错误:Error-First 模式 ==========
fs.readFile('file.txt', (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(data)
})

// ========== 4. 事件错误:error 事件 ==========
const server = http.createServer()
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error('端口被占用')
  }
})

// ========== 5. 全局错误捕获(兜底) ==========
// 未捕获的异常
process.on('uncaughtException', (err) => {
  console.error('未捕获的异常:', err)
  // 记录日志 → 优雅关闭 → 重启进程
  process.exit(1) // 必须退出,状态可能已损坏
})

// 未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise 拒绝:', reason)
  // 记录日志,但不一定需要退出
})
javascript 复制代码
// ✅ Express 全局错误处理最佳实践
// 包装 async 路由,自动捕获错误
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next)
}

// 使用
app.get('/users', asyncHandler(async (req, res) => {
  const users = await User.find() // 如果报错,自动传给错误中间件
  res.json(users)
}))

// 错误中间件
app.use((err, req, res, next) => {
  console.error(err)
  res.status(err.status || 500).json({
    code: -1,
    message: process.env.NODE_ENV === 'production'
      ? '服务器错误'
      : err.message,
  })
})

💡 面试加分点uncaughtException 发生后进程状态可能已损坏,应该记录日志后退出进程(配合 PM2 自动重启)。unhandledRejection 在 Node.js 15+ 默认也会导致进程退出。


12. Node.js 中的 Buffer 是什么?

Buffer 是 Node.js 用来处理二进制数据的类,在处理文件、网络传输、加密等场景中必不可少。

javascript 复制代码
// ========== 创建 Buffer ==========
const buf1 = Buffer.from('Hello, 世界')          // 从字符串创建
const buf2 = Buffer.from([0x48, 0x65, 0x6c])     // 从字节数组创建
const buf3 = Buffer.alloc(10)                      // 创建 10 字节的空 Buffer
const buf4 = Buffer.allocUnsafe(10)                // 不初始化(更快,但可能有旧数据)

// ========== 基本操作 ==========
console.log(buf1.length)           // 字节长度(注意:中文 3 字节)
console.log(buf1.toString())       // 'Hello, 世界'
console.log(buf1.toString('hex'))  // 十六进制表示
console.log(buf1.toString('base64')) // Base64 编码

// ========== 拷贝与拼接 ==========
const buf5 = Buffer.concat([buf1, buf2])  // 拼接多个 Buffer

// ========== 比较 ==========
Buffer.compare(buf1, buf2)  // 0(相等) / -1 / 1
buf1.equals(buf2)           // true/false

// ========== 实际应用:文件转 Base64 ==========
const fileBuffer = await fsPromises.readFile('image.png')
const base64 = fileBuffer.toString('base64')
const dataUrl = `data:image/png;base64,${base64}`

// ========== 实际应用:处理 HTTP 请求体 ==========
const server = http.createServer((req, res) => {
  const chunks = []
  req.on('data', (chunk) => chunks.push(chunk))
  req.on('end', () => {
    const body = Buffer.concat(chunks).toString()
    const data = JSON.parse(body)
    res.end(JSON.stringify({ received: data }))
  })
})
方法 说明
Buffer.from(str) 字符串转 Buffer
Buffer.alloc(size) 创建指定大小的空 Buffer(零填充)
Buffer.allocUnsafe(size) 创建未初始化 Buffer(更快)
Buffer.concat(list) 拼接多个 Buffer
buf.toString(encoding) 转为字符串(utf8/hex/base64)

💡 面试加分点Buffer.alloc() 会将内存初始化为 0(安全),Buffer.allocUnsafe() 不初始化(快但可能泄露旧数据)。处理用户输入时始终使用 alloc()。Buffer 底层使用 V8 堆外内存(C++ 层面),不受 V8 内存限制。


13. Node.js 的进程间通信(IPC)有哪些方式?

方式 场景 特点
child_process.fork + IPC 父子进程通信 内置,简单
cluster 模块 多进程 HTTP 服务 共享端口
worker_threads + MessagePort 线程间通信 可共享内存
Socket / 管道 任意进程通信 xxxxxxxxxx // 解决 JS 阻塞渲染的方案​// 1. 将耗时任务分片(时间切片)function processLargeArray(array, callback) { const chunkSize = 1000 let index = 0​ function processChunk() { const end = Math.min(index + chunkSize, array.length) for (; index < end; index++) { callback(arrayindex) } if (index < array.length) { requestAnimationFrame(processChunk) // 下一帧继续 } }​ requestAnimationFrame(processChunk)}​// 2. 使用 Web Worker 在后台线程处理const worker = new Worker('heavy-task.js')worker.postMessage(largeData)worker.onmessage = (e) => console.log('处理完成:', e.data)​// 3. 使用 requestIdleCallback 在空闲时执行requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 0 && tasks.length > 0) { processTask(tasks.shift()) }})javascript
共享内存(SharedArrayBuffer) 高性能数据共享 需自行同步
javascript 复制代码
// ✅ 1. fork + IPC(最常用)
// parent.js
const { fork } = require('child_process')
const child = fork('./worker.js')

child.send({ type: 'CALCULATE', data: [1, 2, 3, 4, 5] })
child.on('message', (result) => {
  console.log('计算结果:', result) // { sum: 15 }
})

// worker.js
process.on('message', (msg) => {
  if (msg.type === 'CALCULATE') {
    const sum = msg.data.reduce((a, b) => a + b, 0)
    process.send({ sum })
  }
})
javascript 复制代码
// ✅ 2. worker_threads + SharedArrayBuffer(高性能)
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads')

if (isMainThread) {
  // 创建共享内存
  const sharedBuffer = new SharedArrayBuffer(4)
  const sharedArray = new Int32Array(sharedBuffer)
  sharedArray[0] = 0

  const worker = new Worker(__filename, {
    workerData: { sharedBuffer },
  })

  worker.on('message', () => {
    console.log('共享内存中的值:', sharedArray[0]) // 100
  })
} else {
  // 工作线程直接操作共享内存
  const sharedArray = new Int32Array(workerData.sharedBuffer)
  Atomics.store(sharedArray, 0, 100) // 原子操作
  parentPort.postMessage('done')
}

💡 面试加分点SharedArrayBuffer 允许多线程共享内存(零拷贝),但需要使用 Atomics 进行原子操作来避免竞态条件。这是 Node.js 中最高性能的线程间数据交换方式。


14. 如何用 Node.js 实现 JWT 认证?

javascript 复制代码
const express = require('express')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')

const app = express()
app.use(express.json())

const SECRET = process.env.JWT_SECRET || 'your-secret-key'
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-refresh-secret'

// ✅ 注册
app.post('/api/register', async (req, res) => {
  const { username, password } = req.body

  // 密码加密存储
  const salt = await bcrypt.genSalt(10)
  const hashedPassword = await bcrypt.hash(password, salt)

  const user = await User.create({ username, password: hashedPassword })
  res.status(201).json({ message: '注册成功' })
})

// ✅ 登录(签发 Token)
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body

  const user = await User.findOne({ username })
  if (!user) return res.status(401).json({ message: '用户不存在' })

  const isMatch = await bcrypt.compare(password, user.password)
  if (!isMatch) return res.status(401).json({ message: '密码错误' })

  // 签发 Access Token(短期有效)
  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    SECRET,
    { expiresIn: '2h' }
  )

  // 签发 Refresh Token(长期有效)
  const refreshToken = jwt.sign(
    { userId: user.id },
    REFRESH_SECRET,
    { expiresIn: '7d' }
  )

  res.json({ accessToken, refreshToken })
})

// ✅ 认证中间件
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ message: '缺少认证信息' })
  }

  const token = authHeader.split(' ')[1]
  try {
    const decoded = jwt.verify(token, SECRET)
    req.user = decoded
    next()
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ message: 'Token 已过期', code: 'TOKEN_EXPIRED' })
    }
    return res.status(401).json({ message: 'Token 无效' })
  }
}

// ✅ 权限中间件
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ message: '权限不足' })
  }
  next()
}

// ✅ 受保护的路由
app.get('/api/profile', authenticate, (req, res) => {
  res.json({ userId: req.user.userId })
})

app.delete('/api/users/:id', authenticate, authorize('admin'), (req, res) => {
  // 仅管理员可访问
})

// ✅ 刷新 Token
app.post('/api/refresh', (req, res) => {
  const { refreshToken } = req.body
  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET)
    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      SECRET,
      { expiresIn: '2h' }
    )
    res.json({ accessToken: newAccessToken })
  } catch (err) {
    res.status(401).json({ message: 'Refresh Token 无效' })
  }
})

💡 面试加分点 :JWT 的三段结构(Header.Payload.Signature)中,Payload 是 Base64 编码(非加密),任何人都能解码查看内容,因此绝不能存储敏感信息(如密码)。签名部分用于防篡改。


15. Node.js 如何连接数据库(MongoDB / MySQL)?

javascript 复制代码
// ========== MongoDB(使用 Mongoose) ==========
const mongoose = require('mongoose')

// 连接数据库
mongoose.connect('mongodb://localhost:27017/mydb', {
  maxPoolSize: 10,      // 连接池大小
  serverSelectionTimeoutMS: 5000,
})

mongoose.connection.on('connected', () => console.log('MongoDB 已连接'))
mongoose.connection.on('error', (err) => console.error('连接错误:', err))

// 定义 Schema 和 Model
const userSchema = new mongoose.Schema({
  name:      { type: String, required: true, trim: true },
  email:     { type: String, required: true, unique: true, lowercase: true },
  age:       { type: Number, min: 0, max: 150 },
  role:      { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now },
})

// 添加索引
userSchema.index({ email: 1 })
userSchema.index({ name: 'text' }) // 全文搜索索引

const User = mongoose.model('User', userSchema)

// CRUD 操作
const createUser = (data) => User.create(data)
const findUsers = (query, page = 1, size = 10) =>
  User.find(query)
    .select('name email age')  // 指定返回字段
    .sort({ createdAt: -1 })
    .skip((page - 1) * size)
    .limit(size)
    .lean()                     // 返回纯 JSON(性能更好)

const updateUser = (id, data) =>
  User.findByIdAndUpdate(id, data, { new: true, runValidators: true })

const deleteUser = (id) => User.findByIdAndDelete(id)
javascript 复制代码
// ========== MySQL(使用 mysql2 + 连接池) ==========
const mysql = require('mysql2/promise')

// 创建连接池
const pool = mysql.createPool({
  host: 'localhost',
  port: 3306,
  user: 'root',
  password: 'password',
  database: 'mydb',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
})

// ✅ CRUD 操作(使用参数化查询防 SQL 注入)
const getUsers = async (page = 1, size = 10) => {
  const offset = (page - 1) * size
  const [rows] = await pool.execute(
    'SELECT id, name, email FROM users LIMIT ? OFFSET ?',
    [size, offset]  // ✅ 参数化查询,防止 SQL 注入
  )
  return rows
}

const createUser = async (name, email) => {
  const [result] = await pool.execute(
    'INSERT INTO users (name, email) VALUES (?, ?)',
    [name, email]
  )
  return result.insertId
}

// ✅ 事务操作
const transferMoney = async (fromId, toId, amount) => {
  const conn = await pool.getConnection()
  try {
    await conn.beginTransaction()
    await conn.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId])
    await conn.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId])
    await conn.commit()
  } catch (err) {
    await conn.rollback()
    throw err
  } finally {
    conn.release() // 归还连接到连接池
  }
}

💡 面试加分点 :MySQL 操作必须使用参数化查询 防止 SQL 注入(? 占位符)。连接池(Pool)可以复用数据库连接,避免频繁创建/销毁连接的开销。Mongoose 的 .lean() 方法跳过 Document 实例化,查询性能提升 3-5 倍。


16. Node.js 如何实现定时任务?

javascript 复制代码
// ✅ 方式1:node-cron(推荐)
const cron = require('node-cron')

// Cron 表达式:秒 分 时 日 月 周
cron.schedule('0 0 * * *', () => {
  console.log('每天凌晨 0 点执行')
  cleanExpiredData()
})

cron.schedule('*/5 * * * *', () => {
  console.log('每 5 分钟执行')
  checkServiceHealth()
})

cron.schedule('0 9 * * 1-5', () => {
  console.log('工作日每天 9 点执行')
  sendDailyReport()
})

// ✅ 方式2:setTimeout / setInterval
// 简单延迟任务
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

// 轮询任务(带错误处理和退避)
async function poll(fn, interval = 5000, maxRetries = 3) {
  let retries = 0

  while (true) {
    try {
      await fn()
      retries = 0 // 成功后重置重试计数
    } catch (err) {
      retries++
      console.error(`执行失败 (${retries}/${maxRetries}):`, err.message)
      if (retries >= maxRetries) {
        console.error('达到最大重试次数,暂停任务')
        await delay(interval * 10) // 暂停更长时间
        retries = 0
      }
    }
    await delay(interval)
  }
}

// ✅ 方式3:使用消息队列(生产级方案)
// Bull(基于 Redis 的任务队列)
const Queue = require('bull')
const emailQueue = new Queue('email', 'redis://127.0.0.1:6379')

// 添加任务
emailQueue.add({ to: 'user@example.com', subject: '验证码' }, {
  attempts: 3,        // 失败重试 3 次
  backoff: 5000,      // 重试间隔 5 秒
  delay: 0,           // 延迟执行
  removeOnComplete: true,
})

// 处理任务
emailQueue.process(async (job) => {
  await sendEmail(job.data)
  return { sent: true }
})

💡 面试加分点 :简单定时任务用 node-cron,分布式/可靠性要求高的场景用消息队列(Bull/BullMQ)。setInterval 不适合做定时任务 ------ 进程重启后任务丢失,且不支持分布式。


17. Node.js 如何实现日志管理?

javascript 复制代码
// ✅ 使用 winston(Node.js 最流行的日志库)
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }), // 打印错误堆栈
    winston.format.json()
  ),
  defaultMeta: { service: 'user-api' },
  transports: [
    // 控制台输出(开发环境)
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      ),
    }),
    // 按日期滚动的文件日志(生产环境)
    new DailyRotateFile({
      filename: 'logs/app-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',      // 单文件最大 20MB
      maxFiles: '14d',     // 保留 14 天
      level: 'info',
    }),
    // 错误日志单独存放
    new DailyRotateFile({
      filename: 'logs/error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '30d',
      level: 'error',
    }),
  ],
})

// 使用
logger.info('用户登录', { userId: 123, ip: '192.168.1.1' })
logger.warn('请求频率过高', { userId: 123, count: 100 })
logger.error('数据库连接失败', { error: err.message, stack: err.stack })

// ✅ Express 请求日志中间件
const requestLogger = (req, res, next) => {
  const start = Date.now()
  res.on('finish', () => {
    logger.info('HTTP 请求', {
      method: req.method,
      url: req.originalUrl,
      status: res.statusCode,
      duration: `${Date.now() - start}ms`,
      ip: req.ip,
      userAgent: req.get('User-Agent'),
    })
  })
  next()
}
app.use(requestLogger)
日志级别 含义 场景
error 错误 影响功能的异常
warn 警告 潜在问题,但不影响功能
info 信息 正常业务流程记录
debug 调试 开发时的详细信息

💡 面试加分点:生产环境日志的要点:1)结构化日志(JSON 格式,方便 ELK 采集);2)日志分级;3)日志轮转(按日期/大小切割);4)不记录敏感信息(密码、Token);5)添加请求 ID(traceId)用于链路追踪。


18. Node.js 的内存管理和性能优化?

V8 内存限制

arduino 复制代码
V8 默认内存限制:
- 64 位系统:~1.5GB
- 32 位系统:~0.7GB

可通过启动参数调整:
node --max-old-space-size=4096 app.js  ← 设为 4GB
javascript 复制代码
// ✅ 查看内存使用情况
const formatMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`

setInterval(() => {
  const mem = process.memoryUsage()
  console.log({
    rss: formatMB(mem.rss),           // 常驻集大小(总内存占用)
    heapTotal: formatMB(mem.heapTotal), // V8 堆总大小
    heapUsed: formatMB(mem.heapUsed),   // V8 堆使用量
    external: formatMB(mem.external),   // C++ 对象占用(如 Buffer)
  })
}, 10000)

常见内存泄漏场景

javascript 复制代码
// ❌ 1. 全局变量堆积
const cache = {}  // 永远不清理,越来越大
app.get('/data', (req, res) => {
  cache[req.query.key] = fetchData() // 无限增长!
})

// ✅ 使用 LRU 缓存(有容量限制)
const LRU = require('lru-cache')
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 })

// ❌ 2. 事件监听器未移除
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter()
setInterval(() => {
  emitter.on('data', handler)  // 每次都添加新监听器!
}, 1000)

// ✅ 使用 once 或手动移除
emitter.once('data', handler)
// 或
emitter.on('data', handler)
// ... 不需要时
emitter.removeListener('data', handler)

// ❌ 3. 闭包引用
function createLeak() {
  const bigData = new Array(1000000).fill('x')
  return () => {
    // 即使没用到 bigData,闭包仍然引用它
    console.log('leak')
  }
}

// ❌ 4. 定时器未清除
const timer = setInterval(() => {
  // 某些操作
}, 1000)
// 忘记 clearInterval(timer)
javascript 复制代码
// ✅ 性能优化清单
// 1. 使用 Stream 处理大文件(避免一次性加载到内存)
// 2. 使用连接池(数据库、Redis)
// 3. 使用 cluster / PM2 利用多核 CPU
// 4. 使用缓存(Redis / LRU)减少重复计算
// 5. 压缩响应(gzip/brotli)
// 6. 使用 worker_threads 处理 CPU 密集任务
// 7. 避免同步 I/O(如 fs.readFileSync)
// 8. 使用 --inspect 进行性能分析

💡 面试加分点 :使用 node --inspect app.js + Chrome DevTools 可以进行内存快照分析和 CPU 性能分析。生产环境可以使用 clinic.js 工具自动诊断性能瓶颈。


19. Node.js 的安全最佳实践?

javascript 复制代码
// ========== 1. 防止 SQL/NoSQL 注入 ==========
// ❌ 危险:直接拼接用户输入
const user = await User.findOne({ username: req.body.username })
// 攻击者可以传入 { "$gt": "" } 绕过认证

// ✅ 安全:输入验证 + 类型检查
const { username } = req.body
if (typeof username !== 'string') {
  return res.status(400).json({ message: '参数类型错误' })
}
const user = await User.findOne({ username: String(username) })

// ========== 2. 使用 Helmet 设置安全响应头 ==========
const helmet = require('helmet')
app.use(helmet()) // 一键设置多个安全头

// 等价于设置:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 1; mode=block
// Strict-Transport-Security: max-age=...
// Content-Security-Policy: ...

// ========== 3. 限流(防止暴力破解和 DDoS) ==========
const rateLimit = require('express-rate-limit')

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 分钟
  max: 5,                     // 最多 5 次
  message: { message: '登录尝试过多,请 15 分钟后再试' },
  standardHeaders: true,
  legacyHeaders: false,
})
app.use('/api/login', loginLimiter)

// ========== 4. 输入验证(使用 Joi 或 Zod) ==========
const Joi = require('joi')

const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(150),
})

app.post('/api/users', (req, res) => {
  const { error, value } = userSchema.validate(req.body)
  if (error) {
    return res.status(400).json({ message: error.details[0].message })
  }
  // value 是经过验证的安全数据
})

// ========== 5. 环境变量管理 ==========
// ❌ 硬编码密钥
const SECRET = 'my-super-secret-key'

// ✅ 使用环境变量
require('dotenv').config()
const SECRET = process.env.JWT_SECRET

💡 面试加分点:Node.js 安全核心:1)永远不信任用户输入;2)使用参数化查询;3)密码使用 bcrypt 加盐哈希;4)使用 Helmet 设置安全头;5)限流防暴力破解;6)敏感配置用环境变量,不提交到代码库。


20. 如何部署 Node.js 应用到生产环境?

bash 复制代码
# ========== 1. 使用 PM2 进程管理(必备) ==========
npm install -g pm2

# 启动应用(集群模式)
pm2 start app.js -i max --name "my-api"

# 常用命令
pm2 list                    # 查看所有进程
pm2 monit                   # 实时监控
pm2 logs                    # 查看日志
pm2 reload my-api           # 零停机重启
pm2 stop my-api             # 停止
pm2 delete my-api           # 删除

# 开机自启
pm2 startup
pm2 save
javascript 复制代码
// ecosystem.config.js ------ PM2 配置文件
module.exports = {
  apps: [{
    name: 'my-api',
    script: './app.js',
    instances: 'max',           // CPU 核心数个进程
    exec_mode: 'cluster',       // 集群模式
    max_memory_restart: '1G',   // 内存超过 1G 自动重启
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    env_development: {
      NODE_ENV: 'development',
      PORT: 3000,
    },
    // 日志配置
    log_date_format: 'YYYY-MM-DD HH:mm:ss',
    error_file: './logs/error.log',
    out_file: './logs/out.log',
    merge_logs: true,
  }],
}
// 使用:pm2 start ecosystem.config.js --env production
nginx 复制代码
# ========== 2. Nginx 反向代理 ==========
# /etc/nginx/conf.d/my-api.conf

upstream node_app {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;

    # HTTPS 重定向
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # 静态文件直接由 Nginx 处理
    location /static/ {
        alias /var/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # API 请求代理到 Node.js
    location /api/ {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache_bypass $http_upgrade;
    }

    # Gzip 压缩
    gzip on;
    gzip_types text/plain application/json application/javascript text/css;
    gzip_min_length 1024;
}

部署检查清单

项目 说明
✅ 使用 PM2 集群模式 利用多核 CPU、进程守护
✅ Nginx 反向代理 负载均衡、静态文件服务、HTTPS
✅ 设置环境变量 敏感配置不提交代码
✅ 日志管理 结构化日志 + 日志轮转
✅ 健康检查接口 GET /health 返回服务状态
✅ 优雅关闭 收到 SIGTERM 后完成当前请求再退出
✅ 监控告警 CPU/内存/响应时间监控
javascript 复制代码
// ✅ 优雅关闭(Graceful Shutdown)
process.on('SIGTERM', async () => {
  console.log('收到 SIGTERM 信号,准备关闭...')

  // 停止接收新连接
  server.close(async () => {
    console.log('HTTP 服务器已关闭')

    // 关闭数据库连接
    await mongoose.connection.close()
    console.log('数据库连接已关闭')

    // 退出进程
    process.exit(0)
  })

  // 强制超时退出(防止卡死)
  setTimeout(() => {
    console.error('强制退出')
    process.exit(1)
  }, 10000)
})

💡 面试加分点 :生产部署三件套:PM2 (进程管理)+ Nginx (反向代理)+ Docker(容器化)。优雅关闭是面试常考点 ------ 收到 SIGTERM 后应先停止接收新请求,等现有请求处理完毕,再关闭数据库连接,最后退出进程。

相关推荐
weixin_461408582 小时前
npm npx yarn
开发语言·前端·javascript
四千岁2 小时前
RAG系统中的分块
前端·javascript·后端
START_GAME3 小时前
MSSQL$SQL2016
java·服务器·前端
Captaincc3 小时前
Show me your works & token -稀土掘金上线内测作品广场和用量统计
前端·掘金社区·vibecoding
进击的明明3 小时前
闭包:JavaScript里的“随身背包” 🎒
前端·javascript·面试
紫幽4 小时前
从 0 用 Vue 做屏并生成 LVGL 单片机代码(完整源码备忘)
前端·单片机
众人皆醒我独醉4 小时前
源码导读:一张地图看懂 KServe 仓库
面试·云计算·gpu
郭邯4 小时前
用 AI 辅助开发一个文件大小转换工具:从需求到上线的完整过程
前端
勾勾圈圈蛋蛋4 小时前
前中台项目
前端
触底反弹4 小时前
🔐 前端鉴权不再难!手把手带你搞懂 JWT + Zustand + Axios 的完整登录方案
前端·axios