Skip to content

组件通信

在 Vue 应用中,组件之间需要进行数据传递和事件通信。Vue 提供了多种组件通信方式,适用于不同的场景。

父子组件通信

Props Down, Events Up

这是 Vue 中最基本的通信模式:父组件通过 props 向子组件传递数据,子组件通过事件向父组件发送消息。

vue
<!-- 父组件 ParentComponent.vue -->
<template>
  <div class="parent-component">
    <h2>父子组件通信示例</h2>
    
    <div class="demo-section">
      <h3>用户管理</h3>
      
      <!-- 用户列表 -->
      <div class="user-list">
        <UserCard
          v-for="user in users"
          :key="user.id"
          :user="user"
          :selected="selectedUserId === user.id"
          @select="handleUserSelect"
          @edit="handleUserEdit"
          @delete="handleUserDelete"
        />
      </div>
      
      <!-- 用户表单 -->
      <div class="user-form-section">
        <h4>{{ editingUser ? '编辑用户' : '添加用户' }}</h4>
        <UserForm
          :user="editingUser"
          :loading="formLoading"
          @submit="handleUserSubmit"
          @cancel="handleFormCancel"
        />
      </div>
      
      <!-- 操作日志 -->
      <div class="action-logs">
        <h4>操作日志</h4>
        <div class="logs-container">
          <div 
            v-for="(log, index) in actionLogs" 
            :key="index"
            class="log-entry"
          >
            <span class="log-time">{{ log.time }}</span>
            <span class="log-action">{{ log.action }}</span>
            <span class="log-details">{{ log.details }}</span>
          </div>
        </div>
        <button @click="clearLogs" class="clear-logs-btn">
          清空日志
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import UserCard from './UserCard.vue'
import UserForm from './UserForm.vue'

// 响应式数据
const users = ref([
  {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    role: 'admin',
    avatar: '',
    status: 'active'
  },
  {
    id: 2,
    name: '李四',
    email: 'lisi@example.com',
    role: 'user',
    avatar: '',
    status: 'active'
  },
  {
    id: 3,
    name: '王五',
    email: 'wangwu@example.com',
    role: 'moderator',
    avatar: '',
    status: 'inactive'
  }
])

const selectedUserId = ref(null)
const editingUser = ref(null)
const formLoading = ref(false)
const actionLogs = ref([])

// 方法
function addLog(action, details) {
  actionLogs.value.unshift({
    time: new Date().toLocaleTimeString(),
    action,
    details
  })
  
  // 限制日志数量
  if (actionLogs.value.length > 50) {
    actionLogs.value = actionLogs.value.slice(0, 50)
  }
}

function handleUserSelect(userId) {
  selectedUserId.value = userId
  const user = users.value.find(u => u.id === userId)
  addLog('选择用户', `选择了用户: ${user?.name}`)
}

function handleUserEdit(user) {
  editingUser.value = { ...user }
  addLog('编辑用户', `开始编辑用户: ${user.name}`)
}

function handleUserDelete(userId) {
  const user = users.value.find(u => u.id === userId)
  if (user && confirm(`确定要删除用户 "${user.name}" 吗?`)) {
    users.value = users.value.filter(u => u.id !== userId)
    if (selectedUserId.value === userId) {
      selectedUserId.value = null
    }
    addLog('删除用户', `删除了用户: ${user.name}`)
  }
}

function handleUserSubmit(userData) {
  formLoading.value = true
  
  // 模拟 API 调用
  setTimeout(() => {
    if (editingUser.value) {
      // 更新用户
      const index = users.value.findIndex(u => u.id === editingUser.value.id)
      if (index !== -1) {
        users.value[index] = { ...userData, id: editingUser.value.id }
        addLog('更新用户', `更新了用户: ${userData.name}`)
      }
    } else {
      // 添加用户
      const newUser = {
        ...userData,
        id: Date.now(),
        status: 'active'
      }
      users.value.push(newUser)
      addLog('添加用户', `添加了用户: ${userData.name}`)
    }
    
    editingUser.value = null
    formLoading.value = false
  }, 1000)
}

