普通视图

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

前端向架构突围系列 - 基建与研发效能 [10 - 1]:物料体系的工程化治理

2026年2月11日 14:00

前言

很多公司吹嘘自己有“自研组件库”,点开一看,其实就是把 Ant Design 的按钮改了个颜色,再套个壳。

真正的物料基建,不是为了解决“按钮长什么样”,而是为了解决“为什么我的项目里有 18 个逻辑一模一样的搜索框,且没一个能直接复用”。如果你的基建不能让业务开发在面对 PM 的奇葩需求时少写 50% 的代码,那它就是个摆设。

image.png


一、 认知突围:物料是“业务逻辑”的载体

在架构师眼中,物料的治理不应停留在 UI 视觉层,而应深入到业务语义层

1.1 从“工具包”到“资产库”

  • UI 组件 (Low Level): 解决的是“样式统一”。(如:Modal, Select)

  • 业务物料 (High Level): 解决的是“行为统一”。

    • 例子: “用户选择器”不仅是一个下拉框,它背后关联着:接口鉴权、防抖搜索、分页加载、头像渲染。
    • 深度治理: 如果每个业务线都自己写一遍这套逻辑,那就是 10 倍的维护成本。

1.2 为什么大部分物料库会走向“腐烂”?

  • 过度封装: 为了支持所有场景,给一个组件开了 50 个 Props,最后代码里全是 if/else
  • 文档滞后: 开发者看文档像是在猜灯谜,最后发现“看源码比看文档快”。
  • 版本割裂: 核心库升级了,业务线不敢升,最后全公司跑着 5 个版本的组件库。

二、 深度工程化:物料的“生产流水线”

要让物料体系真正流转起来,架构师必须构建一套**“非人治”**的自动化链路。

2.1 基于 AST 的“自动化元数据提取”

别再让开发者手写文档了。利用 TypeScript 的编译器 API(Compiler API),在物料发布时自动扫描源码:

  • 自动提取 Props 定义、注释、默认值。
  • 自动生成 API 表格。
  • 自动识别依赖项。
  • 意义: 确保“代码即文档”,从根源上消灭文档与代码不一致的问题。

2.2 视觉回归测试:基建的“保险杠”

在企业级治理中,你最怕的就是:改了 A 组件的一个边距,结果 B 业务线的老页面直接塌陷了。

  • 方案: 引入 Visual Regression Testing(如 Playwright + Pixelmatch)。
  • 实战: 在 CI 环节,自动化对比组件修改前后的像素差异。哪怕只是偏移了 1px,也要在 PR 阶段被拦截。

三、 治理逻辑:如何让物料“好找且敢用”

3.1 建立“物料索引市场” (Discovery System)

如果一个物料不能在 30 秒内被开发者搜到,那它就不存在。

  • 智能搜索: 不止搜名称,更要搜“功能描述”。(搜“上传照片”,能关联出“图片裁剪”和“头像上传”)。
  • 在线 Sandbox: 必须提供即时预览代码试运行。开发者应该在“买”之前,先在浏览器里把玩一下。

3.2 影子测试与灰度策略

核心物料升级时,利用 Babel 插件Webpack 插件,在编译阶段分析业务代码的覆盖情况。

  • 深度实践: 统计哪些业务方使用了该物料的哪些属性。如果某属性没有任何人用,直接在下一版本废弃(Deprecate),保持物料库的“轻盈”。

四、 架构师的权衡:标准化 vs 灵活性

这是一个经典的架构陷阱:物料封装得越死,复用性越高,但灵活性越差。

4.1 经典陷阱:“千手观音”组件

想象一下,你们团队需要一个“开关 (Switch/Toggle)”组件。

起初(标准化阶段): 基础架构组设计了一个极其标准的 <StandardSwitch />。它只有两个属性:checkedonChange。样式是写死的:圆角、蓝色背景。大家用得很开心,规范统一。

后来(灵活性需求爆发):

  • 业务线 A:我们的产品主色调是红色,能改颜色吗?
  • 业务线 B:我们要搞促销活动,这个开关得是方形的,里面还要加个文字图标。
  • 业务线 C:我们需要把开关放在一个极小的空间里,尺寸能自定义吗?

结果(架构腐化): 为了满足这些需求,<StandardSwitch /> 被迫增加了几十个 Props:color, borderRadius, size, showIcon, iconContent...

最终,这个组件变成了一个长着无数只手的“千手观音”,内部充斥着复杂的样式判断逻辑,维护成本极高,且性能堪忧。

4.2 破局思路:Headless (无头) 组件

Headless 的核心思想是:将“逻辑的脑子”与“渲染的皮囊”彻底分离。

  • 有头组件 (Traditional): 买电脑送显示器。你想换个 4K 屏?对不起,主机和屏幕焊死在一起了。
  • 无头组件 (Headless): 只卖主机。你爱接 4K 屏、带鱼屏还是投影仪,随你便。

架构图解:分离的艺术

