普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月2日首页

恒生指数收涨2.76%,恒生科技指数涨4%

2026年1月2日 16:12
36氪获悉,恒生指数收涨2.76%,恒生科技指数涨4%。“港股GPU第一股”壁仞科技上市首日大涨超75%;半导体、国防军工板块涨幅居前,华虹半导体涨超9%,西锐涨超8%,中芯国际涨超5%。

分析师:美联储料将继续放松政策以迈向中性利率

2026年1月2日 16:00
Navellier & Associates首席投资官Louis Navellier在一份报告中称,美联储将在2026年再降息四次,以迈向中性利率。他表示,疲软的房价正在引发通缩担忧,美联储需要加以应对。“此外,当美国经济并未创造大量就业岗位时,美联储没有理由继续保持限制性政策,“他称。他还表示,如果通缩压力加剧,可能需要更多次降息。中性利率既不刺激也不限制经济。(新浪财经)

紫金矿业新任董事长邹来昌表示公司将扩大全球矿产资源规模

2026年1月2日 15:11
紫金矿业新任董事长邹来昌发表新年致辞称,公司将加大战略性矿产资源获取力度,以金、铜为重点发展矿种,全面形成具有全球竞争力的锂板块。邹来昌表示,紫金矿业将密切关注有重大影响力的超大型矿产及中型矿业公司并购机会,并加大国内重点区域的资源投资开发力度。(新浪财经)

巴克莱:美联储下次降息料在3月

2026年1月2日 14:45
巴克莱的美国经济学家在一份报告中称,该行维持其对美联储在2026年降息两次的预期,分别在3月和6月各降息25个基点。他们认为,围绕这一基线预测的风险偏向于推迟降息。这些经济学家称,美联储12月政策会议(美联储在该次会议上降息25个基点)的纪要与巴克莱关于1月会议将按兵不动的预期一致,“因为美国联邦公开市场委员会需要时间来评估近期降息的影响“。(新浪财经)

中国LED影厅数量全球第一

2026年1月2日 14:41
国家电影局发布的最新数据显示,2025年全年电影票房为518.32亿元,同比增长21.95%,净超2024年93亿元。其中2025年贺岁档电影票房超53亿元,创八年来新高。在518亿元票房中,国产影片票房为412.93亿元,占比为79.67%。中国电影的进化,离不开硬核科技的支撑。截至2025年底,我国LED影厅数量稳居全球第一。(央视财经)

假期冰雪游热度持续升温,铁路部门将加开多趟冰雪旅游专列

2026年1月2日 14:21
今年的元旦假期,全国各地冰雪游热度持续升温,大数据显示,铁路、民航客流也是持续攀升。数据显示,黑龙江的中国雪乡风景区、哈尔滨亚布力滑雪度假区门票预订量分别增长2.7倍和2倍。与“哈尔滨”相关的搜索热度同比暴涨5倍以上。元旦前后火车票和机票整体搜索热度同比增长3倍多。元旦假期,北京至哈尔滨、沈阳、长春等东北城市仅有少量余票,1月3日由哈尔滨、长春、沈阳等地返回北京的车票已基本售罄。铁路部门将加开多趟普速和动车冰雪旅游专列。(央视新闻)

防抖与节流:前端性能优化的两大利器

作者 ohyeah
2026年1月2日 13:50

在现代 Web 开发中,用户交互越来越频繁,而每一次交互都可能触发复杂的逻辑处理或网络请求。如果不加以控制,这些高频操作会带来严重的性能问题。为此,防抖(Debounce)节流(Throttle) 成为了前端开发中不可或缺的性能优化手段。

本文将结合一段实际代码和详细注释,深入浅出地讲解防抖与节流的核心思想、实现方式以及适用场景,并重点解析其中的关键逻辑。


一、为什么需要防抖和节流?

设想这样一个场景:用户在搜索框中输入关键词,每按一次键就发起一次 AJAX 请求获取搜索建议。如果用户快速输入“react”,那么会依次触发 rrereareacreact 五次请求。

  • 问题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 和参数传递:通过 callapply 确保原函数在正确的上下文中执行,并传入正确的参数。

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 等工具库已提供了成熟的 debouncethrottle 实现,支持更多选项(如 leadingtrailing 开关)。但理解其底层原理,才能在复杂场景中灵活应对。

希望本文能帮助你更清晰地掌握防抖与节流的本质。欢迎在评论区分享你的使用经验!

大模型Function Calling的函数如何调用的?

作者 海云前端1
2026年1月2日 13:48

在真实开发中,大模型的 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 是一个安全代理机制

  1. 我们先向模型声明可用函数及其参数 schema
  2. 模型返回结构化调用请求(非执行);
  3. 后端严格校验参数、权限、路径,在沙箱中执行真实函数;
  4. 将结果反馈给模型,形成多轮推理闭环。

核心原则:模型只负责“决策”,不负责“执行” ——这是生产系统安全落地的底线。

海云前端丨前端开发丨简历面试辅导丨求职陪跑

全岛封关后首个元旦假期,海南迎旅游“开门红”