function handleFormCancel() {
  editingUser.value = null
  addLog('取消操作', '取消了表单操作')
}

function clearLogs() {
  actionLogs.value = []
  addLog('清空日志', '清空了所有操作日志')
}
</script>

<style scoped>
.parent-component {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  background-color: #f8f9fa;
  border-radius: 12px;
  padding: 20px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 20px 0;
  color: #333;
  border-bottom: 2px solid #007bff;
  padding-bottom: 8px;
}

.user-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 15px;
  margin-bottom: 30px;
}

.user-form-section {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 30px;
  border: 1px solid #dee2e6;
}

.user-form-section h4 {
  margin: 0 0 15px 0;
  color: #333;
}

.action-logs {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
  border: 1px solid #dee2e6;
}

.action-logs h4 {
  margin: 0 0 15px 0;
  color: #333;
}

.logs-container {
  max-height: 300px;
  overflow-y: auto;
  background-color: #f8f9fa;
  border-radius: 6px;
  padding: 10px;
  margin-bottom: 15px;
}

.log-entry {
  display: flex;
  padding: 8px;
  margin-bottom: 5px;
  background-color: white;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  border: 1px solid #e9ecef;
}

.log-time {
  color: #6c757d;
  margin-right: 15px;
  min-width: 80px;
}

.log-action {
  color: #007bff;
  font-weight: bold;
  margin-right: 15px;
  min-width: 80px;
}

.log-details {
  color: #333;
  flex: 1;
}

