普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月24日首页

SSE与流式传输(Streamable HTTP)

2025年12月24日 18:10

SSE被冷落了十多年,终于随AI火了一把。过去大家宁可用websocket也不愿意使用SSE,以至于AI出来后,很多人认为这是个新技术。实际上它很久以前就是HTML5标准中的一部分了。

MCP兴起后,有些人认为SSE与Streamable HTTP是两个概念,其实不然。本文将理清SSE和Streamalbe HTTP两者的概念与关系,并给出实践中的一些小建议。

SSE

SSE是Streamable HTTP的一个实现:SSE不仅对请求头有要求——必须设置Content-Type: text/event-stream,而且对传输格式、数据解析做了详细约定:

image.png

除此之外还有自动重连机制,具体可见HTML5标准 html.spec.whatwg.org/multipage/s…

除了这些具体的规范外,SSE只能发送get请求,并且只能由客户端主动关闭。另外,从"text/event-stream"上可以看出,SSE聚焦于文本内容传输,要传二进制内容就比较麻烦。

总的来说,SSE是由HTML5标准规定,针对前端场景特殊规定的流式传输协议。基于SSE的流式传输,可以通过EventSource对象实现,也可以通过fetch自行处理请求/解析/重连。

Streamable HTTP

Streamable HTTP虽然与SSE一样依赖http协议中的keep-alive,但更底层和中立。

它的核心是Transfer-Encoding: chunked(http1.1),此外没有其他约束。

如果使用http2,可以不声明Transfer-Encoding,只要持续写就行了,因为http2能自动分帧。

当服务器返回的响应中缺少Content-Length头部,且连接保持开放(Connection: keep-alive)时,HTTP/1.1 协议会默认使用Transfer-Encoding: chunked编码来传输响应数据,SSE刚好满足这两个条件,因此也是chunked transfer传输的。

从这个角度来说,SSE就是Streamable HTTP传输的一个实现——SSE = Streamable HTTP + 事件编码规范

由于Streamable HTTP并没有规定数据格式和解析方法,因此使用者可以根据场景自行协商:

SSE传输:
data: {"token":"Hello"}
data: {"token":"world"}
data: [DONE]

Streamable HTTP传输:
{"type":"token","content":"Hello"}
{"type":"token","content":"world"}
{"type":"done"}

从内容上可以看出,SSE必须解析data:开头,而Streamable可传输json string line等多种格式。

为什么MCP更青睐Streamable HTTP

原因 说明
🌐 跨语言兼容 SSE 原生仅限浏览器;Streamable HTTP 适配 SDK、CLI、服务端
🧱 结构灵活 支持 NDJSON、JSON Lines、Protocol Buffers
⚙️ 更贴近底层 I/O 方便控制 chunk 大小、流速、关闭机制
🧩 多类型输出 AI 不止发文本,还要发图像、语音、函数调用等
📦 工具链统一 与现代 fetch/Response API 一致

对ai应用来说,SSE过于死板。它规定了传输格式,编码方式,无法以二进制传输。在非浏览器环境中,使用更原始的Streamable HTTP显然更合适。

流式传输的实践建议

  1. 如果没有二进制传输需求,可以使用SSE协议,服务端第三方开源工具也较多
  2. 浏览器端建议使用fetch而不是EventSource,便于传参和认证
  3. 浏览器端使用AbortController取消流式传输
  4. 服务端根据请求头的 Accept: 'text/event-stream' 判断是否以SSE格式传输(如果需要同时支持流式传输和普通分页传输)

Vue3 v-if与v-show:销毁还是隐藏,如何抉择?

作者 kknone
2025年12月24日 16:27

1. v-if 与 v-show 的基本概念

条件渲染是Vue3中控制界面显示的核心能力,而v-ifv-show是实现这一能力的两大核心指令。它们的本质差异,要从“**是否真正改变组件的存在 **”说起。

1.1 v-if:真正的“存在与否”

v-if是“破坏性条件渲染”——当条件为true时,它会创建组件实例并渲染到DOM中;当条件为false时,它会销毁 组件实例并从DOM中移除。换句话说,v-if控制的是“组件是否存在”。

举个例子:


<button @click="toggle">切换文本</button>
<div v-if="isShow">Hello, Vue3!</div>

isShowfalse时,你在浏览器DevTools里找不到这个div——它被完全销毁了。而且,组件的生命周期钩子(如onMounted/ onUnmounted)会随着条件切换触发:销毁时执行onUnmounted,重建时执行onMounted

1.2 v-show:只是“看得见看不见”

v-show是“非破坏性条件渲染”——无论条件真假,它都会先把组件渲染到DOM中,再通过修改CSS的display属性控制可见性。换句话说, v-show控制的是“组件是否可见”,但组件始终存在。

同样的例子,换成v-show


<button @click="toggle">切换文本</button>
<div v-show="isShow">Hello, Vue3!</div>

isShowfalse时,div依然在DOM中,只是多了style="display: none"。此时,组件实例没有被销毁,生命周期钩子也不会触发——它只是“被藏起来了”。

2. 原理拆解:为什么行为差异这么大?

理解原理是选择的关键,我们用“生活比喻”帮你快速记住:

  • v-if像“客房的家具”:客人来了(条件为真),你把家具搬出来(创建组件);客人走了(条件为假),你把家具收起来(销毁组件)。每次搬运都要花时间(切换成本高),但平时不占空间(初始化成本低)。
  • v-show像“客厅的电视”:不管你看不看(条件真假),电视都在那里(存在于DOM);你只是用遥控器(v-show )切换“显示/隐藏”(修改CSS)。切换动作很快(成本低),但始终占地方(初始化成本高)。

3. 性能对比:初始化 vs 切换成本

v-ifv-show的性能差异,本质是**“空间换时间”还是“时间换空间”**的选择:

3.1 初始化成本:v-if 更“省空间”

当初始条件为false时:

  • v-if:不渲染任何内容,DOM中无节点,初始化速度快
  • v-show:强制渲染组件,DOM中存在节点,初始化速度慢

比如,一个“仅管理员可见”的按钮,用v-if更合适——普通用户打开页面时,按钮不会被渲染,减少页面加载时间。

3.2 切换成本:v-show 更“省时间”

当条件需要频繁切换时:

  • v-if:每次切换都要销毁重建组件,涉及DOM操作和生命周期钩子,切换速度慢
  • v-show:仅修改CSS属性,无DOM重建,切换速度快

比如, tabs 切换、弹窗显示隐藏,用v-show更流畅——用户点击时不会有延迟。

4. 选择策略:到底该用谁?

结合原理和性能,我们总结了3条黄金法则

4.1 频繁切换?选v-show!

如果组件需要反复显示/隐藏(如 tabs、弹窗、折叠面板),优先用v-show。比如:

<!-- 频繁切换的弹窗,用v-show -->
<modal v-show="isModalOpen" @close="isModalOpen = false"></modal>

4.2 极少变化?选v-if!

如果条件几乎不会改变(如权限控制、初始化提示),优先用v-if。比如:

<!-- 仅管理员可见的按钮,用v-if -->
<button v-if="isAdmin" @click="deleteItem">删除</button>

4.3 要保留状态?选v-show!

如果组件包含需要保留的状态(如表单输入、播放器进度),必须用v-show——v-if会销毁组件,导致状态丢失。

举个直观的例子:


<template>
    <button @click="toggle">切换输入框</button>
    <!-- v-if:输入内容会重置 -->
    <div v-if="isShow">
        <input type="text" placeholder="v-if 输入框"/>
    </div>
    <!-- v-show:输入内容保留 -->
    <div v-show="isShow">
        <input type="text" placeholder="v-show 输入框"/>
    </div>
</template>

<script setup>
    import {ref} from 'vue'

    const isShow = ref(true)
    const toggle = () => isShow.value = !isShow.value
</script>
往期文章归档
免费好用的热门在线工具

试着输入内容后切换:v-if的输入框会清空(组件销毁),v-show的输入框内容不变(组件存在)。

5. 动手实践:看得到的差异

为了更直观,我们用生命周期钩子验证两者的区别:

  1. 创建子组件Child.vue

    <template><div>我是子组件</div></template>
    <script setup>
    import { onMounted, onUnmounted } from 'vue'
    onMounted(() => console.log('子组件挂载了!'))
    onUnmounted(() => console.log('子组件销毁了!'))
    </script>
    
  2. 父组件中切换:

    <template>
      <button @click="toggle">切换子组件</button>
      <!-- 用v-if时,切换会打印日志 -->
      <Child v-if="isShow" />
      <!-- 用v-show时,切换无日志 -->
      <!-- <Child v-show="isShow" /> -->
    </template>
    
    <script setup>
    import { ref } from 'vue'
    import Child from './Child.vue'
    const isShow = ref(true)
    const toggle = () => isShow.value = !isShow.value
    </script>
    

运行后点击按钮:

  • v-if:切换会打印“子组件销毁了!”和“子组件挂载了!”(组件生死轮回);
  • v-show:无日志(组件始终存在)。

6. 课后Quiz:巩固你的理解

问题:你在开发“用户设置”页面,其中“高级设置”面板可以点击“展开/收起”切换。面板包含多个输入框(如“个性签名”),需要保留用户输入。请问该用 v-if还是v-show?为什么?

答案解析
v-show。原因有二:

  1. 频繁切换:用户可能多次展开/收起,v-show切换成本更低;
  2. 状态保留:输入框需要保留内容,v-show不会销毁组件,状态不会丢失。

7. 常见报错与解决

使用v-if/v-show时,这些“坑”要避开:

问题1:v-show 不能和 v-else 一起用

报错v-else can only be used with v-if
原因v-elsev-if的配套指令,v-show是CSS控制,无法配合。
解决:用v-if代替v-show,或分开写v-show

<!-- 错误 -->
<div v-show="isShow">内容A</div>
<div v-else>内容B</div>

<!-- 正确:用v-if -->
<div v-if="isShow">内容A</div>
<div v-else>内容B</div>

<!-- 正确:分开写v-show -->
<div v-show="isShow">内容A</div>
<div v-show="!isShow">内容B</div>

问题2:v-if 和 v-for 一起用导致性能低

报错场景:同一个元素同时用v-ifv-for


<li v-for="item in list" v-if="item.isActive">{{ item.name }}</li>

原因:Vue3中v-for优先级高于v-if,会先循环所有元素,再逐个判断条件,重复计算导致性能差。
解决:用computed先过滤数组:


<template>
    <li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>
</template>

<script setup>
    import {ref, computed} from 'vue'

    const list = ref([/* 数据 */])
    // 先过滤出active的item
    const activeItems = computed(() => list.value.filter(item => item.isActive))
</script>

问题3:v-show 对 template 无效

报错场景:用v-show控制<template>标签:


<template v-show="isShow">
    <div>内容</div>
</template>

原因<template>是Vue的虚拟标签,不会渲染成真实DOM,v-show无法修改其display属性。
解决:用真实DOM元素(如<div>)包裹,或用<template v-if>

<!-- 正确:用div包裹 -->
<div v-show="isShow">
    <div>内容</div>
</div>

<!-- 正确:用v-if -->
<template v-if="isShow">
    <div>内容</div>
</template>

8. 参考链接

参考链接:vuejs.org/guide/essen…

到底选 Nuxt 还是 Next.js?SEO 真的有那么大差距吗 🫠🫠🫠

作者 Moment
2025年12月23日 09:36

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

关于 NuxtNext.js 的 SEO 对比,技术社区充斥着太多误解。最常见的说法是 NuxtPayload JSON 化会严重影响 SEO,未压缩的数据格式会拖慢页面加载。还有人担心环境变量保护机制存在安全隐患。

实际情况远非如此。框架之间的 SEO 差异被严重夸大了。Nuxt 采用未压缩 JSON 是为了保证数据类型完整性和加速水合过程,这是深思熟虑的设计权衡。所谓的安全问题,本质上是第三方生态设计的挑战,而非框架缺陷。

真正影响搜索引擎排名的是内容质量、用户体验、页面性能和技术实践,而非框架选择。一个优化良好的 Nuxt 网站完全可能比未优化的 Next.js 网站排名更好。

一、服务端渲染机制对比

Next.js:压缩优先

Next.js 新版本使用压缩字符串格式序列化数据,旧版 Page Router 则用 JSON。数据以 <script> 标签形式嵌入 HTML,客户端接收后解压完成水合。

这种方案优势明显:HTML 文档体积小,传输快,初始加载速度优秀。App Router 还支持流式渲染和 Server Components,服务端可以逐步推送内容,不需要等待所有数据准备就绪。

权衡也很清楚:增加了客户端计算开销,复杂数据类型需要额外处理。

Nuxt:类型完整性优先

Nuxt 采用未压缩 JSON 格式,通过 window.__NUXT__ 对象嵌入数据。设计思路基于一个重要前提:现代浏览器的 JSON.parse() 性能极高,V8 引擎对 JSON 解析做了大量优化。相比自定义压缩算法,直接用 JSON 格式解析速度更快。

核心优势是水合速度极快,支持完整的 JavaScript 复杂类型。Nuxt 使用 devalue 序列化库,能够完整保留 MapSetDateRegExpBigInt 等类型,还能处理循环引用。这意味着从后端传递 Map 数据,前端反序列化后依然是 Map,而不会被转换为普通 Object

当然,包含大量数据时 HTML 体积会增大。不过对于大数据场景,Nuxt 已经支持 Lazy Hydration 来处理。

设计哲学差异

Next.js 优先考虑传输速度,适合前后端分离场景。Nuxt 优先保证类型完整性,更适合全栈 JavaScript 应用。由于前后端都用 JavaScript,保持数据类型一致性可以减少大量类型转换代码。

实际测试表明,大多数场景下这种方案不会拖慢首屏加载。一次传输加快速水合的策略,整体性能往往更优。

二、对 SEO 的实际影响

Payload JSON 化的真实影响

从 SEO 角度看,Nuxt 的方案有独特优势。爬虫可以直接从 HTML 获取完整内容,无需执行 JavaScript。即使 JS 加载或执行失败,页面核心内容依然完整存在于 HTML 中。这对 SEO 至关重要,因为搜索引擎爬虫虽然能执行 JavaScript,但更倾向于直接读取 HTML。

HTML 体积增大的影响被严重高估了。实际测试数据显示,即使 HTML 增大 50-100KB,对 TTFB 的影响也在 50-100ms 以内,用户几乎感知不到。而水合速度提升可能节省 100-200ms 的交互响应时间,整体用户体验反而更好。

Next.js 的性能优势

Next.js 采用压缩格式后,HTML 体积更小,服务器响应更快。实际数据显示平均 LCP 为 1.2 秒,INP 为 65ms,这些指标确实优秀。

Next.js 13+Server Components 进一步优化了数据传输。服务端组件的数据不需要序列化传输到客户端,直接在服务端渲染成 HTML,大大减少了传输量。对于主要展示内容的页面,这种方式可以实现接近静态页面的性能。

ISR 功能也很实用。页面可以在构建时生成静态 HTML,然后在后台按需更新,既保证了首屏速度,又能及时更新内容。

核心结论

框架对 SEO 的影响被严重夸大了。Google 的 Evergreen Googlebot 在 2019 年就已经能够完整执行现代 JavaScript。无论 Nuxt 还是 Next.js,只要正确实现了 SSR,搜索引擎都能获取完整内容。

框架选择对排名的影响可能不到 1%。真正影响 SEO 的是内容质量、页面语义化、结构化数据、内部链接结构、技术实践(sitemaprobots.txt)和用户体验指标。

三、SEO 功能特性对比

元数据管理

Next.js 13+Metadata API 提供了类型安全的元数据配置,与 Server Components 深度集成,可以在服务端异步获取数据生成动态元数据:

// Next.js
export async function generateMetadata({ params }) {
  const post = await fetchPost(params.id);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.coverImage] },
  };
}

NuxtuseHead 提供响应式元数据管理,配合 @nuxtjs/seo 模块开箱即用。内置 Schema.org JSON-LD 生成器,可以更方便地实现结构化数据:

// Nuxt
const post = await useFetch(`/api/posts/${id}`);
useHead({
  title: post.value.title,
  meta: [{ name: "description", content: post.value.excerpt }],
});

useSchemaOrg([
  defineArticle({
    headline: post.title,
    datePublished: post.publishedAt,
    author: { name: post.author.name },
  }),
]);

Next.js 同样可以实现结构化数据,但需要手动插入 <script type="application/ld+json"> 标签。虽然需要额外工作,但提供了更大的灵活性。

语义化 HTML 与无障碍性

Nuxt 提供自动 aria-label 注入功能,@nuxtjs/a11y 模块可以在开发阶段自动检测无障碍性问题。Next.js 需要开发者手动确保,可以使用 eslint-plugin-jsx-a11y 检测问题。

语义化 HTML 对 SEO 的重要性常被低估。搜索引擎不仅读取内容,还会分析页面结构。正确使用 <article><section><nav> 等标签,可以帮助搜索引擎更好地理解内容层次。

静态生成与预渲染

