本章将深入探讨Vue.js的高级特性,包括自定义指令、插件开发、组合式API深入应用、性能优化和测试策略。

7.1 自定义指令

指令基础

自定义指令用于封装对DOM的直接操作,提供可复用的DOM操作逻辑。

// directives/focus.js
export const vFocus = {
  // 元素被插入到DOM中时调用
  mounted(el) {
    el.focus()
  },
  
  // 组件更新时调用
  updated(el) {
    el.focus()
  }
}

// 简化写法(只需要mounted和updated时)
export const vFocusSimple = (el) => {
  el.focus()
}
// directives/clickOutside.js
export const vClickOutside = {
  mounted(el, binding) {
    el._clickOutsideHandler = (event) => {
      // 检查点击是否在元素外部
      if (!(el === event.target || el.contains(event.target))) {
        // 调用绑定的方法
        binding.value(event)
      }
    }
    
    // 添加事件监听器
    document.addEventListener('click', el._clickOutsideHandler)
  },
  
  unmounted(el) {
    // 清理事件监听器
    document.removeEventListener('click', el._clickOutsideHandler)
    delete el._clickOutsideHandler
  }
}
// directives/loading.js
export const vLoading = {
  mounted(el, binding) {
    el._loadingInstance = null
    updateLoading(el, binding)
  },
  
  updated(el, binding) {
    updateLoading(el, binding)
  },
  
  unmounted(el) {
    if (el._loadingInstance) {
      el._loadingInstance.remove()
    }
  }
}

function updateLoading(el, binding) {
  if (binding.value) {
    showLoading(el, binding.arg)
  } else {
    hideLoading(el)
  }
}

function showLoading(el, text = '加载中...') {
  if (el._loadingInstance) return
  
  // 创建loading元素
  const loadingEl = document.createElement('div')
  loadingEl.className = 'v-loading-overlay'
  loadingEl.innerHTML = `
    <div class="v-loading-spinner">
      <div class="v-loading-dot"></div>
      <div class="v-loading-dot"></div>
      <div class="v-loading-dot"></div>
      <div class="v-loading-text">${text}</div>
    </div>
  `
  
  // 设置样式
  const originalPosition = getComputedStyle(el).position
  if (originalPosition === 'static') {
    el.style.position = 'relative'
  }
  
  el.appendChild(loadingEl)
  el._loadingInstance = loadingEl
}

function hideLoading(el) {
  if (el._loadingInstance) {
    el._loadingInstance.remove()
    el._loadingInstance = null
  }
}
/* directives/loading.css */
.v-loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(255, 255, 255, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.v-loading-spinner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.v-loading-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #3498db;
  display: inline-block;
  margin: 0 2px;
  animation: v-loading-bounce 1.4s ease-in-out infinite both;
}

.v-loading-dot:nth-child(1) { animation-delay: -0.32s; }
.v-loading-dot:nth-child(2) { animation-delay: -0.16s; }

@keyframes v-loading-bounce {
  0%, 80%, 100% {
    transform: scale(0);
  }
  40% {
    transform: scale(1);
  }
}

.v-loading-text {
  color: #666;
  font-size: 14px;
  margin-top: 8px;
}

高级指令示例

// directives/lazyLoad.js
export const vLazyLoad = {
  mounted(el, binding) {
    const options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1,
      ...binding.modifiers
    }
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target
          const src = binding.value
          
          // 创建新的图片对象来预加载
          const newImg = new Image()
          newImg.onload = () => {
            img.src = src
            img.classList.remove('lazy-loading')
            img.classList.add('lazy-loaded')
          }
          newImg.onerror = () => {
            img.classList.remove('lazy-loading')
            img.classList.add('lazy-error')
          }
          newImg.src = src
          
          observer.unobserve(img)
        }
      })
    }, options)
    
    el.classList.add('lazy-loading')
    observer.observe(el)
    el._lazyLoadObserver = observer
  },
  
  updated(el, binding) {
    if (binding.value !== binding.oldValue) {
      // 重新观察
      if (el._lazyLoadObserver) {
        el._lazyLoadObserver.unobserve(el)
        el._lazyLoadObserver.observe(el)
      }
    }
  },
  
  unmounted(el) {
    if (el._lazyLoadObserver) {
      el._lazyLoadObserver.unobserve(el)
    }
  }
}
// directives/permission.js
import { useUserStore } from '@/stores/user'