.clear-logs-btn {
  padding: 8px 16px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.clear-logs-btn:hover {
  background-color: #c82333;
}

@media (max-width: 768px) {
  .user-list {
    grid-template-columns: 1fr;
  }
  
  .log-entry {
    flex-direction: column;
    gap: 5px;
  }
  
  .log-time,
  .log-action {
    min-width: auto;
  }
}
</style>
vue
<!-- 子组件 UserCard.vue -->
<template>
  <div class="user-card" :class="{ selected: selected }">
    <div class="user-avatar">
      <img v-if="user.avatar" :src="user.avatar" :alt="user.name">
      <div v-else class="avatar-placeholder">
        {{ user.name.charAt(0).toUpperCase() }}
      </div>
      <span class="status-indicator" :class="statusClass"></span>
    </div>
    
    <div class="user-info">
      <h4 class="user-name">{{ user.name }}</h4>
      <p class="user-email">{{ user.email }}</p>
      <span class="user-role" :class="roleClass">{{ user.role }}</span>
    </div>
    
    <div class="user-actions">
      <button @click="handleSelect" class="select-btn" :class="{ active: selected }">
        {{ selected ? '已选择' : '选择' }}
      </button>
      <button @click="handleEdit" class="edit-btn">
        编辑
      </button>
      <button @click="handleDelete" class="delete-btn">
        删除
      </button>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'

// Props
const props = defineProps({
  user: {
    type: Object,
    required: true
  },
  selected: {
    type: Boolean,
    default: false
  }
})

// Emits
const emit = defineEmits(['select', 'edit', 'delete'])

// 计算属性
const statusClass = computed(() => {
  return {
    'status-active': props.user.status === 'active',
    'status-inactive': props.user.status === 'inactive'
  }
})

const roleClass = computed(() => {
  return `role-${props.user.role}`
})

// 方法
function handleSelect() {
  emit('select', props.user.id)
}

function handleEdit() {
  emit('edit', props.user)
}

function handleDelete() {
  emit('delete', props.user.id)
}
</script>

<style scoped>
.user-card {
  background-color: white;
  border-radius: 8px;
  padding: 15px;
  border: 2px solid #e9ecef;
  transition: all 0.3s ease;
  position: relative;
}

.user-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.user-card.selected {
  border-color: #007bff;
  background-color: #f8f9ff;
}

.user-avatar {
  position: relative;
  display: flex;
  justify-content: center;
  margin-bottom: 10px;
}

.user-avatar img {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
}

.avatar-placeholder {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: bold;
}

.status-indicator {
  position: absolute;
  bottom: 0;
  right: calc(50% - 35px);
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid white;
}

.status-active {
  background-color: #28a745;
}

.status-inactive {
  background-color: #dc3545;
}

.user-info {
  text-align: center;
  margin-bottom: 15px;
}

.user-name {
  margin: 0 0 5px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.user-email {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #6c757d;
}

.user-role {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
}

.role-admin {
  background-color: #dc3545;
  color: white;
}

.role-moderator {
  background-color: #ffc107;
  color: #212529;
}

.role-user {
  background-color: #28a745;
  color: white;
}

.user-actions {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.user-actions button {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s;
}

.select-btn {
  background-color: #e9ecef;
  color: #495057;
}

.select-btn.active {
  background-color: #007bff;
  color: white;
}

.select-btn:hover {
  background-color: #007bff;
  color: white;
}

.edit-btn {
  background-color: #ffc107;
  color: #212529;
}

.edit-btn:hover {
  background-color: #e0a800;
}

.delete-btn {
  background-color: #dc3545;
  color: white;
}

.delete-btn:hover {
  background-color: #c82333;
}
</style>

v-model 双向绑定

v-model 是 props 和 events 的语法糖,用于实现双向数据绑定。

vue
<!-- CustomInput.vue -->
<template>
  <div class="custom-input" :class="inputClass">
    <label v-if="label" class="input-label">
      {{ label }}
      <span v-if="required" class="required-mark">*</span>
    </label>
    
    <div class="input-wrapper">
      <input
        :value="modelValue"
        @input="handleInput"
        @focus="handleFocus"
        @blur="handleBlur"
        :type="type"
        :placeholder="placeholder"
        :disabled="disabled"
        :readonly="readonly"
        class="input-field"
        ref="inputRef"
      >
      
      <div v-if="showClearButton" class="input-actions">
        <button @click="clearInput" class="clear-btn" type="button">

        </button>
      </div>
    </div>
    
    <div v-if="error || helperText" class="input-footer">
      <span v-if="error" class="error-message">{{ error }}</span>
      <span v-else-if="helperText" class="helper-text">{{ helperText }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

// Props
const props = defineProps({
  modelValue: {
    type: [String, Number],
    default: ''
  },
  label: String,
  type: {
    type: String,
    default: 'text'
  },
  placeholder: String,
  disabled: Boolean,
  readonly: Boolean,
  required: Boolean,
  error: String,
  helperText: String,
  clearable: Boolean
})

// Emits
const emit = defineEmits({
  'update:modelValue': (value) => true,
  'focus': (event) => event instanceof Event,
  'blur': (event) => event instanceof Event,
  'clear': () => true
})

// 响应式数据
const inputRef = ref(null)
const isFocused = ref(false)

// 计算属性
const inputClass = computed(() => {
  return {
    'custom-input--focused': isFocused.value,
    'custom-input--error': !!props.error,
    'custom-input--disabled': props.disabled
  }
})

const showClearButton = computed(() => {
  return props.clearable && props.modelValue && !props.disabled && !props.readonly
})

// 方法
function handleInput(event) {
  const value = event.target.value
  emit('update:modelValue', value)
}

function handleFocus(event) {
  isFocused.value = true
  emit('focus', event)
}

function handleBlur(event) {
  isFocused.value = false
  emit('blur', event)
}

function clearInput() {
  emit('update:modelValue', '')
  emit('clear')
  inputRef.value?.focus()
}

// 暴露方法给父组件
defineExpose({
  focus: () => inputRef.value?.focus(),
  blur: () => inputRef.value?.blur()
})
</script>

<style scoped>
.custom-input {
  margin-bottom: 15px;
}

.input-label {
  display: block;
  margin-bottom: 5px;
  font-weight: 500;
  color: #333;
  font-size: 14px;
}

.required-mark {
  color: #dc3545;
  margin-left: 2px;
}

.input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.input-field {
  width: 100%;
  padding: 10px 12px;
  border: 2px solid #e9ecef;
  border-radius: 6px;
  font-size: 16px;
  transition: all 0.3s ease;
  background-color: white;
}

.input-field:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}

.custom-input--error .input-field {
  border-color: #dc3545;
}

.custom-input--error .input-field:focus {
  border-color: #dc3545;
  box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
}

.custom-input--disabled .input-field {
  background-color: #f8f9fa;
  color: #6c757d;
  cursor: not-allowed;
}

.input-actions {
  position: absolute;
  right: 8px;
  display: flex;
  align-items: center;
}

.clear-btn {
  width: 20px;
  height: 20px;
  border: none;
  background-color: #6c757d;
  color: white;
  border-radius: 50%;
  cursor: pointer;
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
}

.clear-btn:hover {
  background-color: #495057;
}

.input-footer {
  margin-top: 5px;
  font-size: 12px;
}

.error-message {
  color: #dc3545;
}

.helper-text {
  color: #6c757d;
}
</style>

使用自定义 v-model 组件:

vue
<template>
  <div class="form-demo">
    <h3>自定义表单组件</h3>
    
    <form @submit.prevent="handleSubmit">
      <CustomInput
        v-model="form.username"
        label="用户名"
        placeholder="请输入用户名"
        required
        clearable
        :error="errors.username"
        @blur="validateUsername"
      />
      
      <CustomInput
        v-model="form.email"
        label="邮箱"
        type="email"
        placeholder="请输入邮箱地址"
        required
        clearable
        :error="errors.email"
        @blur="validateEmail"
      />
      
      <CustomInput
        v-model="form.password"
        label="密码"
        type="password"
        placeholder="请输入密码"
        required
        :error="errors.password"
        helper-text="密码至少8个字符,包含字母和数字"
        @blur="validatePassword"
      />
      
      <button type="submit" :disabled="!isFormValid" class="submit-btn">
        提交
      </button>
    </form>
    
    <div class="form-data">
      <h4>表单数据:</h4>
      <pre>{{ JSON.stringify(form, null, 2) }}</pre>
    </div>
  </div>
</template>

<script setup>
import { reactive, computed } from 'vue'
import CustomInput from './CustomInput.vue'

const form = reactive({
  username: '',
  email: '',
  password: ''
})

const errors = reactive({
  username: '',
  email: '',
  password: ''
})

const isFormValid = computed(() => {
  return form.username && 
         form.email && 
         form.password && 
         !errors.username && 
         !errors.email && 
         !errors.password
})

function validateUsername() {
  if (!form.username) {
    errors.username = '用户名不能为空'
  } else if (form.username.length < 3) {
    errors.username = '用户名至少3个字符'
  } else {
    errors.username = ''
  }
}

function validateEmail() {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!form.email) {
    errors.email = '邮箱不能为空'
  } else if (!emailRegex.test(form.email)) {
    errors.email = '请输入有效的邮箱地址'
  } else {
    errors.email = ''
  }
}

function validatePassword() {
  if (!form.password) {
    errors.password = '密码不能为空'
  } else if (form.password.length < 8) {
    errors.password = '密码至少8个字符'
  } else if (!/(?=.*[a-zA-Z])(?=.*\d)/.test(form.password)) {
    errors.password = '密码必须包含字母和数字'
  } else {
    errors.password = ''
  }
}

function handleSubmit() {
  validateUsername()
  validateEmail()
  validatePassword()
  
  if (isFormValid.value) {
    console.log('表单提交:', form)
    alert('表单提交成功!')
  }
}
</script>

Provide / Inject

用于跨层级组件通信,避免 prop drilling(属性钻取)。

vue
<!-- 祖先组件 App.vue -->
<template>
  <div class="app">
    <h1>应用主题管理</h1>
    
    <div class="theme-controls">
      <button 
        v-for="theme in themes" 
        :key="theme.name"
        @click="setTheme(theme)"
        :class="{ active: currentTheme.name === theme.name }"
        class="theme-btn"
        :style="{ backgroundColor: theme.primary }"
      >
        {{ theme.label }}
      </button>
    </div>
    
    <UserDashboard />
  </div>
</template>

<script setup>
import { ref, provide, computed } from 'vue'
import UserDashboard from './UserDashboard.vue'

// 主题配置
const themes = [
  {
    name: 'light',
    label: '浅色主题',
    primary: '#007bff',
    secondary: '#6c757d',
    background: '#ffffff',
    surface: '#f8f9fa',
    text: '#333333'
  },
  {
    name: 'dark',
    label: '深色主题',
    primary: '#0d6efd',
    secondary: '#6c757d',
    background: '#1a1a1a',
    surface: '#2d2d2d',
    text: '#ffffff'
  },
  {
    name: 'blue',
    label: '蓝色主题',
    primary: '#0066cc',
    secondary: '#004499',
    background: '#f0f8ff',
    surface: '#e6f3ff',
    text: '#003366'
  }
]

const currentTheme = ref(themes[0])

// 用户数据
const currentUser = ref({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  role: 'admin',
  avatar: '',
  preferences: {
    language: 'zh-CN',
    notifications: true
  }
})

// 应用配置
const appConfig = ref({
  name: 'Vue 学习宝典',
  version: '1.0.0',
  features: {
    darkMode: true,
    notifications: true,
    analytics: false
  }
})

// 计算属性
const themeVars = computed(() => {
  return {
    '--primary-color': currentTheme.value.primary,
    '--secondary-color': currentTheme.value.secondary,
    '--background-color': currentTheme.value.background,
    '--surface-color': currentTheme.value.surface,
    '--text-color': currentTheme.value.text
  }
})

// 方法
function setTheme(theme) {
  currentTheme.value = theme
}

function updateUser(userData) {
  currentUser.value = { ...currentUser.value, ...userData }
}

function updateConfig(config) {
  appConfig.value = { ...appConfig.value, ...config }
}

// 提供数据给后代组件
provide('theme', {
  current: currentTheme,
  themes,
  setTheme
})

provide('user', {
  current: currentUser,
  updateUser
})

provide('appConfig', {
  config: appConfig,
  updateConfig
})
</script>

<style>
:root {
  --primary-color: v-bind('themeVars["--primary-color"]');
  --secondary-color: v-bind('themeVars["--secondary-color"]');
  --background-color: v-bind('themeVars["--background-color"]');
  --surface-color: v-bind('themeVars["--surface-color"]');
  --text-color: v-bind('themeVars["--text-color"]');
}

body {
  background-color: var(--background-color);
  color: var(--text-color);
  transition: all 0.3s ease;
}

.app {
  min-height: 100vh;
  padding: 20px;
}

.theme-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 30px;
  justify-content: center;
}

.theme-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  color: white;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.3s;
  opacity: 0.8;
}

