Vue3+TS设计模式实战:5个场景让代码优雅翻倍

在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开发中:

  • 单例模式适合全局状态、工具类等唯一实例场景

  • 工厂模式适合动态创建组件、服务等场景

  • 观察者模式适合跨组件通信、事件监听场景

  • 策略模式适合表单验证、算法切换等场景

  • 组合模式适合树形结构、层级数据场景

合理运用这些模式,能让你的代码更优雅、更可维护。当然,设计模式也不是万能的,要根据实际业务场景选择合适的方案,避免过度设计。

相关推荐
少卿4 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技4 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
快起来搬砖了4 小时前
Vue 实现阿里云 OSS 视频分片上传:安全实战与完整方案
vue.js·安全·阿里云
广州华水科技4 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮4 小时前
umi4暗黑模式设置
前端
8***B4 小时前
前端路由权限控制,动态路由生成
前端
军军3605 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1235 小时前
Vue基础知识(一)
前端·javascript·vue.js
terminal0075 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试
我的小月月5 小时前
🔥 手把手教你实现前端邮件预览功能
前端·vue.js