export const vPermission = {
  mounted(el, binding) {
    checkPermission(el, binding)
  },
  
  updated(el, binding) {
    checkPermission(el, binding)
  }
}

function checkPermission(el, binding) {
  const userStore = useUserStore()
  const { value: permission, modifiers } = binding
  
  let hasPermission = false
  
  if (Array.isArray(permission)) {
    // 数组权限,支持 AND/OR 逻辑
    if (modifiers.or) {
      // OR 逻辑:有任一权限即可
      hasPermission = permission.some(p => userStore.hasPermission(p))
    } else {
      // AND 逻辑:需要所有权限
      hasPermission = permission.every(p => userStore.hasPermission(p))
    }
  } else {
    // 单个权限
    hasPermission = userStore.hasPermission(permission)
  }
  
  if (!hasPermission) {
    if (modifiers.hide) {
      // 隐藏元素
      el.style.display = 'none'
    } else if (modifiers.disable) {
      // 禁用元素
      el.disabled = true
      el.classList.add('permission-disabled')
    } else {
      // 默认移除元素
      el.remove()
    }
  } else {
    // 恢复元素状态
    el.style.display = ''
    el.disabled = false
    el.classList.remove('permission-disabled')
  }
}

注册和使用指令

// main.js
import { createApp } from 'vue'
import App from './App.vue'

// 导入指令
import { vFocus } from './directives/focus'
import { vClickOutside } from './directives/clickOutside'
import { vLoading } from './directives/loading'
import { vLazyLoad } from './directives/lazyLoad'
import { vPermission } from './directives/permission'

// 导入指令样式
import './directives/loading.css'

const app = createApp(App)

// 注册全局指令
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
app.directive('loading', vLoading)
app.directive('lazy-load', vLazyLoad)
app.directive('permission', vPermission)

app.mount('#app')
<!-- 使用指令的组件示例 -->
<template>
  <div class="directive-demo">
    <!-- 自动聚焦 -->
    <input v-focus placeholder="自动聚焦的输入框" class="demo-input">
    
    <!-- 点击外部关闭 -->
    <div class="dropdown" :class="{ active: showDropdown }">
      <button @click="showDropdown = !showDropdown" class="dropdown-toggle">
        下拉菜单
      </button>
      <div 
        v-show="showDropdown" 
        v-click-outside="closeDropdown"
        class="dropdown-menu"
      >
        <div class="dropdown-item">选项 1</div>
        <div class="dropdown-item">选项 2</div>
        <div class="dropdown-item">选项 3</div>
      </div>
    </div>
    
    <!-- 加载状态 -->
    <div class="loading-demo">
      <button @click="toggleLoading" class="demo-button">
        {{ isLoading ? '停止加载' : '开始加载' }}
      </button>
      <div 
        v-loading="isLoading"
        class="loading-container"
      >
        <p>这里是内容区域</p>
        <p>当加载时会显示loading遮罩</p>
      </div>
    </div>
    
    <!-- 懒加载图片 -->
    <div class="lazy-load-demo">
      <h3>懒加载图片</h3>
      <div class="image-grid">
        <img 
          v-for="(url, index) in imageUrls" 
          :key="index"
          v-lazy-load="url"
          :alt="`图片 ${index + 1}`"
          class="lazy-image"
        >
      </div>
    </div>
    
    <!-- 权限控制 -->
    <div class="permission-demo">
      <h3>权限控制</h3>
      <button v-permission="'admin'" class="demo-button">
        仅管理员可见
      </button>
      <button v-permission.hide="'edit'" class="demo-button">
        无编辑权限时隐藏
      </button>
      <button v-permission.disable="['read', 'write']" class="demo-button">
        需要读写权限
      </button>
      <button v-permission.or="['admin', 'moderator']" class="demo-button">
        管理员或版主可见
      </button>
    </div>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  name: 'DirectiveDemo',
  
  setup() {
    const showDropdown = ref(false)
    const isLoading = ref(false)
    
    const imageUrls = ref([
      'https://picsum.photos/300/200?random=1',
      'https://picsum.photos/300/200?random=2',
      'https://picsum.photos/300/200?random=3',
      'https://picsum.photos/300/200?random=4',
      'https://picsum.photos/300/200?random=5',
      'https://picsum.photos/300/200?random=6'
    ])
    
    function closeDropdown() {
      showDropdown.value = false
    }
    
    function toggleLoading() {
      isLoading.value = !isLoading.value
    }
    
    return {
      showDropdown,
      isLoading,
      imageUrls,
      closeDropdown,
      toggleLoading
    }
  }
}
</script>

