18 · 共用代码、组件边界与个例保护

适用:同一问题在多页重复出现、修改样式要逐页找、组件开始堆积条件分支,或为了复用把个别页面改坏。抽象的依据是职责、语义和变化原因。

ABS-01 · 按变化原因分层

层级负责什么适合统一调整的内容
基础变量颜色、字号、间距、形状、动效尺度品牌与可读性调整
基础控件按钮、输入、对话框等语义和交互焦点、禁用、忙碌、触控区域
组合模式表单字段、状态区域、分页、页面标题标签、错误位置、状态布局
领域模块订单、项目、成员的规则与数据解释名称校验、权限、错误映射
页面模块组合、路由上下文、特有任务页面流程与确实独有的布局

共享组件不应读取某个页面的路由或根据文案猜业务;基础按钮不应知道什么叫“退款”。页面也不应各自实现一份对话框焦点管理。

ABS-02 · 不同种类的重复分别抽象

纯计算抽函数;有生命周期的状态行为抽 hook/composable;可见结构与交互抽组件;多个页面共享的空间结构抽页面模式;业务限制归领域规则;可编辑文案按语义归内容模块。

两个片段长得相似不证明应该合并。订单总额与图标宽度都写了 24,变化原因完全不同。同一业务规则在提示、校验、计数器和测试中重复,才需要收敛到单一来源。

Vue 的组合式函数适合复用有状态逻辑,其生命周期副作用要清理;参见 Vue Composables。其他框架同样需要明确状态所有者、依赖与释放时机。

ABS-03 · 先用组合,再考虑复杂配置

<ProjectPageShell>
  <PageHeader title="项目" />
  <ProjectFilters v-model="filters" />
  <ProjectList :items="items" :state="state" @retry="refresh" />
  <Pagination v-model:page="page" :total="total" />
</ProjectPageShell>

这是边界示例,不是已提供的组件库。公共结构可组合,领域列表保留自身字段。避免一个 UniversalPage 接收几十个布尔值,根据路由名、用户角色和字段数量拼出所有页面。

变体表达稳定且有限的差异,例如按钮的 intent、size。业务插件或插槽表达内容扩展。若某个变体改变状态机、数据所有者或主要任务,应考虑独立的领域组件。

ABS-04 · 个例必须有明确作用域

exception:
  id: canvas.toolbar.compact
  base: 共享工具条
  reason: 创作工作区需要给画布保留空间
  overrides: [局部间距, 工具排列]
  preserved: [可访问名称, 键盘操作, 焦点, 禁用行为]
  scope: 画布工作区,不能影响设置与账单页面
  verification: 窄屏、触控、键盘、长名称、深色

个例可以是局部 CSS 变量、受控变体或专有组件,不能依赖全局选择器覆盖其他页面。不要通过 !important、第几个子元素或“标题包含某个词”来识别业务例外。

ABS-05 · 拆分要形成可以解释的职责

当一个文件同时承担请求、权限、字段规则、文案、焦点和多个页面模式时,按职责拆分。不是达到某个行数就拆,也不是每三行都新建一个文件。

组件拆分后应能说清输入、输出、状态所有者和副作用;调用者不应为了使用它而知道其内部 DOM。保持 props 只读,业务更新通过事件或明确的状态接口发生。

避免把共享状态放在无意的模块单例里,导致不同页面实例互相污染。纯静态配置可以共享;草稿、计时器和请求控制器通常属于实例。

ABS-06 · 修改公共层必须检查调用方

变更前枚举引用页面、状态与变体,说明默认行为是否改变、个例是否仍成立。需要破坏性修改时提供迁移步骤,避免依赖页面自行补丁。

同一按钮在三处被复制,三处都修并不等于建立了共用代码。新增抽象后应迁移适用调用方,再移除旧实现;不适用的地方写清理由。

ABS-07 · 保留暂时重复的条件

需求尚未稳定、语义不同、未来变化方向不明时,可以暂时保留局部实现并记录候选。发现共同变化后再提取。不要为了减少行数建立一个谁也不敢改的公共框架。

检查:用一次实际改动验证边界,例如修改主控件高度。合理的结果是适用控件一起变化,画布等受保护的个例保持所需差异;不是逐页找数值,也不是整个网站一起被意外改动。