.theme-btn:hover {
  opacity: 1;
  transform: translateY(-1px);
}

.theme-btn.active {
  opacity: 1;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
</style>
vue
<!-- 中间组件 UserDashboard.vue -->
<template>
  <div class="user-dashboard">
    <h2>用户仪表板</h2>
    
    <div class="dashboard-grid">
      <UserProfile />
      <UserSettings />
      <UserNotifications />
    </div>
  </div>
</template>

<script setup>
import UserProfile from './UserProfile.vue'
import UserSettings from './UserSettings.vue'
import UserNotifications from './UserNotifications.vue'
</script>

<style scoped>
.user-dashboard {
  background-color: var(--surface-color);
  border-radius: 12px;
  padding: 20px;
  margin: 20px 0;
}

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
  margin-top: 20px;
}
</style>
vue
<!-- 后代组件 UserProfile.vue -->
<template>
  <div class="user-profile">
    <h3>用户资料</h3>
    
    <div class="profile-content">
      <div class="user-avatar">
        <img v-if="user.current.value.avatar" :src="user.current.value.avatar" :alt="user.current.value.name">
        <div v-else class="avatar-placeholder">
          {{ user.current.value.name.charAt(0).toUpperCase() }}
        </div>
      </div>
      
      <div class="user-details">
        <h4>{{ user.current.value.name }}</h4>
        <p>{{ user.current.value.email }}</p>
        <span class="user-role">{{ user.current.value.role }}</span>
      </div>
      
      <div class="profile-actions">
        <button @click="editProfile" class="edit-btn">
          编辑资料
        </button>
        
        <button @click="changeTheme" class="theme-btn">
          切换主题
        </button>
      </div>
    </div>
    
    <div class="theme-info">
      <p>当前主题: <strong>{{ theme.current.value.label }}</strong></p>
      <p>应用版本: <strong>{{ appConfig.config.value.version }}</strong></p>
    </div>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 注入祖先组件提供的数据
