普通视图
菲律宾国家银行:确认未来三到四年内未就任何融资举措做出最终决定
LME铝价自2022年以来首次触及每吨3000美元
恒生指数收涨2.76%,恒生科技指数涨4%
分析师:美联储料将继续放松政策以迈向中性利率
长安汽车:2025年销量291.3万辆,同比增长8.5%
紫金矿业新任董事长邹来昌表示公司将扩大全球矿产资源规模
巴克莱:美联储下次降息料在3月
中国LED影厅数量全球第一
假期冰雪游热度持续升温,铁路部门将加开多趟冰雪旅游专列
道明证券:节后市场流动性下降,可能会放大白银价格波动幅度
防抖与节流:前端性能优化的两大利器
在现代 Web 开发中,用户交互越来越频繁,而每一次交互都可能触发复杂的逻辑处理或网络请求。如果不加以控制,这些高频操作会带来严重的性能问题。为此,防抖(Debounce) 与 节流(Throttle) 成为了前端开发中不可或缺的性能优化手段。
本文将结合一段实际代码和详细注释,深入浅出地讲解防抖与节流的核心思想、实现方式以及适用场景,并重点解析其中的关键逻辑。
一、为什么需要防抖和节流?
设想这样一个场景:用户在搜索框中输入关键词,每按一次键就发起一次 AJAX 请求获取搜索建议。如果用户快速输入“react”,那么会依次触发 r → re → rea → reac → react 五次请求。
-
问题1:请求开销大
每次请求都需要消耗带宽、服务器资源,甚至可能造成接口限流。 -
问题2:用户体验差
如果请求响应慢,旧的请求结果可能会覆盖新的输入内容,导致显示错乱。
因此,我们需要一种机制来减少不必要的执行次数,只保留关键的操作。这就是防抖和节流要解决的问题。
防抖:在一定时间内,只执行最后一次操作。
节流:每隔固定时间,最多执行一次操作。
二、防抖(Debounce)——“只认最后一次”
1. 核心思想
无论执行多少次,只执行最后一次。
就像王者荣耀中的“回城”技能:如果你在回城过程中被攻击,回城会被打断并重新计时。只有当你完整地等待一段时间后,回城才会真正生效。
2. 代码实现与闭包应用
// 高阶函数 参数或者返回值是函数 (返回值是函数 -> 闭包)
function debounce(fn, delay) {
var id; // 自由变量,闭包保存
return function(args) {
if (id) clearTimeout(id); // 清除已有定时器,重新计时
var that = this; // 保存 this 上下文
id = setTimeout(function() {
fn.call(that, args); // 延迟执行原函数 并绑定正确的this和参数
}, delay);
// 这样只有最后一次触发后等待delay毫秒后才会真正执行
};
}
关键点解析:
-
闭包的作用:
id是一个自由变量,被返回的函数所引用,从而在多次调用之间保持状态。这使得每次触发都能访问并清除上一次的定时器。 -
clearTimeout(id):确保只有最后一次触发后的delay时间才会真正执行函数。 -
this和参数传递:通过call或apply确保原函数在正确的上下文中执行,并传入正确的参数。
3. 使用示例
const inputb = document.getElementById('debounce');
let debounceAjax = debounce(ajax, 200);
inputb.addEventListener('keyup', function(e) {
debounceAjax(e.target.value);
});
用户快速输入时,只有停止输入 200ms 后,才会发送最终的完整关键词请求,极大减少了无效请求。
三、节流(Throttle)——“冷却期内不执行,但最后补一次”
1. 核心思想
每隔一定时间,最多执行一次。
但注意:我们实现的是带尾随执行(trailing)的节流,即在冷却期结束后,如果期间有触发,会补一次执行。
就像技能有 CD(冷却时间),但如果你在 CD 期间一直按技能,CD 结束后会自动释放一次。
2. 代码实现与“尾随执行”逻辑
function throttle(fn, delay) {
let last, deferTimer; // last上一次执行事件 deferTimer延迟执行的定时器
return function() {
let that = this;
let _args = arguments; // 类数组对象 保存所有参数
let now = +new Date(); // 拿到当前时间戳 +强制类型转换 毫秒数
if (last && now < last + delay) {
// 处于冷却期 上次执行时间存在 且当前时间还没到下次允许执行的时间
clearTimeout(deferTimer);
deferTimer = setTimeout(function() {
last = now;
fn.apply(that, _args);
}, delay);
} else {
// 已过冷却期,立即执行
last = now;
fn.apply(that, _args);
}
};
}
重点解析 if (last && now < last + delay) 分支:
-
条件成立含义:已经执行过至少一次(
last存在),且当前时间距离上次执行不足delay毫秒 → 正处于冷却期。 - 但不能忽略这次触发!因为这可能是用户最后一次有效操作(比如完整输入了“react”)。
- 所以我们设置一个延迟定时器,计划在冷却期结束后执行。
-
clearTimeout(deferTimer)的作用:用户可能在冷却期内多次触发,但我们只关心最后一次,所以每次都要清除旧的定时器,只保留最新的。
3. 为什么需要“尾随执行”?
核心原因:避免丢失最后一次有效操作。
假设用户想搜 “react”,在 200ms 内快速打完,而节流 delay = 500ms:
-
简单节流(无尾随) :
-
r(0ms)→ 立即执行 -
re(100ms)→ 被忽略 -
rea(150ms)→ 被忽略 -
react(200ms)→ 被忽略
→ 用户停止输入 但永远不会发送'react' 搜索框显示的是r的结果 而不是用户真正想搜的react
-
-
带尾随的节流:
-
r(0ms)→ 立即执行(last = 0) -
re(100ms)→ 冷却期,设 timer(600ms 执行) -
rea(150ms)→ 更新 timer(650ms) -
react(200ms)→ 更新 timer(700ms)
→ 用户停止输入后,在 700ms 自动执行ajax('react'),结果正确!
-
4. 什么时候不需要尾随?
按钮防连点:用户点击“提交”按钮,你希望 2 秒内只能点一次。
这种情况下,不需要在 2 秒后自动再提交一次!此时应使用无尾随的简单节流。
四、防抖 vs 节流:如何选择?
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 停止触发后 delay ms 执行 |
每隔 delay ms 最多执行一次 |
| 是否保证最后一次 | ✅ 是 | ✅(带尾随时) |
| 典型场景 | 搜索建议、窗口 resize | 滚动加载、鼠标移动、按钮点击(防连点) |
| 类比 | 回城技能(被打断重计时) | 技能 CD(冷却后可再放) |
- 搜索建议 → 用防抖:用户输入是连续的,我们只关心最终结果。
- 滚动加载 → 用节流:用户持续滚动,我们需要定期检查是否到底部,不能等到停止滚动才加载。
五、总结
防抖和节流虽然都是用于限制函数执行频率,但它们的触发逻辑和适用场景截然不同:
- 防抖强调“只执行最后一次”,适用于用户意图明确、操作连续的场景,如搜索、表单校验。
- 节流强调“定期执行”,适用于高频但需周期性响应的场景,如滚动、拖拽、游戏帧更新。
而我们在实现节流时,特别加入了尾随执行(trailing) 机制,这是为了兼顾性能与用户体验——既避免了过度请求,又确保不会丢失用户的最终操作。
正如注释中所说:
“核心原因:避免丢失最后一次有效操作。”
通过合理运用闭包、定时器和上下文绑定,我们不仅实现了功能,还保证了代码的健壮性和可复用性。这些技巧,正是前端工程师在性能优化道路上的必备武器。
小提示:在实际项目中,Lodash 等工具库已提供了成熟的 debounce 和 throttle 实现,支持更多选项(如 leading、trailing 开关)。但理解其底层原理,才能在复杂场景中灵活应对。
希望本文能帮助你更清晰地掌握防抖与节流的本质。欢迎在评论区分享你的使用经验!
大模型Function Calling的函数如何调用的?
在真实开发中,大模型的 Function Calling(函数调用)不是“模型直接执行代码”,而是一套“声明-生成-解析-执行-反馈”的安全闭环机制。以下是我在项目中(如智能编程助手、自动化运维 Agent)的实际做法:
一、核心流程(生产级标准做法)
![]()
二、具体步骤
1. 注册函数
在调用 LLM 前,向模型描述有哪些函数可用(OpenAI 格式为例):
const tools = [{
type: "function",
function: {
name: "read_file",
description: "读取项目中的文件内容",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "文件相对路径,如 src/main.ts" }
},
required: ["path"]
}
}
}];
关键:参数必须有明确 schema,防止模型传非法值。
2. 调用 LLM 并启用工具
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [...],
tools, // ← 注册的函数列表
tool_choice: "auto" // 模型可自主决定是否调用
});
3. 解析模型返回
模型不会执行函数,而是返回结构化调用请求:
{
"tool_calls": [
{
"id": "call_abc123",
"function": {
"name": "read_file",
"arguments": "{"path":"src/utils.ts"}"
}
}
]
}
4. 安全执行函数
- 绝不直接 eval!而是通过白名单映射:
const toolMap = {
read_file: (args) => {
// 1. 校验路径是否在项目目录内
if (!args.path.startsWith("src/")) throw new Error("Access denied");
// 2. 读取文件(沙箱隔离)
return fs.readFileSync(args.path, "utf8");
}
};
const result = await toolMap[funcName](parsedArgs);
- 所有操作在受限环境中执行(如 Docker 沙箱、只读文件系统)。
5. 将结果反馈给模型
把函数执行结果作为“tool message”送回对话:
messages.push({
role: "tool",
tool_call_id: "call_abc123",
content: result // 文件内容
});
→ 模型基于此生成下一步(继续调用 or 最终回答)。
三、真实项目中的关键实践
| 问题 | 解决方案 |
|---|---|
| 模型传错参数(如 path: "../../../etc/passwd") | 参数校验 + 路径归一化 + 白名单目录 |
| 函数执行超时/卡死 | 设置 timeout(如 5s) + AbortController |
| 敏感操作(如删文件) | 禁止高危函数,或需用户二次确认 |
| 多次调用循环 | 限制最大 tool_calls 次数(如 5 次) |
| 调试困难 | 记录完整 trace:prompt → tool_call → result → final answer |
四、为什么不用模型直接“写代码执行”?
- 安全风险极高(任意代码执行 = RCE 漏洞);
- 不可控(无法限流、审计、降级);
- 不可靠(模型可能生成语法错误代码)。
正确做法:Function Calling 是“受控 API 调用”,不是“代码生成执行” 。
总结:
在实际项目中,大模型的 Function Calling 是一个安全代理机制:
- 我们先向模型声明可用函数及其参数 schema;
- 模型返回结构化调用请求(非执行);
- 后端严格校验参数、权限、路径,在沙箱中执行真实函数;
- 将结果反馈给模型,形成多轮推理闭环。
核心原则:模型只负责“决策”,不负责“执行” ——这是生产系统安全落地的底线。
海云前端丨前端开发丨简历面试辅导丨求职陪跑
全岛封关后首个元旦假期,海南迎旅游“开门红”
淡水河谷印尼公司暂停镍矿开采,因2026年生产计划未获政府批准
React 中的 Props:组件通信与复用的核心机制
在 React 的组件化开发范式中,Props(属性) 是连接父子组件、实现数据流动与功能定制的关键桥梁。如果说状态(state)是组件内部的“私有记忆”,那么 Props 就是外部世界与组件对话的“公共接口”。通过 Props,父组件可以向子组件传递数据、回调函数、甚至其他组件本身,从而构建出高度可复用、可组合且职责清晰的 UI 体系。
组件即函数:参数驱动的 UI 单元
React 中的组件本质上是 JavaScript 函数。正如函数通过参数接收外部输入,组件也通过 props 对象接收来自父组件的配置信息:
function Greeting(props) {
const { name, message, showIcon } = props;
return (
<>
{showIcon && <span>👋</span>}
<div>{name} {message}</div>
</>
);
}
当在父组件中使用 <Greeting name="张三" message="你好" showIcon /> 时,这些属性会被打包成一个对象传入 Greeting 函数。这种设计使得组件行为完全由输入决定,符合纯函数的思想,极大提升了可预测性与可测试性。
类型约束:提升健壮性与协作效率
为避免因传入错误类型的数据导致运行时错误,React 社区广泛采用 prop-types 库进行运行时类型检查:
import PropTypes from 'prop-types';
Greeting.propTypes = {
name: PropTypes.string.isRequired,
message: PropTypes.string,
showIcon: PropTypes.bool
};
通过声明 name 为必需的字符串、showIcon 为布尔值,开发者能在控制台收到清晰的警告信息,尤其在团队协作中,这相当于一份自文档化的 API 契约,显著降低沟通成本。
children:内容分发的灵活通道
除了普通属性,React 还提供了一个特殊 prop —— children,用于传递组件标签之间的内容:
const Card = ({ children, className = '' }) => {
return <div className={`card ${className}`}>{children}</div>;
};
// 使用
<Card className="user-card">
<h2>张三</h2>
<p>高级前端工程师</p>
</Card>
children 可以是任意 JSX、文本或组件,使得 Card 成为一个通用容器,其内部结构由使用者自由定义。这种模式类似于 Web Components 中的 <slot>,是实现高阶组件和布局复用的核心技巧。
组件作为 Prop:极致的定制能力
更进一步,Props 甚至可以接收整个组件作为值,从而实现动态 UI 结构:
const MyHeader = () => <h2 style={{ margin: 0, color: 'blue' }}>自定义标题</h2>;
const MyFooter = () => (
<button onClick={() => alert('关闭弹窗')}>关闭</button>
);
<Modal HeaderComponent={MyHeader} FooterComponent={MyFooter}>
<p>这是一个弹窗内容</p>
</Modal>
在 Modal 内部,通过 {<HeaderComponent />} 动态渲染传入的组件:
function Modal({ HeaderComponent, FooterComponent, children }) {
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<HeaderComponent />
<div style={styles.content}>{children}</div>
<FooterComponent />
</div>
</div>
);
}
这种方式将模态框的头部、尾部与主体内容完全解耦,调用方可以按需注入任意逻辑,使 Modal 具备极强的通用性和扩展性。
状态与 Props 的分工协作
在一个典型应用中,状态通常集中在上层组件(如页面级组件)管理,而下层 UI 组件则通过 Props 接收数据与行为:
// App.jsx(持有状态)
function App() {
const [user] = useState({ name: "张三", role: "前端工程师" });
return <Card><UserInfo user={user} /></Card>;
}
// UserInfo.jsx(仅展示)
function UserInfo({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.role}</p>
</div>
);
}
这种“状态提升”模式确保了数据流的单向性:父组件负责数据来源与更新逻辑,子组件专注渲染。当需求变化时,只需调整父组件的状态管理,子组件无需修改,极大增强了系统的可维护性。
样式传递:兼顾封装与灵活性
组件常需支持自定义样式。通过 className Prop,可在保留内部默认样式的前提下,允许外部覆盖:
/* Card.css */
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
// 合并类名
<div className={`card ${className}`}>{children}</div>
这样,<Card className="user-card"> 既能继承 .card 的基础样式,又能应用 .user-card 的特定风格,实现样式层面的“开闭原则”。
总结
Props 不仅是数据传递的通道,更是 React 组件设计哲学的体现:通过明确的输入输出契约,构建可组合、可复用、可测试的 UI 单元。从简单的字符串属性,到复杂的组件函数与嵌套内容,Props 提供了多层次的定制能力。配合类型检查、children 模式与组件作为 Prop 的高级用法,开发者能够像搭积木一样,将小型组件组装成复杂界面,同时保持各部分的独立性与清晰职责。掌握 Props 的各种使用场景,是编写高质量 React 应用的基石。
法国拟禁止15岁以下群体使用社交媒体
React Hooks:函数组件的状态与副作用管理艺术
在现代 React 开发中,函数组件已不再是“无状态”的代名词。借助 Hooks——以 use 开头的一系列内置函数,开发者可以在不编写类的前提下,轻松管理组件的状态、执行副作用、订阅外部数据源,甚至自定义逻辑复用机制。这一设计不仅让代码更贴近原生 JavaScript 的表达习惯,也推动了组件逻辑的清晰化与模块化。
useState:声明响应式状态
useState 是最基础的 Hook,用于在函数组件中引入可变状态:
const [num, setNum] = useState(0);
它返回一个包含当前状态值和更新函数的数组。值得注意的是,useState 的初始值可以是一个函数,适用于需要复杂同步计算的场景:
const [num, setNum] = useState(() => {
const a = 1 + 2;
const b = 2 + 3;
return a + b; // 返回确定的初始值
});
这种形式确保初始化逻辑仅在组件首次渲染时执行一次,避免不必要的重复计算。但需注意:该函数必须是同步且纯的,不能包含异步操作(如 fetch),因为状态必须在渲染前确定。
此外,setNum 不仅能接收新值,还可接受一个函数,其参数为上一次的状态:
<button onClick={() => setNum(prev => prev + 1)}>
{num}
</button>
当状态更新依赖于前一状态时(如计数器、列表追加),使用函数式更新能避免因闭包捕获旧值而导致的竞态问题,确保状态演进的正确性。
useEffect:统一处理副作用
如果说 useState 负责“记忆”,那么 useEffect 就负责“行动”。它用于执行副作用操作——即那些不影响组件渲染结果但必须发生的逻辑,如数据请求、定时器、DOM 操作等。
useEffect(() => {
console.log('组件挂载完成');
}, []);
通过传入空依赖数组 [],该副作用仅在组件首次挂载后执行一次,等效于类组件中的 componentDidMount。
当依赖项变化时,useEffect 会重新运行:
useEffect(() => {
console.log(`num 变为 ${num}`);
}, [num]);
这类似于 componentDidUpdate,可用于监听特定状态或 props 的变化并作出响应。
清理副作用:防止内存泄漏
许多副作用需要在组件卸载或重新执行前进行清理,例如清除定时器、取消网络请求、移除事件监听器等。useEffect 支持返回一个清理函数:
useEffect(() => {
const timer = setInterval(() => {
console.log(num);
}, 1000);
return () => {
console.log('清除定时器');
clearInterval(timer);
};
}, [num]);
该清理函数会在以下两种情况下被调用:
- 组件卸载时:释放资源,防止内存泄漏;
- 下一次副作用执行前(若依赖项变化):先清理旧副作用,再执行新副作用。
这种机制确保了副作用的生命周期与组件状态严格同步,避免了常见的“已卸载组件仍尝试更新状态”错误。
副作用的本质:打破纯函数的边界
React 组件本质上应是一个纯函数:给定相同的 props 和 state,始终返回相同的 JSX。而副作用(如修改全局变量、发起网络请求、改变 DOM)则打破了这一原则,因其结果具有不确定性或对外部环境产生影响。
例如,以下函数存在副作用:
function add(nums) {
nums.push(3); // 修改了外部数组
return nums.reduce((a, b) => a + b, 0);
}
调用后,原始 nums 数组被改变,后续代码行为不可预测。而在 React 中,useEffect 正是将这类“不纯”的操作集中管理的容器,使主渲染逻辑保持纯净,提升可测试性与可维护性。
实际应用:数据获取与条件渲染
结合 useState 与 useEffect,可实现典型的数据驱动 UI:
function App() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
return data ? <div>{data}</div> : <p>加载中...</p>;
}
这里,数据请求作为副作用在挂载后执行,成功后通过 setData 触发重新渲染,展示最新内容。整个流程清晰、线性,无需关心生命周期钩子的切换。
总结
React Hooks 通过 useState 和 useEffect 等核心 API,将状态管理和副作用处理从类组件的生命周期中解放出来,赋予函数组件完整的逻辑表达能力。它们以声明式的方式描述“何时做什么”,而非“在哪个阶段做什么”,更符合直觉。同时,依赖数组机制强制开发者显式声明副作用的触发条件,提升了代码的可读性与健壮性。掌握 Hooks,不仅是使用现代 React 的必备技能,更是迈向函数式、响应式前端开发思维的关键一步。
第3章 Nest.js拦截器
3.1 拦截器介绍
Nest.js的拦截器和axios的拦截器是类似的,可以在网络请求处理的前后去执行额外的逻辑。拦截器从字面意思理解就是拦截,假设有流程A->B,拦截器要做的是A到B的过程中,将内容拦截下来处理后再丢给B,变成了A->拦截器->B。
在网络请求的逻辑中,拦截器的拦截位置如下:
- 客户端请求->拦截器(前置逻辑)->路由处理器->拦截器(后置逻辑)->客户端响应。
Nest.js拦截器效果如图3-1所示。
![]()
图3-1 Nest.js拦截器
Nest.js拦截器主要的用途有以下5点:
(1)统一响应格式:将返回数据包装成统一格式。
(2)日志记录:记录请求耗时、请求参数等信息。
(3)缓存处理:对响应数据进行缓存。
(4)异常映射:转换异常类型。
(5)数据转换:对响应数据进行序列化/转换。
在英语AI项目中,主要使用到第5点数据转换,因此我们主要学习这一点。
3.2 拦截器创建
如表1-2所示,可以通过nest g itc interceptor快速创建一个拦截器(interceptor可以替换为任何你想取的拦截器名称)。通过该命令会在src文件夹下创建interceptor文件夹,而interceptor文件夹下存放interceptor.interceptor.ts文件。
根据命令的生成规则,我们知道文件夹和文件的名称取决于我们命令对拦截器的名称,从而生成xxx文件夹和xxx.interceptor.ts文件。并且在这唯一的文件中,会提前生成好对应的Demo示例。
//src/interceptor/interceptor.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
拦截器有两种使用方式:
(1)局部使用。
(2)全局使用。
当想局部使用时,例如只想在src文件夹下的user模块使用,我们只需要注册到user模块中。那怎么注册?有3种注册方式,在user.module.ts、user.controller.ts以及user.controller.ts都可以注册,最主要的区别在于局部作用范围不同。Nest.js拦截器局部注册如表3-1所示。
表3-1 Nest.js拦截器局部注册
| 注册方式 | 作用范围 | 代码位置 | 优点 | 缺点 |
|---|---|---|---|---|
| 模块级别 | 整个模块所有控制器 | user.module.ts | 统一管理,自动应用到所有路由 | 无法灵活排除某些路由 |
| 控制器级别 | 单个控制器所有路由 | user.controller.ts | 控制器粒度控制 | 需在每个控制器添加装饰器 |
| 路由级别 | 单个路由方法 | user.controller.ts | 最精细的控制 | 代码重复,管理复杂 |
局部使用的具体代码不演示,可通过AI或者官方文档学习使用。
3.3 全局拦截器使用
在英语AI项目中会使用到全局使用,我们这里学习具体如何全局使用。步骤为以下2步:
(1)使用nest g itc <拦截器名称>快速创建一个拦截器。
(2)将拦截器注册到main.ts文件中,即在main.ts文件中导入刚创建的拦截器,并且使用Nest应用程序实例方法useGlobalInterceptors()。
// main.ts文件
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { InterceptorInterceptor } from './interceptor/interceptor.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new InterceptorInterceptor());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
当然由于InterceptorInterceptor拦截器是一个类,所以我们需要使用new运算符创建拦截器的实例以供使用。到这里,InterceptorInterceptor拦截器就是全局使用,即每一个接口都会经过该拦截器。Nest.js全局注册的官方文档如图3-2所示。
![]()
图3-2 Nest.js拦截器全局注册
此时来编写InterceptorInterceptor拦截器内的逻辑,可见引入了来自rxjs的Observable类,rxjs是Nest.js内部自带的,主要用于处理流的,使用频率不高。通常获取数据需要区分同步与异步,同步直接获取,而异步通过Promise的then或者catch方法获取。如果此时有rxjs,就不需要我们去关注获取的数据是同步或者异步的问题,减少心智负担。rxjs会将这些数据统一转成一个数据流,然后通过管道(pipe)去接收,接收到之后可由我们处理该数据格式,无论是通过map遍历处理还是filter过滤等等,最终将处理好的数据格式返回就行。
以上是rxjs的核心理念,除此之外,它还可以同时处理多个异步,而then或者catch方法每次只能处理一个。
像InterceptorInterceptor拦截器中的所返回的next.handle()就是一个Observable(数据流),所以我们需要通过pipe(管道)去接收数据然后使用rxjs的map方法对数据处理之后再返回数据。
我们将原始数据包裹在一个标准响应结构中,添加了时间戳、请求路径、业务状态码、成功标志和自定义消息。这样确保了所有经过此拦截器的HTTP响应都遵循统一的JSON格式,包括 { timestamp, data, path, message, code, success } 等标准化字段,前端可以统一处理和错误追踪。
// src/interceptor/interceptor.interceptor.ts文件
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';
@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 将通用的执行上下文切换到HTTP特定的上下文
const ctx = context.switchToHttp();
// 获取当前HTTP请求的详细对象,包含了请求方法、URL、请求头、参数、主体等所有信息。
const request = ctx.getRequest<Request>();
return next.handle().pipe(map((data) => {
return {
timestmap: new Date().toISOString(),
data: data,
path: request.url,
message: 'success',//业务逻辑自定义
code: 200,//业务逻辑自定义
success: true,
};
}));
}
}
此时在浏览器的URL输入http://localhost:3000/user/123,访问带参数的get请求,get请求拦截效果如图3-3所示。在这里体现的是:路由处理器->拦截器(后置逻辑)->客户端响应。
![]()
图3-3 Nest.js全局拦截器-get请求拦截效果
message和code字段属于业务逻辑的部分,后续完成英语AI项目时,会根据业务实际逻辑去自定义设置。
3.4 优化全局拦截器
但此时全局拦截器还有一个很大的Bug,假如接口返回一个很大的数据,我们通过BigInt数据类型去处理返回,那么在通过全局拦截器时就会出现报错情况,全局拦截器处理BigInt类型报错如图3-4所示。
![]()
图3-4 全局拦截器处理BigInt类型报错
报错是error TS2322: Type 'bigint' is not assignable to type 'string'。即bigint类型无法赋值给string类型,这是很正常的。因为全局拦截器的这些参数都是通过JavaScript标准内置对象JSON.stringify()进行格式化的,而JSON.stringify()是没办法处理BigInt值的。在MDN文档中是这样表述这一异常情况:当尝试去转换 BigInt类型的值会抛出TypeError("BigInt value can't be serialized in JSON")(BigInt 值不能 JSON 序列化)。
// src/app.service.ts文件
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello() {
return BigInt(123456789123456789123456789)
}
}
所以我们需要针对BigInt类型的值去处理,通过编写transformBigInt方法去单独处理这一情况,主要处理的事情是当遇到BigInt类型的值就将它转成一个字符串。
const transformBigInt = (data: any) => {
if (typeof data === 'bigint') {
return data.toString();
}
return data;
};
此时将接口(get请求)返回给用户的data数据放入transformBigInt方法中即可。
// src/interceptor/interceptor.interceptor.ts文件
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';
const transformBigInt = (data: any) => {
if (typeof data === 'bigint') {
return data.toString();
}
return data;
};
@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
return next.handle().pipe(map((data) => {
return {
timestmap: new Date().toISOString(),
data: transformBigInt(data),
path: request.url,
message: 'success',//业务逻辑自定义
code: 200,//业务逻辑自定义
success: true,
};
}));
}
}
但此时还会报错同样的问题(Type 'bigint' is not assignable to type 'string'),这是很正常的。我们来梳理下流程:
(1)接口返回数据给前端。
(2)全局拦截器拦截接口返回的数据进行处理。
(3)全局处理后的数据返回给前端。
我们已经在全局拦截器中处理好类型转换问题(BigInt转String),如果还有问题,就只能在第一步的接口返回数据给前端的步骤中。前端访问的是接口,而接口是体现形式是路由,路由层从业务层获取数据返回给前端。因此在业务层的数据是BigInt类型,则路由层所拿到的数据也会是BigInt类型。由于Nest.js是强制使用TypeScript的,所以我们需要到app.controller.ts文件中将get默认请求所返回的类型限制从string改成any类型或者string和bigint的联合类型。此时就能正常运行代码。
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string | bigint {
return this.appService.getHello();
}
}
出于严谨的考虑,我们需要处理相应的边界判断,假如BigInt类型在数组里,在对象里呢?原有的处理方式就又解析不了了。
return [BigInt(123456789123456789123456789)];
return { a: BigInt(123456789123456789123456789) };
所以需要进一步强化transformBigInt方法,对数组遍历处理内部可能存在的BigInt类型,而对象则通过Object.entries()静态方法将对象切换成保存键值对的二维数组后,遍历键值对并针对其中的value值处理可能存在的BigInt类型,最后通过Object.fromEntries()静态方法将键值对形式的二维数组重新转换回原始对象。
-
对象打印效果:{ foo: "bar", baz: 42 }。
-
将可迭代对象切成二维数组:[ ['foo', 'bar'], ['baz', 42] ]。
将对象切成二维数组更方便找到键值对的值并进行遍历操作。
const transformBigInt = (data: any) => {
if (typeof data === 'bigint') {
return data.toString();
}
if(Array.isArray(data)){
return data.map(transformBigInt);
}
if(typeof data === 'object' && data !== null){
return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]));
}
return data;
};
做完以上的优化后,我们会发现接口要返回Date日期没办法正常返回给前端了,因为我们把对象全部都处理了,而JavaScript标准内置对象Date的使用是通过new运算符调用的实例对象,实例对象也是对象,也会被transformBigInt方法一并处理,所以在判断对象的内部逻辑中还需要判断是否是Date类型,若为Date类型则直接原路返回,不处理。
if(typeof data === 'object' && data !== null){
if(data instanceof Date){
return data
}
return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]));
}
完整的全局拦截器如下代码所示,后续英语AI项目中,会将该全局拦截器直接拿过去使用。
// src/interceptor/interceptor.interceptor.ts文件
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Request } from 'express';
//同步 异步 then catch ->数据流->pipe -> map filter -> 返回
const transformBigInt = (data: any) => {
if (typeof data === 'bigint') {
return data.toString();
}
if(Array.isArray(data)){
return data.map(transformBigInt);
}
if(typeof data === 'object' && data !== null){
if(data instanceof Date){
return data
}
return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, transformBigInt(value)]));
}
return data;
};
@Injectable()
export class InterceptorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
return next.handle().pipe(map((data) => {
return {
timestmap: new Date().toISOString(),
data: transformBigInt(data),
path: request.url,
message: 'success',//业务逻辑自定义
code: 200,//业务逻辑自定义
success: true,
};
}));
}
}
接下来对异常也格式化统一处理一下,逻辑思路与全局拦截器类似。当前端发起不符合规范和要求的网络请求,后端就会返回异常信息,方便前端去统一处理。
![]()
图3-5 异常情况的处理
此时我们需要总结nest命令的表1-2,找到filter命令来生成一个过滤器。命令是:nest g f <过滤器名称>,我们就通过nest g f exceptionFilter来生成一份过滤器吧。成功在src文件夹下创建exception-filter文件夹和exception-filter文件夹下的exception-filter.filter.ts文件,这些生成文件的命名规则都是一致的,不再赘述。
// src/exception-filter/exception-filter.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
@Catch()
export class ExceptionFilterFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
通过以上exception-filter.filter.ts文件的代码,我们发现异常处理@Catch()装饰器是空的,空的表示处理所有的异常操作,包括非HTTP请求都会处理,但我希望这个业务只处理和HTTP相关的异常就可以了。所以我们需要从@nestjs/common中引入一个HttpException类,然后让@Catch()装饰器去继承HttpException类就可以了。
// src/exception-filter/exception-filter.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class ExceptionFilterFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
在这里我们可以看到这个很有意思的设计理念,通过Nest命令生成的内容,它希望我们都能用得上,这种思想和TypeScript所想表达的含义是一致的,只写用得上且必要的部分。因此在通过Nest CLI 在生成过滤器模板时,会会默认使用 @Catch()(不带任何参数),示例性地展示如何捕获所有异常。但它只是一个类模板,需要我们手动把它注册为全局过滤器,或者在控制器上使用。
只有当我们明确在@Catch()中指定具体的异常类型(如 @Catch(HttpException) 或 @Catch(WsException)),过滤器才会从“捕获所有异常”转变为“仅处理特定类型的异常”。如图3-6所示的官方文档也说明了不同协议层(HTTP 与 WebSocket)对应的异常类型不同,因此需要在 @Catch() 中明确指定对应的异常类型。
![]()
图3-6 HTTP异常过滤层的说明
接下来我们来对异常处理情况进行统一的格式化处理。这里的code(异常状态码)就不采用我们自定义的,而是使用exception内部定义的状态码,因为Nest内置的HttpException已经为所有常见错误定义了标准化的状态码(如 400、401、403、404、500 等),这些状态码符合 HTTP 协议本身的语义。直接使用exception.getStatus()可以确保服务端返回的错误信息在网络层面是可预测和通用的。Nest.js内置异常处理层说明如图3-7所示。
![]()
图3-7 Nest.js内置异常处理层说明
当token过期了,exception.getStatus()会自动识别并设置成401状态码,没有权限则403状态码。因此exception.getStatus()会自动化的根据实际情况去调整,非常方便。对应的详细讲解可阅读Nest.js的官方文档:Exception filters | NestJS - A progressive Node.js framework。
如果再自定义一套error code,就等于需要维护两套错误体系:HTTP 状态码 + 我们自己额外设计的业务错误码,这会造成重复劳动、文档负担加重以及维护难度上升。而直接使用 HttpException 内部的状态码可以保持异常捕获逻辑与框架一致,不需要额外重复造轮子。
// src/exception-filter/exception-filter.filter.ts文件
import { ArgumentsHost, Catch, ExceptionFilter,HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class ExceptionFilterFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const request = ctx.getRequest<Request>()
const response = ctx.getResponse<Response>()
return response.status(exception.getStatus()).json({
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
code: exception.getStatus(),
success: false,
})
}
}
最后,过滤器和拦截器一样,在main.ts文件中全局注册一下,则可以作用于整个项目的异常情况处理。
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { InterceptorInterceptor } from './interceptor/interceptor.interceptor';
import { ExceptionFilterFilter } from './exception-filter/exception-filter.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new InterceptorInterceptor());
app.useGlobalFilters(new ExceptionFilterFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
全局异常情况的过滤处理效果如图3-8所示。
![]()
图3-8 全局异常情况的过滤处理效果