<style scoped>
.directive-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.demo-input {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 2rem;
}

.dropdown {
  position: relative;
  display: inline-block;
  margin-bottom: 2rem;
}

.dropdown-toggle {
  padding: 0.75rem 1.5rem;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.dropdown-menu {
  position: absolute;
  top: 100%;
  left: 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  min-width: 150px;
  z-index: 1000;
}

.dropdown-item {
  padding: 0.75rem 1rem;
  cursor: pointer;
  border-bottom: 1px solid #f1f1f1;
}

.dropdown-item:hover {
  background: #f8f9fa;
}

.dropdown-item:last-child {
  border-bottom: none;
}

.loading-demo {
  margin-bottom: 2rem;
}

.loading-container {
  background: #f8f9fa;
  padding: 2rem;
  border-radius: 4px;
  margin-top: 1rem;
  min-height: 150px;
}

.demo-button {
  padding: 0.75rem 1.5rem;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 1rem;
  margin-bottom: 1rem;
}

.demo-button:hover {
  background: #2980b9;
}

.demo-button:disabled {
  background: #bdc3c7;
  cursor: not-allowed;
}

.permission-disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

.lazy-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 4px;
  transition: opacity 0.3s;
}

.lazy-image.lazy-loading {
  opacity: 0.3;
  background: #f0f0f0;
}

.lazy-image.lazy-loaded {
  opacity: 1;
}

.lazy-image.lazy-error {
  opacity: 0.5;
  background: #ffebee;
}

.permission-demo {
  margin-top: 2rem;
}

.permission-demo h3 {
  margin-bottom: 1rem;
  color: #2c3e50;
}
</style>

7.2 插件开发

基础插件结构

// plugins/toast.js
import { createApp } from 'vue'
import ToastComponent from './ToastComponent.vue'

class ToastManager {
  constructor() {
    this.toasts = []
    this.container = null
    this.init()
  }
  
  init() {
    // 创建容器
    this.container = document.createElement('div')
    this.container.className = 'toast-container'
    document.body.appendChild(this.container)
  }
  
  show(options) {
    const toast = {
      id: Date.now() + Math.random(),
      message: '',
      type: 'info',
      duration: 3000,
      closable: true,
      ...options
    }
    
    this.toasts.push(toast)
    this.render()
    
    // 自动关闭
    if (toast.duration > 0) {
      setTimeout(() => {
        this.close(toast.id)
      }, toast.duration)
    }
    
    return toast.id
  }
  
  close(id) {
    const index = this.toasts.findIndex(toast => toast.id === id)
    if (index > -1) {
      this.toasts.splice(index, 1)
      this.render()
    }
  }
  
