Vue练习项目重构统一框架
前面已经整理了多篇案例文档,但如果只停留在“一个项目一篇总结”,这些经验仍然容易散。更有价值的做法,是把这些案例抽象成一套可以重复套用的统一框架。
这篇文档的目标只有一个:把前面零散的 Vue 重构案例,串成一条稳定的方法论。以后不管你面对的是待办应用、点餐页、内容社区、表单组件、表格组件还是树组件,都可以先按这套框架诊断,再决定怎么拆。
这套框架覆盖的 22 个案例
这次整理的统一框架,来自下面这些案例:
- 待办应用核心逻辑重构
- 点餐页购物车联动重构
- 说说应用发布与互动链路重构
- 表单组件校验体系重构
- 表格列扩展渲染体系重构
- 树组件递归与勾选联动重构
- SSR数据预取与水合重构
- Nuxt数据获取与渲染模式重构
- Vuex状态中心与Pinia迁移重构
- 动画与复杂交互链路重构
- 认证请求与路由权限链路重构
- 路由视图缓存与布局编排重构
- 错误边界与客户端降级渲染重构
- 异步组件与懒加载降级重构
- Nuxt中间件与插件注入边界重构
- Nuxt路由规则与混合渲染策略重构
- Nuxt生命周期与副作用时机重构
- Nuxt自动导入与隐式依赖边界重构
- Nuxt Server Routes与BFF接口边界重构
- Nuxt组合式能力分层与页面编排边界重构
- Nuxt业务模块与feature目录边界重构
- Nuxt应用公共层与共享能力治理重构
它们看起来题材不同,但底层问题高度一致:状态归属混乱、组件职责过重、扩展协议不清晰、通信路径隐式、运行时边界不清,以及副作用和展示层缠在一起。
统一框架先回答 4 个总问题
在真正开始改任何一个 Vue 项目之前,先用这 4 个问题做总体诊断:
- 当前页面或组件的状态,谁才是唯一数据源?
- 当前组件到底是在做展示,还是同时在做业务编排和副作用?
- 当前交互是靠显式契约在流动,还是靠隐式事件和层级耦合在流动?
- 当前能力是一次性业务实现,还是值得沉淀成可复用模块?
如果这 4 个问题答不清楚,后面的重构通常很容易变成“拆了几个文件,但复杂度没降”。
第一步:先判断这是哪一类重构对象
前面的这些案例,实际上可以被归到几类典型重构对象里:
- 页面型:待办应用、点餐页、说说应用
- 基础组件型:表单组件、表格组件、树组件
- 运行时边界型:SSR 数据预取、水合与同构状态流
- 框架场景型:Nuxt 页面数据获取、渲染模式与 hydration 边界
- 状态管理型:Vuex 状态中心设计、Pinia 迁移与 store 领域拆分
- 复杂交互型:动画协调层、多浮层状态与复杂链路反馈
- 认证与权限型:登录态、请求拦截、路由守卫与页面准入
- 路由与布局型:应用壳层、路由出口、页面缓存与布局切换
- 容错与降级型:错误边界、客户端渲染边界、fallback 与恢复路径
- 异步加载型:懒加载策略、占位内容、timeout 与恢复动作
- Nuxt 运行时边界型:中间件职责、插件注入与页面消费分层
- Nuxt 响应策略型:routeRules、缓存策略与混合渲染分层
- Nuxt 执行时机型:生命周期分段、SSR 与客户端副作用边界
- Nuxt 依赖边界型:自动导入、公共入口与隐式依赖控制
- Nuxt 接口边界型:Server Routes、BFF 聚合与服务端适配层
- Nuxt 页面编排型:页面入口收口、页面业务 composable 与局部步骤协同
- Nuxt 模块组织型:业务主线聚合、feature 私有实现与公共层收口
- Nuxt 公共层治理型:应用基础设施、共享协议与伪复用回收机制
- 混合型:既有页面编排,又带基础交互能力的中间组件
不同类别,重构重点不同。
页面型项目更关注:
- 页面容器是否过重
- 状态是否有唯一来源
- 异步请求和副作用是否已经收口
- 展示组件是否只做 props / emits
基础组件型项目更关注:
- 扩展协议是否稳定
- 组件通信是否清晰
- 状态逻辑是否能从组件实现中分离
- 消费端 API 是否足够简单
所以第一步不是“开始拆文件”,而是先识别你正在重构的是页面系统,还是基础能力。
第二步:找到真正的复杂度中心
所有值得重构的 Vue 项目,几乎都有自己的复杂度中心。前面这些案例可以很清楚地看到这件事:
- 待办应用的复杂度中心是“筛选 + 列表 + 输入 + 统计”共存于一个页面
- 点餐页的复杂度中心是“目录 + 购物车 + 详情 + 动画”的联动链路
- 说说应用的复杂度中心是“会话 + 内容流 + 多个弹窗”的前台闭环
- 表单组件的复杂度中心是“字段注册 + 校验规则 + 表单上下文”
- 表格组件的复杂度中心是“列扩展协议 + 单元格渲染能力”
- 树组件的复杂度中心是“递归结构 + 父子状态传播”
真正有效的重构,不是平均用力,而是先找到复杂度中心,再围绕它拆。
一个很实用的判断标准是:
- 哪一块逻辑最容易继续长大
- 哪一块逻辑最常在多个组件里重复出现
- 哪一块逻辑最依赖上下文,最容易一改牵一片
那一块,通常就是你最该先收口的地方。
第三步:先稳定模型,再拆组件
前面这些案例有一个共同点:所有真正有效的重构,都是先稳定模型,再拆视图。
这一步经常被忽略。很多时候大家一上来就拆 Header、List、Item,但如果底层对象结构没先收敛,拆出来的组件只是把混乱复制到更多文件里。
所以更稳的顺序应该是:
- 先定义领域模型
- 再定义状态中心
- 再定义组件边界
- 最后才拆展示层
对应到前面案例里就是:
- 待办应用先定义
TodoItem / TodoFilter - 点餐页先定义
FoodItem / CartEntry / FoodCategory - 说说应用先定义
SessionUser / PostItem / ReplyContext - 表单组件先定义
FormRule / FieldState / FormContext - 表格组件先定义
TableColumn / TableCellContext - 树组件先定义
TreeNodeItem / TreeCheckEvent
这一步的本质,是先把“系统在操作什么”说清楚。
第四步:判断状态应该放在页面、组件还是 composable
这是这套统一框架里最关键的一步。
你可以用下面这条规则来判断状态归属:
- 只影响当前一小块展示的状态,留在组件内部
- 会被多个子组件共享或会持续增长的业务状态,抽到 composable
- 只属于页面编排和页面生命周期的状态,放在页面容器
把这条规则代入前面案例会很清楚:
- 待办列表、筛选和统计属于
useTodos - 购物车条目、金额和数量属于
useCart - 登录态属于
useSession - 路由准入与回跳意图属于认证权限链路
- 页面缓存与布局切换属于路由壳层链路
- 错误边界与客户端降级属于容错链路
- 异步加载与懒加载占位属于加载链路
- 中间件与插件注入属于 Nuxt 运行时链路
- routeRules 与混合渲染属于 Nuxt 响应策略链路
- 表单字段注册和整体验证属于
useForm - 树的勾选传播属于
useTreeState - 表格列协议标准化属于
useTableColumns
只要你发现某个组件既保存核心状态,又负责派生统计,又负责副作用,又负责 UI 渲染,那它大概率就已经超载了。
第五步:用“容器组件 + 展示组件”重新切边界
前面的页面型案例,几乎都可以用同一套组件边界去重构:
- 页面容器:负责路由、请求、权限、状态装配
- 功能容器:负责连接 composable 与 UI
- 展示组件:只接 props 和 emits
- 基础组件:只提供最小交互能力
这套边界在前面案例里的映射非常一致:
TodoApp负责装配TodoCreateForm / TodoList / TodoFooterMenuPage负责装配目录、购物车、详情层和动画队列Feed页面负责装配会话、内容流和弹窗控制
而在基础组件型案例里,这个思路会变成另一种形式:
- 根组件:负责提供上下文和对外暴露 API
- 中间桥接层:负责协议转换
- 最小渲染单元:只做一件事
例如:
BaseForm提供表单上下文,BaseFormItem负责单字段,BaseInput只负责输入BaseTable组织结构,BaseTableCell负责渲染决策,TableRenderCell负责桥接 renderBaseTree负责树状态,BaseTreeNode负责递归节点展示
无论是页面还是基础组件,核心原则都一样:让每一层只保留一种主要责任。
第六步:显式数据流优先,隐式通信后退
前面几个基础组件案例都反复出现了同一个问题:早期 demo 很喜欢靠事件名、组件名查找、递归 watch 去完成通信,但长期来看,这些方式都偏脆弱。
统一框架里的建议非常明确:
- 页面和展示组件之间,优先用 props / emits
- 深层上下文共享,优先用 provide / inject
- 多组件共享业务能力,优先用 composable
- 桥接外部扩展能力,优先用显式协议
这条规则在 6 个案例里都成立:
- 表单组件从
Emitter走向provide/inject - 树组件从
findComponentUpward走向树上下文 - 表格组件从模板内分支,走向统一列协议
- 说说应用从多个弹窗各自处理状态,走向
useDialogController
如果一个组件必须依赖“知道父组件叫什么”“知道上层会不会监听某个字符串事件”,那通常就是通信层还值得继续重构的信号。
第七步:把副作用和派生状态分开
很多 Vue demo 难维护,不是因为逻辑太多,而是因为派生状态和副作用混在了一起。
统一框架里要强制区分这两类内容:
- 派生状态:应该优先使用
computed - 副作用:应该收口到明确的 action 或 watcher 中
前面的案例已经说明了这一点:
- 待办剩余数、过滤列表属于派生状态
- 购物车总数、总价、支付文案属于派生状态
- 表单
isRequired、树的父节点选中态、表格列渲染模式也都属于派生状态
而请求、登录、发帖、删除、校验执行、动画触发,才属于真正的副作用。
一旦这两类东西分开,组件体积通常就会明显下降。
第八步:先收口协议,再谈扩展能力
这是基础组件案例里最重要的一条。
扩展能力不是“多留几个 slot”这么简单,而是要先问:外部究竟通过什么协议接入内部能力?
这在 3 个基础组件案例里尤其明显:
- 表单组件需要
FormRule / FieldState / FormContext - 表格组件需要
TableColumn / TableCellContext - 树组件需要
TreeNodeItem / TreeCheckEvent / TreeContext
只有协议稳定了,组件的扩展能力才不会失控。否则你今天加一个 render,明天加一个 slot,后天再加一个 formatter,最后谁都说不清哪种方式才是主路径。
所以这套框架里有一条硬规则:
- 凡是基础组件,一定先设计协议,再设计模板
第九步:让页面消费层尽量薄
前面的所有案例,不管是页面型还是基础组件型,最后都指向同一个结果:消费层越薄,整个系统越稳。
理想状态下,页面或使用方应该只负责:
- 提供数据
- 监听事件
- 组合能力
- 保存业务状态
而不应该负责:
- 理解底层组件的内部通信细节
- 处理内部状态传播算法
- 兼顾多个实现层的同步问题
这也是为什么前面的案例最后都收敛到了类似结构:
- 页面层只装配
useXxx()和组件 - 展示组件只接收输入输出
- 算法与状态都被推回 composable 或纯函数
一套可直接复用的 Vue 重构流程
如果以后要继续整理新的 Vue demo,可以直接复用下面这条流程:
识别项目类型
先判断它更接近页面系统,还是基础组件。
圈出复杂度中心
找出最容易继续长大的那块逻辑,不要平均拆分。
稳定领域模型
先定义对象结构、上下文结构和对外事件结构。
收口状态中心
把多处共享、会派生、会增长的状态移动到 composable。
重画组件边界
把“容器 / 展示 / 基础交互 / 桥接层”分开。
清理通信路径
把组件名查找、隐式事件、递归副作用改成显式契约。
分离派生状态与副作用
让 computed 和 action 各做各的事。
最后再处理模板与体验
结构稳定以后,再去优化模板可读性和交互细节。
一个统一的判断清单
当你准备结束一次重构时,可以用这份清单做最后确认:
- 领域模型是否已经明确命名
- 关键状态是否已经有唯一数据源
- 展示组件是否只保留 props / emits
- 页面容器是否只保留编排职责
- 深层通信是否已经改成显式上下文
- 派生状态是否已经从副作用中分离
- 基础组件是否已经有清晰协议
- 外部消费方式是否比原来更简单
如果这 8 个问题大部分都能答“是”,那这次重构通常已经真正降低了复杂度,而不只是换了文件位置。
这套统一框架真正想沉淀什么
前面这些案例分别解决了不同场景的问题,但串起来以后,会发现它们最终都在做同一件事:把“混在组件里的复杂度”,重新分配到模型、状态中心、协议和组件边界上。
这也是这套统一框架真正想沉淀的核心:
- 先识别复杂度,不要急着拆文件
- 先收口状态和协议,再拆展示层
- 让页面和基础组件都拥有更清晰的职责边界
- 让通信和扩展方式尽量显式、稳定、可复用
以后再整理新的 Vue 练习项目时,不必从零开始想“该怎么重构”,可以先拿这篇框架做总入口,再回到对应案例里找更细的落地方法。
