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 可配置性原则:把变数交给使用者
可配置性,说白了就是「别把路走死」。你设计的模板,应该允许使用者在某些维度上进行定制,而不需要修改模板本身的代码。
我习惯把配置分为三个层次:
- 样式配置:颜色、字体、间距、主题
- 行为配置:是否显示某个模块、排序方式、分页大小
- 内容配置:标题文案、占位符、提示信息
举个例子,一个通用的弹窗组件:
// 可配置的弹窗组件
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 多个业务模块。我们设计了一套基础模板,每个模块通过扩展点来定制自己的行为。后来新加了一个「数据看板」模块,开发同学只用了两天就完成了,因为大部分功能都是通过扩展点组合出来的。
2.5 四个原则的关系
这四个原则不是孤立的,它们相互配合:
我个人的经验是:DRY 是基础,单一职责是骨架,可配置性是血肉,可扩展性是灵魂。没有 DRY,模板会冗余;没有单一职责,模板会混乱;没有可配置性,模板会僵化;没有可扩展性,模板会过时。
嗯,这四个原则说起来简单,做起来需要不断练习。我刚开始带团队时,也经常违反这些原则。但每次重构后,看到代码变得更清爽、维护变得更轻松,就觉得值了。
下一章,我们来聊聊模板的目录结构设计。这个看似简单的问题,其实藏着不少门道。