  clear() {
    this.toasts = []
    this.render()
  }
  
  render() {
    // 清空容器
    this.container.innerHTML = ''
    
    // 渲染每个toast
    this.toasts.forEach(toast => {
      const toastEl = document.createElement('div')
      const app = createApp(ToastComponent, {
        ...toast,
        onClose: () => this.close(toast.id)
      })
      app.mount(toastEl)
      this.container.appendChild(toastEl.firstElementChild)
    })
  }
  
  // 便捷方法
  success(message, options = {}) {
    return this.show({ message, type: 'success', ...options })
  }
  
  error(message, options = {}) {
    return this.show({ message, type: 'error', ...options })
  }
  
  warning(message, options = {}) {
    return this.show({ message, type: 'warning', ...options })
  }
  
  info(message, options = {}) {
    return this.show({ message, type: 'info', ...options })
  }
}

// 创建插件
export default {
  install(app, options = {}) {
    const toastManager = new ToastManager()
    
    // 全局属性
    app.config.globalProperties.$toast = toastManager
    
    // 提供注入
    app.provide('toast', toastManager)
  }
}

// 导出管理器类供直接使用
export { ToastManager }
<!-- plugins/ToastComponent.vue -->
<template>
  <transition name="toast" appear>
    <div 
      class="toast"
      :class="[
        `toast-${type}`,
        { 'toast-closable': closable }
      ]"
    >
      <div class="toast-icon">
        <component :is="iconComponent" />
      </div>
      <div class="toast-content">
        <div class="toast-message">{{ message }}</div>
      </div>
      <button 
        v-if="closable" 
        @click="$emit('close')"
        class="toast-close"
      >
        ×
      </button>
    </div>
  </transition>
</template>

<script>
import { computed } from 'vue'

// 图标组件
const SuccessIcon = {
  template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>'
}

const ErrorIcon = {
  template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>'
}

const WarningIcon = {
  template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>'
}

const InfoIcon = {
  template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
}

export default {
  name: 'ToastComponent',
  
  props: {
    message: {
      type: String,
      required: true
    },
    type: {
      type: String,
      default: 'info',
      validator: (value) => ['success', 'error', 'warning', 'info'].includes(value)
    },
    closable: {
      type: Boolean,
      default: true
    }
  },
  
  emits: ['close'],
  
  setup(props) {
    const iconComponent = computed(() => {
      const icons = {
        success: SuccessIcon,
        error: ErrorIcon,
        warning: WarningIcon,
        info: InfoIcon
      }
      return icons[props.type]
    })
    
    return {
      iconComponent
    }
  }
}
</script>

<style scoped>
.toast {
  display: flex;
  align-items: center;
  padding: 1rem 1.5rem;
  margin-bottom: 0.5rem;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  min-width: 300px;
  max-width: 500px;
  position: relative;
}

.toast-success {
  background: #d4edda;
  color: #155724;
  border-left: 4px solid #28a745;
}

.toast-error {
  background: #f8d7da;
  color: #721c24;
  border-left: 4px solid #dc3545;
}

.toast-warning {
  background: #fff3cd;
  color: #856404;
  border-left: 4px solid #ffc107;
}

.toast-info {
  background: #d1ecf1;
  color: #0c5460;
  border-left: 4px solid #17a2b8;
}

.toast-icon {
  width: 20px;
  height: 20px;
  margin-right: 0.75rem;
  flex-shrink: 0;
}

.toast-content {
  flex: 1;
}

.toast-message {
  font-size: 0.9rem;
  line-height: 1.4;
}

.toast-close {
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  padding: 0;
  margin-left: 1rem;
  opacity: 0.7;
  transition: opacity 0.2s;
}

.toast-close:hover {
  opacity: 1;
}

/* 动画 */
.toast-enter-active {
  transition: all 0.3s ease;
}