Next.jsISR 允许为每个页面设置不同的重新验证时间。首页可能每 60 秒重新生成,文章页面可能每天重新生成。这种精细化控制使得网站能在性能和内容新鲜度之间找到平衡:

// Next.js ISR
export const revalidate = 3600; // 每小时更新

Nuxt 3 支持混合渲染模式,可以在同一应用中同时使用 SSR、SSG 和 CSR,为不同页面选择最适合的渲染策略:

// Nuxt 混合渲染
export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true },
    "/posts/**": { swr: 3600 },
    "/admin/**": { ssr: false },
  },
});

Next.js 14Partial Prerendering 更进一步,允许在同一页面混合静态和动态内容。静态部分在构建时生成,动态部分在请求时渲染,结合了 SSG 的速度和 SSR 的灵活性。

四、性能指标与爬虫友好性

Core Web Vitals 表现

从各项指标看,Next.js 平均 LCP 为 1.2 秒,表现优秀。Nuxt 的 LCP 可能受 HTML 体积影响,但在 FCP 和 INP 方面得益于快速水合机制,同样表现出色。

需要强调的是,这些差异在实际 SEO 排名中影响极其有限。Google 在 2021 年将 Core Web Vitals 纳入排名因素,但权重相对较低。内容相关性、反向链接质量、域名权威度等传统因素权重仍然更高。

更重要的是,Core Web Vitals 分数取决于真实用户体验数据,而非实验室测试。网络环境、设备性能、缓存状态等因素对性能的影响远大于框架本身。一个优化良好的 Nuxt 网站完全可能比未优化的 Next.js 网站获得更好的分数。

两个框架都提供了丰富的优化工具。Next.jsnext/image 提供自动图片优化、懒加载、响应式图片。Nuxt@nuxt/image 提供类似功能,并支持多种图片托管服务。图片优化对 LCP 的影响往往比框架选择更大。

爬虫友好性

两个框架在爬虫友好性方面几乎没有差别。都提供完整的服务端渲染内容,无需 JavaScript 即可获取页面信息,能正确返回 HTTP 状态码,支持合理的 URL 结构。

Nuxt 的额外优势在于内置 Schema.org JSON-LD 生成器,有助于搜索引擎生成富文本摘要。结构化数据对现代 SEO 至关重要,通过嵌入 JSON-LD 格式的数据,可以告诉搜索引擎页面内容的具体含义。这些信息会显示在搜索结果中,提高点击率。

两个框架在处理动态路由、国际化、重定向等 SEO 关键功能上都提供了完善支持。

五、安全性问题澄清

环境变量保护机制

关于 Nuxt 会将 NUXT_PUBLIC 环境变量暴露到 HTML 的问题,需要明确这并非框架缺陷。Nuxt 的机制是只有显式设置为 NUXT_PUBLIC_ 前缀的变量才会暴露到前端,非 public 变量不会出现在 HTML 中。

正常情况下,开发者不应该将重要信息设置为 public。任何重要信息都不应该放到前端,这是前端开发的基本原则,与框架无关。

Nuxt 3 的环境变量系统被彻底重新设计。运行时配置分为公开和私有两部分:

// Nuxt 配置
export default defineNuxtConfig({
  runtimeConfig: {
    // 私有配置,仅服务端可用
    apiSecret: process.env.API_SECRET,

    // 公开配置,会暴露到客户端
    public: {
      apiBase: process.env.API_BASE_URL,
    },
  },
});

Next.js 使用类似机制,以 NEXT_PUBLIC_ 前缀的变量会暴露到客户端:

// 服务端组件中
const apiSecret = process.env.API_SECRET; // 可用

// 客户端组件中
const apiBase = process.env.NEXT_PUBLIC_API_BASE; // 可用
const apiSecret = process.env.API_SECRET; // undefined

实际开发中的安全挑战

真正的问题在于某些第三方库要求在前端初始化时传入密钥。一些 BaaS 服务(如 SupabaseFirebase)的前端 SDK 设计就需要在前端初始化,开发者无法控制这些第三方生态的设计。

Supabase 为例,它提供 anon key(匿名密钥)和 service role key(服务角色密钥)。anon key 设计为可以在前端使用,通过行级安全策略在数据库层面控制权限。service role key 则绕过所有安全策略,只能在服务端使用。

理想解决方案是将依赖密钥的库放到服务端,通过 API 调用使用。如果某个库需要在前端运行明文密钥,这个库本身就存在重大安全风险。但现实往往更复杂,生态适配问题难以完全避免。

值得注意的是,Next.js 同样存在类似的安全考量。两个框架在环境变量保护方面的机制基本一致,问题根源在于第三方生态设计,而非框架缺陷。

对 SEO 的影响

环境变量问题本质上是安全问题,而非 SEO 问题。只要正确配置,不会影响搜索引擎爬取。

真正影响 SEO 的安全问题是:网站被攻击后注入垃圾链接、恶意重定向、隐藏内容等。这些行为会直接导致网站被搜索引擎惩罚,甚至从索引中移除。防止这类问题需要全方位的安全措施,远超框架本身的范畴。

六、实际应用场景

内容密集型网站

对于博客、新闻网站、文档站点,Nuxt 往往表现更好。内容块水合速度快,开箱即用的 SEO 功能完善,开发体验好。

Nuxt@nuxt/content 模块提供了基于 Markdown 的内容管理系统,支持全文搜索、代码高亮、自动目录生成。配合 @nuxtjs/seo 模块,可以快速构建 SEO 友好的内容网站:

// Nuxt Content 使用
const { data: post } = await useAsyncData("post", () =>
  queryContent("/posts").where({ slug: route.params.slug }).findOne()
);

技术博客、文档网站特别适合这种方案。VuePressVitePress 等静态站点生成器也是基于类似思路构建的。

动态应用

对于电商平台、SaaS 应用等需要高级渲染技术和复杂交互的场景,Next.js 可能更合适。ISR 和部分预渲染能够更好地应对动态内容需求。

电商网站的 SEO 挑战在于商品数量庞大、内容动态变化、需要个性化推荐。Next.jsISR 可以为每个商品页面设置合适的重新验证时间,既保证 SEO 友好,又能及时更新库存、价格:

// Next.js 电商页面优化
export default async function ProductPage({ params }) {
  const product = await fetchProduct(params.id);

  return (
    <>
      <ProductInfo product={product} />
      <Suspense fallback={<Skeleton />}>
        <AddToCartButton productId={params.id} />
      </Suspense>
    </>
  );
}

export const revalidate = 1800; // 30分钟重新验证

混合场景

对于兼具内容和应用特性的混合场景,两个框架都能很好胜任。许多现代网站都是混合型的:既有内容页面(博客、文档),又有应用功能(用户中心、后台管理)。

关键是为不同类型页面选择合适的渲染策略。Nuxt 3routeRules 提供路由级别的渲染控制:

// Nuxt 混合渲染场景
export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true }, // 首页预渲染
    "/blog/**": { swr: 3600 }, // 博客缓存 1 小时
    "/dashboard/**": { ssr: false }, // 用户中心客户端渲染
    "/api/**": { cors: true }, // API 路由
  },
});

Next.js 通过不同文件约定实现类似功能。可以在 app 目录中使用 Server Components 和 Client Components 组合,在 pages 目录中使用传统 SSR/SSG 方式。

七、开发者的真实痛点

超越 SEO 的实际考量

通过分析实际案例发现,开发者选择框架的真正原因往往不是 SEO。许多从 Nuxt 迁移到 Next.js 的团队,主要原因包括第三方生态兼容性问题(如 Supabase 等 BaaS 服务的前端依赖),以及开发体验(启动速度慢、构建时间长)。

客观来说,Nuxt 在开发服务器启动速度和构建时间方面确实存在优化空间。不过随着 RolldownOxc 等下一代工具链的完善,这些问题有望改善。这说明 SEO 和安全问题可能被过度强调了,真正影响开发者选择的是开发体验和生态适配。

开发服务器启动速度直接影响开发效率。如果每次重启都需要等待 30-60 秒,一天下来可能浪费几十分钟。构建时间同样重要,尤其在 CI/CD 环境中。构建时间过长会延迟部署,影响快速迭代能力。

生态兼容性是另一个重要因素。某些库只提供 React 版本,某些只提供 Vue 版本。虽然可以通过适配器跨框架使用,但会增加维护成本。

技术方案的权衡

没有完美的技术方案,只有最适合的选择。Nuxt 优先保证数据类型完整性和快速水合,Next.js 追求更小体积和更快 TTFB。

不同开发者有不同需求:内容型网站与应用型产品侧重点不同,小团队与大型商业项目考量各异。技术讨论中有句话很好地总结了这点:几乎所有框架都在解决同样的问题,差别只在于细微的实现方式。

对小团队来说,开发效率可能比性能优化更重要。快速实现功能、快速迭代,比追求极致性能指标更有价值。对大型团队来说,长期维护性和可扩展性更重要。

技术债务是另一个需要考虑的因素。随着项目发展,早期为了快速开发而做的权衡可能成为瓶颈。选择一个社区活跃、持续演进的框架很重要。Next.jsNuxt 都有强大的商业支持(Vercel 和 NuxtLabs),这保证了框架的长期发展。

八、综合评估与选择建议

SEO 能力评分

从 SEO 能力看,Next.js 可以获得 4.5 分(满分 5 分)。优势在于优秀的 Core Web Vitals 表现、更小的 HTML 体积、成熟的 SEO 生态,以及 ISR 和部分预渲染等先进特性。不足是需要手动配置结构化数据,语义化 HTML 需要开发者特别注意。

Nuxt 可以获得 4 分。优势包括内置 Schema.org JSON-LD 生成器、自动语义化 HTML 支持、在内容型网站的优秀表现,以及快速水合带来的良好体验。Payload JSON 导致 HTML 体积增大在实际应用中影响微小,已有 Lazy Hydration 等解决方案。开发服务器启动和构建速度慢是开发体验问题,与 SEO 无关。

需要说明的是,两者 SEO 能力实际上都接近满分,0.5 分的差异主要体现在开箱即用的便利性上,而非实际 SEO 效果。在真实搜索引擎排名中,这 0.5 分的差异几乎不会产生可察觉的影响。

选择 Next.js 的场景

如果项目对 Core Web Vitals 有极高要求,或者需要复杂渲染策略的动态应用,Next.js 是更好的选择。以下场景推荐使用:

  • 电商平台,需要 ISR 平衡性能和内容新鲜度
  • SaaS 应用,对交互性能要求极高
  • 国际化大型网站,需要精细性能优化
  • 团队已有 React 技术栈,迁移成本低
  • 需要使用大量 React 生态的第三方库
  • 对 Vercel 平台部署优化感兴趣
  • 需要 Server Components 的先进特性
  • 项目规模大,需要严格的 TypeScript 类型检查

选择 Nuxt 的场景

如果项目是内容密集型网站(博客、新闻、文档),或者需要快速开发并利用框架的 SEO 便利功能,Nuxt 是理想选择。以下场景推荐使用:

  • 技术博客、文档站点,内容是核心
  • 新闻、媒体网站,需要快速发布内容
  • 企业官网,强调 SEO 和内容展示
  • 团队已有 Vue 技术栈,迁移成本低
  • 需要使用 Vue 生态的 UI 库(如 Element Plus、Vuetify)
  • 快速原型开发,需要开箱即用的功能
  • 需要 @nuxt/content 的 Markdown 内容管理
  • 项目需要传递复杂的 JavaScript 对象(Map、Set、Date 等)

决策思路

对于需要优秀 SEO 表现、服务端渲染和良好开发体验的项目,两个框架都完全能够胜任。选择关键在于团队技术栈、第三方生态适配、开发体验等实际因素,而不应该仅仅基于 SEO 考虑。

在中小型项目、团队对两个框架都不熟悉、项目没有特殊渲染需求的情况下,两个框架都是合理选择。可以考虑以下因素决策:

  • 团队成员的个人偏好(React vs Vue)
  • 公司的技术战略和长期规划
  • 现有项目的技术栈,保持一致性
  • 招聘市场,React 开发者相对更多
  • 社区资源,React 生态整体更成熟
  • 学习曲线,Vue 的 API 相对更简单

九、核心结论

框架差异的真实影响

几乎所有现代框架都能很好地支持 SEO,差异只在于细微的实现方式。框架选择对 SEO 排名的影响远小于内容质量和技术实现。Payload JSON 化影响 SEO 的说法被夸大了,这是经过深思熟虑的设计权衡。对大多数应用来说,一次传输加快速水合的策略更优。

从搜索引擎角度看,只要页面能正确渲染、内容完整可见、HTML 结构合理、meta 标签正确,框架是什么并不重要。Google 的爬虫既可以处理传统静态 HTML,也可以执行复杂的 JavaScript 应用,还可以理解 SPA 的路由。

真正影响 SEO 的是:内容质量和原创性、页面加载速度(但 100-200ms 差异可以忽略)、移动端友好性、内部链接结构、外部反向链接、域名权威度、用户行为指标(点击率、停留时间、跳出率)、技术 SEO 实践(sitemaprobots.txt、结构化数据)。

框架选择在这些因素中的权重微乎其微。用 Nuxt 还是 Next.js,对实际排名的影响可能不到 1%。

性能指标的误区

Next.js 在 HTML 体积、TTFB 和 Core Web Vitals 平均值方面具有优势。Nuxt 则在数据类型支持、水合速度和网络传输效率方面表现出色。但这些差异在实际 SEO 排名中影响微乎其微。

常见的性能误区包括:过度关注实验室测试数据,忽略真实用户体验;追求极致分数,忽略边际收益递减;认为性能优化就是 SEO 优化;忽略其他更重要的 SEO 因素;在框架选择上纠结,而不是优化现有代码。

实际上,同一框架下,优化和未优化的网站性能差异可能是 10 倍,而不同框架之间的性能差异可能只有 10-20%。把精力放在代码优化、资源优化、CDN 配置等方面,往往比纠结框架选择更有价值。

决策因素梳理

技术因素方面,应该考虑团队技术栈(React vs Vue)、第三方生态适配、开发体验(启动速度、构建速度)。业务因素方面,需要评估项目类型(内容型 vs 应用型)、团队规模和能力、时间和预算。不应该主要基于 SEO 来选择框架,因为两者在 SEO 方面能力基本相当。

决策优先级建议:

第一优先级:团队技术栈和能力。团队熟悉什么就用什么,学习成本和招聘成本很高。

第二优先级:项目类型和需求。内容型倾向 Nuxt,应用型倾向 Next.js,混合型都可以。

第三优先级:生态和工具链。需要的第三方库是否支持,部署平台的支持情况,开发工具的成熟度。

第四优先级:性能和 SEO。只在前三者相同时考虑,实际影响很小。

十、实践建议

SEO 优化核心原则

内容质量永远是第一位的。框架只是工具,内容才是核心,技术优化是锦上添花而非雪中送炭。正确实现 SSR 比框架选择更重要。

SEO 最佳实践清单:确保所有重要内容在 HTML 中可见、为每个页面设置唯一的 titledescription、使用语义化 HTML 标签、正确使用标题层级、为图片添加 alt 属性、实现结构化数据、生成 sitemap.xml 并提交、配置合理的 robots.txt、使用 HTTPS、优化页面加载速度、确保移动端友好、构建合理的内部链接结构、定期发布高质量原创内容、获取高质量的外部链接、监控和分析 SEO 数据。

Nuxt 优化建议

充分利用框架优势,包括数据类型完整性的便利。对于大数据量场景,使用 Lazy Hydration 功能。不要因为 payload 问题过度担心,实际影响很小。

性能优化技巧:使用 nuxt generate 生成静态页面、配置 routeRules 为不同页面选择合适渲染策略、使用 @nuxt/image 优化图片加载、使用 lazy 属性延迟加载不关键组件、优化 payload 大小避免传递不必要数据、使用 useState 管理 SSR 和 CSR 之间共享的状态、配置合理的缓存策略、监控 payload 大小必要时拆分数据。

// Nuxt 性能优化配置
export default defineNuxtConfig({
  experimental: {
    payloadExtraction: true,
    inlineSSRStyles: false,
  },
  routeRules: {
    "/": { prerender: true },
    "/blog/**": { swr: 3600 },
  },
  image: {
    domains: ["cdn.example.com"],
  },
});

Next.js 优化建议

充分利用性能优势,包括 ISR 和部分预渲染,优化 Core Web Vitals 指标。在处理复杂数据类型时,MapSet 等需要额外处理,要确保序列化和反序列化的正确性。

性能优化技巧:使用 ISR 为动态内容设置合理重新验证时间、尽可能使用 Server Components 减少客户端 JavaScript、使用 next/image 自动优化图片、使用 next/font 优化字体加载、配置 experimental.ppr 启用部分预渲染、使用 Suspenseloading.js 改善感知性能、代码分割按需加载、优化第三方脚本加载、使用 @next/bundle-analyzer 分析包大小、配置合理的缓存策略。

// Next.js 性能优化配置
const nextConfig = {
  experimental: {
    ppr: true,
    optimizeCss: true,
    optimizePackageImports: ["lodash", "date-fns"],
  },
  images: {
    domains: ["cdn.example.com"],
    formats: ["image/avif", "image/webp"],
  },
};

