阅读视图

发现新文章,点击刷新页面。

HUNT0 上线了——尽早发布,尽早发现

HUNT0 上线了——尽早发布,尽早发现

✨文章摘要(AI生成)

HUNT0 是一个面向 maker 和 indie hacker 的社区驱动发布目录,想把“发布”变成一件有仪式感、可被发现、可获得反馈的事:你可以预约发布日期,通过声望加权投票与评论拿到早期反馈,再通过榜单与 Explore(按筛选意图组织)快速发现值得关注的新产品。

v1.0.0 先把核心闭环跑通:launch → discover → vote → discuss → recap/reward。我们做了实时同步投票状态、结构化的提交流程(免费排队 + Premium Launch)、以及提醒/奖项等自动化机制,目标是减少噪音,让早期发现更“有章可循”。

一切都始于一张有点“丑”的草图:一把向前倾斜的梯子。

这张梯子草图最终成为了 HUNT0 的 logo。它提醒我们:进步是一格一格爬出来的——与其等完美,不如先把不完美的东西发出去。

那张后来变成 HUNT0 logo 的原始梯子草图

这也是口号 “Ship Early, Hunt Early” 的由来:创作者尽早发布,社区才能尽早发现——去浏览、投票、讨论,帮助好产品找到第一批用户。

如果你在公开构建(build in public),欢迎来这里发布;如果你喜欢探索新东西,就从“hunt”开始。

什么是 HUNT0?

HUNT0 是一个社区驱动的发布目录,让“发布”和“发现”同频发生:

  • 预约发布日期,把发布变成一件明确的事件
  • 通过投票与评论获得早期反馈
  • 用榜单与 Explore 按时间与兴趣组织“新产品”,而不是按噪音排序

上线 v1.0.0:我们做了什么

v1.0.0 先聚焦在核心闭环:launch → discover → vote → discuss → recap/reward

1)首页榜单:“今天该看什么?”

首页按时间窗口组织,方便你快速扫到值得关注的内容:

  • Top Products Launching Today:今天(UTC)发布的产品
  • Yesterday / This Week / This Month:按更长时间窗口回顾

投票状态会在页面内 实时同步:你在任意位置给某个产品投票,页面上的所有实例会立刻一起更新,无需刷新。

2)Explore:按意图筛选,而不是靠运气

Explore 支持 分类、标签(最多 10 个)、时间范围、全文搜索,并支持分页。

在相同筛选条件下,Premium Launch 会在排序上优先于免费发布——在“更需要曝光”的时刻更有效。

3)产品页:展示、访问、讨论一站式

每个产品都有独立详情页,便于更深入地了解与互动:

  • 核心信息与外链(Visit)
  • 截图画廊与更长的产品介绍(About)
  • 声望加权的投票与评论
  • 榜单/奖项徽章(例如日榜/周榜/月榜 Top 3)

4)提交:免费排队 + Premium Launch