const theme = inject('theme')
const user = inject('user')
const appConfig = inject('appConfig')

// 方法
function editProfile() {
  const newName = prompt('请输入新的用户名:', user.current.value.name)
  if (newName && newName.trim()) {
    user.updateUser({ name: newName.trim() })
  }
}

function changeTheme() {
  const currentIndex = theme.themes.findIndex(t => t.name === theme.current.value.name)
  const nextIndex = (currentIndex + 1) % theme.themes.length
  theme.setTheme(theme.themes[nextIndex])
}
</script>

<style scoped>
.user-profile {
  background-color: var(--background-color);
  border: 1px solid var(--secondary-color);
  border-radius: 8px;
  padding: 20px;
}

.user-profile h3 {
  margin: 0 0 15px 0;
  color: var(--text-color);
}

.profile-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  margin-bottom: 20px;
}

.user-avatar {
  margin-bottom: 15px;
}

.user-avatar img {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
}

.avatar-placeholder {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background-color: var(--primary-color);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
  font-weight: bold;
}

.user-details h4 {
  margin: 0 0 5px 0;
  color: var(--text-color);
}

.user-details p {
  margin: 0 0 10px 0;
  color: var(--secondary-color);
}

.user-role {
  display: inline-block;
  padding: 4px 12px;
  background-color: var(--primary-color);
  color: white;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
}

