这节课解决的问题是:
为什么一个页面功能越来越多之后,不能一直把所有代码堆在同一个 .vue 文件里?
答案很简单,因为页面一旦变大,就会立刻出现这些问题:
- 代码太长,不好读
- 状态、事件、模板混在一起,不好找
- 某一块逻辑改动时,容易影响别的区域
- 相同结构难复用
- 以后加功能时,成本越来越高
所以组件拆分不是"为了高级",而是为了让页面继续可维护。
先讲结论
拆组件最重要的一句话是:
按职责拆,不要按"想拆就拆"
也就是说,你不是看到代码长就随便切几刀,
而是要先判断页面里是不是已经存在"职责独立的一块区域"。
对当前 TasksView.vue 来说,已经非常明显了。
它至少有 4 块天然独立的区域:
- 页面头部
- 统计卡片
- 筛选栏
- 任务表格
- 新增任务弹窗
这说明它已经到了适合拆分的阶段。
这节课看哪些文件
src/views/TasksView.vuedocs/03-tasks-view.md
这节课暂时先讲拆分思路,不急着改代码。
先把"怎么拆才合理"想清楚,再动手会更稳。
先判断:什么时候该拆组件
你以后看到一个页面,如果同时满足下面几点,就应该开始考虑拆组件了:
1. 模板区域已经明显分块
比如当前任务页:
- 标题区
- 卡片区
- 筛选区
- 表格区
- 弹窗区
这些区块天然就不是一回事。
2. 某块区域有独立输入和输出
比如筛选栏:
- 输入:关键字、状态、优先级
- 输出:筛选条件变化事件
再比如弹窗:
- 输入:是否显示、表单默认值
- 输出:提交事件、关闭事件
只要一个区域已经有清晰的输入输出,它就具备组件化条件。
3. 某块区域以后可能复用
例如统计卡片。
今天它出现在任务页,
明天仪表盘页也可能出现类似结构。
这种区域就值得拆成独立组件。
4. 某块区域逻辑已经开始影响阅读
当前 TasksView.vue 里,
模板、筛选逻辑、弹窗表单、表格插槽都放在一起。
结果就是:
你看筛选逻辑时,很容易被弹窗代码打断;
你看表格结构时,又会被表单默认值打断。
这就是一个非常典型的"该拆了"的信号。
组件拆分最常见的错误
在正式讲怎么拆之前,你先避开两个大坑。
错误 1:拆得太碎
比如一个按钮也拆组件,一个标题也拆组件,一个纯文本段落也拆组件。
这样会让项目看起来"很组件化",但实际上阅读成本更高。
所以不要为了拆而拆。
错误 2:把所有状态也一起打散
初学者常见错误是:
- 筛选状态放一个组件
- 列表状态放一个组件
- 表单状态再放一个组件
结果状态关系反而更乱。
初学阶段更稳的原则是:
先拆 UI 区块,再决定状态要不要下沉或上移
当前任务页最合理的拆分方案
如果我们要重构当前任务页,我建议拆成下面这些组件:
1. TaskPageHeader.vue
职责:
- 显示页面标题
- 显示页面说明
- 显示"新增任务"按钮
输入:
- 无或少量文案 props
输出:
create事件
为什么值得拆:
因为它是一个完整、清晰、低耦合的头部区域。
2. TaskStatsCards.vue
职责:
- 展示顶部四张统计卡片
输入:
stats数组
输出:
- 基本没有,只负责展示
为什么值得拆:
这是一块典型的"纯展示组件"。
3. TaskFilterBar.vue
职责:
- 展示关键字输入框
- 展示状态筛选
- 展示优先级筛选
输入:
- 当前关键字
- 当前状态值
- 当前优先级值
- 状态选项
- 优先级选项
输出:
- 更新关键字
- 更新状态
- 更新优先级
为什么值得拆:
因为它是一块独立的交互区域,职责非常清楚。
4. TaskTable.vue
职责:
- 展示任务表格
- 展示空状态
- 负责状态标签和优先级标签的显示
输入:
tasks
输出:
- 暂时没有复杂事件
为什么值得拆:
因为表格模板通常最容易变长,而且以后还可能继续加:
- 操作列
- 编辑按钮
- 删除按钮
- 分页
- 行点击
所以越早独立出来越好。
5. TaskCreateDialog.vue
职责:
- 控制新增任务弹窗内容
- 维护表单展示
- 提交表单时抛出结果
输入:
visible- 默认负责人
- 状态选项
- 优先级选项
输出:
update:visiblesubmitclose
为什么值得拆:
弹窗表单本身就已经是一整块小业务了。
如果按这个方案拆,页面会变成什么样
重构后的 TasksView.vue 应该更像一个"组合页":
vue
<template>
<div class="page-shell">
<TaskPageHeader @create="openCreateDialog" />
<TaskStatsCards :stats="taskStats" />
<section class="section-card">
<TaskFilterBar
:keyword="keyword"
:status-filter="statusFilter"
:priority-filter="priorityFilter"
:status-options="statusOptions"
:priority-options="priorityOptions"
@update:keyword="keyword = $event"
@update:status-filter="statusFilter = $event"
@update:priority-filter="priorityFilter = $event"
/>
<TaskTable :tasks="filteredTasks" />
</section>
<TaskCreateDialog
:visible="dialogVisible"
:status-options="statusOptions"
:priority-options="priorityOptions"
:default-assignee="userStore.displayName"
@update:visible="dialogVisible = $event"
@submit="createTask"
@close="resetTaskForm"
/>
</div>
</template>
你现在不用先关心语法细节,
先感受这个结构变化:
TasksView.vue 从"大量细节堆在一起",变成了"负责组装多个小部件"。
这就是父页面最理想的状态。
组件拆分后,父组件应该保留什么
这是一个非常重要的问题。
当前任务页如果拆组件,父组件最适合保留这些内容:
- 主数据
tasks - 筛选条件
- 弹窗开关状态
- 新增任务主逻辑
- 统计与筛选这类核心
computed
也就是说,父组件更像"页面控制中心"。
你可以把它理解成:
父组件保留页面级状态和业务编排,子组件负责各自区域的显示与交互
子组件应该负责什么
展示型组件
比如 TaskStatsCards.vue:
- 接收数据
- 负责渲染
- 尽量少管业务
交互型组件
比如 TaskFilterBar.vue:
- 接收当前值
- 用户操作后把变化通知父组件
- 自己不决定最终业务结果
表单型组件
比如 TaskCreateDialog.vue:
- 负责输入控件和弹窗 UI
- 收集用户输入
- 提交时把结果抛给父组件
为什么"父组件管状态,子组件管展示和局部交互"是一个好原则
因为这样有 3 个好处:
1. 状态更容易集中管理
你不用到处找"这条数据到底在哪个子组件里改了"。
2. 子组件更容易复用
只要输入输出稳定,它就能在别的页面继续用。
3. 调试时思路更清楚
你会更容易回答这些问题:
- 数据在哪儿?
- 谁改了数据?
- 谁只是在显示数据?
这比"每个组件都自己存一部分核心状态"稳得多。
拆组件时最重要的 3 个设计问题
每次准备拆组件时,你都先问自己这 3 个问题:
1. 这块区域的职责是什么
如果你一句话都说不清它负责什么,
通常说明它还不适合单独拆出去。
2. 它的输入是什么
也就是:
- 它需要哪些 props
- 它依赖哪些外部值
3. 它的输出是什么
也就是:
- 它要抛出什么事件
- 父组件怎么知道它内部发生了变化
只要输入输出清楚,组件边界通常就会很清楚。
以 TaskFilterBar 为例,怎么理解输入输出
如果我们设计 TaskFilterBar.vue,
它最自然的输入输出大概就是这样:
输入:
keywordstatusFilterpriorityFilterstatusOptionspriorityOptions
输出:
update:keywordupdate:statusFilterupdate:priorityFilter
这说明:
- 它不拥有最终筛选数据
- 它只是用户操作的入口
- 真正状态还是由父组件统一持有
这个思路非常重要。
以 TaskCreateDialog 为例,为什么它比别的组件更复杂
因为它内部不只是"显示"。
它通常还会涉及:
- 表单默认值
- 表单校验
- 提交按钮
- 关闭弹窗
- 重置表单
所以它是一个典型的"局部复杂组件"。
这时候你就要考虑:
是让它内部自己维护表单状态,还是由父组件统一传入?
初学阶段我建议你先采用一个更稳的方式:
- 父组件控制
visible - 子组件内部管理局部表单输入
- 提交时通过事件把表单结果抛回父组件
这样复杂度比较合适。
拆组件后最容易获得的 4 个好处
1. 代码更短
每个文件只关注一小块职责。
2. 阅读路径更清楚
你想看筛选,就去筛选组件。
你想看表格,就去表格组件。
3. 以后扩展功能更容易
比如你要给表格加"操作列",
直接改 TaskTable.vue 就够了。
4. 测试更容易写
组件拆开后,单测可以更聚焦:
- 测筛选栏有没有正确抛出更新事件
- 测弹窗表单提交时有没有发出正确 payload
这节课最容易混淆的点
1. 组件拆分不等于状态也必须拆分
很多时候,UI 可以拆,状态仍然保留在父组件里。
2. 不是所有重复结构都要立刻抽组件
如果只是出现一次,而且逻辑很简单,可以先不拆。
拆组件是为了降低复杂度,不是为了追求"组件数量多"。
3. 父组件不是"什么都不干"
拆完之后,父组件通常依然最重要。
它负责把数据、计算属性、事件流和子组件组合起来。
4. 子组件不应该偷偷改父组件核心业务
更稳的做法是:
- 子组件发事件
- 父组件决定怎么处理
这样数据流更清晰。
你现在应该能回答的 10 个问题
- 为什么
TasksView.vue已经到了适合拆组件的阶段? - 组件拆分最核心的原则是什么?
- 为什么不能为了拆而拆?
- 哪些区域属于"天然适合拆出去"的区块?
- 父组件最适合保留哪些内容?
- 展示型组件和交互型组件的差别是什么?
TaskFilterBar的输入和输出分别应该是什么?TaskCreateDialog为什么比统计卡片更复杂?- 为什么拆组件后更容易写测试?
- 为什么"父组件管业务,子组件管展示和交互"是一个好默认策略?
这节课的动手练习
练习 1
先不要改代码,只拿纸或者 Markdown 自己画出 TasksView.vue 的拆分图:
- 哪几块适合拆
- 每块负责什么
- 每块需要什么输入
- 每块会抛出什么输出
目的:
先练"设计边界",不要一上来就写代码。
练习 2
尝试只拆一个最简单的组件:
TaskStatsCards.vue
目的:
先从"纯展示组件"开始,建立拆组件的手感。
练习 3
再尝试思考 TaskFilterBar.vue 的 props 和 emits。
目的:
练习把"交互组件"的输入输出设计清楚。
练习 4
对照现在的 TasksView.vue,看看哪些代码属于:
- 页面级状态
- 展示模板
- 表格显示规则
- 弹窗表单逻辑
目的:
训练你按职责读页面,而不是从上到下机械阅读。
这节课的复习结论
最后把核心内容压缩成 8 句话:
- 页面变大后,组件拆分的核心目标是降低复杂度。
- 组件应该按职责拆,而不是按感觉拆。
- 先拆清晰区块,再考虑状态边界。
- 父组件通常保留页面级状态和业务编排。
- 子组件更适合负责展示和局部交互。
- 组件的输入输出一旦清楚,边界就会清楚。
- 当前任务页最适合拆成头部、统计卡片、筛选栏、表格、弹窗。
- 先会设计组件边界,再去写组件,效率会高很多。