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 中,可以使用 defineComponenth 函数来实现高阶组件。

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 中,可以使用 provideinject 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 中,可以使用 provideinject 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 中,可以使用 createApphrender 函数实现命令式组件。

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. 考虑性能影响

  • 避免不必要的重渲染
  • 合理使用 memoshallowRefshallowReactive
  • 优化大型列表的渲染
  • 合理使用缓存策略

5. 编写可测试的组件

  • 组件设计应易于测试
  • 使用依赖注入解耦组件依赖
  • 编写单元测试和集成测试
  • 使用 Vitest 或 Jest 进行测试

6. 文档化组件

  • 为组件编写清晰的文档
  • 使用 Storybook 展示组件的不同状态
  • 提供使用示例和 API 文档
  • 说明组件的设计模式和最佳实践

常见问题和解决方案

1. 组件复用性问题

问题:组件难以复用,功能过于耦合

解决方案

  • 使用组合式函数提取共享逻辑
  • 使用插槽允许自定义组件内容
  • 使用渲染属性传递渲染逻辑
  • 考虑高阶组件增强组件功能

2. 组件性能问题

问题:组件渲染性能不佳

解决方案

  • 避免不必要的重渲染
  • 使用 memo 优化组件
  • 合理使用 shallowRefshallowReactive
  • 优化列表渲染,使用 v-for 时添加 key
  • 考虑使用虚拟滚动处理大型列表

3. 组件测试问题

问题:组件难以测试

解决方案

  • 使用依赖注入解耦组件依赖
  • 编写单元测试和集成测试
  • 使用 Vitest 或 Jest 进行测试
  • 考虑使用 Cypress 进行 E2E 测试

4. 组件状态管理问题

问题:组件状态管理复杂

解决方案

  • 使用组合式函数管理状态
  • 考虑使用 Pinia 进行状态管理
  • 使用状态提升模式共享状态
  • 使用提供者-消费者模式共享数据

5. 组件通信问题

问题:组件间通信复杂

解决方案

  • 使用 props 和 events 进行父子组件通信
  • 使用 provideinject 进行跨层级通信
  • 使用 Pinia 进行全局状态管理
  • 考虑使用事件总线或发布-订阅模式

进阶学习资源

1. 官方文档

2. 书籍和教程

3. 开源项目和示例

4. 社区资源

实践练习

练习 1:实现渲染属性组件

  1. 创建一个数据获取组件,使用渲染属性传递数据和加载状态
  2. 在不同的场景中使用该组件
  3. 测试组件的各种状态

练习 2:实现高阶组件

  1. 创建一个高阶组件,为组件添加加载状态
  2. 创建另一个高阶组件,为组件添加错误处理
  3. 组合多个高阶组件,增强组件功能

练习 3:实现提供者-消费者模式

  1. 创建一个主题提供者组件,提供主题数据和切换方法
  2. 创建多个消费者组件,使用主题数据
  3. 测试主题切换功能

练习 4:实现组合式函数

  1. 创建一个 useCounter 组合式函数
  2. 创建一个 useFetch 组合式函数
  3. 创建一个 useLocalStorage 组合式函数
  4. 在组件中使用这些组合式函数

练习 5:实现命令式组件

  1. 创建一个 Toast 组件
  2. 创建一个命令式 API 用于显示 Toast
  3. 测试 Toast 的各种配置
  4. 创建一个 Modal 组件和命令式 API

练习 6:实现状态提升模式

  1. 创建一个表单组件,包含多个输入字段
  2. 在父组件中管理表单状态和验证
  3. 测试表单提交和验证功能

练习 7:实现依赖注入模式

  1. 创建一个日志服务
  2. 使用依赖注入在组件中使用日志服务
  3. 测试日志服务的不同方法

练习 8:综合应用

  1. 创建一个复杂的应用,使用多种设计模式
  2. 考虑组件的复用性、性能和可测试性
  3. 编写测试用例验证组件功能
  4. 为组件编写文档

总结

Vue 3 提供了丰富的组件设计模式,从渲染属性、高阶组件到组合式函数、命令式组件等,每种模式都有其适用的场景和优势。通过掌握这些高级组件设计模式,你可以构建高质量、可复用、可维护和可扩展的 Vue 3 组件。在实际开发中,你需要根据项目需求选择合适的设计模式,并遵循最佳实践,确保组件的性能和可维护性。

下一集我们将学习 Vue 3 与微前端架构,敬请期待!

« 上一篇 Vue 3与Astro静态站点生成 - 高性能静态站点解决方案 下一篇 » Vue 3与微前端架构 - 构建可扩展大型前端应用的核心技术