.profile-actions {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}

.edit-btn,
.theme-btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.edit-btn {
  background-color: var(--primary-color);
  color: white;
}

.theme-btn {
  background-color: var(--secondary-color);
  color: white;
}

.edit-btn:hover,
.theme-btn:hover {
  opacity: 0.8;
  transform: translateY(-1px);
}

.theme-info {
  padding: 15px;
  background-color: var(--surface-color);
  border-radius: 6px;
  border: 1px solid var(--secondary-color);
}

.theme-info p {
  margin: 5px 0;
  font-size: 14px;
  color: var(--text-color);
}
</style>

事件总线 (Event Bus)

对于兄弟组件或跨层级组件通信,可以使用事件总线模式。

javascript
// eventBus.js
import { ref } from 'vue'

class EventBus {
  constructor() {
    this.events = {}
  }
  
  // 监听事件
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
    
    // 返回取消监听的函数
    return () => {
      this.off(event, callback)
    }
  }
  
  // 触发事件
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => {
        callback(...args)
      })
    }
  }
  
  // 取消监听
  off(event, callback) {
    if (this.events[event]) {
      const index = this.events[event].indexOf(callback)
      if (index > -1) {
        this.events[event].splice(index, 1)
      }
    }
  }
  
  // 只监听一次
  once(event, callback) {
    const onceCallback = (...args) => {
      callback(...args)
      this.off(event, onceCallback)
    }
    this.on(event, onceCallback)
  }
  
  // 清除所有监听器
  clear() {
    this.events = {}
  }
}

export const eventBus = new EventBus()

// 提供一些常用的事件名称
export const EVENTS = {
  USER_LOGIN: 'user:login',
  USER_LOGOUT: 'user:logout',
  THEME_CHANGE: 'theme:change',
  NOTIFICATION_SHOW: 'notification:show',
  MODAL_OPEN: 'modal:open',
  MODAL_CLOSE: 'modal:close'
}

使用事件总线:

vue
<!-- 组件 A -->
<template>
  <div class="component-a">
    <h3>组件 A - 消息发送者</h3>
    
    <div class="message-form">
      <input 
        v-model="message" 
        placeholder="输入消息..."
        @keyup.enter="sendMessage"
        class="message-input"
      >
      <button @click="sendMessage" :disabled="!message.trim()" class="send-btn">
        发送消息
      </button>
    </div>
    
    <div class="quick-actions">
      <button @click="sendNotification" class="notification-btn">
        发送通知
      </button>
      <button @click="openModal" class="modal-btn">
        打开模态框
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { eventBus, EVENTS } from './eventBus.js'

