在Vue3+TypeScript开发中,写"能跑的代码"很容易,但写"优雅、可维护、可扩展"的代码却需要思考。设计模式不是银弹,但合理运用能帮我们解决重复出现的问题,让代码结构更清晰、逻辑更健壮。
本文结合5个真实业务场景,讲解单例模式、工厂模式、观察者模式、策略模式、组合模式在Vue3+TS中的实践,每个场景都附完整代码示例和优化思路。
场景1:全局状态管理 - 单例模式
场景痛点
项目中需要全局状态管理(如用户信息、主题配置),如果多次创建状态实例,会导致状态不一致,且浪费资源。
设计模式应用:单例模式
单例模式确保一个类只有一个实例,并提供一个全局访问点。Vue3的Pinia本质就是单例模式的实现,但我们可以自定义更灵活的单例逻辑。
代码实现
typescript
// stores/singletonUserStore.ts
import { reactive, toRefs } from 'vue'
// 定义用户状态接口
interface UserState {
name: string
token: string
isLogin: boolean
}
class UserStore {
private static instance: UserStore
private state: UserState
// 私有构造函数,防止外部new
private constructor() {
this.state = reactive({
name: '',
token: localStorage.getItem('token') || '',
isLogin: !!localStorage.getItem('token')
})
}
// 全局访问点
public static getInstance(): UserStore {
if (!UserStore.instance) {
UserStore.instance = new UserStore()
}
return UserStore.instance
}
// 业务方法
public login(token: string, name: string) {
this.state.token = token
this.state.name = name
this.state.isLogin = true
localStorage.setItem('token', token)
}
public logout() {
this.state.token = ''
this.state.name = ''
this.state.isLogin = false
localStorage.removeItem('token')
}
// 暴露响应式状态
public getState() {
return toRefs(this.state)
}
}
// 导出单例实例
export const userStore = UserStore.getInstance()
优雅之处
-
全局唯一实例,避免状态冲突
-
封装性强,状态修改只能通过实例方法,避免直接篡改
-
结合TS接口,类型提示完整,减少类型错误
场景2:动态组件渲染 - 工厂模式
场景痛点
表单页面需要根据不同字段类型(输入框、下拉框、日期选择器)渲染不同组件,如果用if-else判断,代码会臃肿且难以维护。
设计模式应用:工厂模式
工厂模式定义一个创建对象的接口,让子类决定实例化哪个类。在Vue中,我们可以创建"组件工厂",根据类型动态返回对应组件。
代码实现
vue
<template>
<div class="form-container">
<component
v-for="field in fields"
:key="field.id"
:is="getFormComponent(field.type)"
v-model="formData[field.key]"
:label="field.label"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import InputComponent from './components/InputComponent.vue'
import SelectComponent from './components/SelectComponent.vue'
import DatePickerComponent from './components/DatePickerComponent.vue'
// 定义字段类型
type FieldType = 'input' | 'select' | 'date'
interface Field {
id: string
key: string
label: string
type: FieldType
options?: { label: string; value: string }[]
}
// 组件工厂:根据类型返回组件
const getFormComponent = (type: FieldType) => {
switch (type) {
case 'input':
return InputComponent
case 'select':
return SelectComponent
case 'date':
return DatePickerComponent
default:
throw new Error(`不支持的字段类型:${type}`)
}
}
// 表单数据和字段配置
const formData = ref({
username: '',
gender: '',
birthday: ''
})
const fields: Field[] = [
{ id: '1', key: 'username', label: '用户名', type: 'input' },
{
id: '2',
key: 'gender',
label: '性别',
type: 'select',
options: [{ label: '男', value: 'male' }, { label: '女', value: 'female' }]
},
{ id: '3', key: 'birthday', label: '生日', type: 'date' }
]
</script>
优雅之处
-
消除大量if-else,代码结构清晰
-
新增组件类型只需修改工厂函数,符合开闭原则
-
字段配置与组件渲染分离,便于维护
场景3:跨组件通信 - 观察者模式
场景痛点
非父子组件(如Header和Footer)需要通信(如主题切换),用Props/Emits太繁琐,用Pinia又没必要(仅单一事件通信)。
设计模式应用:观察者模式
观察者模式定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。我们可以实现一个简单的事件总线。
代码实现
typescript
// utils/eventBus.ts
class EventBus {
// 存储事件订阅者
private events: Record<string, ((...args: any[]) => void)[]> = {}
// 订阅事件
on(eventName: string, callback: (...args: any[]) => void) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(callback)
}
// 发布事件
emit(eventName: string, ...args: any[]) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(...args))
}
}
// 取消订阅
off(eventName: string, callback?: (...args: any[]) => void) {
if (!this.events[eventName]) return
if (callback) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback)
} else {
delete this.events[eventName]
}
}
}
// 导出单例事件总线
export const eventBus = new EventBus()
使用示例:
vue
<!-- Header.vue -->
<script setup lang="ts">
import { eventBus } from '@/utils/eventBus'
const toggleTheme = () => {
// 发布主题切换事件
eventBus.emit('theme-change', 'dark')
}
</script>
<!-- Footer.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { eventBus } from '@/utils/eventBus'
const theme = ref('light')
const handleThemeChange = (newTheme: string) => {
theme.value = newTheme
}
onMounted(() => {
// 订阅主题切换事件
eventBus.on('theme-change', handleThemeChange)
})
onUnmounted(() => {
// 取消订阅,避免内存泄漏
eventBus.off('theme-change', handleThemeChange)
})
</script>
优雅之处
-
解耦组件,无需关注组件层级关系
-
轻量级通信,比Pinia更适合简单场景
-
支持订阅/取消订阅,避免内存泄漏
场景4:表单验证 - 策略模式
场景痛点
表单需要多种验证规则(必填、邮箱格式、密码强度),如果把验证逻辑写在一起,代码会混乱且难以复用。
设计模式应用:策略模式
策略模式定义一系列算法,把它们封装起来,并且使它们可相互替换。我们可以将不同验证规则封装为"策略",动态选择使用。
代码实现
typescript
// utils/validator.ts
// 定义验证规则接口
interface ValidationRule {
validate: (value: string) => boolean
message: string
}
// 验证策略集合
const validationStrategies: Record<string, ValidationRule> = {
// 必填验证
required: {
validate: (value) => value.trim() !== '',
message: '此字段不能为空'
},
// 邮箱验证
email: {
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: '请输入正确的邮箱格式'
},
// 密码强度验证(至少8位,含字母和数字)
password: {
validate: (value) => /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/.test(value),
message: '密码至少8位,包含字母和数字'
}
}
// 验证器类
class Validator {
private rules: Record<string, string[]> = {} // { field: [rule1, rule2] }
// 添加验证规则
addField(field: string, rules: string[]) {
this.rules[field] = rules
}
// 执行验证
validate(formData: Record<string, string>): Record<string, string> {
const errors: Record<string, string> = {}
Object.entries(this.rules).forEach(([field, rules]) => {
const value = formData[field]
for (const rule of rules) {
const strategy = validationStrategies[rule]
if (!strategy.validate(value)) {
errors[field] = strategy.message
break // 只要有一个规则不通过,就停止该字段验证
}
}
})
return errors
}
}
export { Validator }
使用示例:
vue
<script setup lang="ts">
import { ref } from 'vue'
import { Validator } from '@/utils/validator'
const formData = ref({
email: '',
password: ''
})
const errors = ref<Record<string, string>>({})
const handleSubmit = () => {
// 创建验证器实例
const validator = new Validator()
// 添加验证规则
validator.addField('email', ['required', 'email'])
validator.addField('password', ['required', 'password'])
// 执行验证
const validateErrors = validator.validate(formData.value)
if (Object.keys(validateErrors).length === 0) {
// 验证通过,提交表单
console.log('提交成功', formData.value)
} else {
errors.value = validateErrors
}
}
</script>
优雅之处
-
验证规则与业务逻辑分离,可复用性强
-
新增规则只需扩展策略集合,符合开闭原则
-
验证逻辑清晰,便于维护和测试
场景5:树形结构组件 - 组合模式
场景痛点
开发权限菜单、文件目录等树形组件时,需要处理单个节点和子节点的统一操作(如展开/折叠、勾选),递归逻辑复杂。
设计模式应用:组合模式
组合模式将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
代码实现
typescript
// utils/treeNode.ts
// 定义节点接口
interface TreeNodeProps {
id: string
label: string
children?: TreeNodeProps[]
expanded?: boolean
checked?: boolean
}
class TreeNode {
public id: string
public label: string
public children: TreeNode[] = []
public expanded: boolean
public checked: boolean
constructor(props: TreeNodeProps) {
this.id = props.id
this.label = props.label
this.expanded = props.expanded ?? false
this.checked = props.checked ?? false
// 递归创建子节点
if (props.children) {
this.children = props.children.map(child => new TreeNode(child))
}
}
// 展开/折叠节点
toggleExpand() {
this.expanded = !this.expanded
}
// 勾选节点(并联动子节点)
toggleCheck() {
this.checked = !this.checked
this.children.forEach(child => {
child.setChecked(this.checked)
})
}
// 设置节点勾选状态
setChecked(checked: boolean) {
this.checked = checked
this.children.forEach(child => {
child.setChecked(checked)
})
}
// 获取所有勾选的节点ID
getCheckedIds(): string[] {
const checkedIds: string[] = []
if (this.checked) {
checkedIds.push(this.id)
}
this.children.forEach(child => {
checkedIds.push(...child.getCheckedIds())
})
return checkedIds
}
}
export { TreeNode }
使用示例:
vue
<template>
<ul class="tree-list">
<tree-node-item :node="treeRoot" />
</ul>
</template>
<script setup lang="ts">
import { TreeNode } from '@/utils/treeNode'
import TreeNodeItem from './TreeNodeItem.vue'
// 初始化树形数据
const treeData = {
id: 'root',
label: '权限菜单',
children: [
{
id: '1',
label: '用户管理',
children: [
{ id: '1-1', label: '查看用户' },
{ id: '1-2', label: '编辑用户' }
]
},
{ id: '2', label: '角色管理' }
]
}
const treeRoot = new TreeNode(treeData)
</script>
<!-- TreeNodeItem.vue 递归组件 -->
<template>
<li class="tree-node">
<div @click="node.toggleExpand()" class="node-label">
<span v-if="node.children.length">{{ node.expanded ? '▼' : '►' }}</span>
<input type="checkbox" :checked="node.checked" @change="node.toggleCheck()">
{{ node.label }}
</div>
<ul v-if="node.expanded && node.children.length" class="tree-children">
<tree-node-item v-for="child in node.children" :key="child.id" :node="child" />
</ul>
</li>
</template>
<script setup lang="ts">
import { defineProps } from 'vue'
import { TreeNode } from '@/utils/treeNode'
defineProps<{
node: TreeNode
}>()
</script>
优雅之处
-
统一处理单个节点和子节点,无需区分"部分"和"整体"
-
递归逻辑封装在TreeNode类中,组件只负责渲染
-
树形操作(勾选、展开)职责单一,便于扩展
总结
设计模式不是"炫技",而是解决问题的"方法论"。在Vue3+TS开发中:
-
单例模式适合全局状态、工具类等唯一实例场景
-
工厂模式适合动态创建组件、服务等场景
-
观察者模式适合跨组件通信、事件监听场景
-
策略模式适合表单验证、算法切换等场景
-
组合模式适合树形结构、层级数据场景
合理运用这些模式,能让你的代码更优雅、更可维护。当然,设计模式也不是万能的,要根据实际业务场景选择合适的方案,避免过度设计。