我们来看下 Headless 模式下,组件的分层架构:

如上图所示:

  • 底层 (Headless 逻辑层): 封装了所有“脏活累活”。比如,开关的状态切换、按空格键触发切换、盲人阅读器的 aria-checked 属性支持等。这些逻辑是通用的,与 UI 无关。
  • 顶层 (UI 渲染层): 完全由业务方自己决定。他们可以使用 <div>, <span>, CSS-in-JS, Tailwind CSS,想画成圆的就画成圆的,想画成方的就画成方的。

4.3 代码实例:从“千手观音”到“灵活组装”

我们用 React Hooks 来演示一下这个转变(Vue 的 Composition API 同理)。

场景:实现一个 Switch 开关

1. 定义 Headless Hook (只管逻辑):

这个 Hook 包含了开关的所有核心能力,但不涉及任何 DOM 和 CSS。

// useSwitch.ts (物料库提供)
import { useState, useCallback } from 'react';

export function useSwitch(initialState = false) {
  // 1. 状态管理
  const [isOn, setIsOn] = useState(initialState);

  // 2. 交互逻辑
  const toggle = useCallback(() => setIsOn(v => !v), []);

  // 3. 辅助功能 (A11y) 属性生成器
  const getSwitchProps = () => ({
    role: 'switch',
    'aria-checked': isOn,
    tabIndex: 0,
    onClick: toggle,
    onKeyDown: (e: React.KeyboardEvent) => {
      if (e.key === ' ' || e.key === 'Enter') {
        e.preventDefault();
        toggle();
      }
    }
  });

  // 只返回状态和逻辑方法
  return { isOn, toggle, getSwitchProps };
}

2. 业务方 A 的实现(标准圆角蓝风格):

业务方拿到了逻辑,自己决定怎么渲染。

// BusinessA_Switch.jsx (业务方 A 自定义)
import { useSwitch } from '@my-org/hooks';
import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: ${props => props.isOn ? 'blue' : 'gray'};
  border-radius: 9999px; // 圆角风格
  // ... 其他样式
