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.nextTick、setImmediate |
requestAnimationFrame、requestIdleCallback |
| 阶段 | 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.createReadStream、http.IncomingMessage |
| Writable | 可写流 | fs.createWriteStream、http.ServerResponse |
| Duplex | 双工流(可读可写) | net.Socket、zlib |
| 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而不是回调版的fs。path.join和path.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') // '张三'
💡 面试加分点 :
execvsspawn的核心区别 ------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 后应先停止接收新请求,等现有请求处理完毕,再关闭数据库连接,最后退出进程。