我们把“发布”设计成一个可控流程,而不只是贴个链接:

  • Free Launch:每日容量有限(默认 10 个名额/天
  • Premium Launch:通过 Stripe Checkout 付费,获得更强曝光

如果 Premium 未完成支付,提交会以草稿形态保留在你的 dashboard,不会公开展示,直到支付成功。

提交也支持更丰富的展示信息:

  • 最多 3 个分类
  • 最多 10 个标签
  • 联系方式/社交链接(至少 1 个)
  • logo 与截图,让产品页更完整

声望系统:让贡献“有分量”

投票不是固定的一人一票。HUNT0 使用 Reputation → Level → Vote Weight

  • 通过参与获得声望(每日访问、投票、评论、发布)
  • 等级越高,投票权重越大
  • 榜单按加权投票聚合,更多反映可信贡献者的偏好

它既是激励机制,也是在早期社区里减少噪音的实用手段。

提醒与奖项:把发布当成“事件”

为了让发布更有“时刻感”,我们加了一些自动化:

  • 发布提醒邮件:在 UTC 发布日开始前 1 小时发送
  • 日榜/周榜/月榜奖项:自动计算 Top 3 并通知创作者(可选公开复盘)

开始使用

  • 如果你是创作者:去 Submit 预约发布日期
  • 如果你想发现新产品:去 Explore 按分类/标签筛选
  • 如果你想看 logo 的故事:去 About 看梯子的来源

我们会持续迭代发现、榜单和社区激励机制。Launch something, hunt something——也欢迎告诉我们哪里可以做得更好。

2025年度稀土掘金影响力榜单如约而至!

稀土掘金社区2025年度影响力榜单正式公布

1303-734主视觉 (1) (3).jpg

时光向前,2025年即将落下句点。回首这一程,幸而有伴 —— 每一次深夜的打磨,每一段真诚的分享,每一回评论区里的倾力相助,都为技术探索的漫漫长路,点亮了一盏盏暖灯。

感谢这一年里,将实操经验凝注于代码、把深度思考落笔成文字的创作者。是每一个具体的 “你”,让稀土掘金始终活力满满。

今天,我们以这份榜单,记录那些值得被看见的光芒。它们来自团队扎实的沉淀,来自创作者持久的热情,也来自那些真正帮助过许多人的走心好文。

榜单地址aicoding.juejin.cn/pens/758993…

2025年度优秀创作者 | The Best 10 Creative Writers of 2025

他们是社区里的“解惑人”,把复杂讲得简单,把枯燥变得生动。他们的文字,曾陪伴无数掘友走出技术探索的迷茫时刻。

站内昵称 个人主页
Moment juejin.cn/user/378276…
ConardLi juejin.cn/user/394910…
何贤 juejin.cn/user/277499…
洛卡卡了 juejin.cn/user/888839…
why技术 juejin.cn/user/370281…
恋猫de小郭 juejin.cn/user/817692…
苏三说技术 juejin.cn/user/465848…
coder_pig juejin.cn/user/414261…
张风捷特烈 juejin.cn/user/149189…
德莱厄斯 juejin.cn/user/391911…

2025年度影响力团队 -The Most Influential Teams of 2025

他们以团队的力量,把一线实践沉淀成可复用的经验,如一张张清晰的技术地图,帮助不少同行找到了方向。

团队名称 团队主页
DevUI团队 juejin.cn/user/712139…
vivo互联网技术 juejin.cn/user/993614…
得物技术 juejin.cn/user/239295…
古茗前端团队 juejin.cn/user/323304…
货拉拉技术 juejin.cn/user/176848…
京东零售技术 juejin.cn/user/423357…
奇舞精选 juejin.cn/user/438890…
37手游后端团队 juejin.cn/user/154852…
哔哩哔哩技术 juejin.cn/user/303070…
转转技术团队 juejin.cn/user/606586…

2025年度爆款好文 | High hits articles in 2025

这20篇文章,是从海量分享中脱颖而出的“年度之选”。它们或视角新颖、或剖析深入、或实战性强,在各自领域内获得了认可,成了许多掘友收藏或推荐的那一篇。

文章标题 文章链接 所属作者
前端
《40岁老前端2025年上半年都学了什么?》 juejin.cn/post/752454… 张鑫旭
《前端仔如何在公司搭建 AI Review 系统》 juejin.cn/post/753259… 唐某人丶
《因为写了一个前端脚手架,这个月的KPI 打满了!!!》 juejin.cn/post/745793… 赵小川
《因网速太慢我把20M+的字体压缩到了几KB》 juejin.cn/post/749033… 古茗前端团队
《历经4个月,基于 Tiptap 和 NestJs 打造一款 AI 驱动的智能文档协作平台 🚀🚀🚀》 juejin.cn/post/755316… Moment
后端
《MCP 很火,来看看我们直接给后台管理系统上一个 MCP?》 juejin.cn/post/748149… Hamm
《还在用WebSocket实现即时通讯?试试MQTT吧,真香!》 juejin.cn/post/753935… MacroZheng
《我做了套小红书一键发布系统,运营小姐姐说她不想离开我了》 juejin.cn/post/755248… 洛卡卡了
《Java 实现责任链模式 + 策略模式:优雅处理多级请求的方式》 juejin.cn/post/745736… 后端出路在何方
《CompletableFuture还能这么玩》 juejin.cn/post/745556… 一只叫煤球的猫
移动端
《2025 跨平台框架更新和发布对比,这是你没看过的全新版本》 juejin.cn/post/750557… 恋猫de小郭
《月下载 40 万次的框架是怎么练成的?》 juejin.cn/post/754740… Android轮子哥
《[targetSDK升级为35] 恶心的EdgeToEdge适配 (v7)》 juejin.cn/post/749717… snwrking
《gson很好,但我劝你在Kotlin上使用kotlinx.serialization》 juejin.cn/post/745929… 沈剑心
《开箱即食Flutter通用脚手架》 juejin.cn/post/748278… SunshineBrother
人工智能
《一天 AI 搓出痛风伴侣 H5 程序,前后端+部署通吃,还接入了大模型接口(万字总结)》 juejin.cn/post/751749… 志辉AI编程
《AI 应用开发入门:前端也可以学习 AI》 juejin.cn/post/751719… 唐某人丶
《如何把你的 DeePseek-R1 微调为某个领域的专家?》 juejin.cn/post/747330… ConardLi
《3天,1人,从0到付费产品:AI时代个人开发者的生存指南》 juejin.cn/post/757765… HiStewie
《全网最细,一文带你弄懂 MCP 的核心原理!》 juejin.cn/post/749345… ConardLi

好的社区,是人与人相互照亮

这份榜单,与其说是评选,不如说是一次郑重的致谢。谢谢所有分享者,也谢谢每一位静静学习、默默点赞、热心评论的掘友。

技术之路,日常而长远。2026年,愿你继续在这里写下自己的章节,发出自己的光。我们相信,每一个人的微小光芒,终将汇聚成行业的星辰。

*注:以上排名不分先后,随机排序。本榜单依据2025年1月1日至12月21日期间的数据综合评定,涵盖文章质量、互动热度、创作者影响力、分类分布及评审团意见等多维度,最终解释权归稀土掘金所有。

lecen:一个更好的开源可视化系统搭建项目--页面设计器(表单设计器)--全低代码|所见即所得|利用可视化设计器构建你的应用系统-做一个懂你的人

页面设计

页面设计器的使用,主要界面栏目功能及操作说明。

我们在做系统页面的时候,可以不用像传统开发那样,必须在本地启动开发环境,设计你的页面或者组件等,这里提供了可视化构建页面的能力,让你能够在网页中通过鼠标点点点来创建一个新的系统页面。

它也是我们系统的核心内容,我们将通过页面设计器来设计出统一的代码格式组成的页面,然后再用统一的解析页面来渲染整个系统。

换而言之,系统的所有页面全都由设计器设计出来,并且系统的所有页面全部都由一个页面解析并渲染出来。

由页面设计器设计出来的页面,全部都是所见即所得,设计出来什么样子,实际就是什么样子,不会放大缩小或者变形,我们可以对页面进行任何的设置,并且都是在设计页面实时生效的,所有的效果都与实际页面一样,也就是说不用使用预览功能就可以做到预览与交互。

功能分区

拖拽式可视化编辑创建页面。设计器主要界面

页面设计器

整个设计器大体分为三个部分:

  1. 视图绘制区域:整个页面的结构,这里放页面的组件。
  2. 组件面板:相当于设计页面时所需的物料区,所有的组件都能在这里找到。
  3. 属性面板:组件的操作面板,可以设置它的属性、事件、插槽等等。

组件面板

组件面板给我们提供了经常用到的一些 UI 组件,默认使用element-plus来渲染,你也可以指定为naive-ui,甚至使用你自己创建的组件。

组件面板按钮

可直接使用鼠标点击相应图标,并拖拽到视图绘制区域。

主要分为三栏,从左到右分别为:

基本组件栏: 一般的表单元素组件,或者一些基本元素的组件。

复合组件栏: 包含嵌套结构的元素组件等,也就是由多个基本组件组成。

反馈组件栏: 系统提示弹窗组件等,与用户有较强交互效果的组件。

注意:

页面初始需要放置行列组件,也就是说所有组件都是在最外层的行列组件内。在创建一个新页面时,最开始需要拖入的是行列。

行列图标

就是区域2中的第一个组件,为了方便和显眼,我们把它放在了页面居中并偏左的地方,而且为它设置了一点颜色。

行列可以嵌套行列,以达到无限布局

组件面板提供了非常多的组件,还有一些隐藏组件没有显示出来,只有等到用到他们的时候才会自动展示出来。

将鼠标放到某个组件的右上角,会显示相应的提示信息,当通过外观分辨不出是什么组件的时候,可以通过该种方式查看组件的名称。

组件名称提示

快捷操作

为了进行快速的操作,节省页面设计时间,也是为了更友好的交互,提供了一些快捷键,在实际使用中会经常用到。

按键 说明
Alt 唤出右侧属性面板
空格 唤出底部组件面板
Shift + 空格 隐藏组件面板和属性面板
Shift + 上/下/左/右 切换组件面板位置
Shift + A 唤出/隐藏 组件面板(修改层级)
Shift + D 唤出/隐藏 属性面板(修改层级)

视图绘制区

视图绘制

通过拖拽组件区中的行列组件,我们将它放置到视图绘制区中,这样就可以往行列组件中拖拽任意的组件了。 比如这里我们首先放置了一个新增按钮,然后又放置了一个查询输入框,最后我们又放置了一个查询的按钮。 因为查询按钮是最后放置的,因此目前默认它就是可编辑的状态,所有的设置都会对这个按钮生效。

默认情况下,被拖入的空行会用虚线显示出来,以方便查看和拖入其他组件,实际做好的页面中空行不会显示出来。

空行显示虚线

只要行组件被放置了其他组件,那么该行的虚线轮廓示意将会自动消失,显示成它实际该有的样子。

除此之外,还提供了右键的功能,比如我们在行列组件上右键的话,就会出现下面这样:

行列右键

移除该行 的选项是高亮的,表示我们可以操作,点击后就会将该行以及它所包含的所有内容,全部从页面上删除掉。

填入该列 的选项是置灰的,表示我们目前不可以操作,这是因为还没有选中元素,如果有选中元素的话,那么使用该项就会把选中的元素填入到该列当中。

如果我们将右键放到其他组件上面使用的话,会出现不一样的效果:

组件右键

移除 表示将该组件从当前页面移除掉。

移动 表示我们将要移动该组件,点击之后,该组件将处于可移动状态,也就是被标记为将要移动的状态。

放在前面 如果我们目前有可移动组件,那么我们可以将它放置到该组件的前面

放在后面 如果我们目前有可移动组件,那么我们可以将它放置到该组件的后面

比如我们在新增按钮上面右键,选择移动,然后在查询按钮上面右键,选择放在后面,那么页面就会变成这样:

移动组件

而且我们会遇到一些特殊的情况,当你想要编辑一个下拉框的时候,如果是点击下拉框,那么并不会弹出对应的属性面板,而是展开下拉选项

点击下拉框

这时我们就需要一个特殊的手段来处理它。

因此我们给这种特殊的组件增加了右键选中的功能

右键下拉框

其他组件,比如按钮,是没有右键的选中菜单的,因为这类组件直接点击就可以进行编辑

按钮右键

但是也有特殊情况,比如我们给按钮绑定了一些事件,但是我只想编辑属性而不是触发事件,这时能够通过按住ctrl再点击右键来强制调出选中项:

右键强制选中

选择之后就能够编辑对应的组件了:

选中组件

由于我们拖拽的是一个按钮,那么默认也是编辑的这个按钮的配置,但是也可以点击里面的文字,进行文字的更改:

按钮中的文字

点击页面空白处,也就是没有组件的地方,是会隐藏组件面板和属性面板的。

快捷操作

选中某个组件之后,也可以通过快捷键来帮助做一些便捷的处理。

按键
说明
ctrl + ↑ 选择上一级组件,直到根元素的行组件
ctrl + ← 如果左边有兄弟组件,那么选中它
ctrl + ↓ 如果在行组件上操作,那么将会选中它的第一列,如果是在列组件上操作,那么将会选中它的第一个子组件,如果是在非行列组件上操作,并且有兄弟组件的话,那么选中它的最后一个兄弟组件
ctrl + → 如果右边有兄弟组件,那么选中它
ctrl + click 正常click会打开属性面板,但是如果想单纯的点击组件查看效果,那么可以按住ctrl再点击鼠标左键,这样就跟在真实业务场景下的页面中一样,不会弹出属性面板
Ctrl + 鼠标右键 定位当前元素弹出选中

属性面板

当前添加或选择的组件的属性和事件等的相关配置。如图

组件属性面板

当页面中包含一个组件的时候,我们希望能够对它进行任意的定制,组件属性面板给我们提供了入口,主要提供以下五个方面的配置

基础操作区域

所有组件都可以使用的功能,主要是围绕着组件做一些事情,而不是修改配置信息。

公共操作区域

  • 上一级 如果组件形成了嵌套关系,那么可以通过该按钮快速选择上一级,一直到当前的根元素,也就是行组件为止。

行组件操作区域

行组件这里有一些变化,能够进行上移和下移来调整行位置,并给出提示信息,这些都是可配置的,只要找到对应的配置文件进行修改即可。

  • 左移 如果左边有兄弟组件,那么它将移动到该组件的左边。

  • 右移 如果右边有兄弟组件,那么它将移动到该组件的右边。

  • 移除 移除该组件。

  • 固钉图标 属性面板默认是固定在页面右侧的,可以通过点击图标来解除固定,然后用鼠标按住图钉进行拖动就能够移动了。

  • 当前组件 显示了当前组件的名称,如:按钮、输入框、表格等,右边有一个刷新图标,如果更改组件某个配置页面没有自动更新的话,可以点击该图标手动刷新状态。

  • 滑动条 用来快速滚动页面,如果组件属性配置面板比较长,那么可以通过鼠标点击滑动条来快速切换到对应为止,滑动条中滑动块的位置与页面中的滚动条位置相对应,比如点击滑动条尾部,会直接自动跳转到属性面板的底部。

滑动条

并且属性面板向下滚动的时候,滑动条会固定在面板顶部保持始终可见。

  • 之前添加组件 点击该按钮之后,会自动弹出组件面板,选择对应组件之后,所选择的组件就会被插入到该组件的前面。

之前添加组件

并且该按钮会自动转换成取消按钮,可以点击它来取消此次操作。

之前添加组件选择

可以看到,组件面板中的所有组件全部变成高亮状态,并且显示出了很多隐藏的组件以供选择。

  • 替换当前组件 选择的组件会替换掉当前的组件。

  • 之后添加组件 选择的组件会被插入到该组件的后面。

属性区域

除了原生 div 或者 span 等标签元素,其他所有的组件全部都默认都由 element-plus 中的组件来渲染,因此属性区域可配置项默认包含了 element-plus 官网中对应组件的所有属性,但是也有一些公共的属性需要配置,这里对一些属性做一下陈述:

类名和样式属性

其中有所有组件都有的三个属性配置:

类名

可以给该组件添加类名,能够通过手动书写类名或者预置选择以及可视化设置的方式来添加。

比如我们给查询按钮添加一点样式:文字变成红色、宽度变为100px。

按钮添加类名

这里我们使用手输的方式,给按钮添加了两个类名,具体格式之后会详解,详情参见,现在按钮已经变了样子。

按钮添加类名效果

可以看到,按钮已经按照设置变成了我们想要的效果。

现在用预置选择的方式再来看一下,我这里预置了一个效果。

类名预置选择

有一个圆角凸起的预置选项,我们选中它之后,其他什么都不用改,按钮就会发生变化。

按钮圆角凸起效果

而且它也有了鼠标悬浮等交互效果,每个组件的类名预置选项都是可以配置的,不会冲突,彼此之间相互独立。

点击类名旁边的小手,可以唤醒配置面板,

设置类名

例如我们现在设置一个高度为60px的类名,然后再设置一个圆角为50%的类名

按钮设置类名

点击确定之后,按钮就自动添加上相应的类名了,样式实时生效

按钮效果

样式

如果有更定制化的样式设置,或者样式比较复杂,用单元类名写起来比较繁琐,那么可以使用该功能细致化的定制该组件的样式,所写的所有样式都只针对当前组件生效。

点击编写按钮之后,会弹出一个编辑框,里面就可以编写css代码了。

样式编辑弹窗

有三种编写的方式:

① 直接书写属性对。

② 将属性对包裹在一堆大括号中。

③ 以*开头的大括号内写属性对。

推荐使用第三种方式,因为这样会有代码提示:

编写样式代码

我们给按钮添加一个样式,让它的文字变成蓝色:

蓝色字体样式代码

点击对勾之后,我们就能看到变化啦:

蓝色字体样式效果

深层样式

有时候我们不但要改变当前组件的样式,还要更改它子孙组件的样式,这个时候我们就可以使用深层样式来控制内部组件。

有点相当于我们平时写的深层选择器,为了更好的控制样式生效范围,我们规定必须手动指定一个选择器,再在这个基础上去控制深层组件。

比如现在我们有一个行内卡片,它的默认padding是20px:

行内卡片

显然,通过单元类名或者样式都是无法更改头部区域和内容区域的padding值的,这时我们使用深层样式对它进行更改:

修改card的padding代码

根据上面我们的约定,mini-card就是我们手动指定的选择器,编写完深层样式之后,我们还需要对该组件指定这个选择器:

card增加类名

现在我们想要的效果就已经达成啦:

card的padding效果

这时,再新增的card组件或者其他既有card组件不会收到影响,还是展示默认的效果,如果也想同步这样的效果,只需要给那个组件也加上mini-card类名即可。

其实可以发现,我们不但可以修改当前组件内的深层样式,也可以通过父级来修改当前组件的样式,甚至可以通过根组件来统一管理当前行的所有样式。

除了上面这三个公共属性,还有一些非常重要的属性,只不过它们只存在于一些特殊的组件。

行组件的专有属性

行属性设置列

默认一行有一个列,并且最少有一个列,遵循24栏布局,可以继续添加或者删除列。

也可以设置列的间距,以及水平和垂直方向上的对齐方式等。

model绑定

像输入框、下拉框等表单元素组件,都会产生数据交互,包括一些自定义组件也是,它们都支持model的绑定。

数据绑定

model的绑定一共由两个属性的组合来支持,一个是设定它的key,也可以叫做它的name,对于表单数据对象来说就是key-value对中的key,对于提交到后端的数据来说就是字段的名称name。

另一个就是设置的默认值,也就是未赋值的情况下,该组件显示的值。

model的绑定方式有两种:

  1. 直接写名称,那么它们都会被收集到根对象中以供提取和使用:

注:根对象是针对当前页面的,不同的页面有不同的根对象

比如我们设计了三个输入框,分别是手机号、邮箱、住址,它们的model分别是phone、email、address:

model值查看

collectionData中已经有了,现在给这三个输入框分别输入点内容:

输入框有值

看下它们值的变化:

model值的变化

  1. 通过用点(.)来连接多个名称,那么它们会被自动收集到设定的对象里面,并可以通过跟对象进行访问。

现在我们假定上面的三个输入框属于同一个表单,把他们绑定在对象userInfo上面,类似于这样:

model深层绑定

再看下此时它们被收集到了哪里:

model深层绑定值变化

这样就可以通过userInfo来访问三个输入框的值了,同理,支持无限层级的嵌套。

model的默认值也有两种绑定方式:

  1. 普通数据类型,可以指定为数字、字符串、布尔值、数组等等。

比如输入框可以指定为字符串,多选按钮指定为数组:

输入框默认值

多选框的默认值

这个时候页面上这两个组件的默认值就会自动更新了:

输入框默认值回显

多选框默认值回显

  1. 函数,默认值将是该函数的返回值。

默认值为函数

在这里插入图片描述

填充数据

有一些组件的渲染需要一些动态数据,比如表格、下拉框、复选框组等等,这些数据可以是自定义的,也可以是从后端读取的。

这里又追加了一种list的类型,也是可以填充数据的,主要包含了三个功能点:

填充数据

  1. 填充数据,可以绑定接口返回值,这个需要先给当前页面配置接口。

填充数据有三种方式:通过绑定接口返回值、通过绑定作用域插槽数据、通过绑定 collectionData 中的数据。

填充数据如果是 requestData 中的请求链接里返回的数据,直接从下拉选项选择所返回的对象属性名称即可。如果是从 collectionData 中获取可以在输入框中输入 @属性名@ 符号表示从 collectionData 取值。还可以使用 # 符号,表示在当前作用域插槽下取值,比如 #childrens ,表示取当前组件作用域插槽下的childrens数组。

  • 通过接口绑定

比如现在有一个表格,数据需要从接口获取,已经给页面配置好了一个获取歌曲列表的请求链接:

歌曲请求链接

那么就可以从填充数据中绑定它:

绑定填充数据

这个接口返回一个数组,每一项包含四个字段:title、singer、time、hot。详情可参考 请求链接

现在已经将数据绑定到表格上面了,那么接下来就可以指定列绑定相应的字段来渲染了,首先给表格建四个列:

直接修改文字

直接点击表格头部,就可以编辑对应列的名称了。

绑定表格列内容

点击对应的列,编辑它所渲染的内容,# 表示一级作用域插槽数据,也就是当前作用域插槽的数据,如果是##就表示上级作用域插槽的数据,其中row表示el-table列作用域插槽数据中的行数据,title就表示插槽数据的title字段值。

其中#也可以放在最左边,也就是 row#title#row.title 是一样的,#row.title 可能比较好理解,表示当前作用域的行数据的title字段,上图中那样写是为了将对象写在#左边,#右边只写字段,因为它也可以写成 a.b.c#d 的形式。

然后就能看到表格已经自动渲染了我们所绑定的数据:

表格渲染效果

  • 通过作用域插槽

其实上面已经顺带着演示了作用域插槽的使用,而我们填充的数据同样也是可以从作用域插槽中来获取。

  • collectionData

如果我们在页面初始化或者其他地方给 collectionData 中赋值了一个数据对象,那么填充的数据也可以取的到。比如我们有这样是一个数据:

collectionData赋值数据

collectionData中赋值了一个tableData属性,它的值是一个数组。

[{
    title: '稻香',
    singer: '周杰伦',
    hot: 999,
    time: '03:43'
},{
    title: '关不上的窗',
    singer: '周传雄',
    hot: 999,
    time: '04:56'
},{
    title: '口是心非',
    singer: '张雨生',
    hot: 999,
    time: '04:56'
},{
    title: '水手',
    singer: '郑智化',
    hot: 999,
    time: '04:57'
}]

现在我们将表格的数据源绑定到这个数组上面,作用域插槽使用 # 关键字,要引用 collectionData 中的属性使用 @ 关键字。

绑定collectionData属性

现在就绑定完成啦,表格会自动读取该数组来渲染:

collectionData绑定回显

并且现在修改tableData的数据,页面也会实时修改更新。

  1. 测试数据,可以手动更改请求链接的参数,如果填充数据使用的是接口的返回值,那么有可能这个接口在请求的时候需要动态传一些必填的参数。

在设计阶段,如果接口能够直接发起请求并返回数据,那么页面绑定接口之后就会自动发起请求并将返回数据绑定到页面上。

如果需要动态获取或者根据业务实时改变参数的接口,就可以使用测试数据的功能,它能够让我们先临时指定参数的值,来模拟实际场景的效果。

有一个获取字典的接口,可以通过code来查询对应的字典项,默认code是空的:

获取字典接口

将填充数据绑定到这个接口,那么我们就可以通过测试数据来指定某个code值以让接口发起请求:

绑定接口测试数据

  1. 手动指定数据,有一些填充数据不需要通过上面这些方式绑定,比如性别男女,或者选项是否等,就可以通过手动编辑组件需要渲染的数据。

有一个单选按钮组,通过一个列表渲染出多个radio的选项,手动指定列表的数据如下:

手动指定数据

绑定选项的label属性,内容是通过文本组件渲染,从作用域中获取数据:

单选标签插槽

单选内容插槽

现在页面中就会呈现出效果:

单选手动指定数据效果

比如我们再加一个保密的选项,直接修改手动指定数据的内容即可:

[{
  label: '男',
  value: 1
},{
  label: '女',
  value: 0
},{
  label: '保密',
  value: 2
}]

页面也实时回显效果:

单选手动指定数据效果2

事件区域

可配置组件暴露的事件,它们都能通过编写代码的方式进行事件的添加和绑定。

比如输入框暴露出的事件有:

输入框暴露的事件

单选按钮组暴露出的事件有:

单选按钮组暴露的事件

也可以通过单击【添加】设置 on_事件名 来绑定事件。

组件绑定事件

插槽区域

包含了组件支持的所有插槽,能够对插槽进行各种操作。

例如输入框的插槽:

输入框插槽

所有插槽全部包含三个功能:添加、删除、查看元素

  1. 添加

可以对该插槽添加组件,点击之后自动弹出组件面板,选中组件之后,该组件就会被添加到该插槽当中。

给输入框组件添加一个 prepend 插槽和一个 suffix 插槽。

prepend是一个文本:

插槽文本

suffix是一个图标:

插槽图标

两个插槽添加成功:

输入框插槽效果

如果某些组件更适合被添加,那么会自动高亮,比如给 table 添加插槽,table-column 会高亮,给 select 添加插槽,option 会高亮等等。

添加插槽

  1. 删除

可以直接清空插槽,无论该插槽中有多少个组件都会被一次性删除掉,如果想一个一个删除,那么选择到对应组件,使用组件的移除功能。

删除掉刚才的prepend插槽中的组件:

suffix插槽图标

现在就只剩suffix插槽了。

  1. 元素

可以查看当前插槽中所包含的所有组件,用列表的方式展现,单击列表中的项,能够直接跳转到对应组件的配置面板。

我们查看suffix插槽中的组件:

查看插槽内容

单击icon,就能够编辑这个icon组件的配置了:

图标属性面板

其他定义区域

在这里能够增加权限控制、获取页面DOM、获取渲染的视图、添加事件监听等等。

其他自定义区域

上面是书写规范的说明,下面是一个添加的按钮。

比如给按钮添加一个dataset,点击[+添加]可以输入对应的key和value。

dataset自定义属性

其中key为_data-level,value为1。看下DOM元素:

dateset的dom效果

之后可以对它进行引用和操作了。

也可以添加事件,按钮组件默认没有暴露出任何事件,但是我们可以手动添加点击事件。

自定义click事件

它的key为on_click,value为一个函数:

function _() {
  console.log('点击了按钮')
}

然后我们点击按钮就能够看到函数的执行效果:

click事件效果

既可以直接编写逻辑,也可以调用执行寄连,还能够操作组件、修改变量、发起请求等等等等。

在一个方法中你可以做任何事情。

如果一个属性的的值是通过函数返回的,那么这个value也可以设置为函数,只需要把函数命名为 attribute 即可。

比如将刚才的dataset设置为一个函数返回值:

function attribute() {
  return 2
}

那么页面中也是会实时变更的:

通过函数返回属性

如果一个属性的值,本身就需要是一个函数,比如下拉框的过滤属性filter-method,它的值就是一个函数,那么这时你可以任意命名,建议将函数命名成 _

右边侧栏

除了上面的三个大板块,页面设计器还包含了一些其他的配置功能。

主要用来配置当前页面使用的执行寄连、请求链接,还有页面整体的样式设计,以及选择元组等。还列出了当前设计页面所包含的所有组件。

页面配置按钮

默认是虚化状态,鼠标悬浮会高亮

上面从上到下的按钮依次为:全局配置、元素面板、元组面板、保存。

全局配置

针对整个页面进行配置,点击会展开一个菜单:

页面配置操作面板

按逆时针呈现的三个图标,分别表示:

  1. 整页样式调整

包括class设置、style设置、深层样式设置。

全局样式配置

设置规则同组件一样。见上面

  1. 页面执行寄连配置

弹窗分左右两个列表,右侧列表表示当前已添加的执行寄连。

绑定寄连

支持检索,点击添加按钮即可将该执行寄连绑定到该页面上,也可以点击移除从该页面中移除绑定。

  1. 页面请求链接配置

弹窗分左右两个列表,右侧列表表示当前已添加的请求链接。

绑定请求链接

支持检索,点击添加按钮即可将该请求链接绑定到该页面上,也可以点击移除从该页面中移除绑定。

绑定完请求链接,就可以在组件的填充数据中选择绑定的数据项了。

元素面板

当设计页面时,有可能有些组件不太好找到或者被选取到,这样就不容易配置它的属性,这时就可以从元素面板中找到它,点击对应的名称就可以自动定位到该组件,并打开它的属性配置面板。

页面中元素列表

元组面板

如果设计了多个元组,那么就可以通过元组面板来选取,达到快速设计的目的。

比如我们之前设计的歌曲列表页面。

歌曲列表页面

现在使用这个页面生成一个元组,注意元组是用来做复用的,这里只是一个演示。

那么就可以在元组面板中看到了:

选择元组面板

会以缩略的形式展现出来,单击对应的元组就可以添加到当前页面了,这里目前只有一个元组供选择。

添加元组

该元组已经被添加到刚刚做演示的页面中。

默认情况下,被添加进来的元组是不允许编辑的,因为它是公用的复合组件,如果需要修改,可以去修改元组,这样用到该元组的地方就会全部自动更新。

元组默认不能编辑

点击该元组的任何地方,弹出的属性面板都是空白的,无法配置的。

如果确实需要特殊化处理,只是为了快速复用,有一些独立修改的地方,那么可以点击解除关联绑定,这样就可以对元组进行编辑了,并且不会影响其他用到的页面。同样,修改元组之后该页面也不会变化。

保存

也就是设计完成按钮,点击该按钮会对页面进行保存,如果是第一次设计,会弹出基本信息输入的窗口

设计完成

其他

就像元组一样,我们还有模板的概念,它不同于元组,如果我们设定一个页面为模板的话,那么在用页面设计器新建一个页面的时候,会先提供模板选择的功能。

比如我们没有模板的时候新建页面是这样的:

设计器新建页面

因为没有任何模板可供选择,因此只有一个新建的图标,点击新建图标,就可以全新的设计一个页面了。

再将刚才歌曲列表的页面生成一个模板,那么我们在新建页面的时候就会是下面这样:

设计页面选择模板

就跟word一样,使用模板会快速的生成一个页面,可以自建任意多的模板。

每个页面在生成元组或者模板的时候,都会自动生成缩略图,方便选择。

【项目体验】

系统管理端地址www.lecen.top/manage

系统用户端地址www.liudaxianer.com/user

系统文档地址www.lnsstyp.com/web

RocketMQ高性能揭秘:承载万亿级流量的架构奥秘|得物技术

一、前言

在分布式系统架构中,消息队列如同畅通的“信息神经网络”,承担着解耦、削峰与异步通信的核心使命。在众多成熟方案中,RocketMQ凭借其阿里巴巴与Apache双重基因,以卓越的金融级可靠性、万亿级消息堆积能力和灵活的分布式特性脱颖而出,成为构建高可用、高性能数据流转枢纽的关键技术选型。本文将深入解析RocketMQ的核心架构、设计哲学与实践要义。

二、RocketMQ架构总览

官网图片

RocketMQ架构上主要分为四部分,如上图所示: 

RocketMQ作为一款高性能、高可用的分布式消息中间件,其核心架构采用了经典的四组件协同设计,实现了消息生产、存储、路由与消费的全链路解耦与高效协同。四大组件——生产者(Producer)、消费者(Consumer)、路由中心(NameServer)和代理服务器(Broker)——各司其职,共同构建了其坚实的基石。

生产者(Producer) 作为消息的源头,负责将业务消息高效、可靠地发布到系统中。它支持分布式集群部署,并通过内置的智能负载均衡机制,自动选择最优的Broker节点与队列进行投递。

消费者(Consumer) 是消息的处理终端,同样以集群化方式工作,支持推送(Push)和拉取(Pull)两种消息获取模式。它提供了集群消费与广播消费两种模式,并能动态维护其订阅关系。

路由中心(NameServer) 是整个架构的“注册中心”,扮演着轻量级服务发现的角色。所有Broker节点都会向NameServer注册,并通过定期心跳汇报健康状态。生产者与消费者则从NameServer获取实时的主题路由与Broker信息,从而实现消息寻址的完全解耦。

代理服务器(Broker) 是消息存储与流转的核心,负责消息的持久化存储、投递与查询。为了保障高可用性,Broker通常采用主从(Master-Slave)部署架构,确保数据与服务在故障时能无缝切换。其内部集成了通信处理、存储引擎、索引服务和高可用复制等核心模块。

三、核心组件深度解析

NameServer:轻量级服务发现枢纽

NameServer是RocketMQ的轻量级服务发现与路由中心, 其核心目标是实现生产消费与Broker服务的解耦。 它不存储消息数据,仅管理路由元数据。

核心是一张的路由表 HashMap<String/* Topic */, List>,记录了每个Topic对应在哪些Broker的哪些队列上。

客户端内置了故障规避机制。如果从某个NameServer获取路由失败,或根据路由信息访问Broker失败,会自动重试其他NameServer或Broker。

1. 核心角色与设计哲学: NameServer的设计哲学是 “简单、无状态、最终一致” 。 每个NameServer节点独立运行,节点间互不通信, 这使其具备极强的水平扩展能力和极高的可用性。客户端会配置所有NameServer地址,并向其广播请求。

2. 核心工作机制: 其运作围绕路由信息的生命周期展开,可通过下图一览其核心流程:

3. 和kafka注册中心对比

  • NameServer 采用 “去中心化” 和 “最终一致” 思想,追求极致的简单、轻量和水平扩展, 牺牲了强一致性,以换取架构的简洁和高可用。这非常适合路由信息变动不频繁、客户端具备容错能力的消息场景。
  • Kafka (KRaft) 采用 “中心化” 和 “强一致” 思想,追求数据的精确和系统的自包含。 它将元数据管理深度内化,通过共识协议保证全局一致,但代价是架构复杂度和运维成本更高。

优劣分析: NameServer在运维简易性、集群扩展性、无外部依赖上占优;而Kafka KRaft在元数据强一致性、系统自包含、架构统一性上更胜一筹。选择取决于你对一致性、复杂度、运维成本的具体权衡。

Broker:消息存储与转发的核心引擎

解密存储文件设计

Broker目录下的文件结构

所有核心存储文件均位于Broker节点的 ${storePathRootDir}/store/ 目录下(默认路径为 ~/store/),其下各子目录职责分明:

目录/文件 核心职责 关键设计说明
commitlog/ 消息实体存储库 • 设计:所有Topic的消息顺序混合追加写入。• 文件:以起始物理偏移量命名(20位数字),默认每个1GB。lock文件确保同一时刻只有一个进程写入,保障严格顺序写。
consumequeue/ 逻辑消费队列索引 • 结构:按 {Topic}/{QueueId}/三级目录组织。 • 文件:存储定长记录(20字节/条),包含物理偏移量、长度和Tag哈希码。 • 作用:为消费者提供按Topic和队列分组的逻辑视图,实现高效拉取。
index/ 消息键哈希索引 • 文件:以创建时间戳命名(如20240515080000000)。 • 结构:采用 “哈希槽 + 链表” 结构。 • 用途:支持根据 Message Key 或时间范围进行消息查询,用于运维排查。
config/ 运行时元数据 • 存储Broker运行期间生成的动态数据,如所有Topic的配置消费者组的消费进度(offset) 等。
checkpoint 状态检查点文件 • 记录 commitlog、consumequeue、index等文件最后一次刷盘的时间戳,用于崩溃恢复时确定数据恢复的起点。
abort 异常关闭标志文件 • 该文件存在即表明Broker上一次是非正常关闭,重启时会触发恢复流程。
lock 锁文件 • lock文件确保同一时刻只有一个进程写入,保障严格顺序写。

commitLog

消息主体以及元数据的存储主体, 存储Producer端写入的消息主体内容,消息内容不是定长的。 单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量, 比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;

当我们消息发送到RocketMQ以后,消息在commitLog中,因为body大小是不固定的,所以每个消息的长度也是不固定的,其存储格式如下:

下面每个表格列举了每个字段的含义

字段 字段名 数据类型 字节数 说明与用途
1 MsgLen / TOTALSIZE int 4 消息总长度,即从本字段开始到结束的总字节数,是解析消息的起点。
2 MagicCode int 4 魔术字,固定值(如 0xdaa320a7),用于标识这是一个有效的消息存储起始点,也用于区分消息体文件末尾空白填充区
3 BodyCRC int 4 消息体内容的CRC校验码, 用于校验消息体在存储过程中是否损坏。
4 QueueId int 4 队列ID,标识此消息属于Topic下的哪个逻辑队列。
5 Flag int 4 消息标志位,供应用程序自定义使用,RocketMQ内部未使用。
6 QueueOffset long 8 消费队列偏移量,即此消息在其对应ConsumeQueue中的顺序索引,是连续的
7 PhysicalOffset long 8 物理偏移量,即此消息在所有CommitLog文件中的起始字节偏移量。由于消息长度不定,此偏移量不是连续的
8 SysFlag int 4 系统标志位,是一个二进制组合值,用于标识消息特性,如:是否压缩、是否为事务消息、是否等待事务提交等。
9 BornTimestamp long 8 消息生成时间戳,由Producer客户端在发送时生成。
10 BornHost 8字节 8 消息发送者地址。其编码并非简单字符串,而是将IP的4个段和端口号的2个字节,共6个字节,按大端序组合并填充到8字节中。
11 StoreTimestamp long 8 消息存储时间戳,即Broker收到消息并写入内存的时间。
12 StoreHost 8字节 8 Broker存储地址,编码方式同BornHost。
13 ReconsumeTimes int 4 消息重试消费次数,用于死信队列判断。
14 PreparedTransationOffset long 8 事务消息专用,存储与之关联的事务日志(Transaction Log)的偏移量
15 BodyLength int 4 消息体实际长度,后跟Body内容。
16 Body byte[] 不定 消息体内容,即Producer发送的原始业务数据。
17 TopicLength byte 1 Topic名称的长度(1字节,因此Topic名不能超过255字符)。
18 Topic byte[] 不定 Topic名称的字节数组。
19 PropertiesLength short 2 消息属性长度,后跟Properties内容。
20 Properties byte[] 不定 消息属性,用于存储用户自定义的Key-Value扩展信息。在编码时,Key和Value之间用特殊不可见字符(如\u0001)分隔,因此属性中不能包含这些字符。

ConsumeQueue

消息消费索引,引入的目的主要是提高消息消费的性能。 由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件,根据topic检索消息是非常低效的。

为了解决这个问题中,提高消费时候的速度,RocketMQ会启动后台的 dispatch 线程源源不断的将消息从 commitLog 取出消息在 CommitLog 中的物理偏移量,消息长度以及 Tag Hash 等信息作为单条消息的索引,分发到对应的消费队列,构成了对 CommitLog 的引用。

consumer可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。

consumequeue文件可以看成是基于topic的commitlog索引文件, 故consumequeue文件夹的组织方式如下:

$HOME/store/consumequeue/{topic}/{queueId}/{fileName}

consumequeue文件采取定长设计, 每一个条目共20个字节,前8字节的commitlog物理偏移量、中间4字节的消息长度、8字节tag的hashcode。

indexFile

RocketMQ的IndexFile索引文件提供了通过消息Key或时间区间查询消息的能力,其存储路径为$HOME/store/index/{fileName},其中文件名以创建时间戳命名。单个IndexFile文件大小固定约为400M,可保存2000W个索引,其底层采用类HashMap的哈希索引结构实现。

IndexFile是一个固定大小的文件(约400MB),其物理结构由三部分组成

1.IndexHeader(索引头,40字节)

beginTimestamp: 第一条消息存储时间

endTimestamp: 最后一条消息存储时间

beginPhyoffset: 第一条消息在CommitLog中的物理偏移量

endPhyoffset: 最后一条消息在CommitLog中的物理偏移量

hashSlotCount: 已使用的哈希槽数量

indexCount: 索引单元总数

2.Slots(哈希槽)

每个IndexFile包含500万个哈希槽位,每个Slot槽位(4字节)存储的是链式索引的第一个索引序号,每个槽位可挂载多个索引单元,形成链式结构。

  • 如果Slot值为0:表示该槽位没有索引链
  • 如果Slot值为N:表示该槽位对应的索引链头节点索引序号为N

3.Indexes(索引单元,20字节/个)

每个索引单元包含以下字段:

  • keyHash: 消息Key的哈希值
  • phyOffset: 消息在CommitLog中的物理偏移量
  • timeDiff: 消息存储时间与IndexFile创建时间的差值
  • preIndexNo: 同一哈希槽中前一个索引单元的序号

这个结构和hashmap结构很像,但是支持每个key通过时间排序,就可以进行时间范围的检索。

通过定长索引结构和整体设计可以通过key快速定位索引数据,拿到真实数据的物理偏移量。

4.索引查询流程

消费者通过消息Key查询时,执行以下步骤:

  1. 计算槽位序号slot序号 = key哈希值 % 500万
  2. 定位槽位地址slot位置 = 40 + (slot序号 - 1) × 4
  3. 获取首个索引位置index位置 = 40 + 500万 × 4 + (索引序号 - 1) × 20
  4. 遍历索引链从槽位指向的索引开始,沿preIndexNo链式查找,匹配目标Key并校验时间范围
  5. 获取物理偏移量从匹配的索引单元中读取phyOffset,最终从CommitLog获取完整消息内容

通过此机制,IndexFile实现了基于Key的高效点查和基于时间范围的快速检索。

整体流程

RocketMQ 高性能存储的核心,在于其 “混合存储” 架构,这正是一种精妙的存储层读写分离设计。

其工作流程可以这样理解:

  1. 统一写入,保证极致性能: 所有消息顺序追加写入一个统一的 CommitLog 文件。这种单一的顺序写操作,是它能承受海量消息写入的根本。
  2. 异步构建,优化读取路径: 消息一旦持久化至 CommitLog,即视为安全。随后,后台服务线程会异步地构建出专供消费的 ConsumerQueue(逻辑队列索引)和用于查询的 IndexFile。这相当于为数据建立了高效的“目录”。
  3. 消费消息: 消费者实际拉取消息时,是先读取 ConsumerQueue 找到消息在 CommitLog 中的物理位置,再反查 CommitLog 获取完整消息内容。
  4. 可靠的消费机制: 基于上述持久化保障,配合消费者自身的偏移量管理及Broker的长轮询机制,共同实现了消息的可靠投递与高效获取。

这种 “读写分离” 设计的好处在于:将耗时的写操作(顺序写CommitLog)与复杂的读操作(构建索引、分散查询)解耦,让两者可以异步、独立地进行优化,从而在整体上获得更高的吞吐量和更低的延迟。这体现了“各司其职,异步协同”的经典架构思想。

下图是官方文档的流程图

写入流程

1.消息预处理

基础校验: 检查Topic名称、消息体长度等是否合法。

生成唯一ID: 结合Broker地址和CommitLog偏移量等,生成全局唯一的MsgID。

设置系统标志: 根据消息属性(如是否事务消息、是否压缩)设置SysFlag。

2.CommitLog核心写入

获取MappedFile: 根据当前写入位置,定位或创建对应的1GB内存映射文件。这里采用双重检查锁模式来保证性能和安全。

串行加锁写入: 获取全局或文件级锁(PutMessageLock),确保同一时刻只有一个线程写入文件,严格保证顺序性。

序列化与追加: 将消息按照之前分析的二进制协议, 序列化到MappedByteBuffer中,并更新写入指针。

3.刷盘(Flush)

同步刷盘: 消息写入内存映射区后,会创建一个GroupCommitRequest并放入请求组。写入线程会等待,直到刷盘线程完成该请求对应文件的物理刷盘后,才返回成功给Producer。数据最可靠,但延迟较高。

异步刷盘(默认): 消息写入内存映射区后,立即返回成功给Producer。同时唤醒异步刷盘线程, 该线程会定时或当PageCache中待刷盘数据积累到一定量时,执行一次批量刷盘。性能高,但有宕机丢数风险。

4.异步索引构建

由独立的ReputMessageService线程处理。它不断检查CommitLog中是否有新消息到达。

一旦有新消息被确认持久化(对于同步刷盘是已落盘,对于异步刷盘是已写入映射区),该线程就会读取消息内容。

随后,它会为这条消息在对应的consumequeue目录下构建消费队列索引(记录CommitLog物理偏移量和消息长度),更新index索引文件。

消费流程

1.启动与负载均衡

消费者启动后,会向NameServer获取Topic的路由信息(包含哪些队列、分布在哪些Broker上)。

如果消费者组内有多个实例,会触发队列负载均衡(默认策略是平均分配)。例如,一个Topic有8个队列,两个消费者实例,则通常每个消费者负责消费4个队列。这一步决定了每个消费者“认领”了哪些消息队列。

2.拉取消息循环

每个消费者实例内部都有一个PullMessageService线程,它循环从一个PullRequest队列中获取任务。

PullRequest包含了拉取目标(如Broker-A, 队列3)以及下一次要拉取的位点(offset)。

消费者向指定的Broker发送网络请求,请求体中就携带了这个offset。

3.Broker端处理与返回

Broker收到请求后,根据Topic、队列ID和offset,去查询对应的ConsumeQueue索引文件。

ConsumeQueue中存储的是定长(20字节)的记录,包含消息在CommitLog中的物理偏移量和长度。

Broker根据物理偏移量,从CommitLog文件中读取完整的消息内容,通过网络返回给消费者。

4.消息处理与位点提交

消费者将拉取到的消息提交到内部的消费线程池进行处理,你的业务逻辑就在这里执行。

消费位点的管理至关重要:

位点存储: 位点由OffsetStore管理。在集群模式(CLUSTER) 下,消费位点存储在Broker上;在广播模式(BROADCAST) 下,位点存储在本地。

位点提交: 消费成功后,消费者会异步(默认方式)向Broker提交已消费的位点。Broker将其持久化到store/config/consumerOffset.json文件中。

5.消息重试与死信

如果消息消费失败(抛出异常或超时未返回CONSUME_SUCCESS),RocketMQ会触发重试机制。

对于普通消息,消息会被发回Broker上一个特殊的重试主题(%RETRY%),延迟一段时间(延迟级别:1s、5s、10s…)后再被原消费者组拉取。

如果重试超过最大次数(默认16次),消息会被投递到死信主题(%DLQ%),等待人工干预。死信队列中的消息不会再被自动消费。

一体与分离:Kafka和RocketMQ的核心架构博弈

说起RocketMQ就不能不提起Kafka了,两者都是消息中间件这个领域的霸主,但它们的核心架构设计差异, 直接决定了各自不同的性能特性和适用场景,这也是技术选型时必须深入理解的重点。

核心架构设计差异

Kafka:读写一体的“分区日志”模型, Kafka的架构哲学是极简与统一。 它将每个主题分区抽象为一个仅追加(append-only)的物理日志文件。 生产者和消费者都直接与这个日志文件交互:生产者顺序写入尾部,消费者通过维护偏移量顺序读取。这种设计下,数据的读写路径完全一致, 逻辑与物理结构高度统一。

RocketMQ:读写分离的“二级制”模型 , RocketMQ的架构哲学是分工与优化。 它采用了物理CommitLog + 逻辑ConsumeQueue的二级结构。 所有消息都顺序写入一个统一的CommitLog物理文件,实现磁盘的最高效顺序写。同时,为每个消息队列异步构建一个轻量级的ConsumeQueue索引文件,消费者读取时先查询内存中的ConsumeQueue定位,再到CommitLog中获取消息体。这是一种逻辑与物理分离的设计。

优劣势对比

基于上述架构设计根本差异,两者在关键指标上各显优劣:

维度 Kafka(读写一体) RocketMQ(读写分离)
核心优势 极致吞吐与低延迟:读写同路径,数据写入后立即可读,端到端延迟极低。架构简单:无中间状态,副本同步、故障恢复逻辑清晰。 高并发读与丰富功能:索引与数据分离,支持海量消费者并发读。业务友好:原生支持事务消息、定时/延时消息、消息轨迹查询。
存储效率 磁盘顺序IO最大化:生产和消费都是严格顺序IO,尤其适合机械硬盘。 写性能极致化:所有消息顺序写CommitLog,但存在“写放大” ,一条消息需写多次(1次CommitLog + N次ConsumeQueue)。
读性能 消费者落后时可能触发随机读:若消费者要读取非尾部历史数据,可能需磁盘寻道。但现代SSD和预读机制已大大缓解此问题。 读路径优化:ConsumeQueue小而固定,可全量缓存至内存,读操作变为“内存寻址 + CommitLog顺序/随机读”。在PageCache命中率高时表现优异。
扩展性与成本 文件句柄(inode)开销大:每个分区都是独立目录和文件,海量分区时运维成本高。 存储成本与效率更优:多Topic共享CommitLog,文件数少,特别适合中小消息体、多Topic的场景
典型场景 日志流、指标监控、实时流处理:作为大数据管道,与Flink/Spark生态无缝集成。 电商交易、金融业务、异步解耦:需要严格顺序、事务保障、业务查询的在线业务场景。

总而言之,Kafka像一个设计精良的高速公路系统, 核心目标是让数据车辆(消息)能够高吞吐、低延迟地持续流动,并方便地引向各个处理工厂(流计算)。而RocketMQ则像一个高度可靠的快递网络, 不仅确保包裹(消息)准确送达,还提供预约配送(定时)、签收确认(事务)、异常重投(重试)等一系列服务于业务逻辑的增值功能。

RocketMQ对于随机读取的优化

RocketMQ在消费时候的流程

消费者请求 → ConsumeQueue(内存/顺序)获取commitlog上的物理偏移量 → 根据物理偏移量定位CommitLog(磁盘/随机) → 返回消息

从ConsumeQueue获取到消息在commitlog中的偏移量的时候,回查时候可能产生随机IO

  1. 第一次随机IO: 根据ConsumeQueue中的物理偏移量,在CommitLog中定位消息位置
  2. 可能的连续随机IO: 如果一次拉取多条消息,这些消息在CommitLog中可能物理不连续

为了保证RocketMQ的高性能,采用一些优化措施,尽量避免随机IO

1. ConsumeQueue的内存映射优化

实际上,RocketMQ将ConsumeQueue映射到内存,每个ConsumeQueue约5.72MB,可完全放入PageCache,读索引操作几乎是内存操作。

public class ConsumeQueue {
    private MappedFile mappedFile;  // 内存映射文件
    // 20字节每条:8(offset) + 4(size) + 8(tagHashCode)
}

2. PageCache的充分利用

Linux PageCache工作流程: 

  1. 消息写入CommitLog → 进入PageCache
  2. 消费者读取 → 优先从PageCache获取
  3. 如果PageCache命中:内存速度(≈100ns)
  4. 如果PageCache未命中:磁盘随机读取(≈10ms)

3. 批量读取优化

// DefaultMessageStore.java
public GetMessageResult getMessage(...) {
    // 一次读取多条消息(默认最多32条)
    // 即使这些消息物理不连续,通过批量读取减少IO次数
    for (int i = 0; i < maxMsgNums; i++) {
        // 使用同一个文件channel批量读取
        readMessage(ctx, msgId, consumerGroup);
    }
}

4. 读取顺序性的保持

虽然CommitLog中不同Topic的消息是随机存放的,但同一个Queue的消息在CommitLog中是基本连续的:

Queue1: | Msg1 | Msg3 | Msg5 | ... | 在ConsumeQueue中连续
        ↓      ↓      ↓
CommitLog: | Msg1 | Msg2(T2) | Msg3 | Msg4(T3) | Msg5 |
          ↑_________________________↑
          物理上相对连续,减少磁头寻道

高可用设计:双轨并行的可靠性架构

主从架构(Master-Slave)

经典主从模式: RocketMQ早期采用Master-Slave架构,Master处理所有读写请求,Slave仅作为热备份。这种模式下,故障切换依赖人工干预或半自动脚本, 恢复时间通常在分钟级别。

Dledger高可用集群: RocketMQ 4.5引入的Dledger基于Raft协议实现真正的主从自动切换。 当Master故障时,集群能在秒级(通常2-10秒)内自动选举新Leader,期间消息仍可写入(写入请求会阻塞至新Leader选出)。

多副本机制: 现代部署中,建议采用2主2从或3主3从架构。例如在阿里云上,每个Broker组包含1个Master和2个Slave,形成跨可用区的三副本, 单机房故障不影响服务可用性。

同步/异步复制

同步复制保证强一致(消息不丢失),异步复制追求更高性能。

// Broker配置示例
brokerRole = SYNC_MASTER
// 生产者发送消息后,必须等待至少一个Slave确认
// 确保即使Master宕机,消息也不会丢失
  • 强一致性保证:消息写入Master后,同步复制到Slave才返回成功
  • 性能代价:延迟增加约30-50%,TPS下降约20-40%
  • 适用场景:金融交易、资金变动等对数据一致性要求极高的业务

同步/异步刷盘

同步刷盘保证消息持久化不丢失,异步刷盘提升吞吐。

brokerRole = ASYNC_MASTER
// 消息写入Master即返回成功,Slave异步复制
// 存在极短时间的数据丢失风险
  • 高性能模式: 延迟降低,吞吐量接近单节点性能
  • 风险窗口: Master宕机且数据未同步时,最近几秒消息可能丢失
  • 适用场景: 日志收集、监控数据、可容忍微量丢失的业务消息

刷盘策略的工程优化

同步刷盘(SYNC_FLUSH)

生产者 → Broker内存 → 磁盘强制刷盘 → 返回成功
  • 零数据丢失: 即使机器掉电,消息也已持久化到磁盘
  • 性能瓶颈: 每次写入都触发磁盘IO,机械硬盘下TPS通常<1000
  • 优化手段: 使用SSD硬盘可大幅提升性能

异步刷盘(ASYNC_FLUSH)

生产者 → Broker内存 → 立即返回成功 → 异步批量刷盘
  • 高性能选择: 依赖PageCache,SSD下TPS可达数万至数十万
  • 可靠性依赖: 依赖操作系统的刷盘机制(通常5秒刷盘一次)
  • 配置调优:
# 调整刷盘参数
flushCommitLogLeastPages = 4    # 至少4页(16KB)才刷盘
flushCommitLogThoroughInterval = 10000  # 10秒强制刷盘一次

四、Producer与Consumer:高效的生产与消费模型

Producer

消息路由策略:

// 内置多种队列选择算法
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
// 1. 轮询(默认):均匀分布到所有队列
// 2. 哈希:相同Key的消息路由到同一队列,保证局部顺序
// 3. 机房就近:优先选择同机房的Broker
producer.send(msg, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        // 自定义路由逻辑
        return mqs.get(arg.hashCode() % mqs.size());
    }
});