框架无关的通用优化

无论选择哪个框架,以下优化都是必要的:使用 CDN 加速静态资源、启用 Gzip 或 Brotli 压缩、配置合理的缓存策略、优化首屏渲染延迟加载非关键资源、减少 HTTP 请求数量、使用 HTTP/2 或 HTTP/3、优化数据库查询、使用 Redis 等缓存层、监控真实用户性能数据、定期进行性能审计。

决策流程

如果主要关心 SEO,两个框架都完全够用,选择团队熟悉的即可,把精力放在内容质量和技术实现上。如果项目需要复杂数据结构,Nuxtdevalue 机制会让开发更便捷。如果追求极致性能指标,Next.js 的压缩方案可能略好一点,但差异在实际 SEO 排名中几乎可以忽略。如果被第三方生态绑定,这可能是最重要的决策因素。

决策流程建议:评估团队现有技术栈(如果已有 React/Vue 项目保持一致)、分析项目需求(内容型倾向 Nuxt、应用型倾向 Next.js)、检查第三方依赖(列出必需的库、确认是否有对应生态版本)、考虑部署环境(Vercel 对 Next.js 有特殊优化、Netlify 和 Cloudflare Pages 两者都支持)、评估长期维护成本(框架更新频率和稳定性、社区支持和文档质量)。

结语

通过深入分析技术原理和实际应用案例,可以得出一个明确的结论:框架之间的差异是细微的,对 SEO 的影响更是微乎其微。

选择你熟悉的、团队能驾驭的、生态适合的框架,然后专注于创造优质内容和良好的用户体验。这才是 SEO 成功的根本。技术框架只是实现目标的工具,真正决定网站排名和用户满意度的,始终是内容的质量和服务的价值。

理解技术决策的权衡,认清 SEO 的本质,基于实际需求选择,避免过度优化,把精力放在真正重要的地方。这些才是从技术讨论中应该获得的核心启示。

SEO 是一个系统工程,涉及技术、内容、营销等多个方面。框架选择只是技术层面的一个小环节。即使选择了最适合 SEO 的框架,如果内容质量不佳、用户体验糟糕、营销策略失败,网站依然无法获得好的排名。

相反,即使使用了理论上性能稍差的框架,但如果能够持续输出高质量内容、构建良好的用户体验、实施正确的 SEO 策略,网站依然可以获得优秀的搜索排名。这才是 SEO 的真谛。

最后,技术在不断演进。Next.jsNuxt 都在快速迭代,引入新的特性和优化。今天的分析可能在明天就过时了。保持学习、关注技术动态、根据实际情况调整策略,这才是长久之计。

参考资料

  1. Nuxt SEO 官方文档:nuxtseo.com
  2. Next.js SEO 最佳实践:nextjs.org/docs/app/bu…
  3. Devalue 序列化库:github.com/Rich-Harris…
  4. Google 搜索中心文档:developers.google.com/search
  5. Core Web Vitals 指标说明:web.dev/vitals/
  6. Schema.org 结构化数据规范:schema.org/
  7. Nuxt 官方文档:nuxt.com/docs
  8. Next.js 官方文档:nextjs.org/docs
  9. Nitro 服务引擎:nitro.unjs.io/
  10. Web.dev 性能优化指南:web.dev/performance…

AI Agent 设计模式 - ReAct 模式

作者 子洋
2025年12月23日 08:07

前言

上一篇我们介绍了 AI 应用的发展历程以及 Agent 的整体概念,这一篇将针对 ReAct(Reasoning + Acting)模式,并对其设计思想和工程实现进行一次更为系统、偏实战向的讲解。

在讲解 ReAct 之前,有必要先澄清一个经常被混用的问题:Agent 到底是什么?

在早期以及当下大量工程实践中,不同 AI 应用对 Agent 的定义并不一致。很多所谓的 Agent,本质上更接近一个预先定义好的 AI workflow:流程、工具、策略都由应用侧提前固化,用户只是触发执行。例如,一个 WebSearch 场景往往就对应一个「搜索 Agent」,或者通过特定提示词(如 /agent)来唤醒一组固定的搜索工具。

从工程视角看,这类 Agent 更多是能力封装与产品抽象,而不是研究语境中强调的「具备自主决策与反馈能力的智能体」。也正因为如此,随着概念被频繁复用,agent 这个词在实际讨论中逐渐变得模糊,单独听到它已很难准确判断其具体能力边界。

如果暂时抛开命名争议,从实现层面抽象来看,一个 Agent 的核心逻辑其实非常简单:一个受控的循环(loop)。在这个循环中,模型不断获取上下文、进行推理、执行动作,并根据结果继续调整行为,直到满足终止条件为止。

在工程实现中,这个过程往往可以被近似理解为:

  • 多轮调用 LLM
  • 中间可能伴随工具调用
  • 有明确的退出条件(如任务完成、步数上限、token 预算)

基于这样的背景,各类 Agent 设计模式(也可以称为范式或框架)逐步出现,而其中最经典、也最具代表性的,便是 ReAct 模式。该模式最早发表于 2022 年,其结构至今仍足以支撑大量中低复杂度的 Agent 场景。

ReAct 模式

核心思想

ReAct 的提出,本质上是为了解决一个早期 LLM 应用中的割裂问题:推理与行动往往是分离的

在 ReAct 出现之前,常见的两类模式分别是:

  • Reason-only:模型进行显式推理(如 CoT、Scratchpad),但不与外部环境交互
  • Act-heavy / Tool-driven:模型频繁调用工具获取信息,但推理过程并不显式呈现或不与行动交错

需要说明的是,这里的分类来自 ReAct 论文中的抽象对比,而并非对具体系统内部实现的严格学术归类。例如:

  • SayCan 内部同样包含推理与可行性评估,并非“无 reasoning”
  • WebGPT 也存在内部推理过程,只是推理与行动并未以交错形式呈现给模型

在这种背景下,ReAct(Reasoning + Acting) 的核心思想可以概括为一句话:

在行动中思考,在思考中决定行动。

它尝试将思考行动统一到一个连续的闭环中,模拟人类解决问题时的自然过程:

  1. Thought:分析当前状态与目标
  2. Action:基于判断调用工具或执行操作
  3. Observation:观察行动结果
  4. Thought:根据新信息再次推理
  5. 重复上述过程,直到问题解决

从形式上看,ReAct 并没有引入复杂的新组件,而是通过 Thought → Action → Observation 的反复交替,显著提升了模型在多步任务、信息不完备任务中的表现稳定性。

上图是 ReAct 论文中的一个示例,主要对比了 Standard、Reason Only、Act Only 以及 ReAct 四种不同范式在同一问题下的表现差异。Standard 方式直接给出答案,既不显式展开推理,也不与外部环境交互;Reason Only 虽然在回答前进行了逐步推理,但推理过程完全依赖模型自身的知识,一旦前提判断错误,结论便无法被外部信息纠正;Act Only 则能够多轮调用搜索等工具获取信息,但由于缺乏明确的推理指导,行动过程较为盲目,最终仍然得出了错误结果。相比之下,ReAct 通过多轮 Thought → Act → Observation 的交错执行,使模型能够在行动结果的反馈下不断修正推理路径,最终在后续轮次中得到正确答案。

核心实现

从工程角度看,ReAct 的实现并不复杂,其本质就是一个带有终止条件的循环控制结构。可以用下面这段高度简化的伪代码来概括:

// 简化版实现逻辑
for (let i = 0; i < maxLoops; i++) {
    // 1. 思考:LLM分析当前情况,决定做什么
    const { thought, action, args, final } = await llmThink();
    
    if (final) {
        // 任务完成
        break;
    }
    
    // 2. 行动:调用具体的工具
    const result = await callTool(action, args);
    
    // 3. 观察:将结果作为下一次思考的输入
    context.push(`观察到:${result}`);
}

这段代码已经基本覆盖了 ReAct 的核心机制:

  • 循环驱动:模型在多轮中逐步逼近目标
  • 模型自决策:由 LLM 决定是否继续、是否调用工具
  • 显式终止条件:通过 final 或循环上限避免失控

在真实系统中,通常还会叠加更多安全与成本控制机制,例如:

  • 最大循环次数(maxLoops)
  • token 或调用预算
  • 工具调用白名单

具体实现

下面将结合一份实际可运行的代码示例,展示一个简化但完整的 ReAct Agent 实现。

LLM 调用

这里使用 @ai-sdk 封装多厂商模型调用,示例中支持 OpenAI 与 Azure OpenAI。该部分属于基础设施层,与 ReAct 本身并无强耦合,因此不再展开其原理。具体的介绍和使用方式可以看我之前写的这篇文章 《AI 开发者必备:Vercel AI SDK 轻松搞定多厂商 AI 调用》

import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createAzure } from "@ai-sdk/azure";

/**
 * Build a model client from env/config.
 * Supported providers: 'openai', 'azure'.
 */
export function getModelClient(options = {}) {
  const {
    provider = process.env.AI_PROVIDER || "openai",
    apiKey = process.env.AI_PROVIDER_API_KEY,
    baseURL = process.env.OPENAI_BASE_URL || process.env.AZURE_OPENAI_BASE_URL,
    resourceName = process.env.AZURE_OPENAI_RESOURCE_NAME, // for azure
  } = options;

  if (process.env.AI_MOCK === "1") {
    return { client: null, model: null };
  }

  if (!apiKey) {
    throw new Error("Missing API key: set AI_PROVIDER_API_KEY");
  }

  if (provider === "azure") {
    const azure = createAzure({ apiKey, resourceName });
    return { client: azure, model: azure("gpt-5") };
  }

  const openai = createOpenAI({ apiKey, baseURL });
  const modelName = process.env.OPENAI_MODEL || "gpt-4o-mini";
  return { client: openai, model: openai(modelName) };
}

/**
 * Chat-like step with messages array support.
 * messages: [{ role: 'system'|'user'|'assistant', content: string }]
 */
export async function llmChat({ messages, schema, options = {} }) {
  const { model } = getModelClient(options);
  const system = messages.find((m) => m.role === "system")?.content;

  const result = await generateText({
    model,
    system,
    messages,
    ...(schema ? { schema } : {}),
  });
  return result;
}

核心作用只有一个:以 Chat 形式向模型发送上下文,并获得结构化输出

ReAct 主逻辑

在下面这段代码中,我们完整实现了一个 ReAct Agent 的核心循环逻辑。首先通过 system prompt 对模型的输出形式和行为进行强约束,明确要求其仅以 JSON 格式返回结果,且只能包含 thoughtactionargsfinal 四个字段,并分别约定了调用工具与结束任务时的输出规范。与此同时,在 user 消息中显式告知模型当前可用的 tools 列表,并附带每个工具的功能说明与参数定义,使模型能够在循环过程中基于明确的能力边界自主决策是否进行工具调用。

import { llmChat } from "../llm/provider.js";
import { callTool, formatToolList } from "../tools/index.js";

const SYSTEM = `You are a ReAct-style agent.

You must reason and act using the following loop:
Thought → Action → Observation
This loop may repeat multiple times.

Output format rules:
- You MUST respond with a single JSON object
- The JSON object MUST contain only the following keys:
  - thought (string)
  - action (string, optional)
  - args (object, optional)
  - final (string, optional)
- No additional keys are allowed
- Do NOT use Markdown
- Do NOT include any text outside the JSON object

Behavior rules:
- If you need to call a tool, output "thought", "action", and "args"
- If no further action is required, output "thought" and "final"
- When "final" is present, the response is considered complete and no further steps will be taken
- Do NOT include "action" or "args" when returning "final"

Always follow these rules strictly.`;

export async function runReAct({ task, maxLoops = 6, options = {} } = {}) {
  const messages = [
    { role: "system", content: SYSTEM },
    {
      role: "user",
      content: `Task: ${task}\nAvailable tools: ${formatToolList()}`,
    },
  ];

  const trace = [];

  for (let i = 0; i < maxLoops; i++) {
    const { text } = await llmChat({ messages, options });
    let parsed;
    try {
      parsed = JSON.parse(text);
    } catch (e) {
      // console.warn("Parse failed. Text:", text);
      // console.warn("Error:", String(e));
      messages.push({ role: "assistant", content: text });
      messages.push({
        role: "user",
        content: `Format error: ${String(e)}.
        You previously violated the required JSON format.
        This is a strict requirement.

        If the response is not valid JSON or contains extra text, it will be discarded.
        Retry now.`,
      });
      continue;
    }

    trace.push({ step: i + 1, model: parsed });

    if (parsed.final) {
      console.log("Final result:", parsed.final);
      return { final: parsed.final, trace };
    }

    if (!parsed.action) {
      messages.push({ role: "assistant", content: JSON.stringify(parsed) });
      messages.push({
        role: "user",
        content:
          "No action provided. Please continue with a tool call or final.",
      });
      continue;
    }

    console.log("Action:", parsed.action);
    const observation = await callTool(parsed.action, parsed.args || {});
    trace[trace.length - 1].observation = observation;
    messages.push({ role: "assistant", content: JSON.stringify(parsed) });
    messages.push({
      role: "user",
      content: `Observation: ${JSON.stringify(observation)}. Continue.`,
    });
  }

  return { final: "Max loops reached without final.", trace };
}

Tools

Tools 指的是模型在 ReAct 循环中可以调用的具体外部能力接口。通常,一个工具由工具名称、功能描述、参数定义以及对应的 handler 实现组成。下面示例中实现了两个最基础的文件操作工具:readFileTool 用于读取文件内容,writeFileTool 用于写入文件,两者都完整描述了工具名称、用途、参数 Schema 以及实际执行逻辑。createTool 只是一个用于约定工具输出结构的辅助函数,本身并不涉及核心逻辑,主要用于在非 TS 环境下做基础的参数校验。

这里使用 zod 作为参数校验工具,它可以在 JS / TS 环境中统一使用,通过定义 schema 并在运行时执行 parse 校验,有效缓解模型参数幻觉问题;同时可以直接使用 schema 生成标准的 JSON Schema,作为工具参数说明提供给模型,从而减少手写参数描述的成本。

import fs from "fs/promises";
import path from "path";
import { z } from "zod";
import { createTool } from "./types.js";

const readFileSchema = z.object({ file: z.string() });
const writeFileSchema = z.object({ file: z.string(), content: z.string() });

export const readFileTool = createTool({
  name: "read_file",
  description: "Read a UTF-8 text file from workspace",
  schema: readFileSchema,
  handler: async ({ file }) => {
    const abs = path.resolve(process.cwd(), file);
    const data = await fs.readFile(abs, "utf-8");
    return { ok: true, content: data };
  },
});

export const writeFileTool = createTool({
  name: "write_file",
  description: "Write a UTF-8 text file to workspace (overwrite)",
  schema: writeFileSchema,
  handler: async ({ file, content }) => {
    const abs = path.resolve(process.cwd(), file);
    await fs.mkdir(path.dirname(abs), { recursive: true });
    await fs.writeFile(abs, content, "utf-8");
    return { ok: true, message: `wrote ${file}` };
  },
});

实际效果

基于上述 ReAct 实现,我尝试让模型在本地环境中完成多个小游戏的生成与迭代,包括:2048飞机大战贪吃蛇五子棋。从结果来看,整体完成度和可玩性都明显优于我早期纯手写的一些 demo。当然,需要强调的是:ReAct 并不会凭空提升模型能力,它更多是一种能力放大器。

最终效果在很大程度上仍然依赖于底层模型本身的代码生成、规划与理解能力,而 ReAct 负责的,是为这些能力提供一个稳定的执行框架。

2048

飞机大战

贪吃蛇

五子棋

结语

ReAct 并不是最复杂、也不是最“智能”的 Agent 模式,但它结构清晰、实现成本低、工程可控性强,是理解和实践 Agent 系统非常合适的起点。

在后续更复杂的场景中,往往会在 ReAct 之上叠加:规划(Plan & Execute)、反思(Reflection)、记忆与长期状态,但无论如何,ReAct 所确立的 思考—行动—反馈闭环,仍然是多数 Agent 系统绕不开的基础结构。

在下一篇中,我们将展开对 P&E(Plan and Execute)模式 的详细解析,重点介绍其设计理念、执行流程及具体实现方式。

我已将相关代码开源到 GitHub,感兴趣的同学可以下载到本地后执行一下玩玩: github.com/Alessandro-…

相关资料

昨天 — 2025年12月23日首页

百度一站式全业务智能结算中台

作者 百度Geek说
2025年12月23日 16:41

导读

本文深入介绍了百度一站式全业务智能结算中台,其作为公司财务体系核心,支撑多业务线精准分润与资金流转。中台采用通用化、标准化设计,支持广告、补贴、订单等多种结算模式,实现周结与月结灵活管理。通过业务流程标准化、分润模型通用化及账单测算自动化,大幅提升结算效率与准确性,确保数据合规与业务稳健发展。未来,中台将推进全业务线结算立项线上化、数据智能分析,进一步提升数据分析智能化水平,为公司业务发展提供坚实保障。

