Vue3 核心设计模式实战:5 种模式 + 可复用代码,覆盖 80% 开发场景
在 Vue 项目开发中,设计模式是解决重复业务场景、提升代码可维护性的核心手段。本文聚焦 Vue3 + Composition API,结合真实开发场景,拆解 5 种高频设计模式的落地实现,所有代码可直接复制复用。
一、前言:为什么 Vue 项目需要设计模式?
- 组件化开发中,解决跨组件通信、动态 UI 生成等共性问题;
- 减少冗余代码,比如单例模式避免重复创建 DOM,工厂模式实现配置驱动 UI;
- 提升扩展性,比如装饰器模式可动态增强组件功能,不侵入原逻辑;
- Vue3 原生支持设计模式落地(如 Proxy 响应式、Composition API 函数封装)。
二、5 种核心设计模式实战(Vue3 版)
2.1 单例模式:全局唯一实例的优雅实现
模式解析
保证一个类 / 组件仅有一个实例,提供全局统一访问入口。Vue 中常用于全局状态、弹框组件等场景,避免资源浪费。
实战场景:全局 Toast 提示框(无重复 DOM)
1. 编写 Toast 组件(Toast.vue)
xml
<template>
<div class="toast" :class="{ show: visible }">
{{ message }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const message = ref('')
// 暴露控制方法
export const toast = {
show(msg, duration = 2000) {
message.value = msg
visible.value = true
setTimeout(() => visible.value = false, duration)
}
}
</script>
<style scoped>
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px 20px;
background: rgba(0,0,0,0.7);
color: #fff;
border-radius: 4px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: 9999;
}
.toast.show {
opacity: 1;
}
</style>
2. 单例注册(toast.js)
javascript
import { createApp } from 'vue'
import Toast from './Toast.vue'
import { toast } from './Toast.vue'
let instance = null // 确保实例唯一
export const useToast = () => {
if (!instance) {
const app = createApp(Toast)
const div = document.createElement('div')
document.body.appendChild(div)
instance = app.mount(div)
}
return toast // 统一访问入口
}
3. 项目中使用
xml
<script setup>
import { useToast } from '@/utils/toast.js'
const toast = useToast()
// 多次调用仅生成一个 DOM 节点
const handleClick = () => {
toast.show('操作成功!')
}
</script>
使用注意
- 单例模式适用于全局唯一资源(如 Pinia Store、工具类实例);
- 避免滥用全局实例,防止命名冲突或状态污染。
2.2 工厂模式:配置驱动的动态组件生成
模式解析
通过统一工厂函数创建对象 / 组件,隐藏具体创建逻辑,支持通过配置动态生成 UI 元素(如表单、列表)。
实战场景:动态表单生成(输入框 / 下拉框一键切换)
1. 基础表单组件(Input.vue/ Select.vue)
xml
<!-- Input.vue -->
<template>
<input
v-model="modelValue"
class="form-input"
v-bind="$attrs"
/>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<!-- Select.vue -->
<template>
<select
v-model="modelValue"
class="form-select"
@change="emit('update:modelValue', $event.target.value)"
>
<option v-for="opt in options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps(['modelValue', 'options'])
const emit = defineEmits(['update:modelValue'])
</script>
2. 表单工厂函数(formFactory.js)
typescript
import Input from '@/components/Input.vue'
import Select from '@/components/Select.vue'
/**
* 表单组件工厂:根据类型返回对应组件
* @param {string} type - 组件类型(input/select)
* @returns {Component} 对应的表单组件
*/
export const formFactory = (type) => {
switch (type) {
case 'input':
return Input
case 'select':
return Select
default:
throw new Error(`不支持的表单类型:${type}`)
}
}
3. 页面中动态渲染表单
php
<template>
<div class="dynamic-form">
<div v-for="item in formConfig" :key="item.key" class="form-item">
<label>{{ item.label }}:</label>
<component
:is="formFactory(item.type)"
v-model="formData[item.key]"
v-bind="item.props"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { formFactory } from '@/utils/formFactory.js'
// 表单配置(可从后端接口获取)
const formConfig = ref([
{
key: 'username',
label: '用户名',
type: 'input',
props: { placeholder: '请输入用户名', maxLength: 20 }
},
{
key: 'gender',
label: '性别',
type: 'select',
props: {
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
}
}
])
// 表单数据
const formData = ref({
username: '',
gender: 'male'
})
</script>
核心价值
- 配置驱动 UI,减少重复代码(如多表单页面);
- 新增表单类型时,仅需扩展工厂函数,符合「开闭原则」。
2.3 观察者模式:跨组件通信的极简方案
模式解析
定义一对多的依赖关系,当一个对象状态变化时,所有依赖它的对象都会收到通知。Vue 中常用 EventBus 实现跨组件通信。
实战场景:购物车数量更新(非父子组件通信)
1. 实现 EventBus(eventBus.js)
javascript
import mitt from 'mitt' // 需安装:npm i mitt
// 单例的发布-订阅中心(Vue3 已移除 $on/$emit,推荐用 mitt)
export const bus = mitt()
2. 发布者:加入购物车按钮(AddToCart.vue)
xml
<template>
<button @click="addToCart" class="add-cart-btn">
加入购物车
</button>
</template>
<script setup>
import { bus } from '@/utils/eventBus.js'
const addToCart = () => {
// 发布事件:通知订阅者购物车变化
bus.emit('cartChange', 1) // 传递参数(增加 1 件商品)
}
</script>
3. 订阅者:头部购物车数量(Header.vue)
xml
<template>
<div class="header">
<div class="cart-count">
购物车:{{ cartCount }} 件商品
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { bus } from '@/utils/eventBus.js'
const cartCount = ref(0)
let handleCartChange = null
onMounted(() => {
// 订阅事件
handleCartChange = (num) => {
cartCount.value += num
}
bus.on('cartChange', handleCartChange)
})
onUnmounted(() => {
// 组件卸载时取消订阅,避免内存泄漏
bus.off('cartChange', handleCartChange)
})
</script>
扩展说明
- Vue3 响应式系统的 watch/watchEffect 本质也是观察者模式;
- 复杂项目建议用 Pinia 替代 EventBus,避免事件命名冲突。
2.4 代理模式:不修改原逻辑的功能增强
模式解析
通过代理对象间接访问目标对象,在访问过程中附加额外逻辑(如拦截、增强)。Vue3 响应式系统的核心就是 Proxy 代理。
实战场景:图片懒加载(优化性能)
1. 实现懒加载指令(lazyDirective.js)
javascript
/**
* 图片懒加载指令:进入视口后加载真实图片
*/
export const lazyDirective = {
mounted(el, binding) {
// 创建观察者:监听元素是否进入视口
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// 进入视口:加载真实图片
el.src = binding.value
observer.unobserve(el) // 停止观察,避免重复触发
}
})
// 开始观察目标元素
observer.observe(el)
}
}
2. 注册并使用指令
xml
<template>
<!-- 占位图 + 懒加载真实图片 -->
<img
v-lazy="realImgUrl"
src="@/assets/placeholder.png"
class="lazy-img"
alt="示例图片"
/>
</template>
<script setup>
import { createApp } from 'vue'
import App from './App.vue'
import { lazyDirective } from '@/utils/lazyDirective.js'
// 全局注册指令
const app = createApp(App)
app.directive('lazy', lazyDirective)
// 真实图片地址(可从后端获取)
const realImgUrl = ref('https://picsum.photos/800/600')
</script>
代理模式的核心优势
- 不修改原元素逻辑,仅通过代理增强功能;
- 可复用性强(如请求拦截、权限校验等场景)。
2.5 装饰器模式:组件功能的动态增强
模式解析
动态给对象 / 组件添加额外功能,不改变原对象结构。Vue3 中常用高阶组件(HOC)实现。
实战场景:按钮权限控制(仅管理员可见)
1. 实现权限装饰器(withPermission.js)
javascript
import { h } from 'vue'
import { useUserStore } from '@/stores/user' // 假设用 Pinia 存储用户信息
/**
* 权限装饰器:根据角色控制组件是否渲染
* @param {Component} Component - 被装饰的组件
* @param {string} requiredRole - 所需角色(如 admin/user)
* @returns {Component} 装饰后的组件
*/
export const withPermission = (Component, requiredRole) => {
return (props, { slots }) => {
const userStore = useUserStore()
// 校验权限
if (userStore.role === requiredRole) {
// 有权限:渲染原组件
return h(Component, props, slots)
} else {
// 无权限:渲染提示或空节点
return h('div', { class: 'no-permission' }, '无权限操作')
}
}
}
2. 装饰组件并使用
xml
<!-- 基础按钮组件(EditButton.vue) -->
<template>
<button @click="handleClick" class="edit-btn">
<slot></slot>
</button>
</template>
<script setup>
const emit = defineEmits(['click'])
const handleClick = () => emit('click')
</script>
<!-- 页面中使用装饰后的组件 -->
<template>
<!-- 仅管理员可见的编辑按钮 -->
<DecoratedEditButton @click="handleEdit">
编辑内容
</DecoratedEditButton>
</template>
<script setup>
import EditButton from '@/components/EditButton.vue'
import { withPermission } from '@/utils/withPermission.js'
// 用装饰器包装组件:仅 admin 角色可见
const DecoratedEditButton = withPermission(EditButton, 'admin')
const handleEdit = () => {
// 编辑逻辑
}
</script>
适用场景
- 权限控制、日志打印、loading 状态等通用功能;
- 避免组件内冗余逻辑,保持组件职责单一。
三、总结:Vue3 设计模式的核心价值
| 模式 | 核心解决问题 | Vue3 适配技巧 |
|---|---|---|
| 单例模式 | 全局唯一实例(弹框 / 状态) | 结合 Composition API 封装 hooks |
| 工厂模式 | 配置驱动 UI(表单 / 列表) | 利用 component 动态组件渲染 |
| 观察者模式 | 跨组件通信 | 用 mitt 替代 Vue2 的 EventBus |
| 代理模式 | 功能增强(懒加载 / 拦截) | 基于指令或 Proxy 实现 |
| 装饰器模式 | 组件动态扩展(权限 / 日志) | 用高阶组件包装组件 |
扩展思考
- 除了以上 5 种,Vue3 中还常用「适配器模式」(兼容旧接口)、「策略模式」(表单校验)等;
- 设计模式不是银弹,需结合场景选择(如简单组件无需过度设计);
- 复杂项目建议结合 TypeScript 增强类型约束,提升代码健壮性。