发送模式对比:

模式 特点 性能 适用场景
同步发送 阻塞等待Broker响应 TPS约5000-20000 重要业务消息,需立即知道发送结果
异步发送 回调通知结果 TPS可达50000+ 高并发场景,如日志、监控数据
单向发送 发送后不等待 TPS最高(100000+) 可容忍少量丢失的非关键数据

失败重试与熔断:

  • 智能重试: 发送失败时自动重试(默认2次),可配置退避策略
  • 故障规避: 自动检测Broker可用性,故障期间路由到健康节点
  • 慢请求熔断: 统计发送耗时,自动隔离响应慢的Broker

Consumer

负载均衡策略:

// 集群模式:同一ConsumerGroup内消费者均分队列
consumer.setMessageModel(MessageModel.CLUSTERING);
// 广播模式:每个消费者消费全量队列
consumer.setMessageModel(MessageModel.BROADCASTING);

消费进度管理:

Broker托管: 默认方式,消费进度存储在Broker

本地维护: 某些场景下可自主管理offset(如批量处理)

重置策略:

// 支持多种消费起点
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);  // 从最后
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 从头
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP);    // 从时间点

并发控制优化:

// 关键并发参数
consumer.setConsumeThreadMin(20);     // 最小消费线程数
consumer.setConsumeThreadMax(64);     // 最大消费线程数
consumer.setPullBatchSize(32);        // 每次拉取消息数
consumer.setConsumeMessageBatchMaxSize(1); // 批量消费大小
// 流控机制
consumer.setPullThresholdForQueue(1000);  // 队列堆积阈值
consumer.setPullInterval(0);              // 拉取间隔(0为长轮询)