01 概述

结算中台作为公司财务体系的核心组成部分,承担着多业务线分润计算、结算及资金流转的关键职能。采用通用化、标准化的设计理念,结算中台能够高效支撑公司内数十个业务线的分润需求,确保广告收入、订单收入、内容分发的精准结算,为公司的财务健康与业务稳健发展提供坚实保障。结算中台建设的核心目标是: 构建高效、标准化、智能化的结算中台体系,支撑多业务线分润计算与资金流转,确保结算数据准确性、高时效披露及业务快速迭代能力,同时降低运维复杂度,推动全业务线结算线上化管理。

结算中台已对接了百家号业务、搜索业务、智能体业务、小说等多个业务线的结算需求, 支持广告分润、补贴分润、订单分润三种结算模式。不同业务线根据各自的业务场景使用不同的结算模式,确保每个业务的收益分配准确无误。结算中台功能分层如图:

图片

02 基本功能

1. 结算模式

结算中台支持三种结算模式,以适应不同业务场景的结算需求:

  • 订单结算:基于直接订单数据,按照订单实际金额与分成策略进行分润计算。

  • 补贴结算:针对特定业务或用户群体,提供额外的收益补贴,以增强业务的市场竞争力。

  • 广告结算:根据分发内容的广告变现与渠道分成比例,精确计算媒体与内容的实际收益。

2. 结算能力

结算中台支持周结与月结两种结算能力:

  • 周结:适用于需要快速资金回笼的业务场景,比如短剧快速回款以后能够再次用于投流, 确保资金流转的高效性。

  • 月结:作为默认的结算周期,便于公司进行统一的财务管理与账务处理。

3. 账单测算自动化

结算中台支持重点业务账单自动测算,通过预设的分润模型,自动计算每个渠道、每位作者的应得收益,生成测算报告。这一自动化过程显著提升工作效率,减少人为错误,确保结算数据的绝对准确。

03 需求分析

在推进公司结算业务时,我们致力于实现统一化、标准化,规范业务流程,并确保数据合规化治理,我们面临着诸多问题与挑战,具体表现如下:

1. 流程与规范缺失

  • 结算流程管理混乱:存在结算需求未备案即已上线的情况,或者备案内容与实际实现不一致,甚至缺乏备案流程。

  • 日志规范陈旧:广告分润场景中,内容日志打点冗余,同时缺少扩展性,导致对新的业务场景无法很好兼容。

2. 烟囱式开发成本高

  • 标准化与统一化需求迫切:之前,各个结算业务维护各自的结算系统,涉及不同的技术栈和结算模型,线下、线上结算方式并存,导致人工处理环节多,易出错,case多,管理难度大。为提高效率,需实现结算业务的标准化与统一化,并拓展支持多种业务结算模式。

  • 分润模型通用化设计:多数业务结算方式相同,同时账单计算逻辑也相似或者相同,没有必要每个业务设计一套逻辑,需要做通用化设计。

3. 业务迭代中的新诉求

  • 测算系统需求凸显:在业务快速迭代的过程中,许多业务希望尽快看到结算效果,以推进项目落地。因此,构建高效的测算系统成为迫切需求,以加速业务迭代和决策过程。

  • 提升作者体验:为提升作者等合作伙伴的满意度和忠诚度,结算数据需实现高时效披露,确保他们能及时、准确地获取收益信息。结算账单数据的产出依赖百余条数据源,要保证数据在每天12点前产出,困难重重

  • 数据校验与监控机制:结算数据的准确性和质量直接关系到公司的财务健康和业务发展。因此,需建立完善的数据校验和监控机制,确保结算数据的准确无误和高质量。

04 技术实现

根据结算中台建设的核心目标,结合业务痛点,在结算系统建设中,基于通用化、标准化的理念,从以下五个方面来搭建统一的、规范化的结算中台。

  • 业务流程标准化:建设一套标准来定义三类结算模式下每个数据处理环节的实现方式,包括业务处理流程、数据处理过程。

  • 分润模型通用化:实现不同的账单计算算法,支持各个业务的各类作者收入分配诉求,并且实现参数配置线上化。

  • 技术架构统一:统一整个结算业务的技术栈、部署环境、功能入口和数据出口。

  • 建设账单测算能力:模拟线上结算流程的账单测算能力,支持业务快速验证分润模型参数调整带来的作者收入影响效果。

  • 建设质量保证体系:建设全流程预警机制,通过日志质检、自动对账、数据异常检测来保障账单产出数据时效性、准确性。

1. 业务流程标准化

不同业务场景,采用了通用化、标准化的设计来满足业务的特异性需求,下面是三大结算模式业务流程简图:

图片

在广告模式、补贴模式、订单模式结算流程设计中, 从日志打点、线上化、计算逻辑等方向考虑了通用化、标准化设计, 具体如下:

(1) 日志打点统一化

统一日志标准, 针对业务日志规范陈旧问题,要求所有接入的业务方严格按照统一格式打点日志,删除冗余字段, 确保数据的规范性与一致性,同时保证设计能够覆盖所有业务场景,为后续处理奠定坚实基础。

针对某些业务定制化的需求, 在广告模式、补贴模式、订单模式三种结算方式中,在设计日志打点规范时, 会预留一些扩展字段, 使用时以 JSON 形式表示, 不使用时打默认值。

(2) 账单计算线上化

在补贴结算模式中,之前不同业务都有各自的账单格式设计,同时存在离线人工计算账单的非规范化场景,账单无法统一在线计算、存储、监管。新的结算中台的补贴结算模式,将所有离线结算模式,使用统一的账单格式,全部实现线上化结算,实现了业务结算流程规范化。

(3) 账单计算逻辑优化

比如在广告模式中,百家号业务的公域视频、图文、动态场景中,由于收入口径调整,迭代效率要求,不再需要进行广告拼接,所以专门对账单计算流程做了优化调整。不仅满足业务诉求,同时做了通用化设计考虑,保证后续其他业务也可以使用这套流程的同时, 也能兼容旧的业务流程。

广告模式结算流程优化前:

图片

广告模式结算流程优化后:

图片

2. 分润模型通用化

不同业务场景,不同结算对象,有不同的结算诉求,不仅要满足业务形态多样化要求,还要具有灵活性。因此抽取业务共性做通用性设计,同时通过可插拔式设计灵活满足个性化需求。

图片

(1) 基于流量变化模型

以合作站点的优质用户投流方为代表的用户,他们在为百度提供海量数据获得收益的同时,也有自己的诉求,那就是自己内容的收益不能受到其他用户内容的影响。自己优质内容不能被其他用户冲淡,当然自己的低质内容也不会去拉低别人的收益水平。

对于此部分用户我们提供“基于流量变现的分润”策略,简单来说就是,某一篇内容的收益仅仅由它自己内容页面挂载的广告消费折算而来,这样就保证了优质用户投流方收益的相对独立,也促使优质用户产出更加多的内容。

(2) 基于内容分发模型

  • 部分作者只关注收益回报: 对百家号的某些作者来说,他们的目的很单纯,他们只关注产出的内容是否获得具有竞争力的收益回报,至于收益怎么来他们并不关心。

  • “基于流量变现”策略不妥此时,我们再使用“基于流量变现”的策略的话,就有些不妥,举个极端里的例子,有一个作者比较倒霉,每次分发都没有广告的渲染,那他是不是颗粒无收?这对作者是很不友好的。

  • “基于内容分发的分润”模型: 基于收益平衡性考虑,我们推出了更加适合百家号用户的“基于内容分发的分润”模型。在这种模型下,只要内容有分发,就一定有收益,而不管本身是否有广告消费。

  • 策略平衡考虑: 当然,为了防止海量产出低质内容来刷取利润,在分润模型上,我们同时将内容质量分和运营因子作为分润计算的权重,也就是说作者最终的收益由内容的质量和内容的分发量共同决定,以达到通过调整分润来指导内容产出的目的。

(3) 基于作者标签模型

为了实现对百家号头部优质作者进行激励,促进内容生态良性发展, 会对不同的作者进行打标, 并且使用不同的分润模型, 比如对公域的百家号作者进行打标, 优质作者, 通过动态单价及内容质量权重策略来给到他们更加的分成, 其他的普通作者, 通过内容分发模型来分润。这样不仅保证了优质作者取得高收益,也保证了其他作者也有一定的收益

另外,出于对预算的精确控制,发挥每一笔预算的钱效,优质的作者会占用较大的预算资金池,而普通作者使用占用较少的预算资金池。同时也会对每类资金池进行上下限控制,保证预算不会花超。

(4) 基于运营场景模型

为了实现对百家号作者的精细化运营,比如对一些参与各类短期活动的作者给予一定的阶段性的奖励,可以通过补贴模型来实现。在一些运营活动中,需要控制部分作者的分成上限,分润模型会进行多轮分成计算,如果作者的收益未触顶并且资金池还有余额的情况下,会对余额进行二次分配,给作者再分配一些收益。此类模型主要是为了实现灵活多变的作者分润策略。

3. 技术架构统一

根据业务流程标准化、分润模型通用化的设计原则,建设统一的结算中台。以下是结算中台统一结算业务前后的对比:

图片

图片

4. 建设账单测算能力

为各个需要测算能力的业务,设计了一套通用的测算流程,如下图:

图片

针对每个测算业务,设计了独立的测算参数管理后台,用于管理业务相关的分润模型参数,如下图:

图片

测算流程设计

(1) 功能简述: 每个测算业务, 产品需要登录模型参数管理后台,此后台支持对分润模型参数进行创建、查看、编辑、测算、复制、上线、删除,以及查看测算结果等操作, 出于业务流程合规化的要求, 每次模型参数上线前, 需要对变更的参数完成线上备案流程才可以上线,实现分润流程合规线上化。

(2) 流程简述

  • 流程简述概览: 每次测算时, 产品需要先创建一个版本的账单模型测算参数,并发起参数测算,参数状态变成待测算 。

  • 离线任务与收益计算: 此后,离线任务会轮询所有的待测算参数,提交Spark任务,调用账单计算模型来计算作者收益,最后生成TDA报告。

  • 查看与评估测算报告: 产品在管理平台看到任务状态变成测算完成时, 可以点击 TDA 链接来查看测算报告, 评估是否符合预期。

  • 根据预期结果的操作:如果不符合预期,可以编辑参数再次发起测算;如果符合预期,则可以发起备案流程,流程走完后可以提交上线。

(3) 收益明显: 通过账单测算建设, 不仅解决结算需求未备案即已上线或者备案内容与实际实现不一致,甚至缺乏备案流程的业务痛点问题,  而且把业务线下账单计算的流程做到了线上, 做到留痕可追踪。同时也满足了业务高效迭代的诉求, 一次账单测算耗时从半天下降到分钟级, 大大降低了账单测算的人力成本与时间成本。

5. 建设质量保障体系

为了保证业务质量,从以下几方面来建设:

(1) 建设数据预警机制:为保证作者账单数据及时披露, 分润业务涉及的 百余条数据源都签订了 SLA, 每份数据都关联到具体的接口人, 通过如流机器人来监控每个环节的数据到位时间, 并及时发出报警信息, 并推送给具体的接口负责人。对于产出延迟频次高的数据流,会定期拉相关负责人相关复盘,不断优化数据产出时效,保证账单数据在每天如期产出

(2) 数据异常检测机制:对账单数据进行异常波动性检测, 确保数据准确性 ,及时发现并处理潜在异常问题

(3) 自动对账机制:每天自动进行上下游系统间账单明细核对,保证出账数据流转的准确无误。

(4) 日志质检机制:每日例行对日志进行全面质检分析, 及时发现日志打点日志。

05 中台收益

结算中台作为公司财务体系的核心,承担多业务线分润计算与资金流转重任。

(1) 通过通用化、标准化设计,高效支撑数十个业务线的精准结算,确保广告、订单、内容分发的业务结算稳定、健康。近一年,结算业务零事故、零损失。

(2) 中台支持多种结算模式与灵活周期管理,实现账单测算自动化,账单测算时间从天级降到小时级。提升效率并减少错误,提升业务需求迭代效率。

(3) 通过业务流程标准化、分润模型通用化、账单测算能力建设及质量保证体系,解决了结算业务规范缺失、业务形态多样等问题。累计解决历史结算case数十个,涉及结算金额达千万级。

未来,结算中台将推进全业务线结算立项线上化、周结与测算能力落地、项目全生命周期管理,并依托大模型能力实现数据智能分析,进一步提升数据分析智能化水平,为公司业务稳健发展提供坚实保障。

06 未来规划

1、推进全业务线结算实现立项线上化;

2、推进周结 、测算能力在各业务线上落地;

3、推进项目全生命周期管理,实现项目从上线到下线整体生命周期变化线上化存档,可随时回顾复盘。

4、数据智能分析,依托公司大模型能力,实现通过多轮对话问答来进行数据分析,针对业务问题进行答疑解惑,提升数据分析的智能化水平。

昨天以前首页

年终总结 - 2025 故事集

作者 Jimmy
2025年12月22日 22:51

📕 如果您刚好点了进来,却不想完整阅读该文章但又想知道它记录了什么。可跳到文末总结

前言

时隔四个月,再执笔即将进入了新的一年 2026 年...

2025 & 2026

时间像往常一样无声息地流动,已近年尾,在过去的 2025 年,三百多天时间里面,发生了很多的事情,或喜,或悲,或静,或闹...此时,灯亮着,窗外偶尔有远处汽车的沙沙声。我在其中,开始回顾并记录撞进心底的瞬间和感受。

你好,世界

还是熟悉的四月份的一天凌晨,老妈跟我在走廊里踱步~

随着清脆的哭声响起,二宝如期而至。过了段时间,护士出来报出母女平安是我们听到的此刻最让人心安的话语。

为什么说是熟悉的四月份,因为老大也是四月份出生的

因为老婆在工作日凌晨分娩,所以我的休陪产的单也先提交了。在收到老婆产后无需我协助事情的话语后,我撤销了陪产单,屁颠屁颠地去上班赚奶粉钱了😄

嗯,从准奶爸到首次喜当爹至今,短短三年时间里面,自己已经是两个小孩的爸爸,真是一个让自己意想不到的速度。

自从当了父母之后,我们更加懂得自己父母的无私且伟大,孩子的天真和无知

相对于第一次喜当爹时候,自己慌张无措,老妈辛苦地忙前忙后,手慌脚乱。有了第一次的经验,我们对于二宝的处理还是挺稳定:

  • 在预产期临近的两三天,我们准备好了大包小包的待产包 -> alway stand by
  • 产后的三天时间,请护工照看老婆和新生儿,老妈在旁边陪同,老爸在家照看大宝
  • 出院后,老婆和二宝直接月子中心坐月子。老妈和我在家照看大宝,周末月子中心看二宝

daughters in nursing room

👆即将出月子中心,大宝和二宝的合影👆

在日常里接力的我们

每天,我们都觉得时间不够用,能留出些许空间和时间来放松,已经很满足😌

老婆来回奔波的工作日

在休完三个多月的产假之后,老婆就去复工了。因为二宝还小,老婆会每天中午都回来哺乳。从小孩三个多月到七个多月,雷打不动,公司和家两头跑。

那一台小电驴,隔三差五就需要去充电。小小电驴,已经超出了它的价值~

好不容易,让二宝断奶了。断奶是件很痛苦的事情,要熬夜,涨奶胸痛等。我还记得在成功断奶后的那天晚上,老婆还特意叫我出去买瓶酒回来庆祝一下✨

beer

👆5%-8% vol 的鸡尾酒👆

虽然二宝断奶了,但是老婆在工作不忙的时候,还是会中午回来看看。用我老婆的话说:有点讨厌,但是又有点舍不得二宝

工作日,爷爷奶奶的时光

老婆跟我,工作日都需要上班,嗯~赚奶粉钱😀

然后,两个宝宝,工作日的时候主要给爷爷和奶奶带。

有时候,两个宝宝都需要奶奶抱,这可苦了奶奶的腰板子了。爷爷更多的时候,是充当了厨师的角色,保证一家人的三餐伙食,嗯~老爸的厨艺真好👍

爷爷奶奶一天下来的流程:早上带娃出去晒太阳,遛娃(主要是让大宝动起来,中午好睡觉);中午喂饭,午休(大宝一般中午休息两个钟,下午三或四点起来);下午洗澡(怕冷着小孩,一般天黑前洗完),喂饭,陪玩;晚上,等老婆和我下班回来,爷爷和奶奶才有空闲的时间。一般这个时候,爷爷就喜欢下楼去周边逛,奶奶就会躺着床上直一下腰板子(有时会跟爷爷下楼逛街)。工作日的时候,如果奶奶晚上没有出去逛街,那么,会在九点多喂完奶给大宝,奶奶会哄大宝睡觉;如果奶奶外出,那么我就会哄大宝睡觉。

mother's birthday

👆奶奶生日的时候,两宝和爷爷奶奶合影👆

休息日,我们的时光

工作日,班上完了;休息日,该带娃了。爷爷奶奶休息日放假,想去哪里就去哪里,放松放松。