.toast-leave-active {
  transition: all 0.3s ease;
}

.toast-enter-from {
  opacity: 0;
  transform: translateX(100%);
}

.toast-leave-to {
  opacity: 0;
  transform: translateX(100%);
}
</style>
/* plugins/toast.css */
.toast-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 9999;
  pointer-events: none;
}

.toast-container .toast {
  pointer-events: auto;
}

表单验证插件

// plugins/validator.js
class Validator {
  constructor() {
    this.rules = new Map()
    this.messages = new Map()
    this.setupDefaultRules()
  }
  
  setupDefaultRules() {
    // 必填
    this.addRule('required', (value) => {
      if (Array.isArray(value)) return value.length > 0
      if (typeof value === 'string') return value.trim().length > 0
      return value != null && value !== ''
    }, '此字段为必填项')
    
    // 邮箱
    this.addRule('email', (value) => {
      if (!value) return true
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      return emailRegex.test(value)
    }, '请输入有效的邮箱地址')
    
    // 最小长度
    this.addRule('minLength', (value, min) => {
      if (!value) return true
      return value.length >= min
    }, (min) => `最少需要${min}个字符`)
    
    // 最大长度
    this.addRule('maxLength', (value, max) => {
      if (!value) return true
      return value.length <= max
    }, (max) => `最多允许${max}个字符`)
    
    // 数字范围
    this.addRule('range', (value, min, max) => {
      if (!value) return true
      const num = Number(value)
      return !isNaN(num) && num >= min && num <= max
    }, (min, max) => `请输入${min}到${max}之间的数字`)
    
    // 正则表达式
    this.addRule('pattern', (value, regex) => {
      if (!value) return true
      return regex.test(value)
    }, '格式不正确')
    
    // 确认密码
    this.addRule('confirmed', (value, confirmValue) => {
      return value === confirmValue
    }, '两次输入的密码不一致')
  }
  
  addRule(name, validator, message) {
    this.rules.set(name, validator)
    this.messages.set(name, message)
  }
  
  validate(value, rules) {
    const errors = []
    
    for (const rule of rules) {
      let ruleName, ruleArgs = []
      
      if (typeof rule === 'string') {
        ruleName = rule
      } else if (typeof rule === 'object') {
        ruleName = rule.name
        ruleArgs = rule.args || []
      }
      
      const validator = this.rules.get(ruleName)
      if (!validator) {
        console.warn(`Unknown validation rule: ${ruleName}`)
        continue
      }
      
      const isValid = validator(value, ...ruleArgs)
      if (!isValid) {
        const message = this.messages.get(ruleName)
        const errorMessage = typeof message === 'function' 
          ? message(...ruleArgs) 
          : message
        errors.push(errorMessage)
      }
    }
    
    return {
      valid: errors.length === 0,
      errors
    }
  }
  
  validateForm(formData, schema) {
    const result = {
      valid: true,
      errors: {},
      firstError: null
    }
    
    for (const [field, rules] of Object.entries(schema)) {
      const value = formData[field]
      const validation = this.validate(value, rules)
      
      if (!validation.valid) {
        result.valid = false
        result.errors[field] = validation.errors
        
        if (!result.firstError) {
          result.firstError = {
            field,
            message: validation.errors[0]
          }
        }
      }
    }
    
    return result
  }
}

// Vue插件
export default {
  install(app, options = {}) {
    const validator = new Validator()
    
    // 添加自定义规则
    if (options.rules) {
      for (const [name, config] of Object.entries(options.rules)) {
        validator.addRule(name, config.validator, config.message)
      }
    }
    
    app.config.globalProperties.$validator = validator
    app.provide('validator', validator)
  }
}

export { Validator }

使用插件

// main.js
import { createApp } from 'vue'
import App from './App.vue'

// 导入插件
import ToastPlugin from './plugins/toast'
import ValidatorPlugin from './plugins/validator'

