Node.js 与工程化面试题
Node.js 与工程化面试题
简介
Node.js 和前端工程化是现代 Web 开发的基础设施。本篇涵盖 Node.js 核心概念、模块系统、构建工具、包管理、CI/CD 部署等话题,帮助开发者掌握工程化思维和 Node.js 服务端开发能力。
特点
面试题目
1. Node.js 的事件循环与浏览器的有什么区别?
答: Node.js 的事件循环基于 libuv 库,与浏览器的事件循环在阶段划分上有所不同。
// Node.js 事件循环阶段(按顺序执行)
// 1. timers: 执行 setTimeout/setInterval 回调
// 2. pending callbacks: 系统级回调(如 TCP 错误)
// 3. idle, prepare: 内部使用
// 4. poll: 获取新 I/O 事件,执行 I/O 回调
// 5. check: 执行 setImmediate 回调
// 6. close callbacks: 如 socket.on('close')
// 关键区别 1:setImmediate vs setTimeout(0)
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 在主模块中,执行顺序不确定
// 在 I/O 回调中,setImmediate 总是先于 setTimeout
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 在 I/O 回调中,总是输出: immediate -> timeout
});
// 关键区别 2:process.nextTick 优先级高于所有微任务
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// 输出: nextTick -> promise
// process.nextTick 在当前操作完成后、事件循环继续之前执行
// 微任务(Promise)在每个宏任务之间执行2. CommonJS 和 ES Modules 有什么区别?
答: 两种模块系统在语法、加载机制和用途上有所不同。
// CommonJS(CJS)- Node.js 默认
// 导出
module.exports = { add, subtract };
// 或
exports.add = (a, b) => a + b;
// 导入 - 同步加载,运行时解析
const { add } = require('./math');
const math = require('./math');
// ES Modules(ESM)- 现代标准
// 导出
export const add = (a, b) => a + b;
export default class Calculator { /* ... */ }
// 导入 - 异步加载,编译时解析(静态分析)
import { add } from './math.js';
import Calculator from './calculator.js';
import('./dynamic.js'); // 动态导入
// 关键区别对比
// | 特性 | CommonJS | ES Modules |
// |-----------|-------------------|-------------------|
// | 语法 | require/module.exports | import/export |
// | 加载方式 | 运行时(同步) | 编译时(异步) |
// | 值的类型 | 值的拷贝 | 值的引用(绑定) |
// | Tree-shaking | 不支持 | 支持(静态分析) |
// | Top-level await | 不支持 | 支持 |
// | 使用场景 | Node.js 服务端 | 浏览器 + Node.js |
// CJS 值的拷贝 vs ESM 值的引用
// counter.js (CJS)
let count = 0;
module.exports = {
count,
increment: () => { count++; }
};
// main.js
const { count, increment } = require('./counter');
console.log(count); // 0
increment();
console.log(count); // 0(拷贝,不变)
// counter.js (ESM)
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1(引用,会变)3. 如何构建一个 Node.js REST API?
答: 使用 Express 或 Fastify 构建 REST API,包含路由、中间件、错误处理等。
const express = require('express');
const app = express();
// 中间件
app.use(express.json());
// 请求日志中间件
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} ${res.statusCode} - ${duration}ms`);
});
next();
});
// 内存数据存储
let todos = [
{ id: 1, title: '学习 Node.js', completed: false },
{ id: 2, title: '编写 API', completed: true },
];
let nextId = 3;
// CRUD 路由
// 获取所有 todos(支持过滤和分页)
app.get('/api/todos', (req, res) => {
let result = [...todos];
if (req.query.completed !== undefined) {
const completed = req.query.completed === 'true';
result = result.filter(t => t.completed === completed);
}
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const start = (page - 1) * limit;
const paginated = result.slice(start, start + limit);
res.json({
data: paginated,
total: result.length,
page,
limit,
});
});
// 获取单个 todo
app.get('/api/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === parseInt(req.params.id));
if (!todo) return res.status(404).json({ error: 'Todo not found' });
res.json(todo);
});
// 创建 todo
app.post('/api/todos', (req, res) => {
const { title } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const todo = { id: nextId++, title, completed: false };
todos.push(todo);
res.status(201).json(todo);
});
// 更新 todo
app.put('/api/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === parseInt(req.params.id));
if (!todo) return res.status(404).json({ error: 'Todo not found' });
Object.assign(todo, {
title: req.body.title ?? todo.title,
completed: req.body.completed ?? todo.completed,
});
res.json(todo);
});
// 删除 todo
app.delete('/api/todos/:id', (req, res) => {
const index = todos.findIndex(t => t.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Todo not found' });
todos.splice(index, 1);
res.status(204).send();
});
// 全局错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(3000, () => console.log('Server running on port 3000'));4. 前端构建工具有哪些?各自的优缺点?
答: 主流构建工具包括 Webpack、Vite、Rollup、esbuild 等。
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Webpack | 功能全面、生态丰富、配置复杂 | 大型 SPA 项目 |
| Vite | 开发极快(ESM)、配置简单 | 新项目首选 |
| Rollup | Tree-shaking 优秀、输出纯净 | 库/组件开发 |
| esbuild | 极快的编译速度 | 构建加速器 |
| Turbopack | Vercel 推出、增量计算 | Next.js 项目 |
// Vite 配置示例(vite.config.js)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({ open: true }), // 打包分析
],
resolve: {
alias: {
'@': '/src',
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'dayjs'],
},
},
},
// 构建目标
target: 'es2020',
// 启用 gzip 压缩大小报告
reportCompressedSize: true,
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
// Webpack 关键配置(webpack.config.js)
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
},
};5. npm、yarn、pnpm 有什么区别?
答: 三者都是 JavaScript 包管理器,但在安装速度、磁盘占用和安全性方面有所不同。
# npm - Node.js 自带的包管理器
npm install lodash
npm run build
npm publish
# yarn - Facebook 推出,引入锁文件和并行安装
yarn add lodash
yarn build
# yarn 1: 经典版(独立)
# yarn 2+: Berry(Plug'n'Play)
# pnpm - 使用硬链接和符号链接,大幅节省磁盘空间
pnpm add lodash
pnpm build
# pnpm 的优势
# 1. 节省磁盘:全局存储 + 硬链接
# 2. 严格的依赖隔离:只能访问 package.json 中声明的依赖
# 3. 安装速度快:比 npm 快 2-3 倍
# 4. 支持 monorepo(workspace)
# pnpm workspace 配置(pnpm-workspace.yaml)
# packages:
# - 'apps/*'
# - 'packages/*'6-15. 更多工程化面试题简答
6. 什么是 Monorepo?如何管理? Monorepo 将多个相关项目放在一个仓库中管理。常用工具:pnpm workspace、Turborepo、Nx、Lerna。优点:代码共享方便、统一版本管理、原子化提交。
7. Babel 的作用是什么? Babel 是 JavaScript 编译器,将 ES6+ 代码转换为向后兼容的 ES5 代码。核心流程:解析(Parse)-> 转换(Transform)-> 生成(Generate)。在 Vite 和现代浏览器普及后,Babel 的使用在减少。
8. 什么是 Tree Shaking? Tree Shaking 通过静态分析移除未使用的代码(dead code elimination)。依赖 ES Modules 的静态结构。在 webpack 中通过 mode: 'production' 自动启用。
9. Source Map 是什么? Source Map 是一个映射文件,将构建后的代码映射回源代码,方便在浏览器中调试。生产环境建议使用 hidden-source-map 或 nosources-source-map 避免暴露源码。
10. 如何实现 CI/CD? 使用 GitHub Actions、GitLab CI 或 Jenkins 实现自动化。流程:代码提交 -> 自动测试 -> 自动构建 -> 自动部署。关键步骤包括 Lint 检查、单元测试、E2E 测试、镜像构建、滚动部署。
11. Docker 在前端部署中的应用? 使用 Nginx 镜像部署静态文件,多阶段构建优化镜像大小,docker-compose 编排多个服务。示例:FROM node:18 AS build -> RUN npm build -> FROM nginx:alpine -> COPY dist /usr/share/nginx/html。
12. 什么是热模块替换(HMR)? HMR 在应用运行时替换、添加或删除模块,无需完全刷新页面。Webpack 的 HotModuleReplacementPlugin 和 Vite 的原生 ESM HMR 都支持此功能。
13. ESLint 和 Prettier 的区别? ESLint 负责代码质量检查(未使用变量、潜在错误),Prettier 负责代码格式化(缩进、换行、引号)。两者配合使用,ESLint 执行后再由 Prettier 格式化。
14. 前端监控方案有哪些? 错误监控(Sentry)、性能监控(Web Vitals)、用户行为分析(Google Analytics)、日志收集(ELK)。自建方案可以使用 Performance API + 自定义上报。
15. 如何优化前端项目的构建速度? 缓存(babel-loader cacheDirectory、webpack5 持久化缓存)、并行构建(thread-loader、esbuild-loader)、缩小搜索范围(resolve.modules、include/exclude)、DLL 预编译。
16. Node.js 中如何处理未捕获的异常?
答: 未捕获的异常会导致 Node.js 进程退出,必须正确处理。
// 1. 同步代码异常
try {
JSON.parse('invalid json');
} catch (err) {
console.error('捕获到错误:', err.message);
}
// 2. 异步代码异常(Promise)
Promise.reject(new Error('async error'))
.catch(err => console.error('Promise 错误:', err.message));
// 3. async/await 异常
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (err) {
console.error('请求失败:', err.message);
throw err; // 可以继续向上抛出
}
}
// 4. 全局未捕获异常处理(兜底)
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
// 生产环境应该记录日志并优雅退出
process.exit(1);
});
// 5. 未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason);
});
// 6. Express 错误处理中间件
app.use((err, req, res, next) => {
console.error('Express 错误:', err.stack);
res.status(err.statusCode || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message
});
});
// 注意:uncaughtException 后进程状态不可靠,建议记录日志后退出
// 不要在 uncaughtException 中继续执行业务逻辑17. Node.js 如何实现流(Stream)处理?
答: 流是 Node.js 处理大数据的核心机制,避免一次性加载全部数据到内存。
const fs = require('fs');
const { pipeline } = require('stream');
const { Transform } = require('stream');
// 四种流类型:
// Readable - 可读流(如 fs.createReadStream)
// Writable - 可写流(如 fs.createWriteStream)
// Duplex - 双工流(如 TCP Socket)
// Transform - 转换流(如 zlib、加密)
// 1. 读取大文件(不加载全部到内存)
const readStream = fs.createReadStream('large-file.csv', { highWaterMark: 64 * 1024 });
readStream.on('data', (chunk) => {
console.log(`读取 ${chunk.length} 字节`);
});
readStream.on('end', () => console.log('读取完成'));
// 2. 使用 pipe 管道连接流
const readStream2 = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream2.pipe(writeStream);
// 3. 自定义 Transform 流(处理 CSV 数据)
class CsvToJson extends Transform {
constructor(options) {
super({ ...options, readableObjectMode: true });
this._buffer = '';
this._headers = null;
}
_transform(chunk, encoding, callback) {
this._buffer += chunk.toString();
const lines = this._buffer.split('\n');
this._buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (!this._headers) {
this._headers = line.split(',');
continue;
}
const values = line.split(',');
const obj = {};
this._headers.forEach((h, i) => obj[h.trim()] = values[i]?.trim());
this.push(obj);
}
callback();
}
_flush(callback) {
if (this._buffer) {
// 处理最后剩余的数据
callback();
return;
}
callback();
}
}
// 4. 使用 pipeline(推荐,自动处理错误和资源释放)
pipeline(
fs.createReadStream('data.csv'),
new CsvToJson(),
fs.createWriteStream('data.jsonl'),
(err) => {
if (err) console.error('管道处理失败:', err);
else console.log('处理完成');
}
);
// Stream 的优势:
// - 内存效率高:一次只处理一小块数据
// - 支持背压(Backpressure):下游处理慢时自动暂停上游
// - 可组合:通过 pipe 或 pipeline 连接多个处理步骤18. Node.js 如何实现进程管理?
答: Node.js 提供了 child_process 和 cluster 模块用于进程管理。
const { fork, exec, spawn } = require('child_process');
const cluster = require('cluster');
const os = require('os');
// 1. exec:执行 shell 命令(有缓冲,适合短命令)
exec('git status', (error, stdout, stderr) => {
if (error) return console.error('执行失败:', error);
console.log(stdout);
});
// 2. spawn:启动新进程(流式输出,适合长运行进程)
const child = spawn('node', ['server.js'], {
env: { ...process.env, PORT: '3001' },
stdio: ['ignore', 'pipe', 'pipe']
});
child.stdout.on('data', (data) => console.log(`子进程输出: ${data}`));
child.stderr.on('data', (data) => console.error(`子进程错误: ${data}`));
child.on('close', (code) => console.log(`子进程退出码: ${code}`));
// 3. fork:专门用于 Node.js 子进程(支持 IPC 通信)
const worker = fork('./worker.js');
worker.send({ task: 'heavy-calculation', data: [1, 2, 3, 4, 5] });
worker.on('message', (result) => {
console.log('计算结果:', result);
worker.kill();
});
// 4. Cluster:利用多核 CPU
if (cluster.isPrimary) {
const cpuCount = os.cpus().length;
console.log(`主进程 ${process.pid} 启动 ${cpuCount} 个工作进程`);
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code) => {
console.log(`工作进程 ${worker.process.pid} 退出,重启新进程`);
cluster.fork(); // 自动重启
});
} else {
console.log(`工作进程 ${process.pid} 启动`);
require('./app'); // 启动应用
}
// 生产环境进程管理工具:
// PM2:最常用的 Node.js 进程管理器
// pm2 start app.js -i max // 利用所有 CPU 核心
// pm2 logs // 查看日志
// pm2 monit // 监控面板
// pm2 reload all // 零停机重启
// pm2 startup // 开机自启优点
缺点
总结
Node.js 和前端工程化面试需要掌握模块系统、构建工具、包管理和部署流程等核心知识。建议以 Vite 为切入点理解现代构建原理,通过实际项目积累 Node.js 服务端开发经验,了解 CI/CD 流程和 Docker 部署。在面试中展示对工具选型的理解和工程化思维比记忆具体配置更重要。
这组题真正考什么
- 面试官通常想知道你是否真正理解浏览器、框架和工程化之间的联系。
- 高频追问往往从概念定义延伸到性能、兼容性和线上诊断。
- 如果能结合真实页面问题回答,可信度会明显提高。
60 秒答题模板
- 先说这个概念解决什么问题。
- 再说它在浏览器或框架里的工作机制。
- 最后补一个线上场景或优化案例。
容易失分的点
- 只背 API 名称,不理解执行链路。
- 只说框架,不说浏览器原理。
- 回答性能题时没有指标和验证手段。
刷题建议
- 把浏览器、框架、工程化和性能题分开复习,避免知识点混在一起。
- 每道题尽量补一个页面真实案例,比如登录流程、首屏优化或状态同步。
- 前端题常考对比题,复习时要准备两到三个维度的横向比较。
高频追问
- 这个概念在 React、Vue、原生浏览器里分别怎么体现?
- 如果线上出现白屏、性能抖动或状态错乱,你会怎么定位?
- 这个方案的可维护性和性能代价是什么?
复习重点
- 把每道题的关键词整理成自己的知识树,而不是只背原句。
- 对容易混淆的概念要做横向比较,例如机制差异、适用边界和性能代价。
- 复习时优先补“为什么”,其次才是“怎么用”和“记住什么术语”。
面试作答提醒
- 先说结论与应用场景,再解释机制。
- 讲性能题时尽量带上监控指标。
- 框架题要注意区分版本特性和通用原理。
