{T}

概述

本文档收集了 Vue 2 开发过程中的实用技巧和最佳实践,帮助开发者提高开发效率和代码质量。

组件设计技巧

高阶组件(HOC)

高阶组件是复用组件逻辑的高级技巧:

js
// withLoading.js - 加载状态高阶组件
function withLoading(WrappedComponent) {
  return {
    props: WrappedComponent.options?.props || {},
    data() {
      return {
        loading: false
      }
    },
    methods: {
      async withLoadingState(asyncFn) {
        this.loading = true
        try {
          await asyncFn()
        } finally {
          this.loading = false
        }
      }
    },
    render(h) {
      return h(WrappedComponent, {
        props: {
          ...this.$props,
          loading: this.loading
        },
        on: this.$listeners,
        scopedSlots: this.$scopedSlots
      })
    }
  }
}

// 使用高阶组件
const UserListWithLoading = withLoading(UserList)

Renderless 组件

无渲染组件只提供逻辑,不渲染任何内容:

Vue SFC
<!-- FetchData.vue -->
<script>
export default {
  props: {
    url: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      data: null,
      error: null,
      loading: false
    }
  },
  async created() {
    this.loading = true
    try {
      const response = await fetch(this.url)
      this.data = await response.json()
    } catch (err) {
      this.error = err
    } finally {
      this.loading = false
    }
  },
  render() {
    // 通过作用域插槽暴露状态
    return this.$scopedSlots.default({
      data: this.data,
      error: this.error,
      loading: this.loading
    })
  }
}
</script>
Vue SFC
<!-- 使用无渲染组件 -->
<template>
  <FetchData :url="'/api/users'">
    <template #default="{ data, error, loading }">
      <div v-if="loading">加载中...</div>
      <div v-else-if="error">错误: {{ error.message }}</div>
      <div v-else>
        <UserCard v-for="user in data" :key="user.id" :user="user" />
      </div>
    </template>
  </FetchData>
</template>

混入(Mixins)

混入用于分发可复用的组件逻辑:

js
// mixins/scrollMixin.js
export default {
  data() {
    return {
      scrollTop: 0,
      scrollDirection: null
    }
  },
  mounted() {
    window.addEventListener('scroll', this.handleScroll)
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    handleScroll() {
      const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop
      
      this.scrollDirection = currentScrollTop > this.scrollTop ? 'down' : 'up'
      this.scrollTop = currentScrollTop
      
      // 触发自定义事件
      this.$emit('scroll-change', {
        scrollTop: this.scrollTop,
        direction: this.scrollDirection
      })
    }
  }
}
Vue SFC
<!-- 使用混入 -->
<script>
import scrollMixin from '@/mixins/scrollMixin'

export default {
  mixins: [scrollMixin],
  watch: {
    scrollDirection(newDir) {
      console.log('滚动方向:', newDir)
    }
  }
}
</script>

自定义指令

创建可复用的 DOM 操作:

js
// directives/clickOutside.js
export default {
  bind(el, binding, vnode) {
    el._clickOutside = function(event) {
      // 如果点击的不是元素本身或其子元素
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  unbind(el) {
    document.removeEventListener('click', el._clickOutside)
    delete el._clickOutside
  }
}

// 注册指令
Vue.directive('click-outside', clickOutside)
Vue SFC
<template>
  <div v-click-outside="closeDropdown" class="dropdown">
    <button @click="isOpen = !isOpen">Toggle</button>
    <div v-if="isOpen" class="dropdown-menu">
      <!-- 下拉菜单内容 -->
    </div>
  </div>
</template>

插件开发

封装可复用的功能模块:

js
// plugins/toast.js
const Toast = {
  install(Vue, options = {}) {
    // 创建 Toast 组件
    const ToastComponent = Vue.extend({
      data() {
        return {
          message: '',
          visible: false,
          type: 'info'
        }
      },
      template: `
        <transition name="fade">
          <div v-if="visible" :class="['toast', type]">
            {{ message }}
          </div>
        </transition>
      `
    })
    
    // 创建实例并挂载
    const instance = new ToastComponent()
    const mountPoint = document.createElement('div')
    document.body.appendChild(mountPoint)
    instance.$mount(mountPoint)
    
    // 添加实例方法
    Vue.prototype.$toast = {
      show(message, type = 'info', duration = 3000) {
        instance.message = message
        instance.type = type
        instance.visible = true
        
        setTimeout(() => {
          instance.visible = false
        }, duration)
      },
      success(message) {
        this.show(message, 'success')
      },
      error(message) {
        this.show(message, 'error')
      },
      warning(message) {
        this.show(message, 'warning')
      }
    }
  }
}

export default Toast
js
// main.js
import Toast from '@/plugins/toast'
Vue.use(Toast)

// 使用
this.$toast.success('操作成功!')
this.$toast.error('出错了!')

性能优化技巧

列表渲染优化

Vue SFC
<template>
  <!-- 使用唯一且稳定的 key -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- 避免使用 index 作为 key -->
  <!-- 错误示例 -->
  <div v-for="(item, index) in items" :key="index">
    {{ item.name }}
  </div>
  
  <!-- 大数据量使用虚拟滚动 -->
  <VirtualList
    :size="50"
    :remain="10"
    :items="largeList"
  >
    <template #default="{ item }">
      <div class="item">{{ item.name }}</div>
    </template>
  </VirtualList>
</template>

懒加载组件

js
// 异步组件加载
export default {
  components: {
    // 基础异步组件
    AsyncComponent: () => import('./AsyncComponent.vue'),
    
    // 带加载状态的异步组件
    AsyncComponentWithLoading: () => ({
      component: import('./AsyncComponent.vue'),
      loading: LoadingComponent,
      error: ErrorComponent,
      delay: 200,
      timeout: 10000
    })
  }
}

事件委托

Vue SFC
<template>
  <!-- 不推荐:每个元素都绑定事件 -->
  <div>
    <button v-for="item in items" :key="item.id" @click="handleClick(item)">
      {{ item.name }}
    </button>
  </div>
  
  <!-- 推荐:使用事件委托 -->
  <div @click="handleClick">
    <button v-for="item in items" :key="item.id" :data-id="item.id">
      {{ item.name }}
    </button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick(event) {
      const id = event.target.dataset.id
      if (id) {
        const item = this.items.find(i => i.id === id)
        // 处理点击
      }
    }
  }
}
</script>

图片懒加载

js
// directives/lazy.js
export default {
  inserted(el, binding) {
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          observer.unobserve(el)
        }
      })
    }, {
      rootMargin: '50px'
    })
    
    observer.observe(el)
    el._observer = observer
  },
  unbind(el) {
    if (el._observer) {
      el._observer.disconnect()
    }
  }
}

调试技巧

使用 Vue DevTools

Vue DevTools 是调试 Vue 应用的利器:

  • 组件树:查看组件层级和状态
  • Vuex:追踪状态变化和时间旅行
  • 事件:监听组件事件
  • 性能:分析组件渲染性能

控制台调试技巧

js
// 在控制台获取 Vue 实例
// 1. 通过 DOM 元素
const app = document.querySelector('#app').__vue__

// 2. 通过 Vue DevTools
// $vm0 是当前选中的组件实例

// 3. 查看组件数据
console.log(app.$data)

// 4. 触发方法
app.someMethod()

// 5. 修改数据(触发响应式更新)
app.$set(app, 'someProperty', 'new value')

全局错误捕获

js
// main.js
Vue.config.errorHandler = function(err, vm, info) {
  console.group('Vue Error')
  console.error('Error:', err)
  console.error('Component:', vm.$options.name || 'Anonymous')
  console.error('Info:', info)
  console.error('Props:', vm.$props)
  console.error('Data:', vm.$data)
  console.groupEnd()
  
  // 上报错误
  // trackError(err, vm, info)
}

// 捕获 Promise 错误
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled Promise Rejection:', event.reason)
})

性能分析