`;

export function StandardSwitch() {
  // 使用 Headless 能力
  const { isOn, getSwitchProps } = useSwitch();

  return (
    // 将逻辑属性解构赋值给 UI 元素
    <StyledButton isOn={isOn} {...getSwitchProps()}>
      <span className="thumb" />
    </StyledButton>
  );
}

3. 业务方 B 的实现(方形红色促销风格):

业务方 B 可以完全复用逻辑,画出截然不同的 UI。

// BusinessB_PromoSwitch.jsx (业务方 B 自定义)
import { useSwitch } from '@my-org/hooks';

export function PromoSwitch() {
  const { isOn, getSwitchProps } = useSwitch();

  return (
    // 使用 Tailwind 编写完全不同的方形样式
    <div
      {...getSwitchProps()}
      className={`${isOn ? 'bg-red-500' : 'bg-zinc-300'} w-16 h-8 rounded-none flex items-center cursor-pointer`}
    >
       <div className="bg-white w-6 h-6 mx-1 rounded-none">
         {isOn ? '开' : '关'}
       </div>
    </div>
  );
}

4.4 总结

通过 Headless 模式,架构师完成了对权力的完美让渡:

  • 架构师守住了底线: 核心交互逻辑、状态流转、可访问性标准被统一封装,不会因为业务方的 UI 定制而产生逻辑 Bug。
  • 业务方得到了自由: 他们再也不用为了改个颜色而去求基础架构组加 Props 了。

这就是那句格言的深层含义:

“给业务方留一扇窗(UI 自定义能力),他们就不会想拆掉你的墙(核心逻辑封装)。”


五、 总结:从“重复造轮子”到“按需组装”

5.1 研发复利:架构师的“长期主义”

在一般的团队中,工作量是随项目数量线性增长的;而在拥有顶级物料治理的团队里,工作量曲线应该是对数级的。

研发成本=首次沉淀成本+(极低的单次复用成本+边际维护成本)研发成本 = \text{首次沉淀成本} + \sum (\text{极低的单次复用成本} + \text{边际维护成本})

  • 初期(高投入): 你可能花了 2 周才磨合出一个完美的、Headless 架构的“财务大搜表”物料。
  • 后期(高回报): 当公司要开 5 个新的后台管理页面时,开发者只需要花 10 分钟引入物料并配置 Schema。由于物料已经在 100 个场景下跑过,其**健壮性(Robustness)**是任何新写的代码都无法比拟的。

5.2 案例对比:从“冷启动”到“一键飞升”

我们来看一个实际的业务场景:实现一个带“权限控制”和“自动重试”功能的图片上传组件。

维度 传统“造轮子”模式 (No Infrastructure) “按需组装”模式 (Material Asset)
开发耗时 3 - 5 小时(找文档、调 API、写逻辑、调样式) 5 - 10 分钟(拖拽组件,填写 API Key)
代码量 150+ 行(逻辑散落在各个组件中) 3 行(纯声明式配置)
稳定性 极低(不同人写的代码,异常处理逻辑不一) 极高(物料自带熔断、重试、OSS 分片逻辑)
可维护性 噩梦(后端 API 一改,全项目全局搜索替换) 轻松(物料中心统一升级,全线同步生效)
  • 向上管理: 用数据告诉老板,你搞的基建到底省了多少钱(换算成工时)。
  • 向下优化: 如果某个物料的使用率为 0,说明要么是不好用,要么是没推广,架构师应及时止损,将其踢出资产库。

结语:交付的最后一公里

研发效能不是看你写代码有多快,而是看从“代码提交”到“用户可用”的总时长(Lead Time)。

1. 现状对比:手工业 vs. 工业化流水线

如果交付链路不打通,你的基建就像是在泥潭里开跑车。

环节 人肉运维 (手工时代) DevOps 交付 (架构时代)
构建 开发者在本地执行 npm build,环境不一致导致“我本地明明是好的”。 云端环境统一构建(Docker 镜像),保证 100% 环境一致。
部署 找运维开权限,手动 FTP 上传或 SSH 到服务器执行 git pull 合入即部署。代码通过测试自动触发部署,开发者无需关注服务器。
配置 手动修改 Nginx 转发规则、配置跨域、刷新 CDN 缓存。 配置即代码 (IaC) 。Nginx 和路由配置随代码版本走,一键生效。
回滚 “快!把刚才那个备份压缩包覆盖回去!”(手忙脚乱中可能覆盖错版本)。 秒级回滚。通过镜像版本切换,一键回到任何稳定时刻。

2. 图解:交付链路的“能量损耗”

我们可以用一个“水管模型”来理解交付瓶颈:

警示:

如果中间那段“交付管道”是细窄的、堵塞的,那么你前端基建做得再大(漏斗再宽),最终流向用户的价值流速依然由那根细管子决定。

前端向架构突围系列 - 设计与落地 [9 - 3]:BFF (Backend for Frontend) 层与 Serverless 的架构融合

2026年2月11日 09:34

写在前面

传统的开发模式中,前端是后端的“消费者”。后端喂什么,我们就吃什么。

这种模式在移动端、Web 端、小程序多端并行的今天,效率极低。BFF (Backend for Frontend) 的核心哲学是:谁消费,谁负责。 既然 UI 是前端设计的,那么为了支撑这个 UI 而存在的数据聚合层,也理应由前端来主导。

image.png


一、 为什么前端要搞 BFF?(告别“接口乞讨”)

在没有 BFF 的日子里,前端架构师经常面临三个窘境:

  1. 数据臃肿: 后端为了通用,一个接口返回 2MB 的 JSON,前端只要其中一个 id
  2. 请求地狱: 为了渲染一个首页,前端要在 useEffect 里调 A 接口拿 userId,再调 B 接口拿详情,最后调 C 接口拿评论。
  3. 逻辑混杂: 前端为了适配后端奇怪的字段名,代码里充斥着 data.list[0].user_info_v2.name || '匿名' 这种防错逻辑。
  4. 脆弱的“防御性代码”: 为了防止后端返回 null 或字段缺失,前端代码里充斥着大量的可选链(Optional Chaining)和硬编码的默认值。这其实是**“代码腐烂”**的开始——你根本不敢删掉这些逻辑,因为你不知道哪天接口又会吐出奇怪的数据
  5. 多端兼容噩梦: 同样的后端接口,Web 端需要显示 YYYY-MM-DD,小程序需要显示 MM月DD日,App 需要显示 3分钟前。如果这些逻辑全写在前端,每增加一个端,业务逻辑就要重写一遍。

BFF 的出现,就是为了在前端和微服务之间,加一层“数据洗涤机”。


二、 BFF 的三张王牌:聚合、转换、适配

一个设计良好的 BFF 层,应该承担以下三个核心职责:

2.1 接口聚合 (Aggregator)

将原本需要前端发起 5 次的网络请求,在 BFF 层合并。BFF 与后端微服务走的是内网通信(RPC 或高带宽 HTTP),速度远快于移动端弱网环境下的多次请求。

2.2 数据瘦身与洗涤 (Formatter)

后端给的字段是 user_head_img_url_v2_stable,BFF 转换成 avatar。后端给了 100 个字段,BFF 只吐给前端渲染需要的 5 个。

2.3 跨端重用 (Adapter)

  • Web 端: 返回完整的 HTML(SSR)或大而全的 JSON。
  • App 端: 返回极致压缩的二进制数据或精简 JSON。
  • BFF 逻辑: 核心业务逻辑在 BFF 内部共享,根据请求头自动适配输出格式。

三、 Serverless:BFF 的“完美外壳”

很多前端架构师不敢推行 BFF,是因为害怕**“运维地狱”**:谁来配 Nginx?服务器挂了谁修?内存泄露了怎么办?

Serverless (无服务器架构) 的出现,彻底解开了这个心结。

3.1 为什么是 FaaS + BFF?

  • 免运维: 前端只写函数(Function),不看服务器。部署一个 BFF 就像提交一段 JS 代码一样简单。
  • 弹性伸缩: 只有在接口被调用时才计费。深夜没人访问时,成本几乎为零。
  • 按需交付: 一个页面对应一个云函数(SFF, Serverless For Frontend),边界清晰,互不干扰。

四、 深度融合:BFF + Serverless 的落地挑战

虽然听起来很美,但在实际架构落地中,你需要处理以下深水区问题:

4.1 响应延迟 (Cold Start)

Serverless 函数在久未调用后会有“冷启动”时间。

  • 架构对策: 对实时性要求极高的接口,开启“预热”或“预留实例”;或者使用 Node.js 运行时较轻量级的框架(如 Midway.js 或 Nitro)。

4.2 链路追踪 (Tracing)

当页面报错时,问题可能出在:前端 -> BFF -> 后端微服务 A -> 数据库。

  • 架构决策: 必须引入全链路 Trace ID。在 BFF 层生成统一的 X-Trace-Id 贯穿始终。

4.3 接口定义冲突

如果 BFF 是前端写的,后端改了微服务接口,BFF 怎么知道?

  • 最佳实践: 推动后端导出 OpenAPI (Swagger) 或使用 gRPC。BFF 层通过脚本自动生成类型定义,利用 TypeScript 在编译期就发现接口变动。

五、 架构决策:选型 ROI 分析

架构收益=(用户体验提升+前端开发效率)运维复杂度的增加架构收益 = \frac{(用户体验提升 + 前端开发效率)}{运维复杂度的增加}

在 Serverless 场景下,分母被大幅削减,因此 BFF 几乎成为了中大型前端项目的标配

模式 适用场景 开发成本 运维难度
传统模式 简单项目,后端接口已经很完美 极低
Node.js BFF 复杂业务,前端团队有后端背景 高 (需管集群)
Serverless BFF 主流推荐,追求极致开发效率 低 (只写函数) 极低 (托管)

结语:从“画饼”到“造饼”

BFF 和 Serverless 的结合,是前端架构师**从“UI 实现者”向“全链路设计者”**跨越的关键一步。当你不再受限于后端接口的形状,你才能真正从全局视角去优化性能和用户体验。

昨天 — 2026年2月10日首页

前端向架构突围系列 - 设计与落地 [9 - 1]:核心原则与决策方法论

2026年2月10日 09:39

写在前面

布鲁克斯在《人月神话》中提出过一个著名的命题:“没有银弹”。

在前端领域同样适用:没有一种架构模式能同时解决开发效率、运行性能、系统稳定性和代码可维护性。

架构师的价值,不在于他知道多少个 NPM 包,而在于他能在需求、资源、技术限制的三角博弈中,画出那条最优的平衡线。

image.png


一、 架构设计的四大铁律

在动手写任何一行架构代码前,请将这四条铁律刻在脑子里:

1.1 简单原则:奥卡姆剃刀

  • 现状: 很多架构师为了展示技术深度,在只有 5 个人的团队里强行推行微前端,结果导致打包时间翻倍,维护成本飙升。
  • 铁律: 如果一个简单的方案能解决问题,绝不引入复杂的方案。 架构的复杂度应该是为了解决业务的复杂度,而不是为了满足架构师的虚荣心。

1.2 关注点分离 (SoC) 与高内聚低耦合

  • 战术: * 逻辑与视图分离: 别在组件里写几百行的业务逻辑,去用 Hook 或 Service。

    • 数据与存储分离: 别让 API 的数据结构直接统治你的 UI。
  • 目标: 改变 A 模块时,B 模块不应该无故“躺枪”。

1.3 演进式架构 (Evolutionary Architecture)

  • 认知误区: “我一次性把未来三年的架构都设计好。”

  • 铁律: 好的架构是长出来的,不是画出来的。

    • 架构设计要预留**“可拆卸性”**。当你现在用单体架构时,代码结构要清晰到未来可以随时无痛拆分出微前端。

1.4 康威定律 (Conway's Law)

  • 核心: 系统的架构设计,本质上是组织沟通结构的反映。
  • 应用: 如果你的公司是按业务线划分团队的,那么强行搞一个跨业务线的“巨型单体应用”必然会产生严重的协作摩擦。架构要顺着人流走,而不是逆流而上。

二、 决策方法论:如何科学地“拍脑袋”?

架构师每天都要面临选择:用 Vite 还是 Webpack?用微前端还是 Iframe?用 Monorepo 还是 Multi-repo?

2.1 决策模型:象限分析法

不要只看“好不好”,要看“值不值”。

维度 高投入 (High Effort) 低投入 (Low Effort)
高收益 战略高地: 需长期投入(如:自研 UI 规范) 低垂果实: 优先执行(如:开启压缩)
低收益 技术陷阱: 坚决避免(如:过度重构旧代码) 日常琐事: 顺手而为

2.2 ADR (Architecture Decision Record)

口头决定的架构往往会在三个月后被遗忘,然后被后人骂作“坑”。

  • 方案: 建立 ADR 决策记录
  • 内容: 记录背景(为什么改)、决策(选了哪个)、权衡(放弃了什么,会有什么副作用)。

三、 架构师的禁忌:过度设计 (Over-Engineering)

过度设计是资深开发者的通病。

案例:

为了实现一个简单的“文件上传”,你封装了一个通用的插件系统、三个抽象类、两层适配器,并号称未来可以支持上传到火星。

代价: 同事看代码需要半小时,改 Bug 需要两小时,而业务其实只需要传个图片给后端。

架构师的自我修养: 识别哪些是“必要的灵活性”,哪些是“臆想的需求”。


四、 核心决策流程:从 0 到 1 落地架构

  1. 需求分析: 性能是核心?协作是核心?还是快速交付是核心?
  2. 现状评估: 团队的技术栈储备如何?旧代码的负债有多深?
  3. 技术选型: 调研 2-3 个方案,制作 POC (Proof of Concept) ,用数据对比(包体积、编译速度、上手难度)。
  4. 灰度落地: 先在一个边缘小模块试点,验证后再全量推行。

结语:架构师不仅是技术专家

架构设计不是在真空中进行的。

一个好的架构师,50% 的时间在写代码和设计图,另外 50% 的时间在沟通与说服。你需要说服老板为什么要投入资源搞基建,说服同事为什么要改变原有的开发习惯。

没有完美的架构,只有最契合当前业务阶段的取舍。

Next Step:

既然聊到了“分治”和“协作”,目前大厂里最火、也最容易踩坑的架构莫过于“微前端”了。

为什么很多公司做了微前端后反而更痛苦了?

下一节,我们进入**《微前端架构 (Micro-Frontends) 的设计陷阱与最佳实践》**,帮你避开那些昂贵的学费。

昨天以前首页

前端向架构突围系列 - 状态数据设计 [8 - 4]:有限状态机 (FSM) 在复杂前端逻辑中的应用

2026年2月9日 15:09

写在前面

想象一下,你正在开发一个“自动驾驶”系统。

你绝不会用 if (isAccelerating && !isBraking) 来控制汽车。你会定义清晰的状态:DrivingBrakingParked。因为在 Parked(停车)状态下,踩油门是不应该有反应的。

前端业务逻辑其实就是一套交互系统。

遗憾的是,大部分前端代码都在用“零散的布尔值”来模拟状态,这导致逻辑极难测试,且隐藏着巨大的状态冲突风险。

真正优雅的架构,应该把逻辑抽象成一张

image.png


一、 为什么布尔值会导致“状态爆炸”?

假设你有一个极其简单的“搜索框”。

你定义了三个变量:loading (加载中)、error (报错)、results (数据)。

理论上这只有 3 个变量,但它们的组合可能有 23=82^3 = 8 种。

  • loading: true, error: true —— 这代表什么?一边报错一边加载?
  • loading: false, error: false, results: [] —— 这是初始状态?还是没搜到结果?

随着业务逻辑增加(比如增加了“取消请求”、“重试”、“分步校验”),布尔值的组合会指数级增长。这就是 “状态爆炸”


二、 有限状态机 (FSM) 的核心概念

有限状态机不是一个库,而是一个数学模型。它包含四个要素:

  1. State (状态): 你的应用当前在哪?(如:idleloadingsuccessfailure)。同一时间只能处在一个状态。
  2. Event (事件): 发生了什么?(如:SEARCHCANCELRESOLVE)。
  3. Transition (转换): 状态改变的规则。例如:在 loading 状态下收到 CANCEL 事件,转为 idle
  4. Action (动作): 转换时触发的副作用。例如:进入 loading 状态时,发起 API 请求。

三、 实战:用 XState 终结逻辑乱麻

在 JS 生态中,XState 是状态机的集大成者。我们来看如何重构一个复杂的“文件上传”逻辑。

3.1 定义状态图

与其写一堆 if/else,不如先把图画出来:

import { createMachine, interpret } from 'xstate';

const uploadMachine = createMachine({
  id: 'upload',
  initial: 'idle', // 初始:闲置
  states: {
    idle: {
      on: { SUBMIT: 'validating' } // 收到提交事件 -> 进入校验
    },
    validating: {
      on: {
        VALID_SUCCESS: 'uploading', // 校验成功 -> 开始上传
        VALID_FAIL: 'error'         // 校验失败 -> 报错
      }
    },
    uploading: {
      on: {
        FINISH: 'success',
        FAIL: 'error',
        CANCEL: 'idle' // 上传中途取消 -> 回到闲置
      }
    },
    success: {
      type: 'final' // 终态
    },
    error: {
      on: { RETRY: 'uploading' } // 报错后可以重试
    }
  }
});

3.2 架构层面的收益

  • 防御性编程:success 状态下,哪怕用户疯狂点击 SUBMIT 按钮,状态机也会自动忽略这个事件,因为 success 状态下没有定义处理 SUBMIT 的转换。
  • 逻辑可视化: XState 提供可视化工具,你可以直接把代码生成的图发给产品经理确认:“你看,这是不是我们要的业务流程?”

四、 什么时候该引入状态机?

并不是所有的组件都需要状态机。作为架构师,你需要识别**“高价值逻辑”**。

4.1 推荐使用场景:

  1. 多步骤流程: 注册流程、结账链路、多步表单。
  2. 复杂权限交互: 比如一个按钮,根据登录状态、用户等级、账户余额、活动是否开始,有五六种显示逻辑。
  3. 核心支付/上传: 绝不允许出现非法状态转换的场景。
  4. 游戏逻辑或复杂动效: 状态转换之间有严格的时间顺序。

4.2 什么时候不用?

简单的开关(Toggle)、单纯的 CRUD 列表展示,直接用 useState 或 TanStack Query 就足够了。


五、 状态机 vs. 状态管理库

这是很多人的误区。状态机不是 Redux 的替代品。

  • Redux/Zustand 是“仓库”:负责数据。
  • XState 是“大脑”:负责逻辑。

架构模式: 你可以在 Zustand 里面跑一个 XState。XState 负责计算当前应该是哪个状态,然后把最终的结果(如当前是哪个 Tab,要显示哪个文案)更新到 Zustand 中供全局使用。


结语:迈向“可预测”的架构

我们在第八阶段完成了对数据流的全面重构:

  1. 哲学上: 选择了适合业务的模式(Redux/Atomic)。
  2. 性能上: 理解了细粒度更新(Signals)的内核。
  3. 结构上: 剥离了 API 数据(TanStack Query)。
  4. 逻辑上: 用状态机(FSM)替代了布尔值地狱。

至此,你的前端应用已经像一台精密运行的瑞士钟表:每一滴水的流向(数据)都是可追踪的,每一个齿轮的转动(逻辑)都是可预测的。

Next Step:

下一个阶段,我们将跳出纯代码层面,进入**《第九阶段:前端工程化体系与全链路质量保障》 。 我们将探讨:如何搭建一套让 50 人的团队协作而不打架的 Monorepo 体系?如何设计自动化的 CI/CD 流程,让性能劣化和 Bug 在合并代码前就被击毙? 第一篇,我们将聊聊《架构的地基:基于 Turborepo 与 pnpm 的现代 Monorepo 企业级实战》**。

前端向架构突围系列 - 状态数据设计 [8 - 3]:服务端状态与客户端状态的架构分离

2026年2月9日 09:48

写在前面

架构师的核心能力之一是分类。 如果你觉得状态管理很痛苦,通常是因为你试图用同一种工具处理两种截然不同的东西:

  1. 客户端状态 (Client State): 比如“侧边栏是否展开”、“当前的夜间模式”。它们是同步的、瞬间完成的、由前端完全控制。
  2. 服务端状态 (Server State): 比如“用户订单列表”。它们是异步的、可能失效的、由后端控制。

Redux 并不擅长管理 Server State。 真正专业的做法是:让 Redux 回归 UI,让 TanStack Query (React Query) 接管 API。

image.png


一、 为什么要把 API 赶出 Redux?

1.1 消失的“样板代码”

在传统的 Redux 处理 API 流程中,你需要写:

  • 一个 Constant 定义 FETCH_USER_REQUEST
  • 一个 Action Creator
  • 一个处理 Pending/Success/ErrorReducer
  • 一个 useEffect 来触发请求

而在 TanStack Query 中,这只需要一行代码:

const { data, isLoading } = useQuery({ queryKey: ['user'], queryFn: fetchUser });

1.2 缓存与失效:Redux 的盲区

Server State 最难的不是“获取”,而是**“维护”**。

  • 用户离开页面 5 分钟后回来,数据还是新的吗?
  • 两个组件同时请求同一个接口,会发两次请求吗?
  • 弱网环境下,请求失败了会自动重试吗? 如果要用 Redux 实现这些,你需要写几百行复杂的 Middleware。而这些,是 Server State 管理工具的标配

二、 架构模型:双层数据流

现代前端架构推荐采用 “双层分离” 模型:

2.1 外部层:服务端状态 (Server State)

  • 工具: TanStack Query (React Query) 或 SWR。
  • 职责: 缓存管理、自动预取、失效检查 (Stale-While-Revalidate)、请求去重。
  • 特点: 它是异步的。

2.2 内部层:客户端状态 (Client State)

  • 工具: Zustand, Pinia, Jotai 或简单的 React Context。
  • 职责: 管理纯粹的 UI 逻辑(开关、多语言、主题、临时草稿)。
  • 特点: 它是同步的。

三、 实战战术:从“手动挡”切换到“自动挡”

3.1 自动化的依赖追踪

想象一个场景:你修改了用户的头像,你需要更新所有显示头像的地方。

  • 旧模式 (Redux): 修改成功后,手动发起一个 updateUserAction 去修改 Redux 里的那个大对象。
  • 新模式 (Query): 只需要执行一次“失效(Invalidate)”。
// 当用户修改个人资料成功时
const mutation = useMutation({
  mutationFn: updateProfile,
  onSuccess: () => {
    // 告诉系统:['user'] 这个 key 下的数据脏了,请自动重新拉取
    queryClient.invalidateQueries({ queryKey: ['user'] })
  },
})

架构意义: 你的代码不再需要关心“数据怎么同步”,只需要关心“数据何时失效”。

3.2 乐观更新 (Optimistic Updates)

这是架构高级感的核心。当用户点赞时,我们不等后端返回,直接改 UI。

TanStack Query 允许你在 onMutate 中手动修改缓存副本,如果请求失败,它会自动回滚。这种复杂的逻辑如果写在 Redux 里,会让 Reducer 逻辑变得极度臃肿。


四、 选型决策:什么时候该用谁?

作为架构师,你需要给团队划清界限:

状态类型 典型例子 推荐工具 存储位置
API 数据 商品列表、用户信息 TanStack Query 专用 Cache 池
全局 UI 状态 登录 Token、全局主题 Zustand / Pinia 全局 Store
局部 UI 状态 某个弹窗的开关 useState 组件内部
复杂表单 多步骤注册表单 React Hook Form 专用 Form State

导出到 Google 表格


五、 总结:让 Redux 变“瘦”

通过把 API 逻辑剥离出去,你会发现你的 Redux(或者 Zustand)Store 瞬间缩水了 80% 。 剩下的代码变得极其纯粹:只有纯同步的 UI 逻辑。

这种**“分治”**带来的好处是巨大的:

  1. 心智负担降低: 你不再需要管理复杂的 loading 状态机。
  2. 性能提升: TanStack Query 的细粒度缓存比 Redux 的全量对比快得多。
  3. 开发效率: 团队成员可以更专注地编写业务逻辑,而不是在样板代码中挣扎。

结语:控制的艺术

我们已经成功地将 API 数据和 UI 状态分开了。 但还有一种状态最让架构师头疼:流程状态。 当你的业务逻辑包含“待支付 -> 支付中 -> 支付成功/失败 -> 申请退款 -> 已关闭”这种复杂的链路时,无论你用什么工具,代码里都会充满 if/else

这种逻辑该如何优雅地管理?

Next Step: 下一节,我们将引入一个在航天和游戏领域应用了几十年的数学模型。 我们将学习如何用“图”的思想,终结代码里的逻辑乱麻。

前端向架构突围系列 - 状态数据设计 [8 - 2]:前端框架的“细粒度响应式”原理

2026年2月6日 09:26

写在前面

React 的痛: 在 React 中,一个 State 变了,组件就会重新执行(Re-render)。为了性能,我们不得不搞出 Fiber 架构,搞出时间切片,搞出 useMemo。这就好比:为了能在干草堆里找到一根针,React 发明了一台超级高科技的“干草堆翻找机”。

Signal 的解: 细粒度响应式(Signal)的思路是:在扔针进去的时候,就给针系上一根绳子。要找针的时候,拉绳子就行了。

本篇我们将深入内核,手写一个迷你 Signal 系统,看清它的本质。

image.png


一、 宏观对决:VDOM vs. Fine-Grained (细粒度)

要理解 Signal,首先要理解它想革谁的命。

1.1 VDOM 的“地毯式搜索”

React 的更新模型是 Snapshot(快照) 式的。

  • 流程: 数据变了 -> 运行整个组件函数 -> 生成新的 VDOM 树 -> 对比新旧树 (Diff) -> 找出差异 -> 更新 DOM。
  • 复杂度: 跟组件树的大小成正比。
  • 问题: 哪怕只改了一个文本节点,整个组件(甚至子组件)的逻辑都要重跑一遍。

1.2 Signal 的“点对点狙击”

SolidJS 或 Vue 的更新模型是 Dependency Graph(依赖图) 式的。

  • 流程: 数据变了 -> 直接定位到绑定了该数据的 DOM 节点 -> 更新 DOM。
  • 复杂度: 跟动态节点的数量成正比(通常是 O(1))。
  • 核心: 组件函数只在初始化时运行一次!之后再也不会运行了。

二、 解剖 Signal:发布订阅的进化体

Signal 并不神秘,它本质上就是 “保存值的容器” + “自动依赖追踪” 。 它由两个核心动作组成:Track (追踪/读)Trigger (触发/写)

2.1 核心 API 模拟

以 SolidJS/React 风格为例,我们造一个 Signal:

// 这是一个全局变量,用来记录“当前谁在查我不?”
let activeEffect = null;

function createSignal(initialValue) {
  let value = initialValue;
  const subscribers = new Set(); // 订阅者名单

  // Getter (读)
  const read = () => {
    if (activeEffect) {
      // 1. 依赖收集 (Track):如果有人在关注我,把他记下来
      subscribers.add(activeEffect);
    }
    return value;
  };

  // Setter (写)
  const write = (newValue) => {
    value = newValue;
    // 2. 派发更新 (Trigger):通知名单里所有人干活
    subscribers.forEach(fn => fn());
  };

  return [read, write];
}

2.2 魔法的粘合剂:Effect

光有 Signal 没用,得有人“读”它,订阅关系才能建立。这就需要 createEffect(在 Vue 里叫 watchEffect)。

function createEffect(fn) {
  // 把自己标记为“正在执行的副作用”
  activeEffect = fn;
  
  // 执行一次函数。
  // 注意:函数内部会读取 Signal,从而触发 Signal 的 Getter,
  // 进而把这个 fn 添加到 subscribers 里。
  fn();
  
  // 执行完复原
  activeEffect = null;
}

2.3 跑起来看看

const [count, setCount] = createSignal(0);

createEffect(() => {
  console.log("数字变了:", count()); 
});
// 输出: 数字变了:0 (初始化执行)

setCount(1);
// 输出: 数字变了:1 (自动触发!)

这就是细粒度响应式的最简内核。没有任何 VDOM,没有 Diff,只有精准的函数调用链。


三、 进阶: computed 与依赖图的自动构建

Signal 系统最强大的地方在于它能自动构建依赖图。 在架构设计中,我们经常使用 computed (派生状态)。

computed 既是 消费者(它依赖别的 Signal),又是 生产者(别的 Effect 依赖它)。

3.1 懒计算与缓存 (Memoization)

细粒度框架中的 computed 通常是惰性的(Lazy)。

  • 只有当有人读它时,它才计算。
  • 如果它依赖的 Signal 没变,它直接返回缓存。

3.2 动态依赖收集

这是 React useMemo 永远做不到的。 React 的依赖数组 [a, b] 是手动声明的(静态)。而 Signal 的依赖是运行时动态收集的。

const [show, setShow] = createSignal(true);
const [name, setName] = createSignal("Gemini");
const [age, setAge] = createSignal(18);

createEffect(() => {
  // 动态依赖!
  if (show()) {
    console.log(name()); // 此时依赖是 [show, name]
  } else {
    console.log(age());  // 此时依赖变成 [show, age]
  }
});

架构意义: 这种机制保证了最小化计算。当 show 为 false 时,改变 name 根本不会触发这个 Effect,因为系统知道这一刻 name 不重要。


四、 为什么 React 还在坚持?

既然 Signal 这么好,性能这么高,为什么 React 不把 useState 换成 Signal? 这涉及到底层哲学的冲突。

4.1 UI = f(state) vs. UI = Bind(state)

  • React 哲学: UI 是数据的投影(Snapshot) 。每次渲染都是丢弃旧世界,重建新世界。这符合函数式编程的直觉,心智模型最简单。
  • Signal 哲学: UI 是数据的绑定(Binding) 。初始渲染后,组件就消失了,剩下的只有数据和 DOM 之间的连线。

4.2 代数效应 (Algebraic Effects)

React 团队认为,手动处理 .value 或者 [get, set] 是对开发者心智的负担。他们追求的是 "It just works" 。 React 正在搞的 React Compiler (React Forget) ,其实是一条殊途同归的路:

  • Signal:运行时通过 Proxy 收集依赖,实现细粒度更新。
  • React Compiler:编译时分析代码,自动插入 memoization,模拟细粒度更新的效果。

五、 总结:架构师的选择

理解了原理,我们在架构设计中就能明白:

  1. Vue 3 / Solid: 适合高性能仪表盘、即时通讯、即时编辑类应用。因为它们对 CPU 的利用率极高,没有 VDOM 的 Overhead。

  2. React: 适合大型业务系统、生态依赖重的应用。虽然有一些性能损耗,但其编程模型的一致性(Pure Render)能降低逻辑复杂度。

  3. 趋势: 越来越多的状态管理库(MobX, Valtio, Preact Signals)允许你在 React 中使用 Signal。

    • 架构模式: 使用 Signal 管理频繁变化的局部状态(避免 React 顶层重渲染),使用 React Context 管理低频的全局状态

Next Step: 我们搞懂了前端“怎么存数据”(Redux/Atomic)和“怎么更新数据”(Signal)。 但还有一个最大的麻烦没解决:API 数据。 我们以前总是把后端返回的 JSON 也塞进 Redux 里,导致 Redux 变得臃肿不堪。这真的是对的吗? 下一节,我们将通过 React Query (TanStack Query) 来一场架构大扫除。 请看**《第三篇:分治——把 API 赶出 Redux:服务端状态 (Server State) 与客户端状态的架构分离》**。

❌
❌