从模糊到清晰:一次组件重构里的开发哲学

从模糊到清晰:一次组件重构里的开发哲学

同系列里,之前写过一篇关于「查表法」的文章------AI 能写功能,但优雅需要你的判断力。

这一篇换一条线:不是展示层配色,而是通用组件与定制组件如何分层。故事来自设备类型管理里的「关联因子」弹窗------要在原有勾选能力上,增加单位、上下限的行级编辑。

需求听起来不复杂,但一开始,画面是模糊的。


一、开发哲学:模糊是正常的,先上路

实现一个功能,最初往往想不透。各种方案在脑子里排列组合,像某种「量子纠缠态」------每一种选择都会牵出新的不确定性:改通用组件?复制一份?抽 composable?插槽怎么传?响应式会不会断?

想太多,精力就散了。

这次实践里,我慢慢摸出一条适合自己的节奏:

层层递进、按部就班,不要想太多,just do it。

要有画面感、抽象能力和架构意识,可以想想可能遇到的问题,但别因此卡住整个流程。

先把看得见的、能做的做起来。

车到山前必有路:遇水搭桥,遇上开路,看见一个改一个。

具体到这个需求,路径是这样展开的:

阶段 当时的状态 做出的决定
起点 要在 DeviceTypeBindPollutant 里加 unit / 上下限编辑 很模糊,方案很多
第一次收敛 意识到这是「通用勾选」与「因子配置」的分叉 不能大改 BaseCheckTable,保持职责单一
第二次收敛 需要 tab 筛选、搜索、行级编辑、恢复默认 参考 BaseCheckTable 重写一个专用组件
第三次收敛 发现 70% 壳层重复,但列渲染需要定制 薄包装 BaseCheckTable,编辑状态留在子组件
踏出那一步之后 不确定性从「排列组合」变成「几个具体问题」 插槽怎么透传?columns 里写 h() 响应式行不行?

很有意思的一点是:在真正动手之前,前路是模糊的;一旦选了方向、写出第一版,剩下的往往就只剩两三个技术点要验证。

不是一开始就想通了,是走起来才想通。


二、架构分层:通用勾选 vs 业务编辑

为什么不直接改 BaseCheckTable

BaseCheckTable 被十几个绑定弹窗共用------站房绑监测点、设备绑因子、类型绑类型......它们只需要:

  • 全部 / 未选 / 已选 tab
  • 搜索过滤
  • checkbox 勾选

如果在上面加 editColumns、行级编辑态、恢复默认按钮,通用组件会被设备类型因子的业务逻辑污染。10+ 个简单场景,为一个复杂场景买单,不划算。

为什么不一直独立到底?

第一版 DeviceTypeFactorCheckTableBaseCheckTable 的壳层几乎完整复制了一遍:wrapper、head、search、tableData / searchData / count*、vxe-grid 配置、scoped 样式。

能跑,但不优雅------FactorCheckTable.vue 形成了第三份重复。

最终的分工是:

css 复制代码
DeviceTypeBindPollutant/          ← 弹窗编排、接口、校验
├── server.ts                     ← 数据合并(主数据 + 已保存配置)
├── DeviceTypeFactorCheckTable    ← 列定义 + editState + 行级编辑
└── (复用)BaseCheckTable        ← 勾选壳层:筛选 / 搜索 / grid
组件 职责
BaseCheckTable 勾选基础设施:filterType、searchName、searchData、vxe-grid 透传
DeviceTypeFactorCheckTable 业务扩展:columns、editState、unit/上下限/操作列
DeviceTypeBindPollutant 弹窗生命周期:open、提交、上下限校验

勾选是通用状态,编辑是业务状态------边界清楚了,代码自然就薄了。

最终的 DeviceTypeFactorCheckTable 模板只剩一层:

vue 复制代码
<BaseCheckTable
  :columns="columns"
  :data-source="dataSource"
  :loading="loading ?? false"
  check-field="checked"
  search-placeholder="搜索因子名称/编码..."
  :filter-option="pollutantFilter"