五、核心流程与特性背后的架构支撑

1 .顺序消息如何保证?

全局顺序: 单Topic单队列(牺牲并发)。

分区顺序: 通过MessageQueue选择器确保同一业务键(如订单ID)的消息发往同一队列,Consumer端按队列顺序消费。

2.事务消息的两阶段提交

流程详解: Half Message -> 执行本地事务 -> Commit/Rollback。

架构支撑: Op消息回查机制,解决分布式事务的最终一致性,是架构设计中“状态可回溯”思想的体现。

3.延时消息的实现奥秘

并非真正延迟投递: 为不同延迟级别预设独立的SCHEDULE_TOPIC, 定时任务扫描到期后投递至真实Topic。

设计权衡: 以存储和计算换取功能的灵活与可靠。

六、其他性能优化关键技术点

  1. 零拷贝(Zero-copy): 通过sendfile或mmap+write方式,减少内核态与用户态间数据拷贝,大幅提升网络发送与文件读写效率。
  2. 堆外内存与内存池: 避免JVM GC对大数据块处理的影响,实现高效的内存管理。
  3. 文件预热: 启动时将存储文件映射到内存并写入“假数据”,避免运行时缺页中断。

七、总结:RocketMQ架构设计的启示

RocketMQ的架构设计,尤其是其在简洁性、高性能和云原生演进方面的平衡,为构建现代分布式系统提供了许多宝贵启示。

  1. 在简单与完备间权衡: RocketMQ没有采用强一致性的ZooKeeper,而是自研了极其简单的NameServer。这说明在非核心路径上,牺牲一定的功能完备性来换取简单性和高可用性,可能也是个不错的选择。
  2. 以写定存储,以读优查询: 其存储架构是典型的写优化设计。所有消息顺序追加写入,保证了最高的写入性能。而针对消费和查询这两种主要的“读”场景,则分别通过异步构建索引数据结构(ConsumeQueue和IndexFile)来优化。