2026年1月2日 13:40
海南自贸港迎来封关运作后的首个元旦假期。据去哪儿数据显示,1月1日,飞往海口、三亚的机票预订量增幅全国最高。 在出入境方面,三亚入境游机票订单量同比增幅达5倍,海口增长超3倍,增速位列全国前两位。来自马来西亚、泰国、韩国、越南和澳大利亚的旅客,成为海南入境客源主力。数据显示,元旦假期首日,以三亚国际免税城、海旅免税城为核心的商圈人气旺盛,带动周边酒店入住量大幅攀升,其中海旅免税城周边同比增长约1.5倍,三亚国际免税城增近1倍,“购物+度假”模式成为鲜明特点。(央视新闻)

淡水河谷印尼公司暂停镍矿开采,因2026年生产计划未获政府批准

2026年1月2日 13:20
因年度工作计划未获官方批准, 印尼淡水河谷司镍矿暂停开采活动。这家由巴西淡水河谷公司基本金属部门与印尼政府合资组建的企业在一份公告中表示,尽管目前已暂停运营,但官方批准有望很快下达,此次延期预计不会影响整体运营的可持续性。(新浪财经)

React 中的 Props:组件通信与复用的核心机制

作者 Zyx2007
2026年1月2日 13:02

在 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岁以下群体使用社交媒体

2026年1月2日 13:01
为应对社交媒体对少年儿童的不良影响,法国打算从2026年9月起禁止15岁以下群体使用社交媒体。据法国方面消息,法国政府将在1月初提交草案供立法机构审议。(央视新闻)

React Hooks:函数组件的状态与副作用管理艺术

作者 Zyx2007
2026年1月2日 12:58

在现代 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]);

该清理函数会在以下两种情况下被调用:

  1. 组件卸载时:释放资源,防止内存泄漏;
  2. 下一次副作用执行前(若依赖项变化):先清理旧副作用,再执行新副作用。

这种机制确保了副作用的生命周期与组件状态严格同步,避免了常见的“已卸载组件仍尝试更新状态”错误。

副作用的本质:打破纯函数的边界

React 组件本质上应是一个纯函数:给定相同的 props 和 state,始终返回相同的 JSX。而副作用(如修改全局变量、发起网络请求、改变 DOM)则打破了这一原则,因其结果具有不确定性或对外部环境产生影响。

例如,以下函数存在副作用:

function add(nums) {
  nums.push(3); // 修改了外部数组
  return nums.reduce((a, b) => a + b, 0);
}

调用后,原始 nums 数组被改变,后续代码行为不可预测。而在 React 中,useEffect 正是将这类“不纯”的操作集中管理的容器,使主渲染逻辑保持纯净,提升可测试性与可维护性。

实际应用:数据获取与条件渲染

结合 useStateuseEffect,可实现典型的数据驱动 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 通过 useStateuseEffect 等核心 API,将状态管理和副作用处理从类组件的生命周期中解放出来,赋予函数组件完整的逻辑表达能力。它们以声明式的方式描述“何时做什么”,而非“在哪个阶段做什么”,更符合直觉。同时,依赖数组机制强制开发者显式声明副作用的触发条件,提升了代码的可读性与健壮性。掌握 Hooks,不仅是使用现代 React 的必备技能,更是迈向函数式、响应式前端开发思维的关键一步。

第3章 Nest.js拦截器

作者 XiaoYu2002
2026年1月2日 12:30

3.1 拦截器介绍

Nest.js的拦截器和axios的拦截器是类似的,可以在网络请求处理的前后去执行额外的逻辑。拦截器从字面意思理解就是拦截,假设有流程A->B,拦截器要做的是A到B的过程中,将内容拦截下来处理后再丢给B,变成了A->拦截器->B。

在网络请求的逻辑中,拦截器的拦截位置如下:

  • 客户端请求->拦截器(前置逻辑)->路由处理器->拦截器(后置逻辑)->客户端响应。

Nest.js拦截器效果如图3-1所示。

image-20251212195404071

图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所示。

image-20251212212343423

图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所示。在这里体现的是:路由处理器->拦截器(后置逻辑)->客户端响应。

image-20251212220853053

图3-3 Nest.js全局拦截器-get请求拦截效果

message和code字段属于业务逻辑的部分,后续完成英语AI项目时,会根据业务实际逻辑去自定义设置。

3.4 优化全局拦截器

但此时全局拦截器还有一个很大的Bug,假如接口返回一个很大的数据,我们通过BigInt数据类型去处理返回,那么在通过全局拦截器时就会出现报错情况,全局拦截器处理BigInt类型报错如图3-4所示。

image-20251212222211323

图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,
      };
    }));
  }
}

接下来对异常也格式化统一处理一下,逻辑思路与全局拦截器类似。当前端发起不符合规范和要求的网络请求,后端就会返回异常信息,方便前端去统一处理。

image-20251212235546507

图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() 中明确指定对应的异常类型。

image-20251212235819873

图3-6 HTTP异常过滤层的说明

接下来我们来对异常处理情况进行统一的格式化处理。这里的code(异常状态码)就不采用我们自定义的,而是使用exception内部定义的状态码,因为Nest内置的HttpException已经为所有常见错误定义了标准化的状态码(如 400、401、403、404、500 等),这些状态码符合 HTTP 协议本身的语义。直接使用exception.getStatus()可以确保服务端返回的错误信息在网络层面是可预测和通用的。Nest.js内置异常处理层说明如图3-7所示。

image-20251213000123007

图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所示。

image-20251212235417767

图3-8 全局异常情况的过滤处理效果

❌
❌