>
  <template #unit="{ row, rowIndex }">...</template>
  <template #lowerLimit="{ row, rowIndex }">...</template>
  <template #upperLimit="{ row, rowIndex }">...</template>
  <template #action="{ row, rowIndex }">...</template>
</BaseCheckTable>

这才是我想要的「优雅的状态」。


三、插槽透传与筛选:白名单,而不是无脑全传

要让上面的写法成立,中间层 BaseCheckTable 必须把父组件的 #unit 等插槽透传给内层 vxe-grid

naive 写法是:

vue 复制代码
<template v-for="(_, name) in slots" :key="name" #[name]="slotProps">
  <slot :name="name" v-bind="slotProps ?? {}" />
</template>

问题 :以后如果 BaseCheckTable 自己也要插槽呢?比如 #head-extra(头部额外按钮)、#search-extra(搜索框旁)、#footer(表格底部)------这些会被误传给 vxe-grid,轻则无效,重则奇怪报错。

解法:COMPONENT_SLOT_NAMES 白名单

ts 复制代码
/** BaseCheckTable 自身插槽,其余插槽均透传给 vxe-grid */
const COMPONENT_SLOT_NAMES = ['head-extra', 'search-extra', 'footer'] as const;

const gridSlotNames = computed(() =>
  Object.keys(slots).filter(
    (name) => !COMPONENT_SLOT_NAMES.includes(name as (typeof COMPONENT_SLOT_NAMES)[number])
  )
);

模板里:

vue 复制代码
<div class="head">
  <!-- 组件自己的插槽 -->
  <slot name="head-extra" :counts="{ count0, count1, count2 }" />
</div>

<div class="search">
  <a-input ... />
  <slot name="search-extra" :search-name="state.searchName" />
</div>

<vxe-grid ...>
  <!-- 只透传 grid 插槽 -->
  <template v-for="name in gridSlotNames" :key="name" #[name]="slotProps">
    <slot :name="name" v-bind="slotProps ?? {}" />
  </template>
</vxe-grid>

<slot name="footer" />

数据流:

ini 复制代码
DeviceTypeFactorCheckTable  #unit="{ row, rowIndex }"
        ↓
BaseCheckTable              <slot name="unit" v-bind="slotProps" />
        ↓
vxe-grid                    #unit 渲染单元格

规则 :组件自身插槽 → 显式写在模板对应位置;不在白名单里的 → 自动透传给 vxe-grid

目前 head-extra / search-extra / footer 是预留的------现在没人用,但结构已经就位。以后加组件插槽,只需两步:名字加入白名单 + 模板里挂 <slot>

columns 与插槽的配合

vxe-grid 支持两种列渲染方式:

写法 columns 配置 渲染来源
具名插槽 slots: { default: 'unit' } 模板 #unit
内联函数 slots: { default({ row }) { return h(...) } } columns 里的函数

我们选用第一种------columns 只声明插槽名,具体 UI 写在 template。可读性更好,按钮和表单组件一目了然。

ts 复制代码
const columns = [
  { title: '单位', field: 'unit', slots: { default: 'unit' } },
  { title: '下限', field: 'lowerLimit', slots: { default: 'lowerLimit' } },
  { title: '上限', field: 'upperLimit', slots: { default: 'upperLimit' } },
  { title: '操作', field: 'action', slots: { default: 'action' } },
];

如果全部收进 columnsh() 里,确实可以不再依赖插槽透传------但 action 列的按钮组会变得冗长。这是可读性与组合灵活性的取舍,没有唯一正确答案。


四、响应式实验:AI 不确定的事,实测说了算

在尝试把列渲染收进 columnsslots.default + h() 时,有一个理论上的担忧:

columns 若是普通常量数组,里面的 default 虽然闭包了 editState,但 **editIndex 变化时不一定会触发单元格重绘**。

AI 的建议是:把 columns 做成 computed,显式依赖 editState.editIndex,更稳妥。

我做了个最小 MVP 实验------只把 upperLimit 一列改成 h(InputNumber, ...) 写法,其余列保持 template 插槽,然后手动测:

  1. 点「修改」→ 输入框出现 ✓
  2. 输入数值 → 临时状态更新 ✓
  3. 点「确定」→ 写回 dataSource,回到展示态 ✓
  4. 筛选 / 搜索后编辑 → rowIndex 对应正确 ✓