八、参考资料

往期回顾

1.PAG在得物社区S级活动的落地

2.Ant Design 6.0 尝鲜:上手现代化组件开发|得物技术 

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

4.Go语言在高并发高可用系统中的实践与解决方案|得物技术

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

文 /磊子

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

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

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

手把手实现 Gin + Socket.IO 实时聊天功能

手把手实现 Gin + Socket.IO 实时聊天功能

在 Web 开发中,实时通信场景(如在线聊天、实时通知、协同编辑等)十分常见,而 Socket.IO 作为一款成熟的实时通信库,支持 WebSocket 协议并提供轮询降级方案,能很好地兼容各类浏览器和场景。本文将手把手教你使用 Go 语言的 Gin 框架整合 Socket.IO,搭建一套完整的前后端实时聊天系统,包含房间广播、跨域处理、静态资源托管等核心功能。

一、项目准备

1. 技术栈说明

  • 后端:Go 1.18+、Gin 框架(轻量高性能 HTTP 框架)、googollee/go-socket.io(Socket.IO Go 服务端实现)
  • 前端:原生 JavaScript、Socket.IO 客户端(兼容服务端版本)
  • 运行环境:Windows/Linux/Mac(本文以 Windows 为例,跨平台无差异)

2. 项目目录结构

先搭建规范的项目目录,便于后续开发和维护:

plaintext

chat-demo/
├── go.mod       // Go 模块依赖配置
├── main.go      // 后端核心代码
└── static/      // 前端静态资源目录
    ├── index.html       // 前端聊天页面
    ├── jquery-3.6.0.min.js  // jQuery(可选,本文未实际依赖)
    ├── socket.io-1.2.0.js   // Socket.IO 客户端
    └── favicon.ico      // 网站图标(可选)

3. 初始化 Go 模块

打开终端,进入项目目录,执行以下命令初始化 Go 模块:

bash

运行

go mod init chat-demo

然后安装所需依赖:

bash

运行

# 安装 Gin 框架
go get github.com/gin-gonic/gin
# 安装 Socket.IO Go 服务端
go get github.com/googollee/go-socket.io

二、后端实现:Gin + Socket.IO 服务搭建

后端核心功能包括:Gin 引擎配置、跨域处理、静态资源托管、Socket.IO 服务初始化、房间管理与消息广播。

1. 完整后端代码(main.go)

go

运行

package main

import (
"github.com/gin-gonic/gin"
socketio "github.com/googollee/go-socket.io"
"github.com/googollee/go-socket.io/engineio"
"github.com/googollee/go-socket.io/engineio/transport"
"github.com/googollee/go-socket.io/engineio/transport/polling"
"github.com/googollee/go-socket.io/engineio/transport/websocket"
"log"
"net/http"
)

func main() {
// 1. Gin 引擎优化:生产环境启用 Release 模式,关闭调试日志
gin.SetMode(gin.ReleaseMode)
router := gin.Default()

// 2. 跨域中间件配置:解决前后端跨域通信问题
router.Use(func(c *gin.Context) {
// 允许所有来源跨域(生产环境可指定具体域名,更安全)
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
// 允许的 HTTP 请求方法
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// 允许的请求头
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// 处理 OPTIONS 预检请求
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
return
}
c.Next()
})

// 3. 静态资源托管:映射 static 目录,提供前端页面和静态文件
router.Static("/static", "./static")

// 4. Socket.IO 服务器配置:支持 polling(轮询)和 websocket(优先推荐)
sio := socketio.NewServer(&engineio.Options{
Transports: []transport.Transport{
polling.Default,
websocket.Default,
},
})

// 5. Socket.IO 事件监听:处理连接、消息、加入房间、断开连接等事件
// 5.1 客户端连接事件
sio.OnConnect("/", func(s socketio.Conn) error {
log.Println("客户端已连接:", s.ID())
return nil
})

// 5.2 接收客户端发送的消息事件,并广播到 chat 房间
sio.OnEvent("/", "message", func(s socketio.Conn, msg string) {
log.Println("收到消息:", msg, "(来自:", s.ID(), ")")
// 广播消息到 / 命名空间下的 chat 房间
sio.BroadcastToRoom("/", "chat", "message", msg)
})

// 5.3 客户端加入房间事件
sio.OnEvent("/", "join", func(s socketio.Conn, room string) {
// 让当前客户端加入指定房间
s.Join(room)
log.Println("客户端", s.ID(), "已加入房间:", room)
})

// 5.4 客户端断开连接事件
sio.OnDisconnect("/", func(s socketio.Conn, reason string) {
log.Println("客户端", s.ID(), "已断开连接;原因:", reason)
})

// 5.5 错误处理事件
sio.OnError("/", func(s socketio.Conn, e error) {
log.Println("客户端", s.ID(), "发生错误:", e)
})

// 6. 注册 Socket.IO 路由:将 Socket.IO 请求委托给 Gin 处理
router.GET("/socket.io/*any", gin.WrapH(sio))
router.POST("/socket.io/*any", gin.WrapH(sio))

// 7. 根路径路由:访问 http://127.0.0.1:8080/ 直接返回前端聊天页面
router.GET("/", func(c *gin.Context) {
c.File("./static/index.html")
})

// 8. 启动 Socket.IO 服务器(异步启动,不阻塞 Gin 启动)
go sio.Serve()
defer sio.Close() // 程序退出时关闭 Socket.IO 服务

// 9. 启动 Gin 服务器,监听 8080 端口
if err := router.Run(":8080"); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}

2. 后端核心功能说明

  • Gin 优化:启用 gin.ReleaseMode 关闭调试日志,提升服务性能,适合生产环境部署。

  • 跨域处理:通过自定义中间件设置 CORS 响应头,处理 OPTIONS 预检请求,解决前后端跨域通信障碍。

  • 静态资源托管:通过 router.Static 将 ./static 目录映射到 /static 路由,前端可通过该路径访问 JS、图片等静态资源。

  • Socket.IO 配置:同时支持 polling 和 websocket 传输方式,websocket 为高性能全双工通信,polling 作为降级方案兼容低版本浏览器。

  • 事件处理

    • OnConnect:监听客户端连接,打印客户端唯一 ID;
    • OnEvent("message"):接收客户端消息,并通过 BroadcastToRoom 广播到 chat 房间;
    • OnEvent("join"):处理客户端加入房间请求,通过 s.Join(room) 让客户端加入指定房间;
    • OnDisconnect/OnError:监听客户端断开连接和错误事件,便于问题排查和日志监控。
  • 路由配置:根路径 / 直接返回前端 index.html,无需手动拼接静态资源路径,使用更便捷;Socket.IO 路由注册后,可处理前端的 Socket.IO 连接请求。

三、前端实现:Socket.IO 客户端与页面交互

前端核心功能包括:页面布局搭建、Socket.IO 客户端连接、加入房间、消息发送与接收、页面渲染。

1. 完整前端代码(static/index.html)

html

预览

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Socket.IO 实时聊天示例</title>
    <!-- 引入 jQuery(本文未实际使用,可按需移除) -->
    <script src="/static/jquery-3.6.0.min.js"></script>
    <!-- 引入 Socket.IO 客户端库(需与服务端协议兼容) -->
    <script src="/static/socket.io-1.2.0.js"></script>
    <!-- 网站图标(可选) -->
    <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
</head>

<body>
    <!-- 聊天界面布局:输入框、发送按钮、消息展示区域 -->
    <input type="text" id="message-input" placeholder="输入消息">
    <button id="send-button">发送</button>
    <div id="messages"></div>

    <script>
        // 1. 连接 Socket.IO 服务端
        var socket = io('http://127.0.0.1:8080/', {
            transports: ['websocket', 'polling'], // 优先使用 websocket,降级为 polling
            timeout: 5000 // 连接超时时间:5 秒
        });

        // 2. 监听连接成功事件,连接后立即加入 chat 房间
        socket.on('connect', () => {
            // 发送 join 事件,加入 chat 房间
            socket.emit('join', 'chat');
            console.log('已连接到服务器');
        });

        // 3. 监听服务端广播的 message 事件,渲染消息到页面
        socket.on('message', function (msg) {
            const messagesDiv = document.getElementById('messages');
            const newMessage = document.createElement('p');
            newMessage.textContent = msg;
            messagesDiv.appendChild(newMessage);
        });

        // 4. 绑定发送按钮点击事件,发送消息到服务端
        const sendButton = document.getElementById('send-button');
        const messageInput = document.getElementById('message-input');
        sendButton.addEventListener('click', function () {
            const message = messageInput.value;
            if (message) {
                // 发送 message 事件,携带输入的消息内容
                socket.emit('message', message);
                // 清空输入框
                messageInput.value = '';
            }
        });
    </script>
</body>

</html>

2. 前端核心功能说明

  • Socket.IO 连接:通过 io() 方法连接服务端地址 http://127.0.0.1:8080/,配置传输方式优先级和连接超时时间。
  • 连接成功处理:监听 connect 事件,连接成功后立即发送 join 事件,加入服务端的 chat 房间,确保能接收房间内的广播消息。
  • 消息接收与渲染:监听服务端的 message 事件,收到消息后创建 <p> 标签,将消息内容插入到页面的消息展示区域。
  • 消息发送:绑定按钮点击事件,获取输入框内容,通过 socket.emit('message', message) 发送到服务端,发送后清空输入框,提升交互体验。

四、项目运行与测试