const message = ref('')

function sendMessage() {
  if (message.value.trim()) {
    eventBus.emit('message:send', {
      id: Date.now(),
      text: message.value,
      sender: '组件 A',
      timestamp: new Date().toLocaleTimeString()
    })
    message.value = ''
  }
}

function sendNotification() {
  eventBus.emit(EVENTS.NOTIFICATION_SHOW, {
    type: 'info',
    title: '来自组件 A 的通知',
    message: '这是一条测试通知消息',
    duration: 3000
  })
}

function openModal() {
  eventBus.emit(EVENTS.MODAL_OPEN, {
    title: '模态框标题',
    content: '这是从组件 A 打开的模态框',
    type: 'info'
  })
}
</script>
vue
<!-- 组件 B -->
<template>
  <div class="component-b">
    <h3>组件 B - 消息接收者</h3>
    
    <div class="messages-container">
      <h4>接收到的消息 ({{ messages.length }}):</h4>
      
      <div class="messages-list">
        <div 
          v-for="msg in messages" 
          :key="msg.id"
          class="message-item"
        >
          <div class="message-header">
            <span class="message-sender">{{ msg.sender }}</span>
            <span class="message-time">{{ msg.timestamp }}</span>
          </div>
          <div class="message-text">{{ msg.text }}</div>
        </div>
      </div>
      
      <button @click="clearMessages" class="clear-btn">
        清空消息
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { eventBus } from './eventBus.js'

const messages = ref([])
let unsubscribe = null

function handleMessage(messageData) {
  messages.value.unshift(messageData)
  
  // 限制消息数量
  if (messages.value.length > 50) {
    messages.value = messages.value.slice(0, 50)
  }
}

function clearMessages() {
  messages.value = []
}

onMounted(() => {
  // 监听消息事件
  unsubscribe = eventBus.on('message:send', handleMessage)
})

onUnmounted(() => {
  // 清理事件监听器
  if (unsubscribe) {
    unsubscribe()
  }
})
</script>

状态管理 (Pinia)

对于复杂的应用状态管理,推荐使用 Pinia。

javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // 状态
  const currentUser = ref(null)
  const users = ref([])
  const loading = ref(false)
  const error = ref('')
  
  // 计算属性
  const isLoggedIn = computed(() => !!currentUser.value)
  const userCount = computed(() => users.value.length)
  const activeUsers = computed(() => 
    users.value.filter(user => user.status === 'active')
  )
  
  // 方法
  async function login(credentials) {
    loading.value = true
    error.value = ''
    
    try {
      // 模拟 API 调用
      const response = await mockLogin(credentials)
      currentUser.value = response.user
      return response
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  function logout() {
    currentUser.value = null
  }
  
  async function fetchUsers() {
    loading.value = true
    
    try {
      const response = await mockFetchUsers()
      users.value = response.users
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  function updateUser(userId, userData) {
    const index = users.value.findIndex(user => user.id === userId)
    if (index !== -1) {
      users.value[index] = { ...users.value[index], ...userData }
    }
    
    if (currentUser.value?.id === userId) {
      currentUser.value = { ...currentUser.value, ...userData }
    }
  }
  
  function addUser(userData) {
    const newUser = {
      id: Date.now(),
      ...userData,
      createdAt: new Date().toISOString()
    }
    users.value.push(newUser)
    return newUser
  }
  
  function removeUser(userId) {
    users.value = users.value.filter(user => user.id !== userId)
  }
  
  return {
    // 状态
    currentUser,
    users,
    loading,
    error,
    
    // 计算属性
    isLoggedIn,
    userCount,
    activeUsers,
    
    // 方法
    login,
    logout,
    fetchUsers,
    updateUser,
    addUser,
    removeUser
  }
})

// 模拟 API 函数
function mockLogin(credentials) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (credentials.username === 'admin' && credentials.password === 'password') {
        resolve({
          user: {
            id: 1,
            username: 'admin',
            name: '管理员',
            email: 'admin@example.com',
            role: 'admin'
          },
          token: 'mock-jwt-token'
        })
      } else {
        reject(new Error('用户名或密码错误'))
      }
    }, 1000)
  })
}

