第 4 课:怎么把一个大页面拆成多个组件

这节课解决的问题是:

为什么一个页面功能越来越多之后,不能一直把所有代码堆在同一个 .vue 文件里?

答案很简单,因为页面一旦变大,就会立刻出现这些问题:

  • 代码太长,不好读
  • 状态、事件、模板混在一起,不好找
  • 某一块逻辑改动时,容易影响别的区域
  • 相同结构难复用
  • 以后加功能时,成本越来越高

所以组件拆分不是"为了高级",而是为了让页面继续可维护。


先讲结论

拆组件最重要的一句话是:

按职责拆,不要按"想拆就拆"

也就是说,你不是看到代码长就随便切几刀,

而是要先判断页面里是不是已经存在"职责独立的一块区域"。

对当前 TasksView.vue 来说,已经非常明显了。

它至少有 4 块天然独立的区域:

  1. 页面头部
  2. 统计卡片
  3. 筛选栏
  4. 任务表格
  5. 新增任务弹窗

这说明它已经到了适合拆分的阶段。


这节课看哪些文件

  1. src/views/TasksView.vue
  2. docs/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:visible
  • submit
  • close

为什么值得拆:

弹窗表单本身就已经是一整块小业务了。


如果按这个方案拆,页面会变成什么样

重构后的 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

它最自然的输入输出大概就是这样:

输入:

  • keyword
  • statusFilter
  • priorityFilter
  • statusOptions
  • priorityOptions

输出:

  • update:keyword
  • update:statusFilter
  • update:priorityFilter

这说明:

  • 它不拥有最终筛选数据
  • 它只是用户操作的入口
  • 真正状态还是由父组件统一持有

这个思路非常重要。


TaskCreateDialog 为例,为什么它比别的组件更复杂

因为它内部不只是"显示"。

它通常还会涉及:

  • 表单默认值
  • 表单校验
  • 提交按钮
  • 关闭弹窗
  • 重置表单

所以它是一个典型的"局部复杂组件"。

这时候你就要考虑:

是让它内部自己维护表单状态,还是由父组件统一传入?

初学阶段我建议你先采用一个更稳的方式:

  • 父组件控制 visible
  • 子组件内部管理局部表单输入
  • 提交时通过事件把表单结果抛回父组件

这样复杂度比较合适。


拆组件后最容易获得的 4 个好处

1. 代码更短

每个文件只关注一小块职责。


2. 阅读路径更清楚

你想看筛选,就去筛选组件。

你想看表格,就去表格组件。


3. 以后扩展功能更容易

比如你要给表格加"操作列",

直接改 TaskTable.vue 就够了。


4. 测试更容易写

组件拆开后,单测可以更聚焦:

  • 测筛选栏有没有正确抛出更新事件
  • 测弹窗表单提交时有没有发出正确 payload

这节课最容易混淆的点

1. 组件拆分不等于状态也必须拆分

很多时候,UI 可以拆,状态仍然保留在父组件里。


2. 不是所有重复结构都要立刻抽组件

如果只是出现一次,而且逻辑很简单,可以先不拆。

拆组件是为了降低复杂度,不是为了追求"组件数量多"。


3. 父组件不是"什么都不干"

拆完之后,父组件通常依然最重要。

它负责把数据、计算属性、事件流和子组件组合起来。


4. 子组件不应该偷偷改父组件核心业务

更稳的做法是:

  • 子组件发事件
  • 父组件决定怎么处理

这样数据流更清晰。


你现在应该能回答的 10 个问题

  1. 为什么 TasksView.vue 已经到了适合拆组件的阶段?
  2. 组件拆分最核心的原则是什么?
  3. 为什么不能为了拆而拆?
  4. 哪些区域属于"天然适合拆出去"的区块?
  5. 父组件最适合保留哪些内容?
  6. 展示型组件和交互型组件的差别是什么?
  7. TaskFilterBar 的输入和输出分别应该是什么?
  8. TaskCreateDialog 为什么比统计卡片更复杂?
  9. 为什么拆组件后更容易写测试?
  10. 为什么"父组件管业务,子组件管展示和交互"是一个好默认策略?

这节课的动手练习

练习 1

先不要改代码,只拿纸或者 Markdown 自己画出 TasksView.vue 的拆分图:

  • 哪几块适合拆
  • 每块负责什么
  • 每块需要什么输入
  • 每块会抛出什么输出

目的:

先练"设计边界",不要一上来就写代码。


练习 2

尝试只拆一个最简单的组件:

  • TaskStatsCards.vue

目的:

先从"纯展示组件"开始,建立拆组件的手感。


练习 3

再尝试思考 TaskFilterBar.vue 的 props 和 emits。

目的:

练习把"交互组件"的输入输出设计清楚。


练习 4

对照现在的 TasksView.vue,看看哪些代码属于:

  • 页面级状态
  • 展示模板
  • 表格显示规则
  • 弹窗表单逻辑

目的:

训练你按职责读页面,而不是从上到下机械阅读。


这节课的复习结论

最后把核心内容压缩成 8 句话:

  1. 页面变大后,组件拆分的核心目标是降低复杂度。
  2. 组件应该按职责拆,而不是按感觉拆。
  3. 先拆清晰区块,再考虑状态边界。
  4. 父组件通常保留页面级状态和业务编排。
  5. 子组件更适合负责展示和局部交互。
  6. 组件的输入输出一旦清楚,边界就会清楚。
  7. 当前任务页最适合拆成头部、统计卡片、筛选栏、表格、弹窗。
  8. 先会设计组件边界,再去写组件,效率会高很多。
相关推荐
wb1892 小时前
docker-ce容器技术重习
运维·笔记·docker·容器·云计算
qq_8573058192 小时前
ubuntu 22 源码安装bochs
linux·运维·ubuntu
A-刘晨阳2 小时前
麒麟v10桌面版2403版本运行程序提示权限不足(KYSEC)
运维·云计算·操作系统·银河麒麟·麒麟桌面系统
skywalk81632 小时前
使用DuMate帮助创建 Python 3.9 环境并部署 Kotti CMS
前端·chrome
英俊潇洒美少年2 小时前
Vue Hook 与 React Hook 全面解析:区别、用法、实战及避坑指南
前端·vue.js·react.js
恒创科技HK2 小时前
恒创科技:刚交付的香港云服务器应该做哪些测试
运维·服务器
刘某的Cloud2 小时前
svc中外部流量访问限制
linux·运维·docker·kubernetes·service
weixin_704266052 小时前
项目总结一
java·前端·spring boot·后端·spring
Mintopia2 小时前
接口设计为什么越改越乱:新手最容易踩的三个坑
前端