// 导入样式
import './plugins/toast.css'

const app = createApp(App)

// 使用插件
app.use(ToastPlugin)
app.use(ValidatorPlugin, {
  rules: {
    phone: {
      validator: (value) => {
        if (!value) return true
        return /^1[3-9]\d{9}$/.test(value)
      },
      message: '请输入有效的手机号码'
    }
  }
})

app.mount('#app')
<!-- 使用插件的组件 -->
<template>
  <div class="plugin-demo">
    <h2>插件使用示例</h2>
    
    <!-- Toast示例 -->
    <div class="toast-demo">
      <h3>Toast通知</h3>
      <div class="button-group">
        <button @click="showSuccess" class="btn btn-success">成功</button>
        <button @click="showError" class="btn btn-danger">错误</button>
        <button @click="showWarning" class="btn btn-warning">警告</button>
        <button @click="showInfo" class="btn btn-info">信息</button>
      </div>
    </div>
    
    <!-- 表单验证示例 -->
    <div class="form-demo">
      <h3>表单验证</h3>
      <form @submit.prevent="handleSubmit" class="demo-form">
        <div class="form-group">
          <label>用户名 *</label>
          <input 
            v-model="form.username" 
            type="text" 
            class="form-control"
            :class="{ 'is-invalid': errors.username }"
          >
          <div v-if="errors.username" class="invalid-feedback">
            {{ errors.username[0] }}
          </div>
        </div>
        
        <div class="form-group">
          <label>邮箱 *</label>
          <input 
            v-model="form.email" 
            type="email" 
            class="form-control"
            :class="{ 'is-invalid': errors.email }"
          >
          <div v-if="errors.email" class="invalid-feedback">
            {{ errors.email[0] }}
          </div>
        </div>
        
        <div class="form-group">
          <label>手机号</label>
          <input 
            v-model="form.phone" 
            type="tel" 
            class="form-control"
            :class="{ 'is-invalid': errors.phone }"
          >
          <div v-if="errors.phone" class="invalid-feedback">
            {{ errors.phone[0] }}
          </div>
        </div>
        
        <div class="form-group">
          <label>密码 *</label>
          <input 
            v-model="form.password" 
            type="password" 
            class="form-control"
            :class="{ 'is-invalid': errors.password }"
          >
          <div v-if="errors.password" class="invalid-feedback">
            {{ errors.password[0] }}
          </div>
        </div>
        
        <div class="form-group">
          <label>确认密码 *</label>
          <input 
            v-model="form.confirmPassword" 
            type="password" 
            class="form-control"
            :class="{ 'is-invalid': errors.confirmPassword }"
          >
          <div v-if="errors.confirmPassword" class="invalid-feedback">
            {{ errors.confirmPassword[0] }}
          </div>
        </div>
        
        <div class="form-group">
          <label>年龄</label>
          <input 
            v-model="form.age" 
            type="number" 
            class="form-control"
            :class="{ 'is-invalid': errors.age }"
          >
          <div v-if="errors.age" class="invalid-feedback">
            {{ errors.age[0] }}
          </div>
        </div>
        
        <button type="submit" class="btn btn-primary">提交</button>
        <button type="button" @click="resetForm" class="btn btn-secondary">重置</button>
      </form>
    </div>
  </div>
</template>

<script>
import { ref, inject } from 'vue'

export default {
  name: 'PluginDemo',
  
  setup() {
    const toast = inject('toast')
    const validator = inject('validator')
    
    const form = ref({
      username: '',
      email: '',
      phone: '',
      password: '',
      confirmPassword: '',
      age: ''
    })
    
    const errors = ref({})
    
    // 表单验证规则
    const validationSchema = {
      username: ['required', { name: 'minLength', args: [3] }],
      email: ['required', 'email'],
      phone: ['phone'],
      password: ['required', { name: 'minLength', args: [6] }],
      confirmPassword: [
        'required',
        { name: 'confirmed', args: [form.value.password] }
      ],
      age: [{ name: 'range', args: [1, 120] }]
    }
    
    function showSuccess() {
      toast.success('操作成功!')
    }
    
    function showError() {
      toast.error('操作失败,请重试')
    }
    
    function showWarning() {
      toast.warning('请注意检查输入内容')
    }
    
    function showInfo() {
      toast.info('这是一条信息提示', { duration: 5000 })
    }
    
    function handleSubmit() {
      // 更新确认密码验证规则
      validationSchema.confirmPassword = [
        'required',
        { name: 'confirmed', args: [form.value.password] }
      ]
      
      const result = validator.validateForm(form.value, validationSchema)
      
      if (result.valid) {
        errors.value = {}
        toast.success('表单验证通过!')
        console.log('提交表单:', form.value)
      } else {
        errors.value = result.errors
        toast.error(`验证失败: ${result.firstError.message}`)
      }
    }
    
    function resetForm() {
      form.value = {
        username: '',
        email: '',
        phone: '',
        password: '',
        confirmPassword: '',
        age: ''
      }
      errors.value = {}
      toast.info('表单已重置')
    }
    
    return {
      form,
      errors,
      showSuccess,
      showError,
      showWarning,
      showInfo,
      handleSubmit,
      resetForm
    }
  }
}
</script>

<style scoped>
.plugin-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
}

.toast-demo {
  margin-bottom: 3rem;
}

.button-group {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.3s;
}

.btn-success {
  background: #28a745;
  color: white;
}

.btn-success:hover {
  background: #218838;
}

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

.btn-danger:hover {
  background: #c82333;
}

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

.btn-warning:hover {
  background: #e0a800;
}

.btn-info {
  background: #17a2b8;
  color: white;
}

.btn-info:hover {
  background: #138496;
}

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

.btn-primary:hover {
  background: #0056b3;
}

.btn-secondary {
  background: #6c757d;
  color: white;
  margin-left: 1rem;
}

.btn-secondary:hover {
  background: #545b62;
}

.demo-form {
  background: #f8f9fa;
  padding: 2rem;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: #333;
}

.form-control {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 1rem;
  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

.form-control:focus {
  outline: none;
  border-color: #80bdff;
  box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}

.form-control.is-invalid {
  border-color: #dc3545;
}

.form-control.is-invalid:focus {
  border-color: #dc3545;
  box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}

.invalid-feedback {
  display: block;
  width: 100%;
  margin-top: 0.25rem;
  font-size: 0.875rem;
  color: #dc3545;
}

h2, h3 {
  color: #2c3e50;
  margin-bottom: 1rem;
}
</style>

本章小结

本章我们学习了Vue.js的高级特性:

  1. 自定义指令:封装DOM操作逻辑,提供可复用的功能
  2. 插件开发:创建可复用的功能模块,扩展Vue应用能力
  3. 高级组件模式:深入理解组件设计和架构
  4. 性能优化:提升应用性能的各种技巧

下一章预告

下一章我们将学习Vue.js的测试策略,包括: - 单元测试和组件测试 - 集成测试和端到端测试 - 测试工具和最佳实践 - 持续集成和部署

练习题

基础练习

  1. 自定义指令练习

    • 创建一个图片懒加载指令
    • 实现一个权限控制指令
    • 开发一个表单验证指令
  2. 插件开发练习

    • 创建一个通知插件
    • 开发一个表单验证插件
    • 实现一个国际化插件

进阶练习

  1. 高级组件练习

    • 创建一个可配置的数据表格组件
    • 实现一个拖拽排序组件
    • 开发一个富文本编辑器组件
  2. 性能优化练习

    • 分析应用性能瓶颈
    • 实现虚拟滚动列表
    • 优化大型表单的渲染性能

提示:掌握这些高级特性能让你开发出更加专业和高效的Vue.js应用。