function mockFetchUsers() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        users: [
          {
            id: 1,
            username: 'admin',
            name: '管理员',
            email: 'admin@example.com',
            role: 'admin',
            status: 'active'
          },
          {
            id: 2,
            username: 'user1',
            name: '用户一',
            email: 'user1@example.com',
            role: 'user',
            status: 'active'
          },
          {
            id: 3,
            username: 'user2',
            name: '用户二',
            email: 'user2@example.com',
            role: 'user',
            status: 'inactive'
          }
        ]
      })
    }, 800)
  })
}

在组件中使用 Pinia store:

vue
<template>
  <div class="user-management">
    <h2>用户管理</h2>
    
    <div v-if="!userStore.isLoggedIn" class="login-section">
      <h3>请登录</h3>
      <form @submit.prevent="handleLogin">
        <input 
          v-model="loginForm.username" 
          placeholder="用户名"
          required
        >
        <input 
          v-model="loginForm.password" 
          type="password"
          placeholder="密码"
          required
        >
        <button type="submit" :disabled="userStore.loading">
          {{ userStore.loading ? '登录中...' : '登录' }}
        </button>
      </form>
      <p v-if="userStore.error" class="error">{{ userStore.error }}</p>
    </div>
    
    <div v-else class="user-dashboard">
      <div class="user-info">
        <h3>欢迎, {{ userStore.currentUser.name }}!</h3>
        <button @click="userStore.logout" class="logout-btn">
          退出登录
        </button>
      </div>
      
      <div class="users-section">
        <div class="users-header">
          <h4>用户列表 ({{ userStore.userCount }} 个用户)</h4>
          <p>活跃用户: {{ userStore.activeUsers.length }}</p>
          <button @click="userStore.fetchUsers" :disabled="userStore.loading">
            {{ userStore.loading ? '加载中...' : '刷新用户' }}
          </button>
        </div>
        
        <div class="users-grid">
          <div 
            v-for="user in userStore.users" 
            :key="user.id"
            class="user-card"
          >
            <h5>{{ user.name }}</h5>
            <p>{{ user.email }}</p>
            <span class="user-role">{{ user.role }}</span>
            <span class="user-status" :class="user.status">
              {{ user.status === 'active' ? '活跃' : '非活跃' }}
            </span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { reactive, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const loginForm = reactive({
  username: '',
  password: ''
})

async function handleLogin() {
  try {
    await userStore.login(loginForm)
    // 登录成功后获取用户列表
    await userStore.fetchUsers()
  } catch (error) {
    console.error('登录失败:', error)
  }
}

onMounted(() => {
  if (userStore.isLoggedIn) {
    userStore.fetchUsers()
  }
})
</script>

组件通信最佳实践

1. 选择合适的通信方式

javascript
// ✅ 父子组件:使用 props 和 events
// 父组件传递数据给子组件
<ChildComponent :data="parentData" @update="handleUpdate" />

// ✅ 跨层级组件:使用 provide/inject
provide('theme', themeData)
const theme = inject('theme')

// ✅ 兄弟组件:使用事件总线或状态管理
eventBus.emit('message', data)
const store = useStore()

// ✅ 复杂状态:使用 Pinia
const userStore = useUserStore()

2. 避免过度通信

javascript
// ❌ 避免:过度的 prop drilling
<GrandChild :data="data" :config="config" :theme="theme" :user="user" />

// ✅ 推荐:使用 provide/inject 或状态管理
provide('appContext', { data, config, theme, user })

3. 保持数据流向清晰

javascript
// ✅ 推荐:单向数据流
// 父组件 -> 子组件:props
// 子组件 -> 父组件:events

// ❌ 避免:直接修改 props
props.data.value = newValue // 错误!

// ✅ 正确:通过事件通知父组件
emit('update:data', newValue)

下一步

基于 Vue.js 官方文档构建的学习宝典