休息日带娃,我们的宗旨就是:尽量让娃多动。所以,我们基本都会外出。忙忙碌碌,嗯,我们分享两件事情:

我还记得,某个周末,我们在商场逛了一天,让大宝在商场里面走,她逛得贼开心(这可不,逛得有多累,睡得有多香),推着二宝。中午直接在商场里面解决吃饭的问题,大宝直接在婴儿车上解决了午睡的事情,二宝则是被老婆或者我背在身上睡觉。母婴室没人的时候,我们就会在里面小憩一会。等两宝醒来之后,再逛一下,一天的时间过得慢但是又很快

今年的国庆连着中秋,我们在这个长假期里面,会带他们在小区里面露营(在草坪上铺一垫子),让她们自己玩。大宝走路的年纪,这里走那里走,我得屁颠屁颠跟她后面,从这里把她抱过来那里,从那边把她哄过来这边,真想拿条链子绑着她。相反,二宝就淡定多了,只能在那块布那里爬来爬去,被她妈妈限制着。

Mid-Autumn Festival

👆中秋节当晚,在哄两娃睡着后,老婆跟我在阳台拜月👆

没有惊喜的工位

相对于上一年工作的惊吓,今年的工作可以用没有惊喜来形容。

至于为什么说上一年是惊吓,今年没有惊喜。后面有时间,会出一篇文章来分享下。

简简单单的工位,一水杯,一记事本,一台式电脑,一绿植。屁股一坐,一整天嗖一下就过去了~

在公司,让我活跃起来的,就是中午吃饭的时候。我们的小团体(一安卓,一产品和我)开车去周边吃饭。这段时间,是我们唠嗑的时间,无拘无束,即使我们偶尔会浪费掉午休的时间,但是我还是觉得挺不错的,时间花得值...

工作上糟心的事十根手指可数不过来,触动且温暖了心窝的事情屈指可数。

记得招进来的一个新人,我带了他几天,最后入职短短几天被某人恶心而离职了。他离职的前一天,点了一杯奶茶给我,虽然自己嘴里面说着别客气,但是心里面暖暖的。他才进来短短几天就走人了,自己心里莫名生气:为什么我自己招的人,自己带着熟悉项目后,一转手就被恶心到要离职了???最终他却还温柔地以自我问题作离职的原因。

colleague communication

👆点了份奶茶放我桌面后的对话👆

把明天轻轻放进心里

2026 年悄然将至。在对新的一年有所展望之前,我们先回顾下年终总结 - 2024 故事集中立下的两个 Flags 和完成的情况:

序号 目标 实际 完成率
1 分享文章 20+ 分享文章 18 90%
2 锻炼 30+ 锻炼 32 107%

嗯~ 目标完成率还不赖。

do execise

👆每次锻炼我都会在朋友圈记录,每次耗时 45 分钟左右👆

对于分享文章,一开始就是秉承着记录自己在工作中遇到的一些问题,方便自己和其他人查找的宗旨来进行记录,后面是因为平台搞了奖励而进行的一些创作。而现在,随着 chatgpt, deepseek 等大语言模型的机器人横空出世,浅显的分享和问题的记录都显得鸡肋。所以,在 2026 新的一年内,文章的分享要更加有目的性和实际的意义。2026 年,谁知道会有几篇文章会出炉,也许一篇,也许十篇,也许二十篇,也许零篇。

对于锻炼,这是我长期需要坚持的一件事情,也是最好量化的事情。在新的一年里面,锻炼的次数需 35+

为人父母,为人儿女。我们都有自己的那份责任,2026 年,希望自己更多的时间是回归家庭 - 去听听孩子的欢声笑语,去看看爸妈脸上的笑容,去体验大家聚在一起热热闹闹的氛围 and more

family gathering

👆老爸生日,大姐,二姐大家的娃聚在一起👆

总结

2025 年,简简单单却忙忙碌碌👇:

在生活方面,欢迎二宝加入大家庭。这让我们接下来的一年时间里面,时间安排更加充实紧凑,更感受到当爹妈的不容易,感恩自己的父母在以前那年代含辛茹苦带大了我们三姐弟。在工作方面,没有太多想记录的东西,平平淡淡地打卡上下班。

展望 2026,还是给自己制定了锻炼次数的量化目标。在这个人工智能逐渐成熟的环境下,希望自己能够使用它提升工作效率和帮助自己成长。在 2026 年,自己的重心会放在家庭这边,去感受孩子的成长和家的氛围。

完成于中国广东省广州市

2025 年 12 月 22 日

跨域问题详解

2025年12月22日 16:39

引言:在一个前/后端分离的项目开发中,常常会出现前端向后端发送一个请求时,浏览器报错:Access to XMLHttpRequest at 'http://localhost:8080/' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.,也就是通常说的“跨域访问”的问题,由此导致前端代码不能读取到后端数据。

摘要:所谓“跨域问题”,本质上是浏览器在同源策略约束下,主动阻止 JavaScript 读取跨源请求响应的一种安全保护行为。解决跨域问题主要通过服务器端设置CORS(跨域资源共享)机制——浏览器放行跨域请求响应的数据;或者Nginx/网关的代理功能——跨域的请求实际由网关代发,浏览器端依旧是同源请求。

什么是跨域访问

跨域访问指的是:当前网页所在的“源(Origin)”去访问另一个“不同源”的资源,而该访问被浏览器安全策略所限制或拦截的情况。

在浏览器中一个“源”由三部分组成:协议(Protocol) + 域名(Host) + 端口(Port),只要有一个部分不一样就是跨源,也即跨域。例如:

URL 协议 域名 端口 是否同源
http://example.com http example.com 80 基准
http://example.com:8080 http example.com 8080 跨域(端口不同)
https://example.com https example.com 443 跨域(协议不同)
http://api.example.com http api.example.com 80 跨域(域名不同)

这里需要强调:对“跨域访问”进行限制是浏览器的安全策略导致的,并不是前端或后端技术框架引起的

为什么跨域访问请求“得不到”数据

这里就要展开说明为什么浏览器要对“跨域访问”进行限制,导致(尤其是)Web前端中发送HTTP请求会得不到数据,并在控制台报错。

出于安全性,浏览器会采用同源策略(Same-Origin Policy,SOP)限制脚本内发起的跨源 HTTP 请求,限制一个源的文档或者它加载的脚本如何与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。例如,它可以防止互联网上的恶意网站在浏览器中运行 JavaScript 脚本,从第三方网络邮件服务(用户已登录)或公司内网(因没有公共 IP 地址而受到保护,不会被攻击者直接访问)读取数据,并将这些数据转发给攻击者。

假设在没有同源限制的情况下:

  • 用户已登录银行网站 https://bank.com(Cookie 已保存)
  • 用户同时打开一个恶意网站 https://evil.com
  • evil.com 的 JavaScript 可以:
    • 直接读取 bank.com 的接口返回数据
    • 发起转账请求
    • 窃取用户隐私信息

这是非常严重的安全灾难。

同源策略将跨源之间的访问(交互)通常分为3种:

  • 跨源写操作(Cross-origin writes)一般是被允许的。例如链接、重定向以及表单提交。特定少数的 HTTP 请求需要添加预检请求
  • 跨源资源嵌入(Cross-origin embedding)一般是被允许的,比如<img src="..."><script src="..."><link href="...">
  • 跨源读操作(Cross-origin reads)一般是不被允许的。

再次强调:跨域限制是“浏览器行为”,不是后端服务器的限制。后端服务本身是可以接收来自任何来源的 HTTP 请求的。

比如前端访问fetch("https://api.example.com/data"),而当前页面来自http://localhost:8080,请求可以发出去,但浏览器会拦截响应,不让 JavaScript 读取。

要使不同源可以访问(交互),可以使用 CORS来允许跨源访问。CORSHTTP的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。

怎么解决跨域访问的“问题”

CORS机制

跨源资源共享(Cross-Origin Resource Sharing,CORS,或通俗地译为跨域资源共享)是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己(服务器)的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头(Header)。

对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是GET以外的 HTTP 请求,或者搭配某些MIME类型(多用途互联网邮件扩展,是一种标准,用来表示文档、文件或一组数据的性质和格式)的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如Cookie和HTTP 认证相关数据)。

一般浏览器要检查的响应头有:

  • Access-Control-Allow-Origin:指示响应的资源是否可以被给定的来源共享。
  • Access-Control-Allow-Methods:指定对预检请求的响应中,哪些 HTTP 方法允许访问请求的资源。
  • Access-Control-Allow-Headers:用在对预检请求的响应中,指示实际的请求中可以使用哪些 HTTP 标头。
  • Access-Control-Allow-Credentials:指示当请求的凭据标记为 true 时,是否可以暴露对该请求的响应给脚本。
  • Access-Control-Max-Age:指示预检请求的结果能被缓存多久。

如:

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

