Vue.js最佳实践

掌握Vue.js最佳实践能帮助你编写更高效、可维护和高质量的代码。本文总结了Vue社区经过多年实践沉淀下来的宝贵经验。

1. 组件设计原则

单一职责原则

每个组件应该只关注一件事情,保持组件简洁和可复用。

推荐做法
<!-- UserCard.vue -- 职责单一 -->
<template>
  <div class="user-card">
    <img :src="avatar" alt="用户头像">
    <h3>{{ name }}</h3>
    <p>{{ email }}</p>
  </div>
</template>
避免做法
<!-- 职责过多 -->
<template>
  <div class="user-card">
    <!-- 用户信息 -->
    <img :src="avatar" alt="用户头像">
    <h3>{{ name }}</h3>
    <!-- 用户列表 -->
    <ul>
      <li v-for="user in users">{{ user.name }}</li>
    </ul>
    <!-- 搜索功能 -->
    <input v-model="search" placeholder="搜索用户">
  </div>
</template>

Props设计规范

<script>
export default {
  name: 'MyComponent',
  props: {
    // 必填属性
    title: {
      type: String,
      required: true,
      validator: (value) => value.length > 0
    },
    // 可选属性,带默认值
    count: {
      type: Number,
      default: 0,
      validator: (value) => value >= 0
    },
    // 复杂对象
    user: {
      type: Object,
      default: () => ({ name: '匿名用户' })
    },
    // 数组属性
    items: {
      type: Array,
      default: () => []
    },
    // 自定义验证
    status: {
      type: String,
      validator: (value) => {
        return ['pending', 'active', 'inactive'].includes(value)
      }
    }
  }
}
</script>

2. 代码规范

文件命名规范

文件类型 命名规范 示例
单文件组件 PascalCase(大驼峰) UserProfile.vue
组合式函数 camelCase(小驼峰)+ use前缀 useUserData.js
工具函数 camelCase formatDate.js
常量文件 UPPER_SNAKE_CASE CONSTANTS.js
路由文件 kebab-case user-routes.js

组件选项顺序

export default {
  // 1. 组件标识
  name: 'MyComponent',

  // 2. 继承/组合
  mixins: [],
  extends: {},

  // 3. 组件属性
  props: {},
  emits: {},

  // 4. 组合式 API (Composition API)
  setup() {},

  // 5. 选项式 API (Options API)
  data() {
    return {}
  },
  computed: {},
  watch: {},

  // 6. 生命周期钩子(按调用顺序)
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {},
  activated() {},
  deactivated() {},
  errorCaptured() {},

  // 7. 方法
  methods: {},

  // 8. 渲染函数
  render() {}
}

3. 性能优化

延迟加载
// 路由懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

// 组件懒加载
const LazyComponent = defineAsyncComponent(() =>
  import('./components/LazyComponent.vue')
)
虚拟滚动
<!-- 使用 vue-virtual-scroller -->
<RecycleScroller
  :items="largeList"
  :item-size="50"
  key-field="id"
>
  <template v-slot="{ item }">
    <div>{{ item.name }}</div>
  </template>
</RecycleScroller>

计算属性和侦听器优化

// 推荐:使用计算属性缓存结果
computed: {
  filteredUsers() {
    // 结果会被缓存,只有当依赖变化时才重新计算
    return this.users.filter(user =>
      user.active && user.age > 18
    )
  }
}

// 避免:在模板中进行复杂计算
// <div>{{ users.filter(u => u.active).length }}</div>

// 推荐:使用深度侦听时指定选项
watch: {
  user: {
    handler(newVal, oldVal) {
      // 处理变化
    },
    deep: true,        // 深度侦听
    immediate: true    // 立即执行
  }
}

// 推荐:使用flush选项优化性能
watch: {
  value: {
    handler() {
      // DOM更新后执行
    },
    flush: 'post'
  }
}

4. 安全实践

XSS防护:Vue默认对模板内容进行转义,但仍需注意动态内容的安全性。
安全做法
// 使用v-html时确保内容安全
<div v-html="sanitizedHtml"></div>

// 使用DOMPurify等库清理HTML
import DOMPurify from 'dompurify'

export default {
  computed: {
    sanitizedHtml() {
      return DOMPurify.sanitize(this.unsafeHtml)
    }
  }
}
危险做法
// 危险:直接渲染不可信的HTML
<div v-html="userProvidedContent"></div>

