Skip to content

Vue练习项目重构统一框架

前面已经整理了多篇案例文档,但如果只停留在“一个项目一篇总结”,这些经验仍然容易散。更有价值的做法,是把这些案例抽象成一套可以重复套用的统一框架。

这篇文档的目标只有一个:把前面零散的 Vue 重构案例,串成一条稳定的方法论。以后不管你面对的是待办应用、点餐页、内容社区、表单组件、表格组件还是树组件,都可以先按这套框架诊断,再决定怎么拆。

这套框架覆盖的 22 个案例

这次整理的统一框架,来自下面这些案例:

它们看起来题材不同,但底层问题高度一致:状态归属混乱、组件职责过重、扩展协议不清晰、通信路径隐式、运行时边界不清,以及副作用和展示层缠在一起。

统一框架先回答 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 项目,几乎都有自己的复杂度中心。前面这些案例可以很清楚地看到这件事:

  • 待办应用的复杂度中心是“筛选 + 列表 + 输入 + 统计”共存于一个页面
  • 点餐页的复杂度中心是“目录 + 购物车 + 详情 + 动画”的联动链路
  • 说说应用的复杂度中心是“会话 + 内容流 + 多个弹窗”的前台闭环
  • 表单组件的复杂度中心是“字段注册 + 校验规则 + 表单上下文”
  • 表格组件的复杂度中心是“列扩展协议 + 单元格渲染能力”
  • 树组件的复杂度中心是“递归结构 + 父子状态传播”

真正有效的重构,不是平均用力,而是先找到复杂度中心,再围绕它拆。

一个很实用的判断标准是:

  • 哪一块逻辑最容易继续长大
  • 哪一块逻辑最常在多个组件里重复出现
  • 哪一块逻辑最依赖上下文,最容易一改牵一片

那一块,通常就是你最该先收口的地方。

第三步:先稳定模型,再拆组件

前面这些案例有一个共同点:所有真正有效的重构,都是先稳定模型,再拆视图。

这一步经常被忽略。很多时候大家一上来就拆 HeaderListItem,但如果底层对象结构没先收敛,拆出来的组件只是把混乱复制到更多文件里。

所以更稳的顺序应该是:

  • 先定义领域模型
  • 再定义状态中心
  • 再定义组件边界
  • 最后才拆展示层

对应到前面案例里就是:

  • 待办应用先定义 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 / TodoFooter
  • MenuPage 负责装配目录、购物车、详情层和动画队列
  • Feed 页面负责装配会话、内容流和弹窗控制

而在基础组件型案例里,这个思路会变成另一种形式:

  • 根组件:负责提供上下文和对外暴露 API
  • 中间桥接层:负责协议转换
  • 最小渲染单元:只做一件事

例如:

  • BaseForm 提供表单上下文,BaseFormItem 负责单字段,BaseInput 只负责输入
  • BaseTable 组织结构,BaseTableCell 负责渲染决策,TableRenderCell 负责桥接 render
  • BaseTree 负责树状态,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 练习项目时,不必从零开始想“该怎么重构”,可以先拿这篇框架做总入口,再回到对应案例里找更细的落地方法。

共 20 个模块,1301 篇 Markdown 文档。