可知,若使用CORS解决跨域访问中的问题要在服务器端(通常是后端)进行设置。以Spring Boot的后端为例:

  • 局部的请求:在对应的Controller类或指定方法上使用@CrossOrigin。如下

    @CrossOrigin(
        origins = "http://localhost:3000",
        allowCredentials = "true"
    )
    
  • 全局使用:新建一个配置类并注入Spring框架中。如下:

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/api/**")
                    .allowedOrigins(
                        "http://test.example.com"
                    )
                    .allowedMethods("GET","POST","PUT","DELETE")
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .maxAge(3600);
        }
    }
    

使用CORS 的优点:官方标准;安全、可控;与前后端分离完美匹配。缺点:需要服务端正确配置;初学者容易被预检请求困扰。

通过架构或代理手段

除了使用CORS的方式,还可以通过架构设计或代理的方式让跨域“变成”同源访问

比如通过Nginx / 网关代理浏览器(前端)请求,再由Nginx或网关访问服务器获取数据。

浏览器 → 前端域名 → Nginx → 后端服务

这样的话在浏览器(前端)看到将始终是对当前网站(前端域名)的访问(即使打开开发者工具的网络选项,请求的url地址也是前端域名)。

一个Nginx的配置示例:

server {
    listen 443;
    server_name www.example.com;

    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://backend:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

前端请求示例:axios.get('/api/user')

这是通过Nginx或网关这样的中间件实现的,如果在开发阶段想要快速解决跨域访问问题,可以在相应的项目构建的配置中设置代理。这里以Vite为构建工具的Vue项目为例,在vite.config.js中添加如下的配置项:

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
}

然后请求的URL采用这样的方式axios.get('/api/user'),不在使用axios.get('http://localhost:8080/api/user')

使用代理方式的优点:无跨域;性能好;适合生产环境。缺点:需要额外部署配置。

总结

跨域问题并不是请求被禁止,而是浏览器在同源策略约束下,出于安全考虑,限制前端 JavaScript 对跨源响应数据的访问行为。

跨域问题的根源是 浏览器实现的同源策略(Same-Origin Policy),而不是:

  • HTTP 协议限制
  • 后端服务器限制
  • 前端框架(Vue / React)的问题

浏览器阻止的是JS 获取结果,而不是“阻止请求发送”——跨域请求可以被发出,服务器可以正常返回(比如预检请求响应),浏览器阻止JavaScript访问响应数据。

“跨域问题”只存在于浏览器环境,例如:

  • Java / Node / Python 发 HTTP 请求——没有跨域问题
  • Postman / curl ——没有跨域问题
  • 微服务之间调用——没有跨域问题

因为这些环境不执行浏览器的同源策略跨域问题是浏览器安全模型的一部分,本质上是对跨源资源访问的“读权限控制”,而非通信能力限制。

使用CORS 并不是“绕过”同源策略——浏览器的同源策略始终存在;CORS 是 同源策略的“例外机制”;本质是:服务器显式授权浏览器放行。换句话说:没有 CORS,就没有“合法的跨域读取”

只要不产生跨域,就不会有跨域问题,所以可以使用代理或网关将请求进行转发,而不是由浏览器直接请求服务器端发生跨域问题。

你每天都在用的 JSON.stringify ,V8 给它开了“加速通道”

2025年12月22日 15:49

V8 如何把 JSON.stringify 性能提升 2 倍

JSON.stringify 应该是 JavaScript 里用得最多的函数之一了。

API 响应要序列化,日志要格式化,数据要存 localStorage,调试要打印对象……几乎每个项目都离不开它。

但说实话,用的时候很少会想"这玩意儿快不快"。反正就是调一下,能用就行。

V8 团队显然不这么想。V8 是 Chrome 和 Node.js 背后的 JavaScript 引擎,你写的每一次 JSON.stringify,最后都要靠它来跑。2025 年 8 月,他们发了篇博客,讲了怎么把 JSON.stringify 的性能提升到原来的 2 倍以上。

这篇文章拆解一下他们做了什么。

读者导航:不懂 V8 也能看

先记住三句话就够了:

  1. 绝大多数优化都在“走捷径”:先判断输入是不是“简单、可预测”的对象,是的话走更快的路径。
  2. 很多名词听着硬,其实都在做同一件事:减少检查、减少函数调用、让 CPU 一次干更多活、减少内存搬运。
  3. 你能做的配合也很简单:少用会触发副作用的写法(getter、toJSON、格式化参数),保持数据对象“干净”。

下面遇到生词可以先跳过,看完“对开发者的启示”再回头补。

优化的前提:无副作用检测

JSON.stringify 慢在哪?

一个重要原因是它要处理各种边界情况:对象可能有 toJSON 方法,属性可能是 getter,可能有循环引用……这些都可能产生副作用,导致序列化结果不可预测。

V8 的第一步优化是:检测对象是否"干净"

如果能确定序列化过程不会触发任何副作用,就可以走一条快速路径,跳过大量的安全检查。

"if we can guarantee that serializing an object will not trigger any side effects, we can use a much faster, specialized implementation."

这条快速路径用迭代替代了递归,好处有两个:

  1. 不用担心栈溢出(深层嵌套对象)
  2. 减少函数调用开销

字符串处理:双版本编译

JavaScript 字符串有两种内部表示:单字节(Latin-1)和双字节(UTF-16)。

以前 V8 用统一的方式处理,现在编译了两个特化版本的序列化器,分别针对这两种编码优化。可以简单理解成:如果字符串全是英文数字,就走“单字节快车道”;如果包含中文表情等,就走“UTF-16 车道”。

遇到混合编码的情况(比如一个对象里既有纯 ASCII 字符串,又有中文),会在执行时动态切换。

这种"按需特化"的思路在编译器优化里很常见,但用在 JSON 序列化上还是挺有意思的。

SIMD 加速字符扫描

序列化字符串时,需要扫描哪些字符需要转义(比如 \n\t")。

V8 用了两种硬件加速策略:

  • SIMD 指令:对于较长的字符串,一次处理多个字符(你可以理解成“把 16 个字节打包一起扫一遍”)
  • SWAR 技术:对于较短的字符串,用位运算在普通寄存器上并行处理(SIMD 的“轻量版”)

SWAR(SIMD Within A Register)是个挺老的技术,思路是把一个 64 位寄存器当成 8 个 8 位的"小寄存器"用,通过位运算实现并行。

举个例子,判断一个字节是否需要转义,可以这样:

// 伪代码示意
// 需要转义的字符:< 0x20 或 == 0x22(") 或 == 0x5C(\)
function needsEscape(byte) {
  return byte < 0x20 || byte === 0x22 || byte === 0x5C;
}

用 SWAR 可以一次判断 8 个字节,只要用合适的掩码和位运算。

Hidden Class 标记

V8 内部用 Hidden Class(隐藏类)来优化对象属性访问。你可以把它理解成“对象结构的身份证”:同一类对象(同样的字段、同样的顺序)会复用同一个结构描述。

这次优化加了一个新标记:fast-json-iterable

当一个对象的属性满足特定条件时,就给它打上这个标记。下次序列化同类对象时,直接跳过验证检查。

这就是典型的"用空间换时间"——在对象上多存一个标记,换来后续操作的加速。

数字转字符串:换算法

把数字转成字符串也是序列化的一部分。

V8 以前用 Grisu3 算法,现在换成了 Dragonbox。

你不需要理解算法细节,只要知道:Dragonbox 能更快、更稳定地把浮点数转成最短且精确的十进制表示。这个改动不只影响 JSON 序列化,所有 Number.toString() 都能受益。

内存管理:分段缓冲区

以前序列化大对象时,V8 用一块连续内存作为缓冲区。对象越大,缓冲区就要不断扩容,每次扩容都要重新分配和复制。

新实现用分段缓冲区(segmented buffer),多个小块链起来用,避免了昂贵的重分配。直觉上就是:不强求“一次申请一大块”,而是“够用就多挂一块”。

这个思路和 Linux 内核的 sk_buff 链表类似——不追求内存连续,换来分配效率。

快速路径的适用条件

不是所有 JSON.stringify 调用都能走快速路径。需要满足:

  1. 不传 replacer 和 space 参数

    // 可以走快速路径
    JSON.stringify(obj);
    
    // 不行
    JSON.stringify(obj, null, 2);
    JSON.stringify(obj, ['name', 'age']);
    

    这里的 replacer/space 分别是“过滤/改写字段的回调或白名单”和“为了好看而加的缩进”。它们会让序列化过程更复杂,所以很难走最激进的优化路径。

  2. 纯数据对象

    • 没有 toJSON 方法
    • 没有 getter
    • 没有 Symbol 属性
  3. 字符串键

    • 所有属性名都是字符串(不是数字下标)
  4. 简单字符串值

    • 字符串值本身没有特殊情况

实际项目里,大部分序列化场景都满足这些条件。API 返回的纯 JSON 数据、配置对象、日志数据……基本都能用上。

什么时候能用?

Chrome 138 / V8 13.8 开始可用。

Node.js 的话,需要等对应的 V8 版本合入。目前最新的 Node.js 22 用的是 V8 12.x,还得等一等。

对开发者的启示

虽然优化是 V8 团队做的,但有几点可以参考:

1. 保持对象"干净"

避免在需要序列化的对象上加 getter 或 toJSON 方法。如果必须用,考虑在序列化前转换成纯数据对象。

// 不太好
class User {
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}

// 更好
const user = {
  firstName: 'John',
  lastName: 'Doe',
  fullName: 'John Doe'  // 直接计算好
};

2. 大量序列化时考虑结构一致性

V8 的 Hidden Class 优化依赖对象结构一致。如果你要序列化大量对象,保持它们的属性顺序和类型一致。

// 好
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 }
];

// 不太好(age 类型不一致)
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: '30' },  // 字符串
  { name: 'Charlie', age: null }
];

3. 避免无意义的格式化

开发环境用 JSON.stringify(obj, null, 2) 看着舒服,但这会跳过快速路径。生产环境记得去掉。

// 开发环境
console.log(JSON.stringify(data, null, 2));

// 生产环境
console.log(JSON.stringify(data));

总结

  1. 分析热点:找到可以优化的场景(无副作用对象)
  2. 特化路径:针对常见情况走快速路径,边界情况走通用路径
  3. 硬件加速:用 SIMD 和 SWAR 提升字符处理速度
  4. 利用已有信息:通过 Hidden Class 标记避免重复验证
  5. 改进算法:Dragonbox 替代 Grisu3
  6. 优化内存:分段缓冲区避免重分配

这些技术单拿出来都不新鲜,但组合起来能把一个已经很成熟的 API 性能翻倍,还是挺厉害的。

对我们写代码的人来说,最大的收获可能是:写"干净"的代码不只是为了可读性,有时候还能让引擎更好地优化。

延伸阅读


如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):

全栈项目(适合学习现代技术栈):

  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB

AI Agent 介绍

作者 子洋
2025年12月20日 20:18

前言

这周在组内做了一次关于 Agent 设计模式 的分享,主要介绍和讲解了 ReAct 模式P&A(Plan and Execute)模式。我计划将这次分享拆分为三篇文章,对我在组会中讲解的内容进行更加系统和细致的整理。

在正式进入具体的 Agent 模式实现之前,有一个绕不开的问题需要先回答清楚:

什么是 AI Agent?它解决了什么问题?以及在近几年 AI 技术与应用快速演进的过程中,AI 应用的开发范式经历了哪些关键变化?

这一篇将不直接展开某一种 Agent 模式的实现细节,而是先回到更宏观的视角,从 AI 应用形态与工程范式的演进 入手,梳理 Agent 出现的技术背景与必然性。

需要说明的是,下文对 AI 应用演进阶段的划分,是一种以“应用开发范式”为核心的抽象总结。真实的技术演进在时间上存在明显重叠,但这种阶段化的叙述有助于我们理解:为什么 Agent 会在当下成为主流方向

AI 应用的发展历程

第一阶段:提示词工程

2022 年 11 月,GPT-3.5 发布后,大模型开始从研究领域进入大众视野。对开发者来说,这是第一次可以在实际产品中直接使用通用语言模型。

这一阶段的 AI 应用形态非常简单,大多数产品本质上都是一个对话界面:用户输入问题模型生成回答结束

很快,围绕 Prompt 的工程实践开始出现。由于模型对上下文非常敏感,系统提示词(System Prompt)成为当时最直接、也最有效的控制手段。常见的做法是通过提示词约束模型的角色、输出形式和关注重点,例如:

“你是一个资深的前端开发工程师,请严格以 JSON 格式输出结果……”

这类“身份面具”式的提示,本质上是通过上下文约束来减少模型输出的发散性,让结果更贴近预期。在这一阶段,也陆续出现了 Chain-of-Thought、Few-shot Prompting 等推理增强技巧,但它们依然属于单次生成模式:模型在一次调用中完成全部推理,过程中无法获得外部反馈,也无法根据中间结果调整策略。

第二阶段:RAG

当 AI 开始被用于真实业务场景时,很快暴露出两个问题:模型不了解私有知识,以及生成结果难以校验。以 GPT-3.5 为例,它的训练数据截止在 21 年左右,对于新技术以及企业内部文档、业务规则更是不了解,直接使用往往不可控。

RAG(Retrieval-Augmented Generation)是在这种背景下被广泛采用的方案。它的核心做法是:

  • 将私有知识进行切分和向量化存储;
  • 用户提问时,先进行相似度检索;
  • 将命中的内容作为上下文提供给模型,再由模型完成生成。

通过这种方式,模型不需要记住所有知识,而是在生成时按需获取参考信息。

RAG 的价值不仅在于补充新知识,更重要的是带来了可控性和可追溯性:生成内容可以明确对应到原始文档,这一点在企业场景中尤为关键。

第三阶段:Tool Calling

如果说 RAG 让模型能够“查资料”,那么 Function / Tool Calling 则让模型开始能够“做事情”。

在这一阶段,开发者会把可用能力(如查询数据库、调用接口、执行脚本)以结构化的方式提供给模型,包括函数名、参数说明和功能描述。模型在理解用户意图后,可以返回一个明确的工具调用请求,再由程序完成实际执行。

这一能力的出现,标志着 AI 第一次在工程上具备了可靠调用外部系统的能力。它不再只是一个聊天机器人,而是一个可以触发真实世界动作的“控制器”,这也是后续 Agent 能够落地的关键技术支撑。

第四阶段:AI Workflow

当 RAG 能力和 Tool Calling 能力逐渐成熟后,开发者开始尝试把多个步骤组合起来,形成完整的业务流程。这催生了以 Dify、Coze 为代表的 AI Workflow 范式。

在 Workflow 模式下,一个 AI 应用会被拆解为多个固定节点,并按照预设顺序执行,例如:检索 → 判断 → 工具调用 → 汇总输出。

Workflow 的优势非常明显:

  • 流程清晰,行为可预期;
  • 易于测试和运营;
  • 对非工程人员友好。

但问题也同样明显:流程完全由人设计,模型只是执行者。无论问题复杂与否,都必须走完整条路径。这种方式在应对高度动态或非标准任务时,灵活性有限。

第五阶段:Agent

在 Agent 出现之前,大多数 AI 应用仍然遵循一种典型模式:输入单次/编排好的推理输出

而 Agent 的出现,本质上是将“任务编排”的控制权从人类手中交还给了 AI。在 Agent 架构下,AI 不再是被动执行一段代码,而是一个具备以下核心能力的闭环系统:

  • 将复杂目标拆解为多个可执行步骤;
  • 根据工具执行结果调整后续行动;
  • 在失败时尝试修正策略;
  • 在多步过程中维护上下文状态。

这些能力并不是一次模型调用完成的,而是通过多轮推理与执行形成闭环。也正是在这一点上,Agent 与前面的应用形态拉开了差距。

Agent 设计模式解决的问题

当 Agent 开始承担更复杂的任务时,问题也随之出现:

  • 多步推理容易跑偏;
  • 执行失败后缺乏统一的修正策略;
  • 成本和稳定性难以控制。

Agent 设计模式的作用,就是把这些反复出现的问题抽象成可复用的结构。

无论是 ReAct,还是 Plan and Execute,它们关注的核心并不是“让模型更聪明”,而是:如何在工程上组织模型的推理、行动和反馈过程,使系统整体可控、可维护。

理解这些模式,有助于我们在构建 Agent 系统时少走弯路,而不是每一次都从零开始设计整套交互与控制逻辑。

结语

从最初基于 Prompt 的简单对话,到如今具备一定自主能力的 Agent,我们看到的不只是模型能力的提升,更是 AI 在实际使用方式上的变化。

回顾整个过程会发现,很多关键技术并不是最近才出现的。RAG 的核心思路早在几年前就已经被提出,ReAct 也并非新概念,只是在最近随着模型推理能力提升、工具链逐渐成熟,才真正具备了工程落地的条件。很多时候,并不是想法不存在,而是时机还没到。

理解这些演进背景,有助于我们判断哪些能力是短期噱头,哪些是长期方向。下一篇文章将聚焦 Agent 设计模式中最常见、也最实用的 ReAct 模式,结合实际实现,看看它是如何让 AI 在执行任务的过程中逐步思考、不断调整策略的。

参考资料

为什么协程能让程序不再卡顿?——从同步、异步到 C++ 实战

作者 charlee44
2025年12月19日 09:09
1 引言 在图形界面(GUI)应用中,“卡顿”几乎是所有开发者都会遇到的老问题。一次复杂的计算、一次网络请求、一次磁盘读取,甚至一次大循环,都可能让界面在几百毫秒内完全失去响应,用户看到的就是——窗口

【用户行为监控】别只做工具人了!手把手带你写一个前端埋点统计 SDK | 掘金一周 12.18

作者 掘金一周
2025年12月18日 17:46

本文字数1100+ ,阅读时间大约需要 4分钟。

【掘金一周】本期亮点:

「上榜规则」:文章发布时间在本期「掘金一周」发布时间的前一周内;且符合各个栏目的内容定位和要求。 如发现文章有抄袭、洗稿等违反社区规则的行为,将取消当期及后续上榜资格。

一周“金”选

掘金一周 文章头图 1303x734.jpg

内容评审们会在过去的一周内对社区深度技术好文进行挖掘和筛选,优质的技术文章有机会出现在下方榜单中,排名不分先后。

前端

【AI 编程实战】第 2 篇:让 AI 成为你的前端架构师 - UniApp + Vue3 项目初始化 @HashTang

最终生成的代码不仅逻辑清晰,还处理了很多细节,比如 iOS 的样式适配。这就是 AI 辅助开发的威力:它不仅能写出跑通的代码,还能考虑到平台差异和边界情况。

【用户行为监控】别只做工具人了!手把手带你写一个前端埋点统计 SDK @不一样的少年_

想知道那些“黑科技”是如何拦截点击、统计 PV(页面浏览量)与 UV(独立访客数)、精确计算页面停留时长的吗?本文将从原理角度切入,手把手带你设计并实现一个轻量级、功能完备的用户行为监控 SDK

后端

基于Nacos的轻量任务调度方案 —— 从 XXL-Job 的痛点说起 @踏浪无痕

JobFlow 只是一个想法,一个技术探讨。它的核心不是技术细节,而是一个设计理念:中间件即业务。在云原生时代,调度能力不应该是一个独立部署、独立运维的"平台",而应该是内嵌在微服务体系中的能力模块

Android

十分钟速览 Kotlin Flow 操作符 @RockByte

作为基于协程构建的响应式流 API,Kotlin Flow 让你可以用声明式的方式优雅地处理异步数据流。但要想真正发挥它的强大能力,关键在于熟练掌握各种操作符。

人工智能

✨TRAE SOLO + Holopix AI | 复刻 GBA 游戏-"🐛口袋妖怪 @coder_pig

GBA的图形芯片 (PPU) 没有 "加载图片" 的概念,它只能读取连续的显存块。开发者把所有角色动画帧拼成一张图,通过修改 UV坐标 (读取位置) 来切换帧。

5小时整理60页《Google Agent指南》,不懂Agent的包教包会 @大模型教程

现阶段来说,Tools是Agent真正的核心,而且Tools调用不准也是Agent架构最大的难点,当前我们在生产环境使用Skills技术 + 强意图也最多把准确率做到90%左右。

数据库AI方向探索-MCP原理解析&DB方向实战|得物技术 @得物技术

通过整合多模态数据(文本与二进制)资源使 AI 模型能访问私有或专属知识库(如企业内部文档)、实时外部 API 及系统动态信息,有效突破单一大模型数据孤岛。

AutoGLM 开源实测:一句话让 AI 帮我点个鸡排 @飞哥数智谈

Phone Agent 是一个基于 AutoGLM 构建的手机端智能助理框架,它能够以多模态方式理解手机屏幕内容,并通过自动化操作帮助用户完成任务。

IOS

Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题 @恋猫de小郭

新机制让开发者可以在 Dart 代码中直接指定 PlatformView 的手势拦截策略,而不是依赖全局配置或原生代码,根据不同场景配置不同的拦截处理机制,而 touchBlockingOnly 就是全新的支持。

📖 投稿专区

大家可以在评论区推荐认为不错的文章,并附上链接和推荐理由,有机会呈现在下一期。文章创建日期必须在下期掘金一周发布前一周以内;可以推荐自己的文章、也可以推荐他人的文章。

Java 设计模式:原理、框架应用与实战全解析|得物技术

作者 得物技术
2025年12月18日 14:03

一、概述

简介

设计模式(Design Pattern)是前辈们对代码开发经验的总结,它不是语法规定,是解决特定问题的一系列思想,是面向对象设计原则的具象化实现, 是解决 “需求变更” 与 “系统复杂度” 矛盾的标准化方案 —— 并非孤立的 “代码模板”,而是 “高内聚、低耦合” 思想的落地工具。其核心价值在于提升代码的可复用性、可维护性、可读性、稳健性及安全性。

1994 年,GoF(Gang of Four:Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)合著的《Design Patterns - Elements of Reusable Object-Oriented Software》(中文译名《设计模式 - 可复用的面向对象软件元素》)出版,收录 23 种经典设计模式,奠定该领域的行业标准,即 “GoF 设计模式”。

核心思想

  • 对接口编程,而非对实现编程
  • 优先使用对象组合,而非继承
  • 灵活适配需求:简单程序无需过度设计,大型项目 / 框架必须借助模式优化架构

组件生命周期

模式类型 核心关注点 生命周期阶段 代表模式
创建型模式 对象创建机制 (解耦创建与使用) 组件的创建 单例、工厂方法、抽象工厂、原型、建造者
结构型模式 对象 / 类的组合方式 组件的使用 代理、适配器、装饰器、外观、享元、桥接、组合、过滤器
行为型模式 对象 / 类的运行时协作流程 组件的交互与销毁 策略、观察者、责任链、模板方法、命令、状态、中介者、迭代器、访问者、备忘录、解释器

七大设计原则

原则名称 核心定义 关联模式 实际开发决策逻辑
开闭原则(OCP) 对扩展开放,对修改关闭 (新增功能通过扩展类实现,不修改原有代码) 所有模式的终极目标 新增需求优先考虑 “加类”,而非 “改类”
依赖倒转原则(DIP) 依赖抽象而非具体实现 (面向接口编程,不依赖具体类) 工厂、策略、桥接 类的依赖通过接口注入,而非直接 new 具体类
合成复用原则(CRP) 优先使用组合 / 聚合,而非继承 (降低耦合,提升灵活性) 装饰器、组合、桥接 复用功能时,先考虑 “组合”,再考虑 “继承”
单一职责原则(SRP) 一个类仅负责一项核心职责 (避免 “万能类”) 策略、适配器、装饰器 当一个类有多个修改原因时,立即拆分
接口隔离原则(ISP) 使用多个专用接口替代单一万能接口 (降低类与接口的耦合) 适配器、代理 接口方法拆分到 “最小粒度”,避免实现类冗余
里氏代换原则(LSP) 子类可替换父类,且不破坏原有逻辑 (继承复用的核心前提) 模板方法、策略 子类重写父类方法时,不能改变父类契约
迪米特法则(LOD) 实体应尽量少与其他实体直接交互 (通过中间者解耦) 中介者、外观、责任链 两个无直接关联的类,通过第三方间接交互

二、原理与框架应用

创建型模式

为什么用创建型模式?

  • 创建型模式关注点“怎样创建出对象?”“将对象的创建与使用分离”
  • 降低系统的耦合度
  • 使用者无需关注对象的创建细节
  • 对象的创建由相关的工厂来完成;(各种工厂模式)
  • 对象的创建由一个建造者来完成;(建造者模式)
  • 对象的创建由原来对象克隆完成;(原型模式)
  • 对象始终在系统中只有一个实例;(单例模式)

创建型模式之单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决: 一个全局使用的类频繁地创建与销毁。

何时使用: 当您想控制实例数目,节省系统资源的时候。

如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如首页页面缓存)。

2、避免对资源的多重占用(比如写文件操作)。

缺点:

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

1、要求生产唯一序列号。

2、多线程中的线程池。

3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

4、系统环境信息(System.getProperties())。

单例模式四种实现方案

饿汉式

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 饿汉式单例(线程安全)
 * 核心原理:依赖类加载机制(JVM保证类初始化时线程安全)
 * 适用场景:实例占用资源小、启动时初始化可接受的场景
 */
