Vue 3 高级组件设计模式
概述
组件设计模式是构建可复用、可维护和可扩展组件的核心原则和实践。在 Vue 3 中,随着组合式 API 的引入,组件设计模式有了更多的可能性和灵活性。本集将深入探讨 Vue 3 中的高级组件设计模式,包括渲染属性、高阶组件、提供者-消费者、插槽、组合式函数、状态提升、依赖注入和命令式组件等模式,帮助你构建高质量的 Vue 3 组件。
核心知识点
1. 渲染属性模式
渲染属性(Render Props)是一种通过函数 props 将渲染逻辑传递给组件的模式,允许组件的使用者控制组件的渲染输出。
1.1 基本实现
<!-- src/components/RenderPropsComponent.vue -->
<template>
<div class="render-props">
<h2>Render Props Component</h2>
<!-- 将渲染逻辑通过函数 props 传递 -->
<slot name="render" :count="count" :increment="increment" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Render Props Pattern</h1>
<!-- 使用渲染属性 -->
<RenderPropsComponent>
<template #render="{ count, increment }">
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
</RenderPropsComponent>
</div>
</template>1.2 高级用法
<!-- src/components/DataFetcher.vue -->
<template>
<div class="data-fetcher">
<slot
:data="data"
:loading="loading"
:error="error"
:refetch="refetch"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
url: {
type: String,
required: true
}
})
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(props.url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
function refetch() {
fetchData()
}
onMounted(() => {
fetchData()
})
</script>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Data Fetcher with Render Props</h1>
<DataFetcher url="https://jsonplaceholder.typicode.com/posts/1">
<template #default="{ data, loading, error, refetch }">
<div>
<button @click="refetch">Refetch</button>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="data">
<h2>{{ data.title }}</h2>
<p>{{ data.body }}</p>
</div>
</div>
</template>
</DataFetcher>
</div>
</template>2. 高阶组件模式
高阶组件(Higher-Order Components,HOC)是一种函数,它接收一个组件并返回一个新的增强组件。在 Vue 3 中,可以使用 defineComponent 和 h 函数来实现高阶组件。
2.1 基本实现
// src/hocs/withLoading.js
import { defineComponent, h, ref } from 'vue'
export function withLoading(WrappedComponent) {
return defineComponent({
props: {
isLoading: {
type: Boolean,
default: false
}
},
setup(props, { slots }) {
return () => {
if (props.isLoading) {
return h('div', { class: 'loading' }, 'Loading...')
}
return h(WrappedComponent, props, slots)
}
}
})
}
// src/components/PostComponent.vue
<template>
<div class="post">
<h2>{{ title }}</h2>
<p>{{ body }}</p>
</div>
</template>
<script setup>
const props = defineProps({
title: String,
body: String
})
</script>
// src/App.vue
<template>
<div class="app">
<h1>Higher-Order Component Pattern</h1>
<button @click="toggleLoading">Toggle Loading</button>
<PostWithLoading
:title="post.title"
:body="post.body"
:is-loading="isLoading"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import PostComponent from './components/PostComponent.vue'
import { withLoading } from './hocs/withLoading.js'
// 创建增强组件
const PostWithLoading = withLoading(PostComponent)
const isLoading = ref(false)
const post = ref({
title: 'Example Post',
body: 'This is an example post.'
})
function toggleLoading() {
isLoading.value = !isLoading.value
}
</script>2.2 高级用法
// src/hocs/withErrorHandling.js
import { defineComponent, h, ref, onErrorCaptured } from 'vue'
export function withErrorHandling(WrappedComponent) {
return defineComponent({
setup(props, { slots }) {
const error = ref(null)
// 捕获组件错误
onErrorCaptured((err) => {
error.value = err
return false // 阻止错误继续传播
})
return () => {
if (error.value) {
return h('div', { class: 'error' }, `Error: ${error.value.message}`)
}
return h(WrappedComponent, props, slots)
}
}
})
}
// src/hocs/withLogging.js
import { defineComponent, h, onMounted, onUnmounted } from 'vue'
export function withLogging(WrappedComponent) {
return defineComponent({
setup(props, { slots }) {
onMounted(() => {
console.log(`${WrappedComponent.name || 'Component'} mounted`)
})
onUnmounted(() => {
console.log(`${WrappedComponent.name || 'Component'} unmounted`)
})
return () => h(WrappedComponent, props, slots)
}
})
}
// 组合多个高阶组件
import { compose } from 'lodash'
export function withEnhancements(WrappedComponent) {
return compose(
withLogging,
withErrorHandling,
withLoading
)(WrappedComponent)
}3. 提供者-消费者模式
提供者-消费者(Provider-Consumer)模式允许组件树中的祖先组件向其后代组件提供数据,而无需通过 props 逐层传递。在 Vue 3 中,可以使用 provide 和 inject API 实现。
3.1 基本实现
<!-- src/components/ThemeProvider.vue -->
<template>
<div :class="`theme-${theme}`">
<slot />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
const props = withDefaults(defineProps({
theme: {
type: String,
default: 'light'
}
}), {})
// 提供主题数据
provide('theme', props.theme)
// 提供主题切换方法
const toggleTheme = () => {
// 这里可以添加主题切换逻辑
console.log('Toggle theme')
}
provide('toggleTheme', toggleTheme)
</script>
<style>
.theme-light {
background-color: white;
color: black;
}
.theme-dark {
background-color: black;
color: white;
}
</style>
<!-- src/components/ThemeConsumer.vue -->
<template>
<div class="theme-consumer">
<h3>Current Theme: {{ theme }}</h3>
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入主题数据
const theme = inject('theme', 'light')
// 注入主题切换方法
const toggleTheme = inject('toggleTheme', () => {})
</script>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Provider-Consumer Pattern</h1>
<ThemeProvider theme="dark">
<div>
<h2>Content inside Theme Provider</h2>
<ThemeConsumer />
<div>
<h3>Another level deep</h3>
<ThemeConsumer />
</div>
</div>
</ThemeProvider>
</div>
</template>3.2 高级用法
<!-- src/components/StoreProvider.vue -->
<template>
<div class="store-provider">
<slot />
</div>
</template>
<script setup>
import { provide, ref, reactive } from 'vue'
// 创建全局状态
const state = reactive({
count: 0,
user: null
})
// 创建状态更新方法
const mutations = {
increment() {
state.count++
},
decrement() {
state.count--
},
setUser(user) {
state.user = user
}
}
// 创建计算属性
const getters = {
doubleCount() {
return state.count * 2
}
}
// 提供状态和方法
provide('store', {
state,
mutations,
getters
})
</script>
<!-- src/components/StoreConsumer.vue -->
<template>
<div class="store-consumer">
<h3>Count: {{ store.state.count }}</h3>
<h3>Double Count: {{ store.getters.doubleCount }}</h3>
<button @click="store.mutations.increment">Increment</button>
<button @click="store.mutations.decrement">Decrement</button>
<button @click="setUser">Set User</button>
<div v-if="store.state.user">
<h4>User: {{ store.state.user.name }}</h4>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入 store
const store = inject('store')
function setUser() {
store.mutations.setUser({
name: 'John Doe',
email: 'john@example.com'
})
}
</script>4. 插槽模式
插槽(Slots)是 Vue 中用于组件间内容分发的机制,允许组件的使用者自定义组件的部分内容。Vue 3 支持默认插槽、命名插槽和作用域插槽。
4.1 默认插槽
<!-- src/components/DefaultSlotComponent.vue -->
<template>
<div class="default-slot">
<h2>Default Slot Component</h2>
<!-- 默认插槽 -->
<slot />
</div>
</template>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Default Slot Pattern</h1>
<DefaultSlotComponent>
<p>This is content passed through the default slot.</p>
</DefaultSlotComponent>
</div>
</template>4.2 命名插槽
<!-- src/components/NamedSlotComponent.vue -->
<template>
<div class="named-slot">
<header>
<!-- 命名插槽:header -->
<slot name="header" />
</header>
<main>
<!-- 默认插槽 -->
<slot />
</main>
<footer>
<!-- 命名插槽:footer -->
<slot name="footer" />
</footer>
</div>
</template>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Named Slot Pattern</h1>
<NamedSlotComponent>
<template #header>
<h2>Header Content</h2>
</template>
<p>Main Content</p>
<p>More main content here.</p>
<template #footer>
<p>Footer Content</p>
</template>
</NamedSlotComponent>
</div>
</template>4.3 作用域插槽
<!-- src/components/ScopedSlotComponent.vue -->
<template>
<div class="scoped-slot">
<h2>Scoped Slot Component</h2>
<ul>
<li v-for="item in items" :key="item.id">
<!-- 作用域插槽:传递数据给插槽 -->
<slot :item="item" :index="$index" />
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
])
</script>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Scoped Slot Pattern</h1>
<ScopedSlotComponent>
<template #default="{ item, index }">
<div class="item">
<span>{{ index + 1 }}. </span>
<strong>{{ item.name }}</strong>
</div>
</template>
</ScopedSlotComponent>
<!-- 另一种用法 -->
<ScopedSlotComponent>
<template #default="{ item }">
<button @click="selectItem(item)">{{ item.name }}</button>
</template>
</ScopedSlotComponent>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedItem = ref(null)
function selectItem(item) {
selectedItem.value = item
console.log('Selected item:', item)
}
</script>5. 组合式函数模式
组合式函数(Composables)是 Vue 3 中用于封装和复用状态逻辑的函数,允许将复杂的逻辑分解为更小、更可复用的单元。
5.1 基本实现
// src/composables/useCounter.js
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
increment,
decrement,
reset
}
}
// src/components/CounterComponent.vue
<template>
<div class="counter">
<h3>Count: {{ count }}</h3>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { useCounter } from '../composables/useCounter.js'
// 使用组合式函数
const { count, increment, decrement, reset } = useCounter(5)
</script>
// src/App.vue
<template>
<div class="app">
<h1>Composable Pattern</h1>
<CounterComponent />
<div class="custom-counter">
<h3>Custom Counter</h3>
<p>Count: {{ count }}</p>
<div>
<input v-model.number="step" type="number" placeholder="Step" />
<button @click="incrementByStep">Increment by {{ step }}</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useCounter } from './composables/useCounter.js'
const { count, increment } = useCounter()
const step = ref(1)
function incrementByStep() {
for (let i = 0; i < step.value; i++) {
increment()
}
}
</script>5.2 高级用法
// src/composables/useFetch.js
export function useFetch(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
// 初始加载数据
if (options.autoFetch !== false) {
fetchData()
}
return {
data,
loading,
error,
refetch: fetchData
}
}
// src/composables/useLocalStorage.js
export function useLocalStorage(key, initialValue) {
// 从 localStorage 加载初始值
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)
// 监听值变化,保存到 localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// src/components/FetchComponent.vue
<template>
<div class="fetch-component">
<h3>Fetch Component</h3>
<button @click="refetch">Refetch</button>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="data">
<h4>{{ data.title }}</h4>
<p>{{ data.body }}</p>
</div>
</div>
</template>
<script setup>
import { useFetch } from '../composables/useFetch.js'
const { data, loading, error, refetch } = useFetch('https://jsonplaceholder.typicode.com/posts/1')
</script>
// src/components/LocalStorageComponent.vue
<template>
<div class="local-storage">
<h3>Local Storage Component</h3>
<input v-model="name" type="text" placeholder="Your name" />
<p>Hello, {{ name }}!</p>
<div>
<h4>Count: {{ count }}</h4>
<button @click="count++">Increment</button>
</div>
</div>
</template>
<script setup>
import { useLocalStorage } from '../composables/useLocalStorage.js'
const name = useLocalStorage('name', 'Guest')
const count = useLocalStorage('count', 0)
</script>6. 状态提升模式
状态提升(State Lifting)是将共享状态从子组件提升到父组件,通过 props 将状态传递给子组件,并通过事件将状态更新传递回父组件的模式。
6.1 基本实现
<!-- src/components/ChildComponent.vue -->
<template>
<div class="child-component">
<h3>Child Component</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
const props = defineProps({
count: {
type: Number,
required: true
}
})
const emit = defineEmits(['increment'])
function increment() {
emit('increment')
}
</script>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>State Lifting Pattern</h1>
<h2>Parent Count: {{ count }}</h2>
<ChildComponent
:count="count"
@increment="count++"
/>
<ChildComponent
:count="count"
@increment="count++"
/>
<button @click="count = 0">Reset Count</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const count = ref(0)
</script>6.2 高级用法
<!-- src/components/FormInput.vue -->
<template>
<div class="form-input">
<label :for="id">{{ label }}</label>
<input
:id="id"
:type="type"
:value="modelValue"
@input="handleInput"
:placeholder="placeholder"
/>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup>
const props = withDefaults(defineProps({
id: String,
label: String,
type: {
type: String,
default: 'text'
},
modelValue: String,
placeholder: String,
error: String
}), {})
const emit = defineEmits(['update:modelValue'])
function handleInput(event) {
emit('update:modelValue', event.target.value)
}
</script>
<!-- src/components/ValidationMessage.vue -->
<template>
<div v-if="error" class="validation-message">
{{ error }}
</div>
</template>
<script setup>
const props = defineProps({
error: String
})
</script>
<!-- src/App.vue -->
<template>
<div class="app">
<h1>Advanced State Lifting Pattern</h1>
<h2>User Form</h2>
<form @submit.prevent="handleSubmit">
<FormInput
id="name"
label="Name"
v-model="form.name"
placeholder="Enter your name"
:error="errors.name"
/>
<FormInput
id="email"
label="Email"
type="email"
v-model="form.email"
placeholder="Enter your email"
:error="errors.email"
/>
<FormInput
id="password"
label="Password"
type="password"
v-model="form.password"
placeholder="Enter your password"
:error="errors.password"
/>
<button type="submit">Submit</button>
</form>
<div v-if="submittedForm" class="submitted">
<h3>Submitted Form Data</h3>
<pre>{{ submittedForm }}</pre>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import FormInput from './components/FormInput.vue'
const form = reactive({
name: '',
email: '',
password: ''
})
const submittedForm = ref(null)
// 验证规则
const validateName = (name) => {
if (!name) return 'Name is required'
if (name.length < 3) return 'Name must be at least 3 characters'
return ''
}
const validateEmail = (email) => {
if (!email) return 'Email is required'
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) return 'Invalid email format'
return ''
}
const validatePassword = (password) => {
if (!password) return 'Password is required'
if (password.length < 6) return 'Password must be at least 6 characters'
return ''
}
// 计算错误信息
const errors = computed(() => {
return {
name: validateName(form.name),
email: validateEmail(form.email),
password: validatePassword(form.password)
}
})
// 检查表单是否有效
const isValid = computed(() => {
return Object.values(errors.value).every(error => error === '')
})
function handleSubmit() {
if (isValid.value) {
submittedForm.value = { ...form }
console.log('Form submitted:', form)
} else {
console.log('Form has errors:', errors.value)
}
}
</script>7. 依赖注入模式
依赖注入(Dependency Injection)是一种将依赖项从外部注入到组件中的模式,允许组件解耦并提高可测试性。在 Vue 3 中,可以使用 provide 和 inject API 实现依赖注入。
7.1 基本实现
<!-- src/plugins/logger.js -->
export function createLogger() {
return {
log(message) {
console.log(`[LOG] ${message}`)
},
error(message) {
console.error(`[ERROR] ${message}`)
},
warn(message) {
console.warn(`[WARN] ${message}`)
}
}
}
// src/main.js
import { createApp, provide } from 'vue'
import App from './App.vue'
import { createLogger } from './plugins/logger.js'
const app = createApp(App)
// 提供全局依赖
app.provide('logger', createLogger())
app.mount('#app')
<!-- src/components/DependencyInjectionComponent.vue -->
<template>
<div class="dependency-injection">
<h3>Dependency Injection Component</h3>
<button @click="logMessage">Log Message</button>
<button @click="logError">Log Error</button>
<button @click="logWarning">Log Warning</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入依赖
const logger = inject('logger')
function logMessage() {
logger.log('This is a log message')
}
function logError() {
logger.error('This is an error message')
}
function logWarning() {
logger.warn('This is a warning message')
}
</script>7.2 高级用法
<!-- src/services/api.js -->
export class ApiService {
constructor(baseUrl) {
this.baseUrl = baseUrl
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`)
return response.json()
}
async post(endpoint, data) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
return response.json()
}
}
// src/main.js
import { createApp, provide } from 'vue'
import App from './App.vue'
import { ApiService } from './services/api.js'
const app = createApp(App)
// 提供 API 服务实例
const apiService = new ApiService('https://jsonplaceholder.typicode.com')
app.provide('apiService', apiService)
app.mount('#app')
<!-- src/components/ApiComponent.vue -->
<template>
<div class="api-component">
<h3>API Component</h3>
<button @click="fetchPost">Fetch Post</button>
<div v-if="post">
<h4>{{ post.title }}</h4>
<p>{{ post.body }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { inject } from 'vue'
const apiService = inject('apiService')
const post = ref(null)
async function fetchPost() {
post.value = await apiService.get('/posts/1')
}
</script>8. 命令式组件模式
命令式组件(Imperative Components)是一种通过 JavaScript API 直接控制组件的模式,允许在需要时动态创建、更新和销毁组件。在 Vue 3 中,可以使用 createApp、h 和 render 函数实现命令式组件。
8.1 基本实现
<!-- src/components/ToastComponent.vue -->
<template>
<div class="toast" :class="{ 'toast-visible': visible }">
<div class="toast-content">
<h4>{{ title }}</h4>
<p>{{ message }}</p>
<button @click="close">Close</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
title: String,
message: String,
duration: {
type: Number,
default: 3000
}
})
const emit = defineEmits(['close'])
const visible = ref(true)
function close() {
visible.value = false
emit('close')
}
onMounted(() => {
// 自动关闭
if (props.duration > 0) {
setTimeout(() => {
close()
}, props.duration)
}
})
</script>
<style scoped>
.toast {
position: fixed;
top: 20px;
right: 20px;
background-color: #333;
color: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateX(100%);
transition: opacity 0.3s, transform 0.3s;
z-index: 1000;
}
.toast-visible {
opacity: 1;
transform: translateX(0);
}
.toast-content {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
// src/utils/toast.js
import { createApp, h } from 'vue'
import ToastComponent from '../components/ToastComponent.vue'
export function showToast(options) {
// 创建容器元素
const container = document.createElement('div')
document.body.appendChild(container)
// 创建应用实例
const app = createApp({
setup() {
const close = () => {
app.unmount(container)
document.body.removeChild(container)
}
return () => h(ToastComponent, {
...options,
onClose: close
})
}
})
// 挂载应用
app.mount(container)
return {
close: () => {
app.unmount(container)
document.body.removeChild(container)
}
}
}
// src/App.vue
<template>
<div class="app">
<h1>Imperative Component Pattern</h1>
<button @click="showSuccessToast">Show Success Toast</button>
<button @click="showErrorToast">Show Error Toast</button>
<button @click="showCustomToast">Show Custom Toast</button>
</div>
</template>
<script setup>
import { showToast } from './utils/toast.js'
function showSuccessToast() {
showToast({
title: 'Success',
message: 'Operation completed successfully!',
duration: 3000
})
}
function showErrorToast() {
showToast({
title: 'Error',
message: 'An error occurred!',
duration: 5000
})
}
function showCustomToast() {
const toast = showToast({
title: 'Custom Toast',
message: 'This toast will not auto close.',
duration: 0
})
// 5秒后手动关闭
setTimeout(() => {
toast.close()
}, 5000)
}
</script>8.2 高级用法
// src/utils/modal.js
import { createApp, h } from 'vue'
import ModalComponent from '../components/ModalComponent.vue'
export function showModal(options) {
const container = document.createElement('div')
document.body.appendChild(container)
const app = createApp({
setup() {
const close = () => {
app.unmount(container)
document.body.removeChild(container)
}
return () => h(ModalComponent, {
...options,
onClose: close,
onConfirm: () => {
if (options.onConfirm) {
options.onConfirm()
}
close()
}
})
}
})
app.mount(container)
return {
close
}
}
// src/components/ModalComponent.vue
<template>
<div class="modal-overlay" @click="close">
<div class="modal" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
<button @click="close" class="close-button">×</button>
</div>
<div class="modal-body">
<slot>{{ message }}</slot>
</div>
<div class="modal-footer">
<button @click="close">{{ cancelText }}</button>
<button @click="confirm" class="confirm-button">{{ confirmText }}</button>
</div>
</div>
</div>
</template>
<script setup>
const props = withDefaults(defineProps({
title: {
type: String,
default: 'Modal'
},
message: {
type: String,
default: ''
},
cancelText: {
type: String,
default: 'Cancel'
},
confirmText: {
type: String,
default: 'Confirm'
}
}), {})
const emit = defineEmits(['close', 'confirm'])
function close() {
emit('close')
}
function confirm() {
emit('confirm')
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e8e8e8;
}
.modal-body {
padding: 16px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px;
border-top: 1px solid #e8e8e8;
}
.close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.confirm-button {
background-color: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
</style>
// src/App.vue
<template>
<div class="app">
<h1>Imperative Modal Pattern</h1>
<button @click="showConfirmModal">Show Confirm Modal</button>
<button @click="showCustomModal">Show Custom Modal</button>
<div v-if="confirmResult">
<h3>Confirm Result: {{ confirmResult }}</h3>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { showModal } from './utils/modal.js'
const confirmResult = ref(null)
function showConfirmModal() {
showModal({
title: 'Confirm Action',
message: 'Are you sure you want to perform this action?',
onConfirm: () => {
confirmResult.value = 'Confirmed'
console.log('Action confirmed')
}
})
}
function showCustomModal() {
showModal({
title: 'Custom Modal',
cancelText: 'No',
confirmText: 'Yes',
// 使用插槽内容
message: '<div><p>This is custom HTML content.</p><input type="text" placeholder="Enter something..." /></div>'
})
}
</script>最佳实践
1. 选择合适的设计模式
- 渲染属性:当需要将渲染逻辑传递给组件时使用
- 高阶组件:当需要增强多个组件的功能时使用
- 提供者-消费者:当需要在组件树中共享数据时使用
- 插槽:当需要自定义组件的部分内容时使用
- 组合式函数:当需要复用状态逻辑时使用
- 状态提升:当需要在多个组件间共享状态时使用
- 依赖注入:当需要解耦组件依赖时使用
- 命令式组件:当需要动态控制组件时使用
2. 保持组件的单一职责
- 每个组件只负责一个功能
- 避免组件过于复杂
- 将复杂逻辑拆分为组合式函数或服务
3. 优先使用组合式 API
- 组合式 API 提供了更好的代码组织和复用性
- 适合构建复杂的组件和逻辑
- 提供了更好的 TypeScript 支持
4. 考虑性能影响
- 避免不必要的重渲染
- 合理使用
memo、shallowRef和shallowReactive - 优化大型列表的渲染
- 合理使用缓存策略
5. 编写可测试的组件
- 组件设计应易于测试
- 使用依赖注入解耦组件依赖
- 编写单元测试和集成测试
- 使用 Vitest 或 Jest 进行测试
6. 文档化组件
- 为组件编写清晰的文档
- 使用 Storybook 展示组件的不同状态
- 提供使用示例和 API 文档
- 说明组件的设计模式和最佳实践
常见问题和解决方案
1. 组件复用性问题
问题:组件难以复用,功能过于耦合
解决方案:
- 使用组合式函数提取共享逻辑
- 使用插槽允许自定义组件内容
- 使用渲染属性传递渲染逻辑
- 考虑高阶组件增强组件功能
2. 组件性能问题
问题:组件渲染性能不佳
解决方案:
- 避免不必要的重渲染
- 使用
memo优化组件 - 合理使用
shallowRef和shallowReactive - 优化列表渲染,使用
v-for时添加key - 考虑使用虚拟滚动处理大型列表
3. 组件测试问题
问题:组件难以测试
解决方案:
- 使用依赖注入解耦组件依赖
- 编写单元测试和集成测试
- 使用 Vitest 或 Jest 进行测试
- 考虑使用 Cypress 进行 E2E 测试
4. 组件状态管理问题
问题:组件状态管理复杂
解决方案:
- 使用组合式函数管理状态
- 考虑使用 Pinia 进行状态管理
- 使用状态提升模式共享状态
- 使用提供者-消费者模式共享数据
5. 组件通信问题
问题:组件间通信复杂
解决方案:
- 使用 props 和 events 进行父子组件通信
- 使用
provide和inject进行跨层级通信 - 使用 Pinia 进行全局状态管理
- 考虑使用事件总线或发布-订阅模式
进阶学习资源
1. 官方文档
2. 书籍和教程
3. 开源项目和示例
- VueUse - 实用的 Vue 组合式函数集合
- Vitesse - 现代化的 Vue 3 模板
- Element Plus - 基于 Vue 3 的 UI 组件库
- Ant Design Vue - 基于 Vue 3 的企业级 UI 组件库
4. 社区资源
- Vue Land - Vue 社区资源
- Vue Forum - Vue 论坛
- Vue.js DevTools - Vue 开发者工具
- Storybook - 组件文档工具
实践练习
练习 1:实现渲染属性组件
- 创建一个数据获取组件,使用渲染属性传递数据和加载状态
- 在不同的场景中使用该组件
- 测试组件的各种状态
练习 2:实现高阶组件
- 创建一个高阶组件,为组件添加加载状态
- 创建另一个高阶组件,为组件添加错误处理
- 组合多个高阶组件,增强组件功能
练习 3:实现提供者-消费者模式
- 创建一个主题提供者组件,提供主题数据和切换方法
- 创建多个消费者组件,使用主题数据
- 测试主题切换功能
练习 4:实现组合式函数
- 创建一个
useCounter组合式函数 - 创建一个
useFetch组合式函数 - 创建一个
useLocalStorage组合式函数 - 在组件中使用这些组合式函数
练习 5:实现命令式组件
- 创建一个 Toast 组件
- 创建一个命令式 API 用于显示 Toast
- 测试 Toast 的各种配置
- 创建一个 Modal 组件和命令式 API
练习 6:实现状态提升模式
- 创建一个表单组件,包含多个输入字段
- 在父组件中管理表单状态和验证
- 测试表单提交和验证功能
练习 7:实现依赖注入模式
- 创建一个日志服务
- 使用依赖注入在组件中使用日志服务
- 测试日志服务的不同方法
练习 8:综合应用
- 创建一个复杂的应用,使用多种设计模式
- 考虑组件的复用性、性能和可测试性
- 编写测试用例验证组件功能
- 为组件编写文档
总结
Vue 3 提供了丰富的组件设计模式,从渲染属性、高阶组件到组合式函数、命令式组件等,每种模式都有其适用的场景和优势。通过掌握这些高级组件设计模式,你可以构建高质量、可复用、可维护和可扩展的 Vue 3 组件。在实际开发中,你需要根据项目需求选择合适的设计模式,并遵循最佳实践,确保组件的性能和可维护性。
下一集我们将学习 Vue 3 与微前端架构,敬请期待!