js
// 使用 Performance API
export default {
  methods: {
    measurePerformance() {
      performance.mark('start')
      
      // 执行操作
      this.doSomething()
      
      performance.mark('end')
      performance.measure('doSomething', 'start', 'end')
      
      const measure = performance.getEntriesByName('doSomething')[0]
      console.log(`耗时: ${measure.duration}ms`)
      
      // 清理
      performance.clearMarks()
      performance.clearMeasures()
    }
  }
}

代码组织技巧

目录结构规范

code
src/
├── api/              # API 接口
│   ├── modules/      # 按模块划分
│   │   ├── user.js
│   │   └── product.js
│   └── index.js      # 统一导出
├── assets/           # 静态资源
│   ├── images/
│   ├── styles/
│   └── fonts/
├── components/       # 公共组件
│   ├── common/       # 通用组件
│   ├── form/         # 表单组件
│   └── layout/       # 布局组件
├── directives/       # 自定义指令
├── filters/          # 过滤器
├── mixins/           # 混入
├── plugins/          # 插件
├── router/           # 路由配置
│   ├── modules/      # 路由模块
│   └── index.js
├── store/            # Vuex 状态管理
│   ├── modules/
│   └── index.js
├── utils/            # 工具函数
│   ├── request.js    # 请求封装
│   ├── storage.js    # 存储封装
│   └── validate.js   # 验证工具
└── views/            # 页面组件
    ├── home/
    ├── user/
    └── product/

模块化 API 管理

js
// api/index.js
import user from './modules/user'
import product from './modules/product'

export default {
  user,
  product
}

// api/modules/user.js
import request from '@/utils/request'

export default {
  // 获取用户信息
  getUser(id) {
    return request.get(`/users/${id}`)
  },
  
  // 更新用户信息
  updateUser(id, data) {
    return request.put(`/users/${id}`, data)
  },
  
  // 删除用户
  deleteUser(id) {
    return request.delete(`/users/${id}`)
  }
}

// 组件中使用
import api from '@/api'

export default {
  methods: {
    async fetchUser() {
      const { data } = await api.user.getUser(this.userId)
      this.user = data
    }
  }
}

统一请求封装

js
// utils/request.js
import axios from 'axios'
import { Message } from 'element-ui'

// 创建实例
const request = axios.create({
  baseURL: process.env.VUE_APP_API_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
request.interceptors.request.use(
  config => {
    // 添加 Token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  response => {
    const { data } = response
    // 根据业务状态码处理
    if (data.code !== 0) {
      Message.error(data.message || '请求失败')
      return Promise.reject(new Error(data.message))
    }
    return data
  },
  error => {
    // 统一错误处理
    const { response } = error
    let message = '网络错误'
    
    if (response) {
      switch (response.status) {
        case 401:
          message = '未授权,请重新登录'
          // 跳转登录
          break
        case 403:
          message = '拒绝访问'
          break
        case 404:
          message = '请求资源不存在'
          break
        case 500:
          message = '服务器错误'
          break
        default:
          message = response.data?.message || '请求失败'
      }
    }
    
    Message.error(message)
    return Promise.reject(error)
  }
)

export default request

环境配置管理

js
// config/index.js
const env = process.env.NODE_ENV

const config = {
  development: {
    apiBaseUrl: 'http://dev-api.example.com',
    enableDebug: true
  },
  staging: {
    apiBaseUrl: 'https://staging-api.example.com',
    enableDebug: true
  },
  production: {
    apiBaseUrl: 'https://api.example.com',
    enableDebug: false
  }
}

export default config[env] || config.development

// 使用
import config from '@/config'

console.log(config.apiBaseUrl)

其他实用技巧

防抖和节流封装

js
// utils/performance.js

// 防抖:延迟执行,期间重新触发则重新计时
export function debounce(fn, delay = 300) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

// 节流:固定间隔执行
export function throttle(fn, delay = 300) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

// 组件中使用
import { debounce, throttle } from '@/utils/performance'

export default {
  methods: {
    handleSearch: debounce(function(query) {
      this.search(query)
    }, 300),
    
    handleScroll: throttle(function() {
      this.checkVisibility()
    }, 100)
  }
}

深拷贝

js
// utils/clone.js

// 简单深拷贝(适用于大多数场景)
export function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  if (obj instanceof Date) {
    return new Date(obj)
  }
  
  if (obj instanceof RegExp) {
    return new RegExp(obj)
  }
  
  const clone = Array.isArray(obj) ? [] : {}
  
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      clone[key] = deepClone(obj[key])
    }
  }
  
  return clone
}