public class LibifuTestSingleton {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestSingleton.class);


    // 类加载时直接初始化实例(无延迟加载)
    private static final LibifuTestSingleton INSTANCE = new LibifuTestSingleton();
    // 私有构造器(禁止外部实例化)
    private LibifuTestSingleton() {
        log.info("LibifuTestSingleton 实例初始化完成");
    }
    // 全局访问点(无锁,高效)
    public static LibifuTestSingleton getInstance() {
        return INSTANCE;
    }
    // 业务方法示例
    public void doBusiness() {
        log.info("饿汉式单例(LibifuTestSingleton)执行业务逻辑");
    }
}

懒汉式

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 懒汉式单例(线程安全)
 * 核心原理:第一次调用时初始化,synchronized保证线程安全
 * 适用场景:实例使用频率极低、无性能要求的场景
 */
public class LibifuTestLazySingleton {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestLazySingleton.class);


    // 私有静态实例(初始为null,延迟加载)
    private static LibifuTestLazySingleton instance;
    // 私有构造器(禁止外部实例化)
    private LibifuTestLazySingleton() {
        log.info("LibifuTestLazySingleton 实例初始化完成");
    }
    // 同步方法(保证多线程下唯一实例)
    public static synchronized LibifuTestLazySingleton getInstance() {
        if (instance == null) {
            instance = new LibifuTestLazySingleton();
        }
        return instance;
    }
    // 业务方法示例
    public void doBusiness() {
        log.info("懒汉式单例(LibifuTestLazySingleton)执行业务逻辑");
    }
}

双检锁 (DCL,JDK1.5+)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 双检锁单例(线程安全,高效)
 * 核心原理:volatile禁止指令重排序,双重校验+类锁保证唯一性
 * 适用场景:大多数高并发场景
 */
public class LibifuTestDclSingleton {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestDclSingleton.class);


    // volatile关键字:禁止instance = new LibifuTestDclSingleton()指令重排序
    private volatile static LibifuTestDclSingleton instance;
    // 私有构造器(禁止外部实例化,含防反射攻击)
    private LibifuTestDclSingleton() {
        log.info("LibifuTestDclSingleton 实例初始化完成");
        // 防反射攻击:若实例已存在,直接抛出异常
        if (instance != null) {
            throw new IllegalStateException("单例实例已存在,禁止重复创建");
        }
    }
    // 全局访问点(双重校验+类锁,兼顾线程安全与效率)
    public static LibifuTestDclSingleton getInstance() {
        // 第一次校验:避免频繁加锁(提高效率)
        if (instance == null) {
            // 类锁:保证同一时刻只有一个线程进入实例创建逻辑
            synchronized (LibifuTestDclSingleton.class) {
                // 第二次校验:确保唯一实例(防止多线程并发绕过第一次校验)
                if (instance == null) {
                    instance = new LibifuTestDclSingleton();
                }
            }
        }
        return instance;
    }
    // 防序列化漏洞:反序列化时返回已有实例(而非创建新实例)
    private Object readResolve() {
        return getInstance();
    }
    // 业务方法示例
    public void doBusiness() {
        log.info("双检锁单例(LibifuTestDclSingleton)执行业务逻辑");
    }
}

枚举单例(JDK1.5+)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 枚举单例(天然线程安全、防反射、防序列化)
 * 核心原理:枚举类的实例由JVM管理,天然唯一
 * 适用场景:安全性要求极高的场景(如配置中心、加密工具类)
 */
public enum LibifuTestEnumSingleton {
    INSTANCE;
    private static final Logger log = LoggerFactory.getLogger(LibifuTestEnumSingleton.class);
    // 枚举构造器(默认私有,无需显式声明)
    LibifuTestEnumSingleton() {
        log.info("LibifuTestEnumSingleton 实例初始化完成");
    }
    // 业务方法示例
    public void doBusiness() {
        log.info("枚举单例(LibifuTestEnumSingleton)执行业务逻辑");
    }
}

框架应用

Spring 框架中 Bean 默认作用域为singleton(单例),核心通过AbstractBeanFactory类的缓存机制 + 单例创建逻辑实现 —— 确保每个 Bean 在 Spring 容器中仅存在一个实例,且由容器统一管理创建、缓存与销毁,降低对象频繁创建销毁的资源开销,契合单例模式 “唯一实例 + 全局访问” 的核心思想。

核心逻辑:Bean 创建后存入singletonObjects(单例缓存池),后续获取时优先从缓存读取,未命中则触发创建流程,同时通过同步机制保证多线程安全。

以下选取AbstractBeanFactory中实现单例 Bean 获取的核心代码片段:

// 1. 对外暴露的获取Bean的公共接口,接收Bean名称参数
@Override
public Object getBean(String name) throws BeansException {
    // 2. 委托doGetBean方法实现具体逻辑,参数分别为:Bean名称、所需类型(null表示不指定)、构造参数(null)、是否仅类型检查(false)
    return doGetBean(name, nullnullfalse);
}
// 3. 核心获取Bean的实现方法,泛型T保证类型安全
@SuppressWarnings("unchecked")
protected <T> T doGetBean(
        String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly) throws BeansException {
    // 4. 处理Bean名称:转换别名、去除FactoryBean前缀(如&),得到原始Bean名称
    String beanName = transformedBeanName(name);
    // 5. 从单例缓存中获取Bean实例(核心:优先复用已有实例)
    Object sharedInstance = getSingleton(beanName);
    // 6. 缓存命中(存在单例实例)且无构造参数(无需重新创建)
    if (sharedInstance != null && args == null) {
        // 7. 处理特殊Bean(如FactoryBean):如果是FactoryBean,返回其getObject()创建的实例,而非FactoryBean本身
        T bean = (T) getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
        // 8. 缓存未命中或需创建新实例(非单例、原型等作用域)的逻辑(此处省略,聚焦单例)
    }
    // 9. 返回最终的Bean实例(类型转换后)
    return (T) bean;
}
// 10. 从单例缓存中获取实例的核心方法,allowEarlyReference表示是否允许早期引用(循环依赖场景)
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 11. 从一级缓存(singletonObjects)获取已完全初始化的单例实例(key=Bean名称,value=Bean实例)
    Object singletonObject = this.singletonObjects.get(beanName);


    // 12. 缓存未命中,且当前Bean正在创建中(解决循环依赖)
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 13. 对一级缓存加锁,保证多线程安全(避免并发创建多个实例)
        synchronized (this.singletonObjects) {
            // 14. 从二级缓存(earlySingletonObjects)获取早期暴露的实例(未完全初始化,仅解决循环依赖)
            singletonObject = this.earlySingletonObjects.get(beanName);


            // 15. 二级缓存未命中,且允许早期引用
            if (singletonObject == null && allowEarlyReference) {
                // 16. 从三级缓存(singletonFactories)获取Bean的工厂对象(用于创建早期实例)
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);


                // 17. 工厂对象存在,通过工厂创建早期实例
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    // 18. 将早期实例存入二级缓存,同时移除三级缓存(避免重复创建)
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    // 19. 返回单例实例(可能是完全初始化的,也可能是早期实例)
    return singletonObject;
}

入口: getBean(String name)是获取 Bean 的入口,委托doGetBean实现细节;

名称处理: transformedBeanName统一 Bean 名称格式,避免别名、FactoryBean 前缀导致的识别问题;

缓存优先: 通过getSingleton从三级缓存(singletonObjects→earlySingletonObjects→singletonFactories)获取实例,优先复用已有实例,契合单例模式核心;

线程安全: 对单例缓存加锁,防止多线程并发创建多个实例;

特殊处理: getObjectForBeanInstance区分普通 Bean 和 FactoryBean,确保返回用户预期的实例。

整个流程围绕 “缓存复用 + 安全创建” 实现 Spring 单例 Bean 的管理,是单例模式在框架级的经典落地。

结构型模式

为什么用结构型模式?

  • 结构型模式关注点“怎样组合对象/类”
  • 类结构型模式关心类的组合,由多个类可以组合成一个更大的(继承)
  • 对象结构型模式关心类与对象的组合,通过关联关系在一个类中定义另一个类的实例对象(组合)根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。
  • 适配器模式(Adapter Pattern):两个不兼容接口之间适配的桥梁
  • 桥接模式(Bridge Pattern):相同功能抽象化与实现化解耦,抽象与实现可以独立升级
  • 过滤器模式(Filter、Criteria Pattern):使用不同的标准来过滤一组对象
  • 组合模式(Composite Pattern):相似对象进行组合,形成树形结构
  • 装饰器模式(Decorator Pattern):向一个现有的对象添加新的功能,同时又不改变其结构
  • 外观模式(Facade Pattern):向现有的系统添加一个接口,客户端访问此接口来隐藏系统的复杂性
  • 享元模式(Flyweight Pattern):尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象
  • 代理模式(Proxy Pattern):一个类代表另一个类的功能

结构型模式之外观模式

外观模式(Facade Pattern)为复杂子系统提供统一高层接口,隐藏内部复杂性,简化客户端调用。这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。

意图: 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

主要解决: 降低访问复杂系统的内部子系统时的复杂度,简化客户端之间的接口。

何时使用:

1、客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。

2、定义系统的入口。

如何解决: 客户端不与系统耦合,外观类与系统耦合。

优点:

1、减少系统相互依赖。

2、提高灵活性。

3、提高了安全性。

缺点:

不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

使用场景:

1、JAVA 的三层开发模式

2、分布式系统的网关

外观模式简单应用

程序员这行,主打一个 “代码虐我千百遍,我待键盘如初恋”—— 白天 debug ,深夜改 Bug ,免疫力堪比未加 try-catch 的代码,说崩就崩。现在医院就诊(挂号、缴费、取药等子系统)都是通过 “微信自助程序”来统一入口,下面就使用外观模式简单实现:

子系统组件(就诊各窗口)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 子系统1:挂号窗口
 */
public class LibifuTestRegisterWindow {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestRegisterWindow.class);
    /**
     * 挂号业务逻辑
     * @param name 患者姓名
     * @param department 就诊科室
     */
    public void register(String name, String department) {
        log.info(" {} 已完成{}挂号,挂号成功", name, department);
    }
}
/**
 * 子系统2:医保缴费窗口
 */
public class LibifuTestPaymentWindow {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestPaymentWindow.class);
    /**
     * 医保结算业务逻辑
     * @param name 患者姓名
     * @param amount 缴费金额(元)
     */
    public void socialInsuranceSettlement(String name, double amount) {
        log.info("{} 医保结算完成,缴费金额:{}元", name, amount);
    }
}
/**
 * 子系统3:取药窗口
 */
public class LibifuTestDrugWindow {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestDrugWindow.class);
    /**
     * 取药业务逻辑
     * @param name 患者姓名
     * @param drugNames 药品名称列表
     */
    public void takeDrug(String name, String... drugNames) {
        String drugs = String.join("、", drugNames);
        log.info("{} 已领取药品:{},取药完成", name, drugs);
    }
}

外观类(微信自助程序)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 外观类:微信自助程序(统一就诊入口)
 */
public class LibifuTestWeixinHospitalFacade {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestWeixinHospitalFacade.class);
    // 依赖子系统组件(外观类与子系统耦合,客户端与子系统解耦)
    private final LibifuTestRegisterWindow registerWindow;
    private final LibifuTestPaymentWindow paymentWindow;
    private final LibifuTestDrugWindow drugWindow;
    // 构造器初始化子系统(也可通过依赖注入实现)
    public LibifuTestWeixinHospitalFacade() {
        this.registerWindow = new LibifuTestRegisterWindow();
        this.paymentWindow = new LibifuTestPaymentWindow();
        this.drugWindow = new LibifuTestDrugWindow();
    }
    /**
     * 统一就诊流程(封装子系统调用,对外暴露单一接口)
     * @param name 患者姓名
     * @param department 就诊科室
     * @param amount 缴费金额
     * @param drugNames 药品名称
     */
    public void processMedicalService(String name, String department, double amount, String... drugNames) {
        log.info("\n===== {} 发起微信自助就诊流程 =====", name);
        try {
            // 1. 调用挂号子系统
            registerWindow.register(name, department);
            // 2. 调用医保缴费子系统
            paymentWindow.socialInsuranceSettlement(name, amount);
            // 3. 调用取药子系统
            drugWindow.takeDrug(name, drugNames);
            log.info("===== {} 就诊流程全部完成 =====", name);
        } catch (Exception e) {
            log.error("===== {} 就诊流程失败 =====", name, e);
            throw new RuntimeException("就诊流程异常,请重试", e);
        }
    }
}

测试类

/**
 * 客户端:测试外观模式调用
 */
public class LibifuTestFacadeClient {
    public static void main(String[] args) {
        // 1. 获取外观类实例(仅需与外观类交互)
        LibifuTestWeixinHospitalFacade weixinFacade = new LibifuTestWeixinHospitalFacade();
        // 2. 调用统一接口,完成就诊全流程(无需关注子系统细节)
        weixinFacade.processMedicalService(
            "libifu", 
            "呼吸内科", 
            198.5, 
            "布洛芬缓释胶囊""感冒灵颗粒"
        );
    }
}

运行结果

框架应用

Spring 框架中外观模式(Facade Pattern) 最经典的落地是 ApplicationContext 接口及其实现类。

ApplicationContext 作为「外观类」,封装了底层多个复杂子系统:

  • BeanFactory(Bean 创建 / 管理核心);
  • ResourceLoader(配置文件 / 资源加载);
  • ApplicationEventPublisher(事件发布);
  • MessageSource(国际化消息处理);
  • EnvironmentCapable(环境变量 / 配置解析)。

开发者无需关注这些子系统的交互细节,仅通过 ApplicationContext 提供的统一接口(如 getBean()、publishEvent())即可完成 Spring 容器的所有核心操作 —— 就像程序员通过「微信自助程序」看病,不用关心医院内部挂号 / 缴费 / 取药的流程,只调用统一入口即可,这正是外观模式「简化复杂系统交互」的核心价值。

以下选取ApplicationContext 、AbstractApplicationContext核心代码片段,展示外观模式的落地逻辑:

package org.springframework.context;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
 * 外观接口:整合多个子系统接口,提供统一的容器操作入口
 */
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, 
        HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    // 1. 获取应用上下文唯一ID(封装底层无,仅统一暴露)
    String getId();
    // 2. 获取应用名称(统一接口)
    String getApplicationName();
    // 3. 获取上下文显示名称(统一接口)
    String getDisplayName();
    // 4. 获取上下文首次加载的时间戳(统一接口)
    long getStartupDate();
    // 5. 获取父上下文(封装层级BeanFactory的父容器逻辑)
    ApplicationContext getParent();
    // 6. 获取自动装配BeanFactory(封装底层BeanFactory的自动装配能力,核心子系统入口)
    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
package org.springframework.context.support;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext {
    // ========== 核心1:refresh() - 封装所有子系统的初始化逻辑 ==========
    @Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // 1. 封装子系统初始化前置检查
            prepareRefresh();
            // 2. 封装BeanFactory子系统的创建/刷新(子类实现具体BeanFactory,如DefaultListableBeanFactory)
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            // 3. 封装BeanFactory子系统的基础配置
            prepareBeanFactory(beanFactory);
            try {
                // xxx 其他源码省略
                // 4. 封装BeanFactory后置处理器执行、事件系统初始化、单例Bean初始化等所有子系统逻辑
                finishBeanFactoryInitialization(beanFactory);
                // 5. 封装容器激活、刷新完成事件发布(子系统收尾)
                finishRefresh();
            } catch (BeansException ex) {
                // 6. 封装子系统初始化失败的回滚逻辑
            }
        }
    }
    // ========== 核心2:getBean() - 封装BeanFactory子系统的调用 + 状态检查 ==========
    @Override
    public <T> T getBean(Class<T> requiredType) throws BeansException {
        // 外观层封装:子系统状态检查(客户端无需关注BeanFactory是否活跃)
        assertBeanFactoryActive();
        // 外观层委托:调用底层BeanFactory子系统的getBean,客户端无需关注BeanFactory具体实现
        return getBeanFactory().getBean(requiredType);
    }
    // ========== 抽象方法:委托子类实现具体BeanFactory获取(屏蔽子系统实现) ==========
    public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
}

Spring 通过 ApplicationContext(外观接口)和 AbstractApplicationContext(外观实现)封装了其他子系统的复杂逻辑:

  • 客户端只需调用 ApplicationContext.getBean() 即可获取 Bean,无需关注底层 Bean 的缓存、实例化、状态检查等细节;
  • 外观类屏蔽了子系统的复杂度,降低了客户端与底层 BeanFactory 的耦合,符合外观模式的设计思想。

行为型模式

