概述
本文档收集了 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 Toastjs
// 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>