// 危险:拼接JavaScript代码
const script = `alert('${userInput}')`
eval(script)

API安全

// 使用环境变量存储敏感信息
const apiKey = import.meta.env.VITE_API_KEY

// 请求拦截器中添加认证
import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL
})

api.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器中处理错误
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      // 处理未授权
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

5. 测试策略

单元测试

测试单个组件或函数

npm test -- unit
组件测试

测试组件交互和渲染

npm test -- component
端到端测试

测试完整用户流程

npm test -- e2e

测试示例

// Counter.spec.js
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter.vue', () => {
  it('renders the initial count', () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('Count: 0')
  })

  it('increments count when button is clicked', async () => {
    const wrapper = mount(Counter)
    const button = wrapper.find('button')

    await button.trigger('click')

    expect(wrapper.text()).toContain('Count: 1')
  })

  it('emits increment event', async () => {
    const wrapper = mount(Counter)

    await wrapper.vm.increment()

    expect(wrapper.emitted().increment).toBeTruthy()
    expect(wrapper.emitted().increment[0]).toEqual([1])
  })
})

// 组合式函数测试
import { useCounter } from './useCounter'
import { ref } from 'vue'

describe('useCounter', () => {
  it('should increment count', () => {
    const { count, increment } = useCounter(ref(0))

    increment()

    expect(count.value).toBe(1)
  })
})

6. 状态管理

状态管理选择指南

方案 适用场景 复杂度
ref/reactive 简单组件、局部状态
provide/inject 深层嵌套组件通信
Pinia 中大型应用、需要状态共享
Vuex 遗留项目、需要时间旅行调试

Pinia最佳实践

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

export const useUserStore = defineStore('user', () => {
  // 状态
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  // getter
  const isLoggedIn = computed(() => !!user.value)
  const userName = computed(() => user.value?.name || '访客')

  // action
  async function login(credentials) {
    loading.value = true
    error.value = null

    try {
      const response = await api.login(credentials)
      user.value = response.data
      localStorage.setItem('token', response.token)
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
    localStorage.removeItem('token')
  }

  // 返回状态和方法
  return {
    user,
    loading,
    error,
    isLoggedIn,
    userName,
    login,
    logout
  }
})

7. 项目结构

推荐的项目结构
src/
├── api/                 # API请求模块
│   ├── user.js
│   ├── product.js
│   └── index.js
├── assets/              # 静态资源
│   ├── images/
│   ├── fonts/
│   └── styles/
│       ├── variables.scss
│       ├── mixins.scss
│       └── global.scss
├── components/          # 公共组件
│   ├── common/          # 通用组件
│   ├── layout/          # 布局组件
│   └── ui/              # UI基础组件
├── composables/         # 组合式函数
│   ├── useFetch.js
│   ├── useLocalStorage.js
│   └── useForm.js
├── directives/          # 自定义指令
│   └── clickOutside.js
├── router/              # 路由配置
│   ├── index.js
│   ├── routes.js
│   └── guards.js
├── stores/              # 状态管理
│   ├── userStore.js
│   ├── cartStore.js
│   └── index.js
├── utils/               # 工具函数
│   ├── helpers.js
│   ├── validators.js
│   └── constants.js
├── views/               # 页面组件
│   ├── Home.vue
│   ├── Login.vue
│   └── Dashboard/
│       ├── index.vue
│       └── components/  # 页面私有组件
├── App.vue              # 根组件
└── main.js              # 应用入口

8. 开发体验

自动导入
// vite.config.js
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    Components({
      resolvers: [ElementPlusResolver()],
      dts: true, // 生成类型声明文件
      dirs: ['src/components'] // 自动导入的目录
    })
  ]
})

// 使用组件时无需手动导入
// 直接在模板中使用
<template>
  <MyComponent />
</template>
代码质量工具
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint . --ext .vue,.js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .vue,.js,.jsx,.ts,.tsx --fix",
    "type-check": "vue-tsc --noEmit",
    "test:unit": "vitest",
    "test:e2e": "cypress run",
    "prepare": "husky install"
  }
}
Vue.js最佳实践检查清单

核心要点总结

  • 组件设计:保持组件小而专一,关注单一职责
  • 代码质量:遵循一致的代码规范,使用TypeScript
  • 性能优化:懒加载、虚拟滚动、合理使用计算属性
  • 安全性:防范XSS攻击,安全处理用户输入
  • 可维护性:合理的项目结构,充分的测试覆盖