为什么用行为型模式?

  • 行为型模式关注点“怎样运行对象/类”关注类/对象的运行时流程控制。
  • 行为型模式用于描述程序在运行时复杂的流程控制,描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
  • 行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
  • 模板方法(Template Method)模式:父类定义算法骨架,某些实现放在子类
  • 策略(Strategy)模式:每种算法独立封装,根据不同情况使用不同算法策略
  • 状态(State)模式:每种状态独立封装,不同状态内部封装了不同行为
  • 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开
  • 责任链(Chain of Responsibility)模式:所有处理者封装为链式结构,依次调用
  • 备忘录(Memento)模式:把核心信息抽取出来,可以进行保存
  • 解释器(Interpreter)模式:定义语法解析规则
  • 观察者(Observer)模式:维护多个观察者依赖,状态变化通知所有观察者
  • 中介者(Mediator)模式:取消类/对象的直接调用关系,使用中介者维护
  • 迭代器(Iterator)模式:定义集合数据的遍历规则
  • 访问者(Visitor)模式:分离对象结构,与元素的执行算法

除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。

行为型模式之策略模式

策略模式(Strategy Pattern)指的是一个类的行为或其算法可以在运行时更改,在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象,策略对象改变 context 对象的执行算法。

意图: 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

主要解决: 在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。

何时使用: 一个系统有许多许多类,而区分它们的只是它们之间的行为。

如何解决: 将这些算法封装成一个一个的类,任意地替换。

优点:

1、算法可以自由切换。

2、避免使用多重条件判断。

3、扩展性良好。

缺点:

1、策略类会增多。

2、所有策略类都需要对外暴露。

使用场景:

1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以

动态地让一个对象在许多行为中选择一种行为。

2、一个系统需要动态地在几种算法中选择一种。

3、线程池拒绝策略。

策略模式简单应用

在电商支付系统中,都会支持多种支付方式(微信、支付宝、银联),每种支付方式对应一种 “支付策略”,客户端可根据用户选择动态切换策略,无需修改支付核心逻辑,下面就使用策略模式简单实现:

策略接口(定义统一算法规范)

/**
 * 策略接口:支付策略(定义所有支付方式的统一规范)
 */
public interface LibifuTestPaymentStrategy {
    /**
     * 执行支付逻辑
     * @param amount 支付金额(元)
     * @param orderId 订单ID
     * @return 支付结果(成功/失败)
     */
    String pay(double amount, String orderId);
}

具体策略类 1:微信支付

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 具体策略:微信支付(实现支付策略接口)
 */
public class LibifuTestWechatPayStrategy implements LibifuTestPaymentStrategy {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestWechatPayStrategy.class);
    @Override
    public String pay(double amount, String orderId) {
        log.info("【微信支付】开始处理订单:{},金额:{}元", orderId, amount);
        // 模拟微信支付核心逻辑(签名、调用微信接口等)
        boolean isSuccess = true// 模拟支付成功
        if (isSuccess) {
            String result = String.format("【微信支付】订单%s支付成功,金额:%.2f元", orderId, amount);
            log.info(result);
            return result;
        } else {
            String result = String.format("【微信支付】订单%s支付失败", orderId);
            log.error(result);
            return result;
        }
    }
}

具体策略类 2:支付宝支付

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 具体策略:支付宝支付(实现支付策略接口)
 */
public class LibifuTestAlipayStrategy implements LibifuTestPaymentStrategy {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestAlipayStrategy.class);
    @Override
    public String pay(double amount, String orderId) {
        log.info("【支付宝支付】开始处理订单:{},金额:{}元", orderId, amount);
        // 模拟支付宝支付核心逻辑(验签、调用支付宝接口等)
        boolean isSuccess = true// 模拟支付成功
        if (isSuccess) {
            String result = String.format("【支付宝支付】订单%s支付成功,金额:%.2f元", orderId, amount);
            log.info(result);
            return result;
        } else {
            String result = String.format("【支付宝支付】订单%s支付失败", orderId);
            log.error(result);
            return result;
        }
    }
}

具体策略类 3:银联支付

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 具体策略:银联支付(实现支付策略接口)
 */
public class LibifuTestUnionPayStrategy implements LibifuTestPaymentStrategy {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestUnionPayStrategy.class);
    @Override
    public String pay(double amount, String orderId) {
        log.info("【银联支付】开始处理订单:{},金额:{}元", orderId, amount);
        // 模拟银联支付核心逻辑(加密、调用银联接口等)
        boolean isSuccess = true// 模拟支付成功
        if (isSuccess) {
            String result = String.format("【银联支付】订单%s支付成功,金额:%.2f元", orderId, amount);
            log.info(result);
            return result;
        } else {
            String result = String.format("【银联支付】订单%s支付失败", orderId);
            log.error(result);
            return result;
        }
    }
}

上下文类(封装策略调用,屏蔽算法细节)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 上下文类:支付上下文(持有策略对象,提供统一调用入口)
 * 作用:客户端仅与上下文交互,无需直接操作具体策略
 */
public class LibifuTestPaymentContext {
    private static final Logger log = LoggerFactory.getLogger(LibifuTestPaymentContext.class);
    // 持有策略对象(可动态替换)
    private LibifuTestPaymentStrategy paymentStrategy;
    /**
     * 构造器:初始化支付策略
     * @param paymentStrategy 具体支付策略
     */
    public LibifuTestPaymentContext(LibifuTestPaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    /**
     * 动态切换支付策略
     * @param paymentStrategy 新的支付策略
     */
    public void setPaymentStrategy(LibifuTestPaymentStrategy paymentStrategy) {
        log.info("【支付上下文】切换支付策略:{}", paymentStrategy.getClass().getSimpleName());
        this.paymentStrategy = paymentStrategy;
    }
    /**
     * 统一支付入口(屏蔽策略细节,对外暴露简洁方法)
     * @param amount 支付金额
     * @param orderId 订单ID
     * @return 支付结果
     */
    public String executePay(double amount, String orderId) {
        log.info("【支付上下文】开始处理订单{}的支付请求", orderId);
        return paymentStrategy.pay(amount, orderId);
    }
}

测试类

/**
 * 客户端:测试策略模式(动态切换支付方式)
 */
public class LibifuTestStrategyClient {
    public static void main(String[] args) {
        // 1. 订单信息
        String orderId"ORDER_20251213_001";
        double amount199.99;
        // 2. 选择微信支付策略
        LibifuTestPaymentContext paymentContext = new LibifuTestPaymentContext(new LibifuTestWechatPayStrategy());
        String wechatResult = paymentContext.executePay(amount, orderId);
        System.out.println(wechatResult);
        // 3. 动态切换为支付宝支付策略
        paymentContext.setPaymentStrategy(new LibifuTestAlipayStrategy());
        String alipayResult = paymentContext.executePay(amount, orderId);
        System.out.println(alipayResult);
        // 4. 动态切换为银联支付策略
        paymentContext.setPaymentStrategy(new LibifuTestUnionPayStrategy());
        String unionPayResult = paymentContext.executePay(amount, orderId);
        System.out.println(unionPayResult);
    }
}

运行结果

框架应用

在Spring 中 ,ResourceLoader 接口及实现类是策略模式的典型落地:

  • 策略接口:ResourceLoader(定义 “加载资源” 的统一规范);
  • 具体策略:DefaultResourceLoader(默认资源加载)、FileSystemResourceLoader(文件系统加载)、ClassPathXmlApplicationContext(类路径加载)等;
  • 核心价值:不同资源(类路径、文件系统、URL)的加载逻辑封装为独立策略,可灵活切换且不影响调用方。
  • 以下选取ResourceLoader 、FileSystemResourceLoader核心代码片段,展示策略模式的落地逻辑:

package org.springframework.core.io;
import org.springframework.lang.Nullable;
/**
 * 策略接口:定义资源加载的统一规范(策略模式核心接口)
 */
public interface ResourceLoader {
    // 类路径资源前缀(常量,子系统细节)
    String CLASSPATH_URL_PREFIX = "classpath:";
    /**
     * 策略核心方法:根据资源路径加载Resource(所有具体策略需实现此方法)
     * @param location 资源路径(如classpath:application.xml、file:/data/config.xml)
     * @return 封装后的Resource对象
     */
    Resource getResource(String location);
    /**
     * 辅助方法:获取类加载器(策略实现时依赖)
     */
    @Nullable
    ClassLoader getClassLoader();
}
package org.springframework.core.io;
/**
 * 具体策略:文件系统资源加载器(覆盖扩展点实现文件系统加载)
 */
public class FileSystemResourceLoader extends DefaultResourceLoader {
    /**
     * 覆盖策略扩展点:实现文件系统路径加载
     */
    @Override
    protected Resource getResourceByPath(String path) {
        // 若路径为绝对路径,直接创建FileSystemResource
        if (path.startsWith("/")) {
            return new FileSystemResource(path);
        }
        // 否则创建文件系统上下文资源(支持相对路径)
        else {
            return new FileSystemContextResource(path);
        }
    }
    /**
     * 内部类:文件系统上下文资源(策略辅助实现)
     */
    private static class FileSystemContextResource extends FileSystemResource {
        public FileSystemContextResource(String path) {
            super(path);
        }
        // xxx
    }
}
角色 类 / 接口 作用
策略接口 ResourceLoader 定义getResource统一加载规范,屏蔽不同资源加载的细节
抽象策略 DefaultResourceLoader 实现通用加载逻辑(类路径、URL),提供扩展点getResourceByPath
具体策略 FileSystemResourceLoader 覆盖扩展点,实现文件系统资源加载的专属逻辑
调用方 ApplicationContext(如ClassPathXmlApplicationContext) 依赖ResourceLoader接口,无需关注具体加载策略,可灵活切换

三、实战

背景

除了大家熟悉的"出价还价"列表外,现在订单列表、"想要"收藏列表等场景也能看到心仪商品的还价信息——还价功能,在用户体验上逐步从单一场景向多场景持续演进。

1.0 版本:

在功能初期,我们采用轻量级的设计思路:

  • 聚焦核心场景:仅在还价列表页提供精简高效的还价服务
  • 极简技术实现:通过线性调用商品/库存/订单等等服务,确保功能稳定交付
  • 智能引导策略:内置还价优先级算法,帮助用户快速决策

2.0 版本:

但随着得物还价功能不断加强,系统面临了一些烦恼:

  • 场景维度:订单列表、想要<收藏>列表等新场景接入
  • 流量维度:部分页面的访问量呈指数级增长,峰值较初期上升明显

我们发现原有设计逐渐显现出一些局限性:

  • 用户体验优化:随着用户规模快速增长,如何在高并发场景下依然保持丝滑流畅的还价体验,成为重要关注点
  • 迭代效率:每次新增业务场景都需要重复开发相似逻辑
  • 协作效率:功能迭代的沟通和对接成本增加

改造点

针对上述问题,我们采用策略模式进行代码结构升级,核心改造点包括:

抽象策略接口

public interface xxxQueryStrategy {
    /**
     * 策略类型
     *
     * @return 策略类型
     */
    String matchType();
    /**
     * 前置校验
     *
     * @param ctx xxx上下文
     * @return true-校验通过;false-校验未通过
     */
    boolean beforeProcess(xxxCtx ctx);
    /**
     * 执行策略
     *
     * @param ctx xxx上下文
     * @return xxxdto
     */
    xxxQueryDTO handle(xxxtx ctx);
    /**
     * 后置处理
     *
     * @param ctx xxx上下文
     */
    void afterProcess(xxxCtx ctx);
}

抽象基类 :封装公共数据查询逻辑

@Slf4j
@Component
public abstract class AbstractxxxStrategy {
        /**
         * 执行策略
         *
         * @param ctx xxx上下文
         */
        public void doHandler(xxxCtx ctx) {
            // 初始化xxx数据
            initxxx(ctx);
            // 异步查询相关信息
            supplyAsync(ctx);
            // 初始化xxx上下文
            initxxxCtx(ctx);
            // 查询xxxx策略
            queryxxxGuide(ctx);
            // 查询xxx底部策略
            queryxxxBottomGuide(ctx);
        }
        /**
         * 初始化xxx数据
         *
         * @param ctx xxx上下文
         */
        protected abstract void initxxx(xxxCtx ctx);




        /**
         * 异步查询相关信息
         *
         * @param ctx xxx上下文
         */
        protected abstract void supplyAsync(xxxCtx ctx);


        /**
         * 初始化xxx上下文
         *
         * @param ctx xxx上下文
         */
        protected abstract void initxxxCtx(xxxCtx ctx);


        /**
         * 查询xxx策略
         *
         * @param ctx xxx上下文
         */
        protected abstract void queryxxxGuide(xxxCtx ctx);


        /**
         * 查询xxx底部策略
         *
         * @param ctx xxx上下文
         */
        protected abstract void queryxxxBottomGuide(xxxCtx ctx);


        /**
         * 构建出参
         *
         * @param ctx xxx上下文
         */
        protected abstract void buildXXX(xxxCtx ctx);
}

具体策略 :实现场景特有逻辑

public class xxxStrategy extends AbstractxxxxStrategy implements xxxStrategy {
    /**
     * 策略类型
     *
     * @return 策略类型
     */
    @Override
    public String matchType() {
        // XXX
    }


    /**
     * 前置校验
     *
     * @param ctx xxx上下文
     * @return true-校验通过;false-校验未通过
     */
    @Override
    public boolean beforeProcess(xxxCtx ctx) {
        // XXX
    }


    /**
     * 执行策略
     *
     * @param ctx  xxx上下文
     * @return 公共出参
     */
    @Override
    public BuyerBiddingQueryDTO handle(xxxCtx ctx) {
        super.doHandler(ctx);
        // XXX
    }


    /**
     * 后置处理
     *
     * @param ctx xxx上下文
     */
    @Override
    public void afterProcess(xxxCtx ctx) {
       // XXX
    }


    /**
     * 初始化xxx数据
     *
     * @param ctx xxx上下文
     */
    @Override
    protected void initxxx(xxxCtx ctx) {
        // XXX
    }


    /**
     * 异步查询相关信息
     *
     * @param ctx  XXX上下文
     */
    @Override
    protected void supplyAsync(xxxCtx ctx) {
        // 前置异步查询
        super.preBatchAsyncxxx(ctx);
        // 策略定制业务
        // XXX
    }


    /**
     * 初始化XXX上下文
     *
     * @param ctx XXX上下文
     */
    @Override
    protected void initGuideCtx(xxxCtx ctx) {
        // XXX
    }


    /**
     * 查询XXX策略
     *
     * @param ctx XXX上下文
     */
    @Override
    protected void queryXXXGuide(xxxCtx ctx) {
        // XXX
    }


    /**
     * 查询XXX策略
     *
     * @param ctx XXX上下文
     */
    @Override
    protected void queryXXXBottomGuide(XXXCtx ctx) {
        // XXX
    }


    /**
     * 构建出参
     *
     * @param ctx XXX上下文
     */
    @Override
    protected void buildXXX(XXXCtx ctx) {
        // XXX
    }
}

运行时策略路由

@Component
@RequiredArgsConstructor
public class xxxStrategyFactory {
    private final List<xxxStrategy> xxxStrategyList;


    private final Map<String, xxxStrategy> strategyMap = new HashMap<>();


    @PostConstruct
    public void init() {
        CollectionUtils.emptyIfNull(xxxStrategyList)
                .stream()
                .filter(Objects::nonNull)
                .forEach(strategy -> strategyMap.put(strategy.matchType(), strategy));
    }


    public xxxStrategy select(String scene) {
        return strategyMap.get(scene); 
    }
}

升级收益

1.性能提升 :

  • 同步调用改为CompletableFuture异步编排
  • 并行化独立IO操作,降低整体响应时间

2.扩展性增强 :

  • 新增场景只需实现新的Strategy类
  • 符合开闭原则(对扩展开放,对修改关闭)

3.可维护性改善 :

  • 业务逻辑按场景垂直拆分
  • 公共逻辑下沉到抽象基类
  • 消除复杂的条件分支判断

4.架构清晰度 :

  • 明确的策略接口定义
  • 各策略实现类职责单一

这种架构改造体现了组合优于继承 、面向接口编程等设计原则,通过策略模式将原本复杂的单体式结构拆分为可插拔的组件,为后续业务迭代提供了良好的扩展基础。

四、总结

在软件开发中,设计模式是一种解决特定场景问题的通用方法论,旨在提升代码的可读性、可维护性和可复用性。其核心优势在于清晰的职责分离理念、灵活的行为抽象能力以及对系统结构的优化设计。结合丰富的实践经验,设计模式已经成为开发者应对复杂业务需求、构建高质量软件系统的重要指导原则。

本文通过解析一些经典设计模式的原理、框架应用与实战案例,深入探讨了设计模式在实际开发中的价值与作用。作为代码优化的工具,更作为一种开发哲学,设计模式以简洁优雅的方式解决复杂问题,推动系统的高效与稳健。

当然了,在实际的软件开发中,我们应根据实际需求合理选择和应用设计模式,避免过度设计,同时深入理解其背后的理念,最终实现更加高效、健壮的代码与系统架构。

往期回顾

1.从0到1搭建一个智能分析OBS埋点数据的AI Agent|得物技术

2.数据库AI方向探索-MCP原理解析&DB方向实战|得物技术

3.项目性能优化实践:深入FMP算法原理探索|得物技术

4.Dragonboat统一存储LogDB实现分析|得物技术

5.从数字到版面:得物数据产品里数字格式化的那些事

文 /忘川

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

❌
❌