📌 本章目标:总结项目中的设计模式和最佳实践,帮你把知识系统化
⭐⭐⭐⭐ 难度:进阶 | ⏱️ 预计时间:1.5 小时
11.1 项目中用到的设计模式
设计模式不是「八股文」,而是解决特定问题的「套路」。这个项目里用到了很多经典的设计模式,我们来盘点一下。
门面模式(Facade Pattern)
是什么:对外提供一个统一的大接口,内部封装复杂的子系统。
用在哪里:网关层(Gateway)
上层代码
│
▼
┌─────────────┐
│ Gateway │ ← 统一的大接口
│ getAgents │
│ sendMessage│
│ ... │
└──────┬──────┘
│
┌────┴────┐
▼ ▼
HTTP WS
为什么好:
- 上层不用关心底层是 HTTP 还是 WS
- 换底层实现不影响上层
- 所有数据格式转换集中在网关层做
工厂模式(Factory Pattern)
是什么:用一个工厂函数/类来创建对象,调用者不用知道具体实现。
用在哪里:createGateway() 工厂函数
function createGateway(): WorkbenchGateway {
if (useMock) return new MockGateway();
return new HttpGateway();
}
为什么好:
- 创建逻辑集中,要改只改一个地方
- 调用者只依赖接口,不依赖具体类
- 方便加新的实现
观察者模式(Observer Pattern)
是什么:一个对象变化了,通知所有订阅了它的对象。
用在哪里:MobX 的 observable + reaction、WebSocket 的事件监听
observable 数据变了
│
▼
所有用到它的组件自动更新
为什么好:
- 数据和视图解耦
- 不用手动调「刷新」方法
- 细粒度更新,性能好
策略模式(Strategy Pattern)
是什么:定义一族算法,把每个算法封装起来,使它们可以互相替换。
用在哪里:双 WS 通道策略(chat / work-order)
function getPrimaryWs() {
if (transport === 'chat') return chatWs;
return workOrderWs;
}
为什么好:
- 新增策略不用改原有逻辑
- 运行时可以动态切换
- 每个策略独立,互不干扰
状态模式(State Pattern)
是什么:对象的行为取决于它的状态,状态变了,行为也变。
用在哪里:消息生命周期(pending → sending → sent / error)、流式回复阶段
用户消息的状态:
pending → sending → sent
↘ error
为什么好:
- 状态转换逻辑清晰
- 每个状态下能干什么不能干什么一目了然
- 避免「这个状态下居然能点这个按钮」的 bug
备忘录模式(Memento Pattern)
是什么:保存对象的状态,以后可以恢复。
用在哪里:执行日志缓存(切换会话时恢复最后一次的执行面板)
// 缓存
runCache.set(runId, events);
// 恢复
events = runCache.get(runId);
为什么好:
- 用户切换回来时,之前的状态还在
- 不用每次都重新加载
- 体验更连贯
11.2 MobX 使用最佳实践
✅ 推荐做法
1. 用 makeAutoObservable
最省事、最不容易出错。99% 的场景用它就够了。
class Store {
count = 0;
constructor() {
makeAutoObservable(this); // 自动识别
}
}
2. 细粒度 observable
数据拆分细一点,更新的时候只有变化的部分触发渲染。
// ✅ 好:每个消息独立,改一个只重渲染一个
messages: Message[] = [];
// ❌ 不好:整个对象替换,所有消息都重渲染
setMessages(newMessages);
3. 多用 computed
能从现有数据算出来的,就用 computed,不要手动维护。
// ✅ 好:自动计算、自动缓存
get completedCount() {
return this.todos.filter(t => t.done).length;
}
// ❌ 不好:每次改 todos 都要手动更新这个数
completedCount = 0;
4. 异步操作包 runInAction
异步回调里改状态,用 runInAction 包一下。
async loadData() {
const data = await api.fetch();
runInAction(() => {
this.data = data;
this.loading = false;
});
}
❌ 避免的做法
1. 不要在组件里直接改 observable
// ❌ 不好:业务逻辑散落在组件里
<button onClick={() => store.count++}>
// ✅ 好:通过 action 方法改
<button onClick={() => store.increment()}>
2. 不要把所有东西塞一个 Store
一个 Store 超过 500 行就要考虑拆分了。
3. 不要滥用 reaction/autorun
大部分场景用 computed + observer 就够了。reaction 是应急手段,不是常规武器。
11.3 实时通信最佳实践
WebSocket 必做的 5 件事
| 功能 | 为什么要做 | 怎么做 |
|---|---|---|
| 自动重连 | 网络波动是常态,不能断了就断了 | 指数退避,最多 N 次 |
| 心跳保活 | 检测连接是不是假死 | 定时发 ping,超时就断了重连 |
| 消息队列 | 连接没建立好的时候发的消息不能丢 | 存队列里,连上再发 |
| 重连后同步 | 断连期间的消息要补上 | 重连后拉取最新状态 |
| 错误提示 | 断了要告诉用户 | Toast 提示 + 重连中状态 |
流式回复的经验
1. 本地回显一定要做
发完消息立即显示,别等后端返回。慢个 100ms 体验差很多。
2. 始终以最终内容为准
流式增量可能会有误差(比如后端做了修改),end 事件里的最终内容才是准的。
// delta 阶段:追加
appendAssistantDelta(messageId, delta);
// end 阶段:以最终内容为准
setAssistantFinalContent(messageId, finalContent);
3. 后台会话不渲染
用户没在看的会话,只更新未读数,不加载消息内容。不然几十上百个会话一起流式更新,页面会卡。
if (currentConversationId !== event.conversationId) {
// 后台会话:只标记未读
markUnread(event.conversationId);
return;
}
// 前台会话:更新消息
appendMessage(event.message);
11.4 组件设计最佳实践
容器组件 vs 展示组件
| 类型 | 职责 | 接触什么 |
|---|---|---|
| 容器组件 | 数据获取、业务逻辑 | Store、网关 |
| 展示组件 | 只负责渲染和交互 | Props、本地 state |
// 容器组件:管数据
const MessageListContainer = observer(() => {
const { sessionStore } = useStore();
return <MessageList messages={sessionStore.messages} />;
});
// 展示组件:只管渲染
function MessageList({ messages }) {
return messages.map(msg => <MessageBubble msg={msg} />);
}
💡 小贴士:小项目不用分这么细,但大项目分清楚了维护起来省心。
组件粒度怎么把握
| 情况 | 建议 |
|---|---|
| 一个组件超过 400 行 | 考虑拆分子组件 |
| 一段 JSX 有自己的逻辑和状态 | 拆成独立组件 |
| 两个地方用到了同样的 UI | 抽成公共组件 |
| 拆了之后 props 要传七八个 | 可能拆太细了,合回去 |
一句话:拆到「每个组件做一件事,而且名字能准确描述它做什么」就差不多了。
优先复用 UI 库组件
能用 Ant Design 的组件就别自己写:
- 按钮、弹窗、表单、表格… 直接用
- 样式不满足?用 className 覆盖或 theme 定制
- 功能不满足?先看看 Pro 组件有没有
自己写一个「看起来差不多」的组件,后期维护成本很高。
11.5 性能优化要点
前端性能优化的优先级
先做见效快的,再做费力的。
| 优先级 | 优化手段 | 难度 | 收益 |
|---|---|---|---|
| 🔴 高 | 后台会话不渲染消息 | 低 | 高 |
| 🔴 高 | 代码分割 + 按需加载 | 中 | 高 |
| 🟡 中 | MobX 细粒度更新 | 低 | 中 |
| 🟡 中 | 长列表虚拟滚动 | 中 | 中 |
| 🟢 低 | 图片懒加载 | 低 | 低 |
| 🟢 低 | 防抖节流 | 低 | 低 |
本项目的关键优化点
1. 流式消息的性能
流式消息是最频繁的更新场景,一定要优化好:
- 每条消息独立的 observable,改一条只重渲染一条
- 用
observer包裹消息项组件 - 避免在消息渲染里做复杂计算
2. 切换会话的性能
切换会话是常见操作:
- 切换时清空旧会话的大对象,防止内存泄漏
- 执行日志按 run 缓存,不要全量存
- 图片和文件预览懒加载
3. 首屏加载性能
- 重组件(代码编辑器、文件预览)动态 import
- 路由懒加载
- 字体和图标按需加载
11.6 代码质量保障
规范三件套
| 工具 | 管什么 | 怎么用 |
|---|---|---|
| Prettier | 格式(缩进、引号、换行) | 保存时自动格式化 |
| ESLint | 代码质量(变量用没用到、潜在 bug) | 提交前检查 |
| TypeScript | 类型安全 | 写代码的时候就有提示 |
💡 经验:Prettier 管「长什么样」,ESLint 管「写得对不对」,TypeScript 管「类型对不对」。三者配合,代码质量有基本保障。
测试策略
| 测试类型 | 测什么 | 优先级 |
|---|---|---|
| 单元测试 | 工具函数、Store 逻辑 | 高 |
| 组件测试 | 组件渲染和交互 | 中 |
| E2E 测试 | 完整流程走一遍 | 低(成本高) |
建议:核心逻辑(Store、工具函数)一定要有单测。UI 组件可以少写点,因为 UI 变化快,测试维护成本高。
Code Review 重点
- 有没有越级调用(比如组件直接调 axios)
- 数据改对了吗(有没有竞态条件、状态不一致)
- 边界情况处理了吗(空数据、加载中、错误态)
- 命名清晰吗(变量名、函数名能不能看懂)
- 有没有重复代码(能不能抽成公共的)
11.7 学习路线建议
学完这个教程之后,怎么继续提升?
阶段一:打基础(1-2 个月)
- React Hooks 深入理解(useEffect、useMemo、useCallback)
- TypeScript 高级类型(泛型、条件类型、映射类型)
- MobX 官方文档通读一遍
阶段二:提能力(2-4 个月)
- 学一个状态管理库(Redux / Zustand,和 MobX 对比着学)
- 学 WebSocket 和 SSE 的原理和区别
- 学 React 性能优化(memo、useMemo、虚拟列表)
- 做一个完整的项目练手
阶段三:进阶级(4-6 个月)
- 微前端架构
- 低代码 / 可视化搭建
- 前端工程化(构建原理、CI/CD)
- Node.js 全栈开发
💡 学习心得:别光看,要动手。跟着做一遍比看十遍都管用。遇到问题先自己想、自己查,实在解决不了再问别人。
11.8 全书总结
恭喜你学完了整个教程!🎉
我们从项目概述开始,一路走过了:
- 架构设计(五层架构)
- 工程化(Vite、Less、ESLint)
- 状态管理(MobX、根 Store + 子 Store)
- 网关层(统一通信抽象)
- 实时通信(WebSocket、流式回复)
- 聊天模块(消息列表、输入框)
- 执行面板(工具调用展示)
- 审批机制(安全保障)
- 更多业务模块(定时任务、记忆、设置…)
三句最重要的话
- 分层解耦——每一层只管自己的事,上层不操心下层怎么实现
- 实时优先——AI 产品的核心体验是「实时感」,所有设计围绕这个展开
- 观测并重——不仅能指挥 AI 干活,还要能看清它干了啥、为什么这么干
送给你的话
做项目就像搭积木——先搭骨架(架构),再填血肉(业务模块),最后打磨细节(体验优化)。
一开始不用追求完美,先跑起来,再慢慢迭代。每一个优秀的项目,都是从一个「能跑的简陋版本」开始的。
加油,你可以的!💪
感谢阅读本教程。如果觉得有帮助,记得动手实践——代码是写出来的,不是看出来的。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_56236582/article/details/168109026