1. 启动服务

  1. 将前端文件(index.htmlsocket.io-1.2.0.js 等)放入 static 目录;

  2. 在项目目录终端执行以下命令启动后端服务:

    bash

    运行

    go run main.go
    
  3. 服务启动成功后,终端会打印日志,监听端口为 8080

2. 测试步骤

  1. 打开多个浏览器窗口(或不同浏览器),访问 http://127.0.0.1:8080/
  2. 在任意一个窗口的输入框中输入消息,点击「发送」按钮;
  3. 观察其他窗口,会实时收到该消息,实现多客户端实时聊天功能;
  4. 查看后端终端,可看到客户端连接、加入房间、接收消息、断开连接等日志信息。

五、常见问题与优化建议

1. 常见问题排查

  • 前后端无法通信:大概率是 Socket.IO 客户端与服务端版本不兼容,建议客户端使用 1.x 或 2.x 版本,与 googollee/go-socket.io 保持协议兼容;
  • 跨域报错:检查后端跨域中间件配置,确保 Access-Control-Allow-Origin 配置正确,生产环境建议指定具体域名而非 *
  • 无法接收广播消息:确认前端已发送 join 事件加入 chat 房间,服务端广播时指定了正确的命名空间和房间名。

2. 优化建议

  • 性能优化:后端可调整 Socket.IO 传输方式优先级,优先使用 websocket;Gin 框架可自定义 http.Server 配置,优化 TCP 连接复用和并发处理能力;
  • 体验优化:前端可添加回车键发送消息、消息区分发送者与接收者、自动滚动到最新消息等功能;
  • 安全优化:生产环境中,跨域配置指定具体域名,添加身份验证(如 Token 验证),防止非法客户端连接;
  • 部署优化:可将静态资源部署到 CDN,提升前端加载速度;后端可使用进程管理工具(如 supervisor)保障服务稳定运行。

六、总结

本文通过 Gin 框架与 Socket.IO 的整合,实现了一套完整的前后端实时聊天系统,核心亮点如下:

  1. 后端完成了跨域处理、静态资源托管、Socket.IO 事件监听与房间广播;
  2. 前端实现了 Socket.IO 连接、房间加入、消息发送与接收渲染;
  3. 项目结构清晰,代码可直接复用,支持多客户端实时通信,可扩展为在线客服、实时通知等场景。

通过本文的实战,你不仅能掌握 Gin 与 Socket.IO 的使用方法,还能理解实时通信的核心原理,为后续复杂实时系统的开发打下坚实基础。

面试官: “ 说一下你对 Cookie 的理解 ? ”

1. 什么是 Cookie

Cookie(小甜饼)是 服务器发送给浏览器并保存在客户端的一小段数据,用于:

  • 记录用户状态(如登录信息、购物车内容)
  • 跟踪用户行为(如浏览历史、广告推送)
  • 存储少量配置信息(如主题偏好、语言设置)

特点:

  • 大小限制:通常每个 Cookie 最大 4KB
  • 数量限制:每个域名一般最多 50 个 Cookie
  • 自动携带:浏览器在访问同一域名时会自动将 Cookie 附加在请求头中发送给服务器

2. Cookie 的结构

一个 Cookie 通常包含以下字段:

字段名 说明
Name Cookie 的名称
Value Cookie 的值(通常经过 URL 编码)
Domain 可以访问该 Cookie 的域名
Path 可以访问该 Cookie 的路径
Expires / Max-Age Cookie 的过期时间(Expires 是具体日期,Max-Age 是秒数)
HttpOnly 如果设置,Cookie 不能通过 JavaScript 访问(防止 XSS 攻击)
Secure 如果设置,Cookie 只能通过 HTTPS 传输
SameSite 控制跨站请求时是否发送 Cookie(防止 CSRF 攻击)

3. Cookie 的工作流程

  1. 服务器发送 Cookie : 当浏览器第一次访问服务器时,服务器在响应头中添加:

    Set-Cookie: username=Tom; Path=/; HttpOnly; Secure
    

    浏览器收到后会将该 Cookie 保存到本地。

  2. 浏览器存储 Cookie: Cookie 会被保存在浏览器的某个文件或内存中,根据 Domain 和 Path 来区分。

  3. 浏览器发送请求时携带 Cookie : 之后每次访问同一域名和路径时,浏览器会自动在请求头中添加:

    Cookie: username=Tom
    

    服务器通过读取这个 Cookie 来识别用户。


4. Cookie 的分类

(1)会话 Cookie(Session Cookie)

  • 没有设置 Expires 或 Max-Age
  • 浏览器关闭后自动删除
  • 常用于保存短期会话信息(如登录状态)

(2)持久 Cookie(Persistent Cookie)

  • 设置了 Expires 或 Max-Age
  • 在过期时间前一直有效,即使浏览器关闭
  • 常用于保存长期信息(如记住登录、用户偏好)

5. Cookie 的优缺点

优点

  • 简单易用:服务器和浏览器都原生支持
  • 自动携带:无需手动处理
  • 轻量级:适合存储少量数据

缺点

  • 容量小:每个 Cookie 最大 4KB

  • 安全性差

    • 容易被窃取(XSS 攻击)
    • 容易被伪造(CSRF 攻击)
  • 性能影响

    • 每次请求都会携带,增加带宽消耗
    • 过多 Cookie 会影响页面加载速度

6. Cookie 的安全设置

为了提高安全性,建议设置以下属性:

  • HttpOnly:防止 JavaScript 访问(减少 XSS 风险)

  • Secure:只在 HTTPS 连接中传输

  • SameSite

    • Strict:仅在同站请求时发送
    • Lax:允许部分跨站请求(如 GET 表单提交)
  • 合理的过期时间:短期 Cookie 减少被盗风险


7. 示例:使用 Cookie

(1)服务器设置 Cookie(Node.js + Express)

const express = require('express');
const app = express();

app.get('/login', (req, res) => {
  // 设置一个会话 Cookie
  res.cookie('username', 'Tom', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'Strict',
    maxAge: 3600000 // 1 小时
  });
  res.send('登录成功');
});

app.get('/profile', (req, res) => {
  // 读取 Cookie
  const username = req.cookies.username;
  if (username) {
    res.send(`欢迎你,${username}`);
  } else {
    res.send('请先登录');
  }
});

app.listen(3000);

(2)浏览器查看 Cookie

  • Chrome:F12 → Application → Cookies
  • Firefox:F12 → Storage → Cookies

8. Cookie 与 Token 的区别

