2、模板设计原则:DRY原则、单一职责原则、可配置性原则、可扩展性原则

做模板工程这几年,我踩过不少坑。刚开始带团队时,大家各写各的模板,结果项目一多,维护成本直线飙升。后来我慢慢总结出四个核心原则,今天跟你聊聊。

说白了,模板设计就是一门「偷懒」的艺术。但偷懒也要有章法,否则就是给自己挖坑。

2.1 DRY 原则:别重复你自己

DRY,全称是 Don't Repeat Yourself。这个概念最早出现在《程序员修炼之道》里。我个人的理解是:每一份知识,在系统里只能有一个权威的表述。

举个例子。你写了一个用户列表页,里面有个表格组件。过两天又写一个订单列表页,表格结构几乎一样。如果你直接复制粘贴,那就违反了 DRY 原则。

核心思想: 把公共逻辑抽离成可复用的模板片段或组件。

我在项目中遇到过这样的情况:一个电商后台,有 20 多个列表页。每个页面都自己写分页逻辑、搜索逻辑。后来改需求,要统一加一个「导出 Excel」功能。你猜怎么着?改了整整两天,还漏了两个页面。

正确的做法是这样:

<!-- 错误示范:每个页面都写一遍 -->
<template>
  <div>
    <table>...</table>
    <div class="pagination">
      <button @click="prevPage">上一页</button>
      <span>{{ currentPage }} / {{ totalPages }}</span>
      <button @click="nextPage">下一页</button>
    </div>
  </div>
</template>

<!-- 正确示范:抽离成通用组件 -->
<template>
  <div>
    <data-table :columns="columns" :data="data">
      <template #action>
        <button @click="handleEdit">编辑</button>
      </template>
    </data-table>
    <pagination :total="total" @change="handlePageChange" />
  </div>
</template>
小技巧: 当你发现自己第三次写同样的代码时,就该考虑抽离了。第一次可以复制,第二次可以容忍,第三次必须重构。

2.2 单一职责原则:一个模板只做一件事

这个原则来自 SOLID 设计原则。应用到模板工程里,意思就是:一个模板文件、一个组件,应该只有一个明确的职责。

你想想看,如果一个组件既负责展示数据,又负责处理业务逻辑,还负责样式布局。那它就像一个杂货铺,什么都有,但什么都不精。改起来也麻烦。

我曾经接手过一个项目,有个叫 SuperTable.vue 的组件,足足有 2000 多行。它包含了表格渲染、分页、搜索、排序、导出、打印、行内编辑…… 嗯,你能想到的功能它都有。后来要改一个排序的 bug,我花了整整一个下午才找到对应的代码。

单一职责原则要求我们:

  • 展示组件:只负责 UI 呈现,不关心数据来源
  • 容器组件:只负责数据获取和状态管理
  • 布局组件:只负责页面结构,不包含业务逻辑
  • 工具函数:只封装通用逻辑,不耦合具体业务

来看一个对比:

违反单一职责 符合单一职责
一个组件包含数据请求、渲染、事件处理 数据请求抽离到 hooks / composables
模板里混入大量条件判断 拆分成多个子组件,通过 props 控制
样式、逻辑、模板全部耦合 使用 CSS Modules / Scoped Styles 隔离
注意: 单一职责不是「一个文件只能有一个函数」。而是说,一个模块的改动理由应该只有一个。如果一个组件因为「改样式」和「改接口」两个原因都需要修改,那就说明职责不单一。

2.3 可配置性原则:把变数交给使用者

可配置性,说白了就是「别把路走死」。你设计的模板,应该允许使用者在某些维度上进行定制,而不需要修改模板本身的代码。

我习惯把配置分为三个层次:

  1. 样式配置:颜色、字体、间距、主题
  2. 行为配置:是否显示某个模块、排序方式、分页大小
  3. 内容配置:标题文案、占位符、提示信息

举个例子,一个通用的弹窗组件:

// 可配置的弹窗组件
const defaultConfig = {
  title: '提示',
  width: 420,
  showClose: true,
  closeOnClickModal: true,
  confirmText: '确定',
  cancelText: '取消',
  showCancel: true
}

// 使用者可以覆盖任意配置
const myModal = createModal({
  title: '删除确认',
  width: 360,
  confirmText: '确认删除',
  cancelText: '我再想想',
  showCancel: true
})

我在项目中遇到过这样的场景:一个通用的表单生成器,需要支持不同业务线的定制需求。有的业务线需要「保存并继续添加」,有的需要「保存并关闭」。如果我把这些逻辑写死在模板里,那每次有新需求都要改模板代码。

后来我改成配置驱动:

// 配置驱动的表单行为
const formConfig = {
  submitAction: 'saveAndClose', // 可选: saveAndClose, saveAndContinue, saveAndNew
  showDraftButton: true,
  autoSaveInterval: 30000,
  confirmBeforeLeave: true
}
建议: 配置项要提供合理的默认值。使用者只需要关注他们想改的部分,其他用默认值就好。这叫「约定优于配置」。

2.4 可扩展性原则:为未来留好接口

可扩展性,就是你的模板要能「长」出新的功能。不是靠修改原有代码,而是通过扩展点来增加新能力。

常见的扩展方式有:

  • 插槽(Slots):让使用者可以插入自定义内容
  • 钩子函数(Hooks):在特定时机执行自定义逻辑
  • 插件机制:通过注册插件来扩展功能
  • 继承/组合:基于基础模板创建变体

来看一个可扩展的列表组件设计:

<template>
  <div class="list-container">
    <!-- 头部插槽,允许自定义操作栏 -->
    <slot name="header" :selected="selectedItems">
      <default-header />
    </slot>

    <!-- 列表主体 -->
    <div v-for="item in data" :key="item.id">
      <!-- 行内容插槽,允许自定义每行的渲染 -->
      <slot name="item" :item="item" :index="index">
        <default-item :item="item" />
      </slot>
    </div>

    <!-- 空状态插槽 -->
    <slot name="empty">
      <empty-state />
    </slot>
  </div>
</template>

我曾经参与过一个大型后台管理系统,有 50 多个业务模块。我们设计了一套基础模板,每个模块通过扩展点来定制自己的行为。后来新加了一个「数据看板」模块,开发同学只用了两天就完成了,因为大部分功能都是通过扩展点组合出来的。

核心原则: 对扩展开放,对修改关闭。这是开闭原则(Open-Closed Principle)在模板工程中的体现。

2.5 四个原则的关系

这四个原则不是孤立的,它们相互配合:

模板设计 DRY 原则 单一职责 可配置性 可扩展性 消除重复 职责清晰 灵活定制 预留接口 四个原则共同构成高质量的模板设计

我个人的经验是:DRY 是基础,单一职责是骨架,可配置性是血肉,可扩展性是灵魂。没有 DRY,模板会冗余;没有单一职责,模板会混乱;没有可配置性,模板会僵化;没有可扩展性,模板会过时。

实战建议: 刚开始做模板时,先保证 DRY 和单一职责。这两个原则最容易落地,效果也最明显。等团队成熟了,再逐步引入可配置性和可扩展性。

嗯,这四个原则说起来简单,做起来需要不断练习。我刚开始带团队时,也经常违反这些原则。但每次重构后,看到代码变得更清爽、维护变得更轻松,就觉得值了。

下一章,我们来聊聊模板的目录结构设计。这个看似简单的问题,其实藏着不少门道。