结论:在当前这套 vxe-grid + Vue 3 + ant-design-vue 组合下,非响应式的 columns 常量 + 内联 slot 函数,编辑态切换是正常的。

原因大概是:vxe 渲染单元格时会反复调用 default({ row, rowIndex })editState.editIndex 变化 → 组件重渲染 → slot 函数重跑 → UI 切换。输入过程中则是 onUpdate:value 驱动子组件自身更新。

结论
computed(columns) 可选优化,不是必须
内联 h() 可行,有利于不依赖插槽透传
template 插槽 可读性更好,响应式更直观
AI 的理论边界 要验证,不能盲信

这件事挺有启发:AI 能列出理论风险,但你的实测才是项目里的真相。 没有观察到「点了修改但不切换」的现象,就不必提前加复杂度。

如果以后真遇到偶发不刷新,再考虑 computed(columns)reloadColumn------YAGNI,看见了再改。


五、内聚:子目录也是一种优雅

除了组件分层,还把模块收进了独立目录:

bash 复制代码
src/components/bindRelation/DeviceTypeBindPollutant/
├── DeviceTypeBindPollutant.vue
├── DeviceTypeFactorCheckTable.vue
├── server.ts          ← 从 bindRelation/server.ts 迁出并增强
└── type.ts

getDeviceTypeFactorRows 负责合并「因子主数据」与「已保存配置」,带上 defaultUnit / defaultLowerLimit / defaultUpperLimit 供恢复按钮使用。弹窗只关心 open → 展示 → modalOk 提交。

相关的放在一起,不相关的不要硬凑。 通用的 getPollutantsChecked 仍留在 bindRelation/server.tsDeviceFormModal 用------没有为了「内聚」而过度迁移。


六、回头看:优雅是走出来的

这次重构如果一上来就追求「完美架构」,可能会在这些问题上卡住:

  • 要不要改 BaseCheckTable
  • 插槽怎么透传?要不要白名单?
  • columnsh() 还是 template?要不要 computed
  • 要不要抽 useCheckTableFilter

每一个都值得想,但不值得在起点就想完

实际路径是:

  1. 先建子目录 + 专用表格,快速交付功能
  2. 发现壳层重复,尝试 h() 写法,验证响应式
  3. 确认可以薄包装 BaseCheckTable,补上插槽透传和白名单
  4. 列渲染回到 template 插槽------因为可读性更好

没有哪一步是浪费的。前面的「弯路」换来了后面的确定感。


写在最后

那篇文章里说过:AI 的代码质量,是你认知水平的投影。

这一篇补一句:你的认知,也是在一次次「先上路、再修正」里长出来的。

模糊不可怕。可怕的是因为模糊而站着不动,或者因为想一次想全而什么都写不出来。

车到山前必有路------但路是自己一步一步走出来的。


本文基于真实项目经验整理,手工起草文章大纲,AI 辅助润色,于 2026-06-11

相关推荐
如果超人不会飞1 小时前
TinyRobot AI 对话组件库全组件使用指南
前端·vue.js
如果超人不会飞2 小时前
Vue.js
vue.js
往事随风灬2 小时前
我被 Volta 的“智能”坑了一下午:pnpm 为何无视项目 Node 版本?
前端·vue.js
如果超人不会飞2 小时前
TinyVue Layout 组件完全指南:别再手写 float 和 flex 了,栅格早该这样用
vue.js
AI行业学习3 小时前
CC-Switch v3.16.1 官方下载 | 安装配置详细教程【2026.6.10】
java·开发语言·vue.js·python·mysql·eclipse·html
小二·4 小时前
Spring Boot 3 + Vue 3 全栈开发实战
vue.js·spring boot·后端
阿猫的故乡4 小时前
Vue组合式函数(Composables)从入门到实战:鼠标跟踪、请求封装、本地存储……全案例拆解
前端·vue.js·计算机外设