特性 Cookie Token
存储位置 浏览器 浏览器(LocalStorage/SessionStorage)或 App
传输方式 自动在 HTTP 请求头中发送 需要手动在请求头中添加(如 Authorization: Bearer <token>
容量限制 每个 Cookie 最大 4KB 无固定限制(但过大影响性能)
安全性 较低(易受 XSS/CSRF 攻击) 较高(可结合 HttpOnly、Secure、SameSite)
适用场景 简单会话管理、短期状态存储 复杂认证授权、跨域请求、移动应用

✅ 总结

  • Cookie 是服务器保存在浏览器的一小段数据,用于记录状态和跟踪用户
  • 自动携带在请求头中,方便但有容量和安全限制
  • 常用于简单会话管理,现代 Web 开发中更多与 Token 结合使用
  • 安全使用需设置 HttpOnlySecureSameSite 等属性

🏆2025 AI/Vibe Coding 对我的影响 | 年终技术征文

img_v3_02t9_51022880-c5f3-4f3c-8892-b6f6ca1ba8eg.png

当岁末的钟声临近,我们又站在了一年的重点回望。2025年,对你而言,是怎样的轮廓呢?

它或许是由一行行被AI重构的代码勾勒,是某个深夜与新技术“顿悟时刻”的灵光一现,也可能是生活中因为智能体工具而悄然改变的工作状态。

从智能体(Agent)的横空出世到多模态技术的经验突破,技术愈加深入地流淌尽我们的工作和生活日常,塑造着属于每个人的独特“Vibe”。

今天,稀土掘金正式发起这场专属社区的年终“围炉夜话” ,我们不谈宏大的行业预言,不设苛刻的评审框架。我们只想邀请您,记录下属于你的数字年轮。

✨主题 :2025 AI/Vibe Coding对我的影响

本次征文不再是一场竞赛,而是一次社区的年终“围炉夜话”。我们鼓励所有成员停下脚步,回望2025年技术与个人生活交织的痕迹。无论是代码世界的深刻变革,还是AI工具带来的细微习惯改变,每一次记录,都是我们共同的“数字年轮”。

⁉️如何参与

发布文章时选择带话题 #2025 AI/Vibe Coding 对我的影响# 就可以被统计到哦💁🏻

image.png

🗒️赛道说明

参与者可以任选其一进行投稿,也可同时参与,不限制投稿文章数。

赛道一:2025,我的“Vibe Coding”时刻

主题解读

聚焦技术对“我”的个体影响。可以是一个AI工具、一个开源项目、一次技术决策、一种行业趋势,也可以是技术带来的某种生活方式。

内容方向建议

  • Agent革命

ChatGPT/Gemini如何改变我的开发效率,大模型如何让我的代码如鱼得水?(技术冲破对工作模式/效率带来的影响,任意模型/技术皆可)

  • 生活Vibe

Web3、数字游民等概念开启的新生活尝试,分享自己用AI生成旅行攻略、创作音乐、辅导孩子学习的故事等;

  • 年度总结

以个人视角,复盘你在某个技术领域(前端、后端、AI、运维等)或者生活上这一年的得失与观察。

赛道二:「我和TRAE的这一年」

主题解读

一个针对TRAE的专属回忆录,记录掘金社区TRAE深度用户与其共同成长的故事。

内容方向建议

  • 成长见证

记录自己使用TRAE完成的第一篇文章、第一个获赞、第一次参与开源项目、第一次开发的游戏等;

  • 连接故事

通过社区所结识到的志同道合的TRAE友、参与的线下/线上活动、与TRAE官方运营互动的有趣瞬间;

  • 产品共建

对TRAE功能提出的建议被采纳,或使用各个模式提升学习效率的真实体验瞬间。

❗参加赛道二 的文章除话题 #2025 AI/Vibe对我的影响 外,必须加上#TRAE标签

image.png

☝️征文要求

本次征文活动需要符合主题,对于文章类型以及活动参与方式,我们有以下几点小要求。

  1. 文章须为原创文章,内容符合掘金社区的内容标准和规范

  2. 本次征文不限制文章题材,可以是你对行业的总结和见解,也可以是你对行业未来发展的预测,本次活动不接受下面几种文章:

    1. 资源聚合类文章,例如 Awesome-List;
    2. 翻译类文章和利用AI生产文章;
    3. 与本次主题无关的文章;
    4. 学习笔记/知识点汇总类文章;
    5. 有失中立性、公正性的内容,比如由公司或者公司的代理机构(如公关公司)所撰写,单纯希望宣传自己的商业产品或者公司的内容;
    6. 内容与活动主题不符、非原创内容,有洗稿、营销软文、广告、抄袭嫌疑的文章。
  3. 刷赞、刷量等有作弊行为的文章,直接取消比赛资格,不参与评选;

  4. AI代写文章、AI聊天记录等不计入活动(AI检测超过70% 取消文章获奖资格);

  5. 活动文章需要选择话题:juejin.cn/theme/detai…

  6. 获奖作品,著作权归作者所有,掘金拥有使用权;

🥇奖项设置

文章将根据专家评审得分(占比70%)和文章热度(占比30%)得分加权计算。未获得官方推荐的文章不参与奖项评选。

奖项名称 奖项设置 获奖人数 奖品名称 奖品图
主赛道 优秀文章奖(前10篇) 10名 小熊(Bear)电烧烤炉 多功能料理锅电烤炉 DKL-D12A1 image.png
掘金达人奖(11-40篇) 30名 稀土掘金 Yoyo抱枕新版-450g
阳光普照奖(41-100篇) 60名 稀土掘金 x ByteMall 联名盲盒
TRAE特别赛道鼓励奖(叠加) TOP1-10 10名 价值200元 TRAE连帽卫衣+抱枕
TOP11-30 20名 价值100元 TRAE圆领卫衣
TOP31-50 20名 价值50元 TRAE单肩包
TOP51-100 50名 TRAE小鼠标垫

🤵征文对象

掘金社区全体掘友

⏰活动时间

2025年12月26日——2026年1月25日23:59

👀 活动Q&A

Q1:投稿数量有限制吗?

A:没有。

Q2:一篇文章可以同时参与多个社区活动吗?

A:不行。一篇文章只能参与一个社区活动,可以写多篇文章参与不同的活动。

Q3: 本次活动中写了多篇文章,可以多次获奖吗?

A:参加特别赛道的文章,同时参与主赛道激励文章排名,中奖奖品兼得。但是,主赛道奖项一个用户只能获得一次,多篇文章上榜时选取最高名次获奖。

Q4:如何算是成功参与了活动?

A:活动期间,在掘金平台发布2025 AI/Vibe 对我的影响年终技术征文,选择带话题  #2025 AI/Vibe Coding 对我的影响#  ,即可成功参与活动。注意话题≠标签!

💡注意事项

2026年1月25日活动结束后,约10-15个工作日通过系统消息的形式公示获奖情况,预计填写问卷后的20个工作日内完成奖品发放。

技术浪潮奔涌向前,但属于个体的记忆同样珍贵。让我们用文字,为这不平凡的一年按下存档键。

来掘金,写下你的2025。你的故事,我们都在听。

注:最终解释权归稀土掘金技术社区 官方所有

为什么"Web3"是下一代互联网?——从中心化到去中心化的转变

🌐 为什么"Web3"是下一代互联网?——从中心化到去中心化的转变 🚀

大家好,我是无限大,欢迎收看十万个为什么系列文章

希望今天的内容能对大家有所帮助

今天咱们来聊聊Web3这个"互联网的下一代"!想象一下,你在社交媒体上发的照片被平台随意删除;你辛苦创作的内容,平台说下架就下架;你的个人数据被平台偷偷卖钱——这些糟心的体验,都是Web2时代的痛点!而Web3,就是要解决这些问题!

🤔 核心问题:Web3和Web2有什么区别?为什么需要去中心化互联网?

很多人觉得Web3是"虚无缥缈的概念",其实Web3离我们很近!Web3就像"互联网的民主革命",让用户真正拥有自己的数据和内容,而不是被平台控制。

Web3的本质

Web3是一种去中心化的互联网,基于区块链技术,强调用户数据所有权、去中心化应用和价值互联网。它就像"把互联网从公司手里还给用户",让每个人都能公平地参与和受益。

为什么需要去中心化互联网?

  • 🔑 数据所有权:用户真正拥有自己的数据,不再被平台垄断
  • 🚫 拒绝审查:内容和应用不容易被随意删除或下架
  • 💰 价值回归:创作者可以直接获得收益,中间没有平台抽成
  • 🔗 互操作性:不同应用之间可以无缝协作,没有"围墙花园"
  • ⚖️ 公平参与:每个人都可以参与网络建设,获得相应的奖励

📜 互联网的"进化史":从只读到价值互联网

1. 📖 Web1:"只读互联网"(1990-2004)

Web1时代,互联网就像"只读的百科全书",用户只能浏览内容,不能发布或交互。网站都是静态的,内容由少数人创建。

这就像"只能看不能写的黑板报",你只能看别人写的内容,自己不能上去画。代表网站:早期的雅虎、新浪、网易。

2. 💬 Web2:"读写互联网"(2004-2020)

Web2时代,互联网变成了"互动的社交媒体",用户可以发布内容、评论、分享。但所有数据都保存在平台的服务器上,平台拥有绝对控制权。

这就像"你在别人家里写日记",虽然你可以写,但本子是别人的,别人可以随意翻看、修改甚至销毁你的日记。代表平台:Facebook、微信、抖音、淘宝。

3. 💰 Web3:"价值互联网"(2020-至今)

Web3时代,互联网进化为"价值交换网络",用户真正拥有自己的数据和内容。基于区块链技术,所有数据都保存在去中心化的网络中,没有人能随意控制。

这就像"你在自己家里写日记",本子是你自己的,想怎么写就怎么写,别人没有权利干涉。代表应用:以太坊、Uniswap、OpenSea、Decentraland。

🔧 技术原理:Web3的核心技术

1. ⛓️ 区块链底层技术:"去中心化的数据库"

区块链是Web3的"地基",它是一种去中心化的分布式账本,所有交易都被记录在多个节点上,没有人能随意篡改。

区块链的核心特性

  • 📝 不可篡改:一旦记录,就无法修改
  • 🔗 去中心化:没有中心服务器,所有节点平等
  • 🔒 加密安全:使用密码学保证数据安全
  • ⚖️ 透明公开:所有交易都可以公开查询

2. 📱 去中心化应用(DApp):"不被控制的应用"

DApp是Web3的"应用层",它运行在区块链上,不依赖任何中心化服务器。DApp的代码是开源的,任何人都可以审查和使用。

DApp的特点

  • 🔓 开源代码:所有代码都可以公开查看
  • 🚫 无单点故障:不会因为某个服务器故障而停止运行
  • 🔑 用户控制:用户掌握自己的私钥,拥有完全控制权
  • 自动执行:使用智能合约自动执行规则

代码实例:用Python调用以太坊区块链API

from web3 import Web3

# 连接到以太坊测试网络
web3 = Web3(Web3.HTTPProvider('https://sepolia.infura.io/v3/YOUR_API_KEY'))

# 检查连接是否成功
if web3.is_connected():
    print("✅ 成功连接到以太坊测试网络")
  
    # 获取当前区块号
    block_number = web3.eth.block_number
    print(f"当前区块号:{block_number}")
  
    # 获取账户余额
    account = "0x742d35Cc6634C0532925a3b81643FeD747a70a7D"
    balance_wei = web3.eth.get_balance(account)
    balance_eth = web3.from_wei(balance_wei, 'ether')
    print(f"账户 {account} 的余额:{balance_eth:.6f} ETH")
  
    # 获取最新区块信息
    latest_block = web3.eth.get_block('latest')
    print(f"最新区块哈希:{latest_block.hash.hex()}")
    print(f"最新区块包含交易数:{len(latest_block.transactions)}")
else:
    print("❌ 连接以太坊网络失败")

运行结果

✅ 成功连接到以太坊测试网络
当前区块号:5000000
账户 0x742d35Cc6634C0532925a3b81643FeD747a70a7D 的余额:0.123456 ETH
最新区块哈希:0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
最新区块包含交易数:128

3. 🔑 用户数据所有权:"数据是你的资产"

在Web3中,用户真正拥有自己的数据。数据不再保存在平台的服务器上,而是保存在区块链上,用户通过私钥控制自己的数据。

数据所有权的优势

  • 📊 数据价值回归:用户可以将自己的数据变现
  • 🔒 隐私保护:用户可以选择分享哪些数据
  • 🚫 防止滥用:平台不能随意使用用户数据
  • 💸 数据资产化:数据可以作为资产进行交易

📊 趣味对比:Web2 vs Web3

对比项 Web2(中心化互联网) Web3(去中心化互联网)
数据所有权 数据归平台所有 数据归用户所有
应用控制 平台控制,想下架就下架 社区治理,开源透明
收益分配 平台拿走大部分收益 创作者直接获得收益
内容审查 平台决定内容生死 社区自治,抗审查
隐私保护 平台收集并滥用数据 用户掌握隐私控制权
参与门槛 平台决定谁可以参与 开放,任何人都可以参与
互操作性 平台之间相互隔离 应用之间无缝协作
信任机制 信任平台 信任代码和算法

🏢 Web3的应用场景:已经到来的未来

Web3已经在多个领域开始应用,改变着我们的生活方式:

应用场景 代表项目 Web3的优势
🛍️ 去中心化金融(DeFi) Uniswap、Aave 无需中介,更低手续费
🎨 数字艺术品(NFT) OpenSea、Foundation 真正的所有权,不可篡改
🎮 链游 Axie Infinity、Decentraland 玩游戏也能赚钱
📱 社交媒体 Lens Protocol、Mastodon 用户拥有数据,内容抗审查
🏠 元宇宙 Decentraland、The Sandbox 去中心化的虚拟世界
🔍 搜索 Presearch 隐私保护,用户控制数据
💼 身份认证 Civic、uPort 去中心化身份,更安全

🔍 常见误区纠正

1. "Web3就是区块链,区块链就是Web3?"

不!Web3是一种互联网理念,区块链是实现Web3的技术之一。Web3还包括其他技术,比如IPFS、DAO等。

2. "Web3就是炒币,就是割韭菜?"

不!炒币只是Web3的一小部分,Web3的核心是去中心化和用户数据所有权。真正的Web3应用正在改变各个行业。

3. "Web3太复杂,普通人用不了?"

不!随着技术的发展,Web3应用的易用性正在不断提高。就像早期的互联网一样,Web3会变得越来越简单易用。

4. "Web3会完全取代Web2?"

不!Web3和Web2会长期共存,就像现在Web2和Web1共存一样。Web3会在某些领域取代Web2,但不会完全取代。

5. "去中心化就一定比中心化好?"

不一定!去中心化和中心化各有优缺点。去中心化更安全、更公平,但效率可能较低;中心化效率更高,但容易被滥用。

🔮 未来展望:Web3的发展趋势

1. 🤖 AI + Web3:"智能+去中心化"

AI和Web3的结合会创造出更智能、更公平的应用。比如,AI可以帮助用户管理Web3资产,Web3可以让AI更加透明和可控。

2. 📱 移动Web3:"人人都能使用"

随着Web3钱包和DApp的移动端优化,Web3会变得更加普及。就像现在的移动互联网一样,Web3会通过手机走进每个人的生活。

3. 🏛️ DAO治理:"社区当家作主"

DAO(去中心化自治组织)会成为Web3的重要治理形式。用户可以通过DAO参与项目决策,真正实现"社区当家作主"。

4. 🔗 跨链互操作:"互联互通的Web3"

不同区块链之间的互操作性会越来越强,用户可以在不同区块链之间自由转移资产和数据,实现真正的"互联互通"。

5. 📊 数据经济:"你的数据就是你的资产"

用户的数据会真正成为可交易的资产。用户可以选择将自己的数据出售给需要的企业,获得相应的收益。

🎓 互动小测验:你答对了吗?

问题 答案 你答对了吗?
Web3的核心是什么? 去中心化和用户数据所有权 ✅/❌
Web1、Web2、Web3分别是什么? 只读互联网、读写互联网、价值互联网 ✅/❌
全球Web3钱包用户数量超过多少? 1亿 ✅/❌
去中心化交易所日交易量达多少? 50亿美元 ✅/❌
DApp的特点是什么? 开源、去中心化、用户控制 ✅/❌

🎯 结语:互联网的民主革命

Web3的发展,就是互联网从"中心化控制"到"去中心化自治"的革命。它让用户真正拥有自己的数据和内容,让互联网变得更加公平、透明、开放。

虽然Web3还处于早期阶段,存在很多问题和挑战,但它代表了互联网的未来方向。就像20年前的Web2一样,Web3会逐渐改变我们的生活方式。

下次使用互联网时,不妨想想:你的数据属于谁?你真的拥有自己的内容吗?Web3或许能给你一个更好的答案!


💬 互动话题

  1. 你用过Web3应用吗?体验如何?
  2. 你觉得Web3会取代Web2吗?为什么?
  3. 你最期待Web3在哪个领域的应用?

快来评论区聊聊你的想法!💬 点赞收藏不迷路,咱们下期继续探索计算机的"十万个为什么"!🎉

关注我,下期带你解锁更多计算机的"奇葩冷知识"!🤓

SSE与流式传输(Streamable HTTP)

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:销毁还是隐藏,如何抉择?

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 真的有那么大差距吗 🫠🫠🫠

我正在开发 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 模式

前言

上一篇我们介绍了 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-…

相关资料

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

导读

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

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

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

一、概述

简介

设计模式(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.从数字到版面:得物数据产品里数字格式化的那些事

文 /忘川

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

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

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

❌