// JSON 方式(简单但有局限)
export function jsonClone(obj) {
  return JSON.parse(JSON.stringify(obj))
}

剪贴板操作

js
// utils/clipboard.js

export async function copyToClipboard(text) {
  try {
    // 优先使用现代 API
    if (navigator.clipboard) {
      await navigator.clipboard.writeText(text)
      return true
    }
    
    // 降级方案
    const textarea = document.createElement('textarea')
    textarea.value = text
    textarea.style.position = 'fixed'
    textarea.style.opacity = '0'
    document.body.appendChild(textarea)
    textarea.select()
    
    const success = document.execCommand('copy')
    document.body.removeChild(textarea)
    return success
  } catch (err) {
    console.error('复制失败:', err)
    return false
  }
}

// 使用
import { copyToClipboard } from '@/utils/clipboard'

await copyToClipboard('复制的文本')

本地存储封装

js
// utils/storage.js

class Storage {
  constructor(prefix = 'app_') {
    this.prefix = prefix
  }
  
  getKey(key) {
    return this.prefix + key
  }
  
  get(key, defaultValue = null) {
    const value = localStorage.getItem(this.getKey(key))
    try {
      return value ? JSON.parse(value) : defaultValue
    } catch {
      return value || defaultValue
    }
  }
  
  set(key, value) {
    const data = typeof value === 'string' ? value : JSON.stringify(value)
    localStorage.setItem(this.getKey(key), data)
  }
  
  remove(key) {
    localStorage.removeItem(this.getKey(key))
  }
  
  clear() {
    const keys = Object.keys(localStorage)
    keys.forEach(key => {
      if (key.startsWith(this.prefix)) {
        localStorage.removeItem(key)
      }
    })
  }
}

export const storage = new Storage()

// 使用
storage.set('user', { id: 1, name: 'John' })
const user = storage.get('user')

表单验证

js
// utils/validate.js

export const validators = {
  // 必填
  required: (value, message = '此项为必填') => {
    if (value === undefined || value === null || value === '') {
      return message
    }
    return ''
  },
  
  // 邮箱
  email: (value, message = '请输入有效的邮箱地址') => {
    if (!value) return ''
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return regex.test(value) ? '' : message
  },
  
  // 手机号
  phone: (value, message = '请输入有效的手机号') => {
    if (!value) return ''
    const regex = /^1[3-9]\d{9}$/
    return regex.test(value) ? '' : message
  },
  
  // 最小长度
  minLength: (min) => (value, message) => {
    if (!value) return ''
    return value.length >= min ? '' : (message || `长度不能少于 ${min} 个字符`)
  },
  
  // 最大长度
  maxLength: (max) => (value, message) => {
    if (!value) return ''
    return value.length <= max ? '' : (message || `长度不能超过 ${max} 个字符`)
  }
}

// 验证表单
export function validateForm(rules, data) {
  const errors = {}
  let isValid = true
  
  for (const [field, fieldRules] of Object.entries(rules)) {
    for (const rule of fieldRules) {
      const message = rule(data[field])
      if (message) {
        errors[field] = message
        isValid = false
        break
      }
    }
  }
  
  return { isValid, errors }
}
Vue SFC
<!-- 使用示例 -->
<script>
import { validators, validateForm } from '@/utils/validate'

export default {
  data() {
    return {
      form: {
        email: '',
        phone: ''
      },
      errors: {}
    }
  },
  computed: {
    rules() {
      return {
        email: [
          validators.required,
          validators.email
        ],
        phone: [
          validators.required,
          validators.phone
        ]
      }
    }
  },
  methods: {
    submit() {
      const { isValid, errors } = validateForm(this.rules, this.form)
      
      if (!isValid) {
        this.errors = errors
        return
      }
      
      // 提交表单
    }
  }
}
</script>