面试题[Nodejs]
1. Node.js 基础
1.1 什么是 Node.js
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,用于在服务器端(服务端)执行 JavaScript 代码。
简单总结:Node.js 让 JavaScript 从“前端脚本语言”蜕变为“服务端通用开发语言”。
- 核心特点
- 异步非阻塞 I/O:采用事件驱动模型,处理高并发请求时不会因等待磁盘/网络 I/O 而阻塞线程,适合 I/O 密集型应用(如 API 服务、实时聊天、文件处理)。
- 单线程 + 事件循环:主线程单线程运行,通过事件循环和回调/
Promise/async/await管理异步任务,避免多线程的锁和上下文切换开销。 - 跨平台:支持 Windows、Linux、macOS。
- npm:全球最大的开源包管理器,拥有丰富的第三方库,极大提升开发效率。
- 常见应用场景
- Web 后端(Express、Koa、Nest.js)
- 实时通信(WebSocket、Socket.io)
- 命令行工具(CLI)
- 桌面应用(Electron 使用 Node.js 作为后端)
- 中间层(BFF,Backend for Frontend)
- 与浏览器中 JavaScript 的区别
- 全局对象:Node.js 中是
global,浏览器中是window。 - 模块系统:Node.js 原生支持 CommonJS(
require)和 ES Modules(import);浏览器依赖<script>或打包工具。 - API:Node.js 提供文件系统(
fs)、进程(process)、网络(http)等系统级 API,浏览器则聚焦 DOM/BOM 操作。
- 全局对象:Node.js 中是
1.2 什么是 npm
npm 是 Node Package Manager 的缩写,是 Node.js 的默认包管理器,也是全球最大的开源代码库(注册中心)。
主要功能:
- 安装依赖:通过
npm install <package>为项目安装第三方包。 - 管理版本:在
package.json中记录依赖及其版本范围,确保环境一致性。 - 脚本运行:通过
npm run <script>执行自定义命令(如启动、测试、构建)。 - 发布包:开发者可以将自己的模块发布到 npm registry,供他人使用。
- 安装依赖:通过
常见命令
bashnpm init # 初始化项目,生成 package.json npm install # 安装所有依赖 npm install express --save # 安装并保存到 dependencies npm install -g nodemon # 全局安装命令行工具 npm update # 更新依赖 npm publish # 发布包
1.3 什么是 package.json
package.json 是 Node.js 项目的核心配置文件,位于项目根目录,使用 JSON 格式描述项目的元信息和依赖关系。
- 主要作用
- 管理依赖:列出项目所需的所有第三方包及其版本范围(
dependencies、devDependencies等)。 - 定义脚本:通过
scripts字段定义常用命令(如npm start、npm test)。 - 记录项目信息:包括项目名称、版本、作者、许可证等。
- 锁定入口:通过
main字段指定模块的入口文件。
- 管理依赖:列出项目所需的所有第三方包及其版本范围(
- 如何生成
- 运行
npm init交互式创建,或npm init -y快速生成默认配置。
- 运行
- 重要性
- 确保项目在不同环境中安装相同的依赖版本(配合
package-lock.json)。 - 是分享、发布和部署 Node.js 项目的基础文件。
- 确保项目在不同环境中安装相同的依赖版本(配合
1.4 Node.js 全局对象
在 Node.js 中,全局对象是 global,它类似于浏览器中的 window,但存在一些重要差异。
核心全局对象:
global- 在任何模块中都可以直接访问
global及其属性。 - 在 Node.js REPL(交互式环境)中,顶层作用域就是
global。 - 注意:在普通模块文件中,使用
var、let、const声明的变量不会自动挂载到global对象上(模块作用域),这与浏览器不同。
- 在任何模块中都可以直接访问
常用的全局对象/属性
以下变量和方法无需
require即可直接使用:名称 说明 __dirname当前模块所在的目录的绝对路径 __filename当前模块文件的绝对路径 console用于打印日志( log、error、warn等)process提供当前 Node.js 进程信息和控制(如 p、rocess.env process.argv、process.exit())Buffer用于处理二进制数据 setTimeout/clearTimeout定时器(与浏览器类似) setInterval/clearInterval循环定时器 setImmediate/clearImmediate立即执行回调(在 I/O 回调之前执行) globalThisES2020 标准,跨平台统一全局对象(Node.js 中等于 global)
1.5 Node.js REPL
REPL 是 Read-Eval-Print Loop 的缩写,是 Node.js 自带的交互式编程环境。
在终端输入 node 命令(不加任何文件参数)即可进入 REPL 环境:
$ node
>- 工作流程
- Read:读取用户输入的 JavaScript 代码。
- Eval:在当前的上下文中执行代码。
- Print:打印执行结果(如表达式返回值、变量值)。
- Loop:循环上述过程,直到用户退出(按两次
Ctrl+C或输入.exit)。
- 常见用法
- 快速测试 JavaScript 语法、函数、正则表达式。
- 探索 Node.js 内置模块(如
fs、http)。 - 运行多行代码(自动检测未闭合的花括号、括号等)。
- 使用下划线
_获取上一次表达式的值。
1.6 Node.js 中间件
Node.js 中间件是在请求(Request)和响应(Response)生命周期中,用于处理、修改或终止流程的函数。它可以看作请求到达最终处理程序前的一系列处理工序,或请求处理流程中的“切面”。
中间件的工作机制
- 在Express、Koa等框架中,中间件通过函数栈(或“洋葱模型”)工作。开发者通过
app.use()注册的函数,会按注册顺序依次处理请求。 - 一个典型的中间件函数接收
request、response和next三个参数。调用next()会将控制权传递给下一个中间件;如果不调用,请求-响应周期就在此终止。
- 在Express、Koa等框架中,中间件通过函数栈(或“洋葱模型”)工作。开发者通过
中间件的主要作用
中间件的优势在于让应用开发更模块化和易于维护:
- 执行任意代码:在请求处理前后,执行日志记录、身份验证、数据校验等通用逻辑。
- 修改请求和响应对象:在流程中给
req或res对象添加属性或方法,供后续中间件使用。 - 终止请求-响应周期:当请求不符合条件(如身份验证失败)时,直接向客户端发送响应,不再继续向下执行。
- 调用下一个中间件:通过
next()函数,决定是否将控制权传递给流程中的下一个中间件。
常用的Node.js中间件
类别 中间件 描述 代码示例 官方/相关链接 请求体解析 express.json()内置中间件,用于解析 Content-Type为application/json的请求体,解析后的结果在req.body中。app.use(express.json());Express 官方文档 express.urlencoded()内置中间件,用于解析表单提交的 URL 编码数据,解析结果在 req.body中。app.use(express.urlencoded({ extended: true }));Express 官方文档 body-parser早期的第三方中间件,用于解析多种格式的请求体。在 Express 4.16+ 之后,其功能已被内置替代。 npm install body-parserbody-parser 日志记录 morganHTTP 请求日志中间件,可灵活配置输出格式(开发用 dev,生产用combined等),便于调试和监控。npm install morganmorgan winston或pino功能更全面的通用日志库,常与 morgan配合使用,将请求日志输出到文件或外部系统。npm install winstonwinston 安全 helmet通过设置一系列安全的 HTTP 头,帮助应用抵御常见的 Web 漏洞(如 XSS、点击劫持等),提升安全性。 npm install helmethelmet cors处理跨域资源共享,是构建前后端分离应用的必需中间件。 npm install corscors 身份验证 passport极其强大的身份认证中间件,通过“策略”支持多种登录方式(本地、OAuth、JWT 等),提供灵活的非侵入式集成。 npm install passportpassport 文件上传 multer专门用于处理 multipart/form-data类型的表单数据,即文件上传。npm install multermulter 静态文件 express.static()内置中间件,将指定目录下的静态文件(如图片、CSS、JS 文件)直接提供给客户端访问。 app.use(express.static('public'));Express 官方文档 性能监控 response-time记录服务器响应请求所花费的时间,可在响应头中添加 X-Response-Time字段。npm install response-timeresponse-time compression对响应体进行 Gzip 等压缩,可有效减少传输数据量,提高应用性能。 npm install compressioncompression 辅助工具 cookie-parser解析客户端请求中的 Cookie,方便开发者以 JavaScript 对象的形式读取和处理。 npm install cookie-parsercookie-parser express-session简化会话管理,通过中间件提供用户会话数据的创建、读取和存储。 npm install express-sessionexpress-session 如何自定义中间件
自定义中间件是一个接收
req,res,next的函数。在 Express 中使用app.use()或router.use()即可注册。以下是两个常见例子:简单日志中间件:记录请求的方法和 URL,然后调用
next()继续。javascriptfunction logger(req, res, next) { console.log(`${req.method} ${req.url}`); next(); // 放行 } app.use(logger);身份验证中间件:检查请求头
Authorization,验证不通过则直接return终止请求,否则放行。javascriptfunction authenticate(req, res, next) { const token = req.header('Authorization'); if (!token || token !== 'my-secret-token') { return res.status(401).json({ message: 'Unauthorized' }); } next(); } app.use(authenticate);
1.7 Node.js 模块加载机制
Node.js 中的模块加载机制基于 CommonJS 规范(同时逐步支持 ES Modules),核心是 require 函数,负责加载、缓存和执行模块。
核心流程(以 CommonJS 为例)
路径解析
require('moduleName')按以下顺序查找模块:- 核心模块:如
fs、http,直接返回。 - 相对路径:
./、../或/开头的文件模块,根据扩展名补全(.js、.json、.node)。 - 第三方模块:从当前目录的
node_modules开始,逐级向上查找至根目录。 - 目录作为模块:查找目录下的
package.json的main字段,或默认index.js。
- 核心模块:如
加载与编译
找到文件后,Node.js 根据扩展名处理:
.js:读取文件内容,包裹在函数中(见下),然后执行。.json:JSON.parse后直接返回。.node:C++ 插件,通过process.dlopen加载。
每个模块文件在执行前会被包裹成一个函数:
javascript(function(exports, require, module, __filename, __dirname) { // 模块代码 });这保证了模块内的变量不会泄漏到全局。
缓存
require.cache对象保存已加载模块的导出结果。- 重复
require同一模块时,直接返回缓存,不会再次执行。
导出与导入
module.exports或exports定义模块对外接口。require返回的就是module.exports的值。
ES Modules (ESM)
Node.js 从 v12+ 开始支持原生 ESM(.mjs 或 "type":"module" 的 .js),使用 import/export 语法,解析逻辑略有不同(强制文件扩展名、支持 URL 路径等),且默认异步加载。
1.8 Node.js 标准输入、输出和错误流
在 Node.js 中,标准输入(stdin)、标准输出(stdout) 和 标准错误(stderr) 是三个核心的 I/O 流,它们通过 process 对象暴露,用于与命令行环境交互。这三个流提供了与执行环境进行低开销文本交互的标准方式,是构建命令行 Node.js 程序的基础。
- 流的性质
process.stdin:可读流(Readable Stream),用于读取程序输入(如键盘输入、管道传来的数据)。process.stdout:可写流(Writable Stream),用于输出正常的程序结果。process.stderr:可写流,用于输出错误或诊断信息。
- 异步与同步注意事项
process.stdout.write是同步的(在输出到文件或终端时可能表现为异步,但通常无需关心)。- 读取
stdin总是异步的,需通过事件或流 API 处理。
- 实际应用场景
- CLI 工具:使用
stdin接收用户命令参数或管道数据。 - 日志系统:将普通日志输出到
stdout,错误日志输出到stderr,便于运维分离。 - 子进程通信:父进程通过子进程的
stdin/stdout管道进行 IPC。
- CLI 工具:使用
1.9 Node.js 事件循环
Node.js 中的事件循环是它实现非阻塞 I/O 和异步操作的核心机制。它允许 Node.js 在单线程下处理大量并发请求,而不会因为等待磁盘、网络等操作而阻塞主线程。
简单理解:事件循环就像一个永不停止的“调度员”,它不断检查各种任务队列(比如定时器回调、I/O 回调、setImmediate 回调等),并按照固定的阶段顺序依次执行这些回调。
事件循环的工作流程(简化版)
Node.js 启动时,会初始化事件循环。每次执行一个“滴答”(tick),它都会依次经过以下 6 个阶段:
~ ┌───────────────────────────┐ ┌─>│ timers │ 执行 setTimeout / setInterval 的回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ 执行延迟到下一次循环的 I/O 回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ 内部使用,忽略 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ poll │ 最关键阶段:检索新的 I/O 事件,执行 I/O 回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ check │ 执行 setImmediate 回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ 执行关闭回调(如 socket.on('close')) └───────────────────────────┘每个阶段都有一个先进先出的回调队列。事件循环会清空当前阶段的队列,或者执行到系统设定的最大限制后,再进入下一阶段。
每轮循环按顺序经历以下阶段:
阶段一:timers(定时器)
- 检查
setTimeout和setInterval的回调队列。 - 执行所有已到期的定时器回调(会尽可能多地执行,直到队列清空或达到系统限制)。
- 注意:定时器只是“尽可能快”地执行,实际执行时间取决于后续阶段是否空闲。
阶段二:pending callbacks(待定回调)
- 执行某些系统操作的回调(例如 TCP 连接错误等)。普通开发者一般不需要直接操作此阶段。
阶段三:idle, prepare(内部使用)
- 仅供 libuv 内部使用,可以忽略。
阶段四:poll(轮询)⭐ 最核心的阶段
- 如果 poll 队列中有回调:依次同步执行,直到队列清空或达到硬性限制。
- 如果 poll 队列为空:
- 如果有
setImmediate回调等待,则立即跳过 poll 等待,进入下一阶段(check 阶段)。 - 如果没有
setImmediate等待,则阻塞在此阶段,等待新的 I/O 事件到来。一旦有 I/O 完成,其回调会被加入 poll 队列并立即执行。 - 等待时还可以设置一个超时时间(通常是当前定时器中最早到期的时长),避免永久阻塞。
- 如果有
- poll 阶段是处理文件 I/O、网络 I/O 回调的主要场所。
阶段五:check(检查)
- 专门执行
setImmediate的回调函数。这些回调会在 poll 阶段空闲或完成后被立即执行。
阶段六:close callbacks(关闭回调)
- 执行关闭事件的回调,例如
socket.on('close', ...)或process.on('exit', ...)。
- 检查
典型误区澄清
- “事件循环是持续运行的”:不,当没有更多待处理的任务时,事件循环会退出,Node.js 进程结束。
- “
setTimeout(fn, 0)就一定比setImmediate快”:在事件循环启动时,timers 阶段先于 check 阶段,但setTimeout(fn, 0)的触发受系统计时器精度影响(通常为 1ms)。如果在主代码中连续调用,可能setImmediate更快。 - “
process.nextTick属于事件循环的一个阶段”:不,它是在每个阶段之间检查并执行的独立队列,不属于任何阶段。 - 事件循环开始时间:同步代码已执行完毕,事件循环开始。
- 事件循环的核心:在不阻塞主线程的前提下,调度不同优先级的异步回调。
- 阶段顺序固定:timers → pending → poll → check → close,循环往复。
- poll 阶段是主力:它负责等待和分发绝大多数 I/O 回调。
setImmediate在 check 阶段执行,比setTimeout(fn, 0)在计时器阶段执行,但启动时因计时器精度问题顺序可能不确定。- 微任务插队:
process.nextTick和Promise回调会在每个阶段之间优先执行,nextTick优先级高于Promise。 - 递归调用危险:在
nextTick中无限递归会永久阻塞事件循环;setImmediate中递归则相对安全,因为每次会先让出控制权给其他阶段。
2. Node.js 进阶
如何在 Node.js 中
2.1 创建 HTTP 服务器
在 Node.js 中,可以使用内置的 http 模块快速创建一个 HTTP 服务器。以下是最简单的示例:
// 1. 引入 http 模块
const http = require('http');
// 2. 创建服务器实例
const server = http.createServer((req, res) => {
// req: 请求对象(包含 url、method、headers 等)
// res: 响应对象(用于返回数据)
// 设置响应状态码和头信息
res.writeHead(200, { 'Content-Type': 'text/plain' });
// 发送响应内容并结束请求
res.end('Hello World\n');
});
// 3. 监听端口(例如 3000)
const PORT = 3000;
server.listen(PORT, () => {
console.log(`服务器已启动,访问 http://localhost:${PORT}`);
});2.2 创建 TCP 服务器
在 Node.js 中创建 TCP 服务器可以使用内置的 net 模块。以下是完整的示例,包括服务器创建、事件处理以及客户端测试。
TCP 服务器代码(server.js)
javascriptconst net = require('net'); // 创建 TCP 服务器 const server = net.createServer((socket) => { console.log('客户端已连接,地址:', socket.remoteAddress, '端口:', socket.remotePort); // 接收客户端数据 socket.on('data', (data) => { const message = data.toString().trim(); console.log('收到消息:', message); // 回显消息给客户端 socket.write(`服务器收到:${message}\n`); }); // 客户端关闭连接 socket.on('end', () => { console.log('客户端断开连接'); }); // 错误处理 socket.on('error', (err) => { console.error('Socket 错误:', err.message); }); }); // 监听端口 const PORT = 3000; server.listen(PORT, () => { console.log(`TCP 服务器已启动,监听端口 ${PORT}`); }); // 服务器全局错误 server.on('error', (err) => { console.error('服务器错误:', err); });测试客户端(client.js)—— 使用
net模块javascriptconst net = require('net'); const client = net.createConnection({ port: 3000 }, () => { console.log('已连接到服务器'); client.write('Hello, TCP Server!'); }); client.on('data', (data) => { console.log('服务器返回:', data.toString()); client.end(); // 发送结束信号 }); client.on('end', () => { console.log('与服务器断开连接'); }); client.on('error', (err) => { console.error('连接错误:', err); });运行方法
- 启动服务器:
node server.js - 另开终端运行客户端:
node client.js
- 启动服务器:
2.3 发送 HTTP 请求
在 Node.js 中发送 HTTP 请求有多种方式,从内置模块到第三方库,各有适用场景。
使用内置
http/https模块(无依赖,但较底层)javascriptconst https = require('https'); https.get('https://api.example.com/data', (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { console.log(JSON.parse(data)); }); }).on('error', (err) => { console.error(err); });javascriptconst http = require('http'); const postData = JSON.stringify({ name: 'John' }); const options = { hostname: 'example.com', port: 80, path: '/api', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }; const req = http.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => console.log(data)); }); req.write(postData); req.end();使用
node-fetch(轻量,API 与浏览器 fetch 一致)使用
axios(功能最丰富,自动 JSON 解析,支持拦截器等)
2.4 获取命令行参数
在 Node.js 中,获取命令行参数主要通过 process.argv 属性实现。
process.argv 是一个数组,包含启动 Node.js 进程时的所有命令行参数:
- 第 0 个元素:Node.js 可执行文件的绝对路径(通常为
node命令的路径)。 - 第 1 个元素:当前执行的 JavaScript 文件的绝对路径。
- 从第 2 个元素开始:用户传入的自定义参数,以空格分隔。
简单示例:
// 创建 cli.js 文件:
// cli.js
console.log(process.argv);# 在终端执行:
$ node cli.js hello --name John 123输出类似:
[
'/usr/local/bin/node',
'/path/to/cli.js',
'hello',
'--name',
'John',
'123'
]处理键值对参数:手动解析 --key value 或 --key=value 格式:
const args = process.argv.slice(2);
const parsed = {};
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--')) {
const key = args[i].slice(2);
const value = args[i + 1];
parsed[key] = value;
i++; // 跳过下一个参数(值)
}
}
console.log(parsed); // { name: 'John' }使用第三方库简化(推荐):对于复杂命令行工具,使用成熟的库可以省去手动解析的麻烦:
minimist:轻量级yargs:功能强大,自动生成帮助信息commander:流行的 CLI 框架
2.5 获取和设置环境变量
在 Node.js 中,环境变量通过全局对象 p 进行访问和修改。该对象包含了当前进程的环境变量,并且对其所做的更改只会影响当前进程及其子进程。
获取环境变量
使用
p即可读取,如果变量不存在则返回rocess.env.变量名 undefined。javascript// 获取环境变量 const dbHost = process.env.DB_HOST; console.log(dbHost); // 例如 'localhost' // 带默认值 const port = process.env.PORT || 3000;注意:变量名大小写敏感(在 Linux/macOS 上)但 Windows 不区分大小写。为了跨平台兼容,建议始终使用相同的大小写(通常为大写)。
设置环境变量
直接给
p的属性赋值,值会被转换为字符串。rocess.env javascript// 设置环境变量 process.env.NODE_ENV = 'production'; process.env.API_KEY = 'abc123';删除环境变量
javascriptdelete process.env.TEMP_VAR;检查环境变量是否存在
javascriptif ('DEBUG_MODE' in process.env) { console.log('调试模式已开启'); }使用
.env文件管理环境变量(推荐)在实际项目中,通常使用
dotenv包从.env文件加载环境变量。bashnpm install dotenv创建
.env文件:textDB_HOST=localhost DB_USER=root DB_PASS=s1mpl3
2.6 实现纳秒级别的高精度计时
Node.js 提供了多种实现高精度计时的方案,其中 process.hrtime.bigint() 是达到纳秒级(nanosecond, ns)精度的首选方法,精度可达1纳秒。
| API / 方法 | 核心精度 | 返回类型 | 关键特性 | 主要使用场景 |
|---|---|---|---|---|
Date.now() | 毫秒级 (ms) | 数字 (Number) | 通用时间戳,受系统时钟影响 | 日常日志、数据库时间戳 |
console.time() | 毫秒级 (ms) | 控制台输出 | 语法简单,快速调试 | 开发阶段简单性能评估 |
performance.now() | 微秒级 (μs) | 数字 (Number) | 高精度、单调递增、跨平台一致 | 常规性能测试、动画帧计时 |
process.hrtime.bigint() | 纳秒级 (ns) | 大整数 (BigInt) | 最高精度、单调递增、不受时钟调整影响 | 基准测试、微性能分析 |
perf_hooks 模块 | 纳秒级 (ns) | 事件对象 | 提供 performance.mark, measure 等高级API | 复杂性能追踪与分析 |
2.7 创建和解析 URL
在 Node.js 中处理 URL 主要有两种方式:旧版 url 模块的 parse 方法(现在标记为遗留)和更现代、更符合 Web 标准的 WHATWG URL API。强烈推荐使用后者。
使用 WHATWG URL API(推荐)
Node.js 从
v7.0.0开始支持标准的URL和URLSearchParams全局类,无需额外引入模块。javascript// 解析 URL const urlString = 'https://user:pass@example.com:8080/path/to/page?name=Alice&age=25#section'; const myUrl = new URL(urlString); console.log(myUrl.protocol); // 'https:' console.log(myUrl.username); // 'user' console.log(myUrl.password); // 'pass' console.log(myUrl.hostname); // 'example.com' console.log(myUrl.port); // '8080' console.log(myUrl.pathname); // '/path/to/page' console.log(myUrl.search); // '?name=Alice&age=25' console.log(myUrl.hash); // '#section' console.log(myUrl.origin); // 'https://example.com:8080'创建 / 拼接 URL
javascriptconst base = 'https://api.example.com/v1/'; const endpoint = new URL('users/123', base); endpoint.searchParams.set('expand', 'profile'); // 添加查询参数 console.log(endpoint.href); // 'https://api.example.com/v1/users/123?expand=profile'解析查询字符串(URLSearchParams)
javascriptconst params = myUrl.searchParams; params.get('name'); // 'Alice' params.has('age'); // true params.getAll('hobby'); // 如果没有 hobby,返回 [] // 遍历所有参数 for (const [key, value] of params) { console.log(`${key}=${value}`); } // 输出: // name=Alice // age=25 // 也可以直接操作 URL 对象 myUrl.searchParams.append('city', 'NYC'); console.log(myUrl.href); // ...原 URL...&city=NYC旧版
url.parse()(不推荐用于新项目)旧接口存在于
url模块中,可能会在未来版本中被废弃。用法如下:javascriptconst url = require('url'); const parsed = url.parse('https://example.com/path?foo=bar#anchor', true); // 第二个参数 true 表示自动解析查询字符串为对象 console.log(parsed.host); // 'example.com' console.log(parsed.query); // { foo: 'bar' } console.log(parsed.pathname); // '/path'但旧 API 有以下不足:
- 不符合浏览器标准
- 对
user:pass等部分的处理不可靠 - 默认不处理 Unicode 和 IDNA
2.8 执行外部命令或脚本
在 Node.js 中执行外部命令或脚本,主要使用内置的 child_process 模块。该模块提供了多个方法,适用于不同场景。
| 方法 | 回调风格 | 返回数据方式 | 适用场景 |
|---|---|---|---|
exec | 回调 | 一次性全部返回 stdout/stderr(缓冲) | 命令输出量小、希望简单获取字符串 |
execFile | 回调 | 一次性全部返回 stdout/stderr(缓冲) | 直接执行可执行文件,不经过 shell(更安全) |
spawn | 流式 | 通过事件流式返回 stdout/stderr | 输出量大、需要实时处理或交互 |
fork | 流式 / IPC | 专门用于启动新的 Node.js 进程(支持消息通信) | 执行另一个 Node.js 模块,需进程间通信 |
安全最佳实践:
- 永远不要将未经过滤的用户输入直接拼接到
exec的命令字符串中。 - 优先使用
execFile或spawn,并显式传递参数数组。 - 限制缓冲区大小(使用
maxBuffer选项,默认 1MB)。 - 对于长时间运行的子进程,务必监听
error、close事件,并正确处理stdout/stderr。
安全示例(推荐):
const { spawn } = require('child_process');
const userInput = 'somefile.txt';
const ls = spawn('ls', ['-l', userInput]); // 用户输入作为独立参数,不会被当做命令执行2.9 process.nextTick() 和 setImmediate() 的区别
在 Node.js 中,process.nextTick() 和 setImmediate() 都用于将回调函数的执行推迟到当前操作完成后,但它们在事件循环中的执行时机和优先级有本质区别。
| 特性 | process.nextTick() | setImmediate() |
|---|---|---|
| 执行阶段 | 当前操作结束,事件循环继续之前 | 事件循环的 check 阶段 |
| 执行顺序 | 优先于任何 I/O 或计时器回调 | 在当前事件循环迭代的末尾执行 |
| 递归风险 | 容易导致事件循环饥饿(递归调用会阻塞 I/O) | 相对安全,不会阻塞 |
| 适用场景 | 立即处理错误、在调用栈清空后继续执行 | 将任务推迟到下一个事件循环迭代 |
形象理解:
nextTick像是在当前代码执行完后的“插队”(仍在同一轮事件循环中),而setImmediate则是“下一轮再处理”。尽管名字相反,但nextTick比setImmediate要“立即”得多。
process.nextTick()适用场景- 在异步 API 中确保回调异步执行:例如将同步回调转为异步,避免调用顺序意外。类似
process.nextTick(callback)能保证回调在当前代码执行完毕后、任何 I/O 之前触发。 - 延迟执行但要求尽快:如在构造函数中触发事件,但要确保监听器已注册。
- 错误处理和资源清理:在同步代码中发生错误后,尽快执行清理逻辑。
- 在异步 API 中确保回调异步执行:例如将同步回调转为异步,避免调用顺序意外。类似
setImmediate()适用场景- 将大型任务拆分到下一个事件循环迭代,防止阻塞 I/O。
- 需要真正“稍后执行”,而不是立刻挤占当前事件循环的后续阶段。
- 与 I/O 操作配合:例如在文件读取完成后,需要执行后续逻辑但不希望阻塞其他 I/O。
3. Node.js 拓展
3.1 Node.js 应用的安全问题
确保 Node.js 应用程序安全是一场需要贯穿在整个开发和部署生命周期中的持久战。安全的基本原则是 纵深防御 (Defence in Depth),即不依赖任何单一措施,而是通过多层次、多样化的防护手段来构建一个稳固的整体防御体系。
Node.js 应用的安全问题可归为两大类:易被攻击的漏洞和不安全的配置与编码。以下是一套系统性的方案清单,并附有具体的防护措施。
| 风险类别 | 主要安全问题 | 核心应对策略 | 相关框架与库 |
|---|---|---|---|
| 注入攻击 (SQL, NoSQL, 命令注入) | 攻击者通过用户输入注入恶意代码,操控或破坏数据库或服务器。 | 1. 使用参数化查询或ORM,确保输入被当作数据,而非代码。 2. 使用schema验证库,对所有用户输入进行严格的类型校验。 3. 避免使用exec,改用execFile或spawn并传递参数数组。 | 验证: Zod, Joi, Yup, Valibot, ArkType, express-validator ORM/ODM: Sequelize, TypeORM, Prisma, Mongoose 安全: @arcis/node, secure-shield |
| 跨站脚本 (XSS) | 恶意脚本被注入网页,窃取用户数据或篡改页面内容。 | 1. 对用户输出进行上下文转义/编码。 2. 使用安全的模板引擎,或DOMPurify等清理库。 3. 实施严格的内容安全策略 (CSP),限制可加载的资源。 | 输出转义: DOMPurify, validator库的escape()方法, xss 安全头: helmet, koa-helmet |
| 身份认证与授权失效 | 认证/授权机制存在漏洞,导致攻击者可冒充用户或越权操作。 | 1. 使用bcrypt等安全算法哈希并加盐存储密码。 2. 对会话ID设置HttpOnly、Secure、SameSite属性。 3. 实施严格的基于角色的访问控制 (RBAC),并验证资源所有权。 | 认证/授权: passport, jsonwebtoken, bcrypt, accesscontrol, CASL |
| 安全配置错误 | 系统、框架或中间件的默认配置不安全或开启调试信息。 | 1. 使用helmet中间件,快速设置安全的HTTP头。 2. 在生产环境关闭错误堆栈信息。 3. 严格配置cors。 | HTTP安全头: helmet, koa-helmet, fastify-helmet 反参数污染: hpp CORS: cors |
| 敏感数据暴露 | 敏感数据(如密码、API密钥)明文存储或传输。 | 1. 强制使用HTTPS加密传输。 2. 敏感信息通过环境变量注入,禁止硬编码。 3. 使用bcrypt或scrypt存储密码。 | 密码加密/哈希: bcrypt, scrypt 环境变量: dotenv-safe, dotenv |
| 资源耗尽与暴力破解 | 攻击者通过大量请求耗尽服务器资源 (DoS) 或暴力破解密码。 | 1. 对API和敏感端点实施速率限制 (Rate Limiting)。 2. 限制请求体大小,防止大负载攻击。 3. 分布式部署时,使用Redis实现共享限流。 | 速率限制核心库: express-rate-limit, rate-limit-redis 高级限流: express-slow-down |
| 不安全的依赖包 | 项目中使用了包含已知漏洞的第三方npm包。 | 1. 运行npm audit定期检查并修复漏洞。 2. 在CI流程集成npm audit或Snyk。 3. 使用静态安全测试 (SAST) 工具扫描代码模式。 | 依赖扫描: npm audit, Snyk, DepGuard, NodeSecure CLI 代码/CI扫描: njsscan, semgrep, audit-ci, secure-shield |
| 跨站请求伪造 (CSRF) | 诱导用户在已认证的Web应用上执行非本意的操作。 | 1. 使用CSRF防护中间件 2. 验证同源请求头 3. 实施双重提交Cookie模式 | 框架集成: lusca (Express), @arcis/node (内置CSRF) |
一键式综合防护框架:如果你希望更快、更简单的集成,上述许多防护手段已经可以被部分综合性框架所覆盖:
| 框架/中间件 | 功能描述 |
|---|---|
@arcis/node | 号称“一行代码”的Node.js安全中间件,可处理SQL/NoSQL注入、XSS、命令注入、路径遍历、原型污染等16+种攻击向量。 |
secure-shield | 综合性安全中间件,提供SQL/NoSQL注入检测与防御、XSS防护、速率限制、请求验证和安全头管理等。 |
@risingstack/protect | 为Express提供开箱即用的保护,包括SQL注入、XSS、限流和安全头设置。 |
helmet | 专注HTTP安全头,设置CSP、HSTS、X-Frame-Options等十几项头信息。 |
lusca | Express专用,提供CSRF、XSS防护、点击劫持、CSP等中间件。 |