简体中文 繁體中文 English Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français Japanese

站内搜索

搜索
AI 风月

活动公告

03-01 22:34
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

深入浅出Vue3代码规范与最佳实践构建可维护项目从团队协作到性能优化全方位指南成为优秀前端开发者必备技能

3万

主题

586

科技点

3万

积分

白金月票

碾压王

积分
32701

立华奏

发表于 2025-9-4 02:40:07 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

Vue.js作为目前最流行的前端框架之一,其第三个版本Vue3带来了许多新特性和改进。随着项目规模的扩大和团队成员的增加,建立一套完善的代码规范和最佳实践变得尤为重要。本文将深入探讨Vue3的代码规范与最佳实践,从项目结构设计到性能优化,从团队协作到测试策略,全方位帮助开发者构建可维护、高性能的Vue3应用,成为优秀的前端开发者。

Vue3项目结构与组织

一个良好的项目结构是可维护项目的基础。Vue3项目通常采用以下结构:
  1. src/
  2. ├── assets/          # 静态资源文件
  3. │   ├── images/     # 图片资源
  4. │   ├── styles/     # 样式文件
  5. │   └── fonts/      # 字体文件
  6. ├── components/     # 公共组件
  7. │   ├── base/       # 基础组件
  8. │   └── business/   # 业务组件
  9. ├── composables/    # 组合式函数
  10. ├── directives/     # 自定义指令
  11. ├── hooks/          # 自定义钩子
  12. ├── layout/         # 布局组件
  13. ├── plugins/        # 插件
  14. ├── router/         # 路由配置
  15. ├── stores/         # 状态管理
  16. ├── utils/          # 工具函数
  17. ├── views/          # 页面组件
  18. ├── App.vue         # 根组件
  19. └── main.js         # 入口文件
复制代码

项目结构最佳实践

1. 按功能模块组织:对于大型项目,可以按照功能模块组织代码,而不是按照技术类型:
  1. // 按功能模块组织的结构
  2. src/
  3. ├── modules/
  4. │   ├── user/
  5. │   │   ├── components/
  6. │   │   ├── composables/
  7. │   │   ├── views/
  8. │   │   └── store/
  9. │   └── product/
  10. │       ├── components/
  11. │       ├── composables/
  12. │       ├── views/
  13. │       └── store/
复制代码

1. 绝对路径别名:在vite.config.js或vue.config.js中配置路径别名,避免使用相对路径:
  1. // vite.config.js
  2. import { defineConfig } from 'vite'
  3. import path from 'path'
  4. export default defineConfig({
  5.   resolve: {
  6.     alias: {
  7.       '@': path.resolve(__dirname, 'src'),
  8.       '@components': path.resolve(__dirname, 'src/components'),
  9.       '@views': path.resolve(__dirname, 'src/views'),
  10.       '@stores': path.resolve(__dirname, 'src/stores'),
  11.       '@utils': path.resolve(__dirname, 'src/utils'),
  12.       // 其他别名...
  13.     }
  14.   }
  15. })
复制代码

1. 环境变量管理:使用环境变量管理不同环境的配置:
  1. // .env.development
  2. VITE_API_BASE_URL=https://dev-api.example.com
  3. VITE_APP_TITLE=My App (Dev)
  4. // .env.production
  5. VITE_API_BASE_URL=https://api.example.com
  6. VITE_APP_TITLE=My App
复制代码

组件设计与命名规范

组件命名规范

1. 多单词命名:组件名应该始终是多单词的,避免与HTML元素冲突:
  1. <!-- 推荐 -->
  2. <template>
  3.   <UserProfile />
  4.   <ButtonItem />
  5. </template>
  6. <!-- 不推荐 -->
  7. <template>
  8.   <user />
  9.   <button />
  10. </template>
复制代码

1. PascalCase vs kebab-case:在单文件组件中使用PascalCase,在模板中使用kebab-case:
  1. <!-- UserProfile.vue -->
  2. <template>
  3.   <div class="user-profile">
  4.     <!-- 内容 -->
  5.   </div>
  6. </template>
复制代码
  1. <!-- 使用组件 -->
  2. <template>
  3.   <user-profile />
  4. </template>
复制代码

1. 基础组件命名:基础组件(即展示型、无逻辑的组件)应该以Base开头:
  1. <!-- BaseButton.vue -->
  2. <template>
  3.   <button class="base-button" :class="type" @click="$emit('click')">
  4.     <slot />
  5.   </button>
  6. </template>
复制代码

1. 单例组件命名:只应该有一个活跃实例的组件应该以The开头:
  1. <!-- TheHeader.vue -->
  2. <template>
  3.   <header class="the-header">
  4.     <!-- 头部内容 -->
  5.   </header>
  6. </template>
复制代码

组件设计原则

1. 单一职责原则:每个组件应该只做一件事,并做好这件事:
  1. <!-- 推荐:每个组件有明确的职责 -->
  2. <template>
  3.   <UserCard :user="user" />
  4.   <UserList :users="users" />
  5.   <UserForm :user="user" @submit="handleSubmit" />
  6. </template>
  7. <!-- 不推荐:一个组件承担多个职责 -->
  8. <template>
  9.   <div>
  10.     <!-- 用户卡片 -->
  11.     <div v-if="viewMode === 'card'">
  12.       <!-- 卡片内容 -->
  13.     </div>
  14.    
  15.     <!-- 用户列表 -->
  16.     <div v-else-if="viewMode === 'list'">
  17.       <!-- 列表内容 -->
  18.     </div>
  19.    
  20.     <!-- 用户表单 -->
  21.     <div v-else>
  22.       <!-- 表单内容 -->
  23.     </div>
  24.   </div>
  25. </template>
复制代码

1. 自包含组件:组件应该是自包含的,减少对外部依赖:
  1. <!-- 推荐:自包含组件 -->
  2. <template>
  3.   <button
  4.     class="btn"
  5.     :class="[`btn-${type}`, { 'btn-disabled': disabled }]"
  6.     :disabled="disabled"
  7.     @click="handleClick"
  8.   >
  9.     <slot />
  10.   </button>
  11. </template>
  12. <script setup>
  13. const props = defineProps({
  14.   type: {
  15.     type: String,
  16.     default: 'primary',
  17.     validator: (value) => ['primary', 'secondary', 'danger'].includes(value)
  18.   },
  19.   disabled: {
  20.     type: Boolean,
  21.     default: false
  22.   }
  23. })
  24. const emit = defineEmits(['click'])
  25. const handleClick = () => {
  26.   if (!props.disabled) {
  27.     emit('click')
  28.   }
  29. }
  30. </script>
  31. <style scoped>
  32. .btn {
  33.   /* 基础样式 */
  34. }
  35. .btn-primary {
  36.   /* 主要按钮样式 */
  37. }
  38. .btn-secondary {
  39.   /* 次要按钮样式 */
  40. }
  41. .btn-danger {
  42.   /* 危险按钮样式 */
  43. }
  44. .btn-disabled {
  45.   /* 禁用状态样式 */
  46. }
  47. </style>
复制代码

1. 组件通信:优先使用props和events进行父子组件通信,避免直接修改props:
  1. <!-- 父组件 -->
  2. <template>
  3.   <ChildComponent
  4.     :value="count"
  5.     @update:value="count = $event"
  6.   />
  7. </template>
  8. <script setup>
  9. import { ref } from 'vue'
  10. import ChildComponent from './ChildComponent.vue'
  11. const count = ref(0)
  12. </script>
复制代码
  1. <!-- 子组件 -->
  2. <template>
  3.   <button @click="increment">{{ value }}</button>
  4. </template>
  5. <script setup>
  6. const props = defineProps({
  7.   value: {
  8.     type: Number,
  9.     required: true
  10.   }
  11. })
  12. const emit = defineEmits(['update:value'])
  13. const increment = () => {
  14.   emit('update:value', props.value + 1)
  15. }
  16. </script>
复制代码

代码风格与编写规范

代码风格指南

1. 使用Vue 3的Composition API:优先使用<script setup>语法:
  1. <!-- 推荐 -->
  2. <template>
  3.   <div>{{ count }}</div>
  4.   <button @click="increment">Increment</button>
  5. </template>
  6. <script setup>
  7. import { ref } from 'vue'
  8. const count = ref(0)
  9. const increment = () => {
  10.   count.value++
  11. }
  12. </script>
复制代码

1. 响应式数据声明:根据使用场景选择ref或reactive:
  1. <script setup>
  2. import { ref, reactive } from 'vue'
  3. // 简单值类型使用ref
  4. const count = ref(0)
  5. const message = ref('Hello')
  6. // 对象或数组使用reactive
  7. const user = reactive({
  8.   id: 1,
  9.   name: 'John Doe',
  10.   profile: {
  11.     age: 30,
  12.     email: 'john@example.com'
  13.   }
  14. })
  15. const items = reactive([
  16.   { id: 1, text: 'Item 1' },
  17.   { id: 2, text: 'Item 2' }
  18. ])
  19. </script>
复制代码

1. 计算属性:对于需要缓存的复杂计算,使用computed:
  1. <script setup>
  2. import { ref, computed } from 'vue'
  3. const firstName = ref('John')
  4. const lastName = ref('Doe')
  5. const fullName = computed(() => {
  6.   return `${firstName.value} ${lastName.value}`
  7. })
  8. // 带setter的计算属性
  9. const fullNameWithSetter = computed({
  10.   get() {
  11.     return `${firstName.value} ${lastName.value}`
  12.   },
  13.   set(newValue) {
  14.     const [first, last] = newValue.split(' ')
  15.     firstName.value = first
  16.     lastName.value = last
  17.   }
  18. })
  19. </script>
复制代码

1. 侦听器:使用watch和watchEffect响应数据变化:
  1. <script setup>
  2. import { ref, watch, watchEffect } from 'vue'
  3. const question = ref('')
  4. const answer = ref('Questions usually contain a question mark. ;-)')
  5. const loading = ref(false)
  6. // 使用watch侦听特定数据源
  7. watch(question, async (newQuestion, oldQuestion) => {
  8.   if (newQuestion.includes('?')) {
  9.     loading.value = true
  10.     try {
  11.       const res = await fetch('https://api.example.com/answer', {
  12.         method: 'POST',
  13.         body: JSON.stringify({ question: newQuestion })
  14.       })
  15.       answer.value = await res.json()
  16.     } catch (error) {
  17.       answer.value = 'Error! Could not reach the API. ' + error
  18.     } finally {
  19.       loading.value = false
  20.     }
  21.   }
  22. })
  23. // 使用watchEffect自动追踪依赖
  24. watchEffect(async () => {
  25.   if (question.value.includes('?')) {
  26.     loading.value = true
  27.     try {
  28.       const res = await fetch('https://api.example.com/answer', {
  29.         method: 'POST',
  30.         body: JSON.stringify({ question: question.value })
  31.       })
  32.       answer.value = await res.json()
  33.     } catch (error) {
  34.       answer.value = 'Error! Could not reach the API. ' + error
  35.     } finally {
  36.       loading.value = false
  37.     }
  38.   }
  39. })
  40. </script>
复制代码

Props与Emits规范

1. Props定义:始终为props定义类型和默认值:
  1. <script setup>
  2. const props = defineProps({
  3.   // 基本类型检查
  4.   title: String,
  5.   
  6.   // 多个可能的类型
  7.   value: [String, Number],
  8.   
  9.   // 必填字段
  10.   requiredProp: {
  11.     type: String,
  12.     required: true
  13.   },
  14.   
  15.   // 带默认值的数字
  16.   count: {
  17.     type: Number,
  18.     default: 0
  19.   },
  20.   
  21.   // 带默认值的对象
  22.   user: {
  23.     type: Object,
  24.     // 对象或数组的默认值必须从一个工厂函数获取
  25.     default: () => ({
  26.       name: 'John',
  27.       age: 30
  28.     })
  29.   },
  30.   
  31.   // 自定义验证函数
  32.   age: {
  33.     type: Number,
  34.     validator: (value) => {
  35.       return value >= 0 && value <= 120
  36.     }
  37.   },
  38.   
  39.   // 函数类型
  40.   onClick: {
  41.     type: Function,
  42.     default: () => {}
  43.   }
  44. })
  45. </script>
复制代码

1. Emits定义:明确定义组件会触发的事件:
  1. <script setup>
  2. const emit = defineEmits([
  3.   'click',
  4.   'submit',
  5.   'update:modelValue'
  6. ])
  7. // 或者使用对象语法进行验证
  8. const emit = defineEmits({
  9.   // 无验证
  10.   click: null,
  11.   
  12.   // 带验证的submit事件
  13.   submit: ({ email, password }) => {
  14.     if (email && password) {
  15.       return true
  16.     } else {
  17.       console.warn('Invalid submit event payload!')
  18.       return false
  19.     }
  20.   }
  21. })
  22. const handleSubmit = () => {
  23.   emit('submit', { email: 'user@example.com', password: 'password' })
  24. }
  25. </script>
复制代码

模板编写规范

1. v-if vs v-show:根据使用场景选择条件渲染指令:
  1. <template>
  2.   <!-- v-if: 条件不满足时完全销毁和重建元素,适合条件很少改变的情况 -->
  3.   <div v-if="isAuthenticated">
  4.     <UserProfile />
  5.   </div>
  6.   
  7.   <!-- v-show: 通过CSS display属性控制显示隐藏,适合频繁切换的情况 -->
  8.   <div v-show="showNotification">
  9.     <Notification />
  10.   </div>
  11. </template>
复制代码

1. v-for与key:始终为v-for提供唯一的key,避免使用索引作为key:
  1. <template>
  2.   <!-- 推荐:使用唯一ID作为key -->
  3.   <ul>
  4.     <li v-for="item in items" :key="item.id">
  5.       {{ item.text }}
  6.     </li>
  7.   </ul>
  8.   
  9.   <!-- 不推荐:使用索引作为key -->
  10.   <ul>
  11.     <li v-for="(item, index) in items" :key="index">
  12.       {{ item.text }}
  13.     </li>
  14.   </ul>
  15. </template>
复制代码

1. 列表渲染与计算属性:对于需要过滤或排序的列表,使用计算属性:
  1. <template>
  2.   <ul>
  3.     <li v-for="user in activeUsers" :key="user.id">
  4.       {{ user.name }}
  5.     </li>
  6.   </ul>
  7. </template>
  8. <script setup>
  9. import { ref, computed } from 'vue'
  10. const users = ref([
  11.   { id: 1, name: 'John', active: true },
  12.   { id: 2, name: 'Jane', active: false },
  13.   { id: 3, name: 'Bob', active: true }
  14. ])
  15. const activeUsers = computed(() => {
  16.   return users.value.filter(user => user.active)
  17. })
  18. </script>
复制代码

状态管理与数据流

Pinia状态管理最佳实践

Vue3推荐使用Pinia作为状态管理解决方案。以下是Pinia的最佳实践:

1. Store结构设计:按功能模块组织store:
  1. // stores/user.js
  2. import { defineStore } from 'pinia'
  3. import { ref, computed } from 'vue'
  4. import { fetchUserProfile, updateUserProfile } from '@/api/user'
  5. export const useUserStore = defineStore('user', () => {
  6.   // state
  7.   const user = ref(null)
  8.   const loading = ref(false)
  9.   const error = ref(null)
  10.   
  11.   // getters
  12.   const isAuthenticated = computed(() => !!user.value)
  13.   const userName = computed(() => user.value?.name || '')
  14.   
  15.   // actions
  16.   const fetchUser = async (userId) => {
  17.     loading.value = true
  18.     error.value = null
  19.    
  20.     try {
  21.       const response = await fetchUserProfile(userId)
  22.       user.value = response.data
  23.     } catch (err) {
  24.       error.value = err.message
  25.       throw err
  26.     } finally {
  27.       loading.value = false
  28.     }
  29.   }
  30.   
  31.   const updateUser = async (userData) => {
  32.     loading.value = true
  33.     error.value = null
  34.    
  35.     try {
  36.       const response = await updateUserProfile(userData)
  37.       user.value = { ...user.value, ...response.data }
  38.     } catch (err) {
  39.       error.value = err.message
  40.       throw err
  41.     } finally {
  42.       loading.value = false
  43.     }
  44.   }
  45.   
  46.   const clearUser = () => {
  47.     user.value = null
  48.   }
  49.   
  50.   return {
  51.     // state
  52.     user,
  53.     loading,
  54.     error,
  55.    
  56.     // getters
  57.     isAuthenticated,
  58.     userName,
  59.    
  60.     // actions
  61.     fetchUser,
  62.     updateUser,
  63.     clearUser
  64.   }
  65. })
复制代码

1. Store组合使用:在组件中使用多个store:
  1. <template>
  2.   <div>
  3.     <h1>User Profile</h1>
  4.     <div v-if="userStore.loading">Loading...</div>
  5.     <div v-else-if="userStore.error">{{ userStore.error }}</div>
  6.     <div v-else>
  7.       <p>Name: {{ userStore.userName }}</p>
  8.       <p>Preferences: {{ settingsStore.theme }}</p>
  9.     </div>
  10.   </div>
  11. </template>
  12. <script setup>
  13. import { useUserStore } from '@/stores/user'
  14. import { useSettingsStore } from '@/stores/settings'
  15. const userStore = useUserStore()
  16. const settingsStore = useSettingsStore()
  17. // 初始化数据
  18. onMounted(() => {
  19.   userStore.fetchUser(1)
  20.   settingsStore.fetchSettings()
  21. })
  22. </script>
复制代码

1. 插件扩展:使用Pinia插件扩展store功能:
  1. // plugins/piniaLogger.js
  2. export function piniaLogger({ store }) {
  3.   store.$onAction(({ name, after, onError }) => {
  4.     const startTime = Date.now()
  5.     console.log(`[Pinia Action Start] ${store.$id}.${name}`)
  6.    
  7.     after((result) => {
  8.       console.log(
  9.         `[Pinia Action End] ${store.$id}.${name} took ${Date.now() - startTime}ms`
  10.       )
  11.     })
  12.    
  13.     onError((error) => {
  14.       console.error(
  15.         `[Pinia Action Error] ${store.$id}.${name} took ${Date.now() - startTime}ms`,
  16.         error
  17.       )
  18.     })
  19.   })
  20. }
  21. // main.js
  22. import { createApp } from 'vue'
  23. import { createPinia } from 'pinia'
  24. import App from './App.vue'
  25. import { piniaLogger } from './plugins/piniaLogger'
  26. const app = createApp(App)
  27. const pinia = createPinia()
  28. pinia.use(piniaLogger)
  29. app.use(pinia)
  30. app.mount('#app')
复制代码

组合式函数(Composables)最佳实践

组合式函数是Vue3中复用逻辑的重要方式:

1. 基础组合式函数:创建可复用的逻辑函数:
  1. // composables/useFetch.js
  2. import { ref, onMounted, onUnmounted } from 'vue'
  3. export function useFetch(url) {
  4.   const data = ref(null)
  5.   const error = ref(null)
  6.   const loading = ref(false)
  7.   
  8.   let controller = null
  9.   
  10.   const fetchData = async () => {
  11.     loading.value = true
  12.     error.value = null
  13.    
  14.     try {
  15.       controller = new AbortController()
  16.       const response = await fetch(url, {
  17.         signal: controller.signal
  18.       })
  19.       
  20.       if (!response.ok) {
  21.         throw new Error(`HTTP error! status: ${response.status}`)
  22.       }
  23.       
  24.       data.value = await response.json()
  25.     } catch (err) {
  26.       if (err.name !== 'AbortError') {
  27.         error.value = err.message
  28.       }
  29.     } finally {
  30.       loading.value = false
  31.     }
  32.   }
  33.   
  34.   onMounted(() => {
  35.     fetchData()
  36.   })
  37.   
  38.   onUnmounted(() => {
  39.     if (controller) {
  40.       controller.abort()
  41.     }
  42.   })
  43.   
  44.   return {
  45.     data,
  46.     error,
  47.     loading,
  48.     refetch: fetchData
  49.   }
  50. }
复制代码

1. 组合式函数使用:在组件中使用组合式函数:
  1. <template>
  2.   <div>
  3.     <div v-if="loading">Loading...</div>
  4.     <div v-else-if="error">{{ error }}</div>
  5.     <div v-else>
  6.       <pre>{{ data }}</pre>
  7.       <button @click="refetch">Refetch</button>
  8.     </div>
  9.   </div>
  10. </template>
  11. <script setup>
  12. import { useFetch } from '@/composables/useFetch'
  13. const { data, error, loading, refetch } = useFetch('https://api.example.com/data')
  14. </script>
复制代码

1. 复杂组合式函数:创建复杂的组合式函数:
  1. // composables/usePagination.js
  2. import { ref, computed, watch } from 'vue'
  3. export function usePagination(items, itemsPerPage = 10) {
  4.   const currentPage = ref(1)
  5.   
  6.   const totalPages = computed(() => {
  7.     return Math.ceil(items.value.length / itemsPerPage)
  8.   })
  9.   
  10.   const paginatedItems = computed(() => {
  11.     const startIndex = (currentPage.value - 1) * itemsPerPage
  12.     const endIndex = startIndex + itemsPerPage
  13.     return items.value.slice(startIndex, endIndex)
  14.   })
  15.   
  16.   const nextPage = () => {
  17.     if (currentPage.value < totalPages.value) {
  18.       currentPage.value++
  19.     }
  20.   }
  21.   
  22.   const prevPage = () => {
  23.     if (currentPage.value > 1) {
  24.       currentPage.value--
  25.     }
  26.   }
  27.   
  28.   const goToPage = (page) => {
  29.     if (page >= 1 && page <= totalPages.value) {
  30.       currentPage.value = page
  31.     }
  32.   }
  33.   
  34.   // 当items变化时重置到第一页
  35.   watch(items, () => {
  36.     currentPage.value = 1
  37.   })
  38.   
  39.   return {
  40.     currentPage,
  41.     totalPages,
  42.     paginatedItems,
  43.     nextPage,
  44.     prevPage,
  45.     goToPage
  46.   }
  47. }
复制代码

性能优化技巧

组件性能优化

1. v-memo指令:使用v-memo缓存模板部分:
  1. <template>
  2.   <div v-for="item in largeList" :key="item.id" v-memo="[item.id === selectedId]">
  3.     <div :class="{ active: item.id === selectedId }">
  4.       {{ item.text }}
  5.     </div>
  6.     <!-- 其他复杂内容 -->
  7.   </div>
  8. </template>
  9. <script setup>
  10. const selectedId = ref(null)
  11. </script>
复制代码

1. 异步组件:使用异步组件延迟加载非关键组件:
  1. <script setup>
  2. import { defineAsyncComponent } from 'vue'
  3. // 简单异步组件
  4. const AsyncComponent = defineAsyncComponent(() =>
  5.   import('./components/AsyncComponent.vue')
  6. )
  7. // 带选项的异步组件
  8. const AsyncComponentWithOptions = defineAsyncComponent({
  9.   loader: () => import('./components/AsyncComponent.vue'),
  10.   loadingComponent: LoadingComponent,
  11.   errorComponent: ErrorComponent,
  12.   delay: 200,
  13.   timeout: 3000
  14. })
  15. </script>
复制代码

1. keep-alive组件:使用keep-alive缓存组件状态:
  1. <template>
  2.   <keep-alive>
  3.     <component :is="currentComponent" />
  4.   </keep-alive>
  5.   
  6.   <!-- 或者指定最大缓存实例数 -->
  7.   <keep-alive :max="10">
  8.     <component :is="currentComponent" />
  9.   </keep-alive>
  10. </template>
复制代码

列表渲染优化

1. 虚拟滚动:对于大型列表使用虚拟滚动:
  1. <template>
  2.   <RecycleScroller
  3.     class="scroller"
  4.     :items="items"
  5.     :item-size="50"
  6.     key-field="id"
  7.     v-slot="{ item }"
  8.   >
  9.     <div class="item">
  10.       {{ item.text }}
  11.     </div>
  12.   </RecycleScroller>
  13. </template>
  14. <script setup>
  15. import { ref } from 'vue'
  16. import { RecycleScroller } from 'vue-virtual-scroller'
  17. import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
  18. // 生成大量数据
  19. const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  20.   id: i,
  21.   text: `Item ${i}`
  22. })))
  23. </script>
  24. <style scoped>
  25. .scroller {
  26.   height: 400px;
  27. }
  28. .item {
  29.   height: 50px;
  30.   padding: 10px;
  31.   box-sizing: border-box;
  32. }
  33. </style>
复制代码

1. 分页加载:实现无限滚动或分页加载:
  1. <template>
  2.   <div class="list-container" ref="listContainer">
  3.     <div v-for="item in visibleItems" :key="item.id" class="list-item">
  4.       {{ item.text }}
  5.     </div>
  6.     <div v-if="loading" class="loading-indicator">
  7.       Loading more items...
  8.     </div>
  9.   </div>
  10. </template>
  11. <script setup>
  12. import { ref, onMounted, onUnmounted, computed } from 'vue'
  13. const items = ref([])
  14. const page = ref(1)
  15. const loading = ref(false)
  16. const hasMore = ref(true)
  17. const listContainer = ref(null)
  18. // 每页加载的项目数
  19. const itemsPerPage = 20
  20. // 计算当前可见的项目
  21. const visibleItems = computed(() => {
  22.   return items.value
  23. })
  24. // 加载数据的函数
  25. const loadItems = async () => {
  26.   if (loading.value || !hasMore.value) return
  27.   
  28.   loading.value = true
  29.   
  30.   try {
  31.     // 模拟API请求
  32.     const response = await fetch(`https://api.example.com/items?page=${page.value}&limit=${itemsPerPage}`)
  33.     const newItems = await response.json()
  34.    
  35.     if (newItems.length === 0) {
  36.       hasMore.value = false
  37.     } else {
  38.       items.value = [...items.value, ...newItems]
  39.       page.value++
  40.     }
  41.   } catch (error) {
  42.     console.error('Error loading items:', error)
  43.   } finally {
  44.     loading.value = false
  45.   }
  46. }
  47. // 滚动事件处理
  48. const handleScroll = () => {
  49.   if (!listContainer.value) return
  50.   
  51.   const { scrollTop, scrollHeight, clientHeight } = listContainer.value
  52.   
  53.   // 当滚动到距离底部100px时加载更多
  54.   if (scrollHeight - scrollTop - clientHeight < 100) {
  55.     loadItems()
  56.   }
  57. }
  58. // 设置滚动监听
  59. onMounted(() => {
  60.   loadItems()
  61.   listContainer.value.addEventListener('scroll', handleScroll)
  62. })
  63. // 清理监听器
  64. onUnmounted(() => {
  65.   if (listContainer.value) {
  66.     listContainer.value.removeEventListener('scroll', handleScroll)
  67.   }
  68. })
  69. </script>
  70. <style scoped>
  71. .list-container {
  72.   height: 500px;
  73.   overflow-y: auto;
  74.   border: 1px solid #eee;
  75. }
  76. .list-item {
  77.   padding: 15px;
  78.   border-bottom: 1px solid #eee;
  79. }
  80. .loading-indicator {
  81.   padding: 15px;
  82.   text-align: center;
  83.   color: #666;
  84. }
  85. </style>
复制代码

资源加载优化

1. 图片懒加载:使用Intersection Observer实现图片懒加载:
  1. <template>
  2.   <div class="image-container">
  3.     <img
  4.       v-for="(image, index) in images"
  5.       :key="index"
  6.       :data-src="image.url"
  7.       :alt="image.alt"
  8.       class="lazy-image"
  9.     />
  10.   </div>
  11. </template>
  12. <script setup>
  13. import { ref, onMounted, onUnmounted } from 'vue'
  14. const images = ref([
  15.   { url: 'https://picsum.photos/400/300?random=1', alt: 'Image 1' },
  16.   { url: 'https://picsum.photos/400/300?random=2', alt: 'Image 2' },
  17.   // 更多图片...
  18. ])
  19. let observer = null
  20. onMounted(() => {
  21.   observer = new IntersectionObserver((entries) => {
  22.     entries.forEach(entry => {
  23.       if (entry.isIntersecting) {
  24.         const img = entry.target
  25.         img.src = img.dataset.src
  26.         observer.unobserve(img)
  27.       }
  28.     })
  29.   }, {
  30.     rootMargin: '50px 0px',
  31.     threshold: 0.01
  32.   })
  33.   
  34.   // 观察所有图片
  35.   setTimeout(() => {
  36.     document.querySelectorAll('.lazy-image').forEach(img => {
  37.       observer.observe(img)
  38.     })
  39.   }, 100)
  40. })
  41. onUnmounted(() => {
  42.   if (observer) {
  43.     observer.disconnect()
  44.   }
  45. })
  46. </script>
  47. <style scoped>
  48. .image-container {
  49.   display: grid;
  50.   grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  51.   gap: 20px;
  52. }
  53. .lazy-image {
  54.   width: 100%;
  55.   height: 200px;
  56.   object-fit: cover;
  57.   background-color: #f0f0f0;
  58. }
  59. </style>
复制代码

1. 代码分割:使用动态导入实现代码分割:
  1. // 路由配置中的代码分割
  2. const routes = [
  3.   {
  4.     path: '/',
  5.     name: 'Home',
  6.     component: () => import('@/views/Home.vue')
  7.   },
  8.   {
  9.     path: '/about',
  10.     name: 'About',
  11.     component: () => import('@/views/About.vue')
  12.   },
  13.   {
  14.     path: '/contact',
  15.     name: 'Contact',
  16.     component: () => import('@/views/Contact.vue')
  17.   }
  18. ]
  19. // 按需加载第三方库
  20. const loadChartLibrary = async () => {
  21.   const chartModule = await import('chart.js')
  22.   return chartModule.default
  23. }
  24. // 在组件中使用
  25. const initChart = async () => {
  26.   const Chart = await loadChartLibrary()
  27.   // 使用Chart初始化图表
  28. }
复制代码

团队协作与代码审查

代码规范工具配置

1. ESLint配置:配置ESLint确保代码质量:
  1. // .eslintrc.js
  2. module.exports = {
  3.   root: true,
  4.   env: {
  5.     browser: true,
  6.     node: true,
  7.     es2021: true
  8.   },
  9.   extends: [
  10.     'plugin:vue/vue3-recommended',
  11.     'eslint:recommended',
  12.     '@vue/typescript/recommended',
  13.     '@vue/prettier',
  14.     '@vue/prettier/@typescript-eslint'
  15.   ],
  16.   parserOptions: {
  17.     ecmaVersion: 2021
  18.   },
  19.   rules: {
  20.     'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  21.     'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  22.     'vue/multi-word-component-names': 'off',
  23.     'vue/no-unused-components': 'warn',
  24.     'vue/no-unused-vars': 'warn',
  25.     'vue/require-default-prop': 'error',
  26.     'vue/require-explicit-emits': 'error',
  27.     '@typescript-eslint/no-explicit-any': 'warn',
  28.     '@typescript-eslint/no-unused-vars': 'warn'
  29.   }
  30. }
复制代码

1. Prettier配置:配置Prettier确保代码风格一致:
  1. // .prettierrc
  2. {
  3.   "semi": false,
  4.   "singleQuote": true,
  5.   "tabWidth": 2,
  6.   "trailingComma": "none",
  7.   "printWidth": 100,
  8.   "bracketSpacing": true,
  9.   "arrowParens": "avoid",
  10.   "endOfLine": "lf",
  11.   "vueIndentScriptAndStyle": true
  12. }
复制代码

1. Stylelint配置:配置Stylelint确保CSS质量:
  1. // .stylelintrc.js
  2. module.exports = {
  3.   extends: [
  4.     'stylelint-config-standard',
  5.     'stylelint-config-recommended-vue'
  6.   ],
  7.   rules: {
  8.     'selector-class-pattern': '^[a-z][a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$',
  9.     'custom-property-pattern': '^[a-z][a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$',
  10.     'declaration-block-no-redundant-longhand-properties': true,
  11.     'alpha-value-notation': 'number',
  12.     'color-function-notation': 'modern',
  13.     'property-no-vendor-prefix': true,
  14.     'value-no-vendor-prefix': true,
  15.     'selector-no-vendor-prefix': true,
  16.     'media-feature-range-operator-space': 'before',
  17.     'media-feature-parentheses-space-inside': 'never',
  18.     'media-feature-name-no-vendor-prefix': true,
  19.     'comment-empty-line-before': 'always',
  20.     'declaration-empty-line-before': 'never',
  21.     'function-comma-newline-after': 'never-multi-line',
  22.     'function-name-case': 'lower',
  23.     'function-url-quotes': 'always',
  24.     'length-zero-no-unit': true,
  25.     'number-leading-zero': 'always',
  26.     'number-no-trailing-zeros': true,
  27.     'string-quotes': 'single',
  28.     'value-keyword-case': 'lower',
  29.     'custom-property-empty-line-before': 'never',
  30.     'declaration-bang-space-before': 'never',
  31.     'declaration-bang-space-after': 'never'
  32.   }
  33. }
复制代码

Git工作流规范

1. Commit消息规范:使用Conventional Commits规范:
  1. # 格式: <type>(<scope>): <subject>
  2. # <body>
  3. # <footer>
  4. # 示例
  5. feat(auth): add user login functionality
  6. - Add email and password validation
  7. - Implement JWT token authentication
  8. - Add remember me functionality
  9. Closes #123
复制代码

1. 分支命名规范:使用语义化分支命名:
  1. # 格式: <type>/<short-description>
  2. # 类型: feature, fix, hotfix, release, docs, chore, test, refactor
  3. # 示例
  4. feature/user-authentication
  5. fix/login-validation-error
  6. hotfix/security-patch
  7. release/v1.0.0
  8. docs/api-documentation
  9. chore/update-dependencies
  10. test/unit-tests-for-auth
  11. refactor/user-service
复制代码

1. Pull Request模板:创建PR模板确保代码审查质量:
  1. ## 变更描述
  2. 简要描述此PR的目的和所做的更改。
  3. ## 变更类型
  4. - [ ] Bug修复
  5. - [ ] 新功能
  6. - [ ] 文档更新
  7. - [ ] 重构
  8. - [ ] 性能优化
  9. - [ ] 测试相关
  10. - [ ] 其他
  11. ## 测试清单
  12. - [ ] 代码已通过本地测试
  13. - [ ] 所有单元测试通过
  14. - [ ] 所有端到端测试通过
  15. - [ ] 手动测试完成
  16. ## 相关问题
  17. Closes #issue-number
  18. ## 截图(如适用)
  19. 添加截图展示变更效果。
  20. ## 检查清单
  21. - [ ] 代码符合项目编码规范
  22. - [ ] 自测通过
  23. - [ ] 文档已更新(如需要)
  24. - [ ] 代码已审查
复制代码

代码审查最佳实践

1. 代码审查清单:创建代码审查清单确保质量:
  1. ## 代码审查清单
  2. ### 通用原则
  3. - [ ] 代码是否实现了预期功能?
  4. - [ ] 代码是否遵循项目的编码规范?
  5. - [ ] 代码是否易于理解和维护?
  6. - [ ] 是否有明显的性能问题?
  7. - [ ] 是否有安全漏洞?
  8. ### Vue组件特定
  9. - [ ] 组件是否遵循单一职责原则?
  10. - [ ] Props和Emits是否正确定义和使用?
  11. - [ ] 是否正确使用了响应式API(ref, reactive, computed等)?
  12. - [ ] 组件是否有适当的错误处理?
  13. - [ ] 组件是否易于测试?
  14. ### 性能考虑
  15. - [ ] 是否有不必要的重新渲染?
  16. - [ ] 大型列表是否使用虚拟滚动或分页?
  17. - [ ] 是否正确使用v-if和v-show?
  18. - [ ] 资源(图片、组件等)是否懒加载?
  19. ### 可访问性
  20. - [ ] 组件是否符合WCAG标准?
  21. - [ ] 是否提供适当的ARIA属性?
  22. - [ ] 键盘导航是否可用?
  23. - [ ] 颜色对比度是否足够?
  24. ### 测试
  25. - [ ] 是否有适当的单元测试?
  26. - [ ] 是否有集成测试或端到端测试?
  27. - [ ] 测试覆盖率是否足够?
复制代码

1. 自动化代码审查:使用GitHub Actions实现自动化代码审查:
  1. # .github/workflows/pr-check.yml
  2. name: Pull Request Checks
  3. on:
  4.   pull_request:
  5.     types: [opened, synchronize, reopened]
  6. jobs:
  7.   lint:
  8.     runs-on: ubuntu-latest
  9.    
  10.     steps:
  11.     - name: Checkout code
  12.       uses: actions/checkout@v3
  13.       
  14.     - name: Setup Node.js
  15.       uses: actions/setup-node@v3
  16.       with:
  17.         node-version: '16'
  18.         cache: 'npm'
  19.         
  20.     - name: Install dependencies
  21.       run: npm ci
  22.       
  23.     - name: Run ESLint
  24.       run: npm run lint
  25.       
  26.     - name: Run Stylelint
  27.       run: npm run stylelint
  28.       
  29.     - name: Run Prettier check
  30.       run: npm run format:check
  31.       
  32.   test:
  33.     runs-on: ubuntu-latest
  34.     needs: lint
  35.    
  36.     steps:
  37.     - name: Checkout code
  38.       uses: actions/checkout@v3
  39.       
  40.     - name: Setup Node.js
  41.       uses: actions/setup-node@v3
  42.       with:
  43.         node-version: '16'
  44.         cache: 'npm'
  45.         
  46.     - name: Install dependencies
  47.       run: npm ci
  48.       
  49.     - name: Run unit tests
  50.       run: npm run test:unit
  51.       
  52.     - name: Run e2e tests
  53.       run: npm run test:e2e
  54.       
  55.     - name: Upload coverage to Codecov
  56.       uses: codecov/codecov-action@v3
  57.       with:
  58.         file: ./coverage/lcov.info
复制代码

测试策略与最佳实践

单元测试

1. 组件测试:使用Vitest测试Vue组件:
  1. // tests/unit/UserProfile.spec.js
  2. import { mount } from '@vue/test-utils'
  3. import { describe, it, expect, vi } from 'vitest'
  4. import UserProfile from '@/components/UserProfile.vue'
  5. describe('UserProfile', () => {
  6.   it('renders user information correctly', () => {
  7.     const user = {
  8.       id: 1,
  9.       name: 'John Doe',
  10.       email: 'john@example.com',
  11.       avatar: 'https://example.com/avatar.jpg'
  12.     }
  13.    
  14.     const wrapper = mount(UserProfile, {
  15.       props: {
  16.         user
  17.       }
  18.     })
  19.    
  20.     expect(wrapper.find('.user-name').text()).toBe(user.name)
  21.     expect(wrapper.find('.user-email').text()).toBe(user.email)
  22.     expect(wrapper.find('.user-avatar').attributes('src')).toBe(user.avatar)
  23.   })
  24.   
  25.   it('emits edit event when edit button is clicked', async () => {
  26.     const wrapper = mount(UserProfile, {
  27.       props: {
  28.         user: {
  29.           id: 1,
  30.           name: 'John Doe',
  31.           email: 'john@example.com'
  32.         }
  33.       }
  34.     })
  35.    
  36.     await wrapper.find('.edit-button').trigger('click')
  37.     expect(wrapper.emitted('edit')).toBeTruthy()
  38.     expect(wrapper.emitted('edit')[0]).toEqual([1])
  39.   })
  40.   
  41.   it('displays loading state when loading prop is true', () => {
  42.     const wrapper = mount(UserProfile, {
  43.       props: {
  44.         user: null,
  45.         loading: true
  46.       }
  47.     })
  48.    
  49.     expect(wrapper.find('.loading-indicator').exists()).toBe(true)
  50.     expect(wrapper.find('.user-profile').exists()).toBe(false)
  51.   })
  52.   
  53.   it('displays error message when error prop is provided', () => {
  54.     const errorMessage = 'Failed to load user data'
  55.     const wrapper = mount(UserProfile, {
  56.       props: {
  57.         user: null,
  58.         error: errorMessage
  59.       }
  60.     })
  61.    
  62.     expect(wrapper.find('.error-message').text()).toBe(errorMessage)
  63.   })
  64. })
复制代码

1. 组合式函数测试:测试组合式函数:
  1. // tests/unit/composables/useFetch.spec.js
  2. import { describe, it, expect, vi, beforeEach } from 'vitest'
  3. import { useFetch } from '@/composables/useFetch'
  4. // 模拟全局fetch
  5. global.fetch = vi.fn()
  6. describe('useFetch', () => {
  7.   beforeEach(() => {
  8.     vi.resetAllMocks()
  9.   })
  10.   
  11.   it('fetches data successfully', async () => {
  12.     const mockData = { id: 1, name: 'Test Data' }
  13.     fetch.mockResolvedValueOnce({
  14.       ok: true,
  15.       json: async () => mockData
  16.     })
  17.    
  18.     const { data, error, loading } = useFetch('https://api.example.com/data')
  19.    
  20.     // 初始状态
  21.     expect(loading.value).toBe(true)
  22.     expect(data.value).toBe(null)
  23.     expect(error.value).toBe(null)
  24.    
  25.     // 等待异步操作完成
  26.     await new Promise(resolve => setTimeout(resolve, 0))
  27.    
  28.     expect(loading.value).toBe(false)
  29.     expect(data.value).toEqual(mockData)
  30.     expect(error.value).toBe(null)
  31.   })
  32.   
  33.   it('handles fetch error', async () => {
  34.     const errorMessage = 'Network error'
  35.     fetch.mockRejectedValueOnce(new Error(errorMessage))
  36.    
  37.     const { data, error, loading } = useFetch('https://api.example.com/data')
  38.    
  39.     // 等待异步操作完成
  40.     await new Promise(resolve => setTimeout(resolve, 0))
  41.    
  42.     expect(loading.value).toBe(false)
  43.     expect(data.value).toBe(null)
  44.     expect(error.value).toBe(errorMessage)
  45.   })
  46.   
  47.   it('handles HTTP error status', async () => {
  48.     fetch.mockResolvedValueOnce({
  49.       ok: false,
  50.       status: 404,
  51.       statusText: 'Not Found'
  52.     })
  53.    
  54.     const { data, error, loading } = useFetch('https://api.example.com/data')
  55.    
  56.     // 等待异步操作完成
  57.     await new Promise(resolve => setTimeout(resolve, 0))
  58.    
  59.     expect(loading.value).toBe(false)
  60.     expect(data.value).toBe(null)
  61.     expect(error.value).toBe('HTTP error! status: 404')
  62.   })
  63.   
  64.   it('can refetch data', async () => {
  65.     const mockData1 = { id: 1, name: 'Test Data 1' }
  66.     const mockData2 = { id: 2, name: 'Test Data 2' }
  67.    
  68.     fetch.mockResolvedValueOnce({
  69.       ok: true,
  70.       json: async () => mockData1
  71.     })
  72.    
  73.     const { data, error, loading, refetch } = useFetch('https://api.example.com/data')
  74.    
  75.     // 等待第一次请求完成
  76.     await new Promise(resolve => setTimeout(resolve, 0))
  77.    
  78.     expect(data.value).toEqual(mockData1)
  79.    
  80.     // 重置mock并准备第二次请求
  81.     fetch.mockResolvedValueOnce({
  82.       ok: true,
  83.       json: async () => mockData2
  84.     })
  85.    
  86.     // 调用refetch
  87.     await refetch()
  88.    
  89.     expect(data.value).toEqual(mockData2)
  90.   })
  91. })
复制代码

端到端测试

1. 使用Cypress进行端到端测试:
  1. // tests/e2e/auth.cy.js
  2. describe('Authentication', () => {
  3.   beforeEach(() => {
  4.     cy.visit('/login')
  5.   })
  6.   
  7.   it('should display login form', () => {
  8.     cy.get('[data-testid="login-form"]').should('be.visible')
  9.     cy.get('[data-testid="email-input"]').should('be.visible')
  10.     cy.get('[data-testid="password-input"]').should('be.visible')
  11.     cy.get('[data-testid="submit-button"]').should('be.visible')
  12.   })
  13.   
  14.   it('should show validation errors for empty inputs', () => {
  15.     cy.get('[data-testid="submit-button"]').click()
  16.    
  17.     cy.get('[data-testid="email-error"]').should('contain', 'Email is required')
  18.     cy.get('[data-testid="password-error"]').should('contain', 'Password is required')
  19.   })
  20.   
  21.   it('should show validation error for invalid email', () => {
  22.     cy.get('[data-testid="email-input"]').type('invalid-email')
  23.     cy.get('[data-testid="password-input"]').type('password123')
  24.     cy.get('[data-testid="submit-button"]').click()
  25.    
  26.     cy.get('[data-testid="email-error"]').should('contain', 'Please enter a valid email')
  27.   })
  28.   
  29.   it('should log in successfully with valid credentials', () => {
  30.     // 拦截登录请求并模拟响应
  31.     cy.intercept('POST', '**/api/login', {
  32.       statusCode: 200,
  33.       body: {
  34.         token: 'fake-jwt-token',
  35.         user: {
  36.           id: 1,
  37.           name: 'John Doe',
  38.           email: 'john@example.com'
  39.         }
  40.       }
  41.     }).as('loginRequest')
  42.    
  43.     cy.get('[data-testid="email-input"]').type('john@example.com')
  44.     cy.get('[data-testid="password-input"]').type('password123')
  45.     cy.get('[data-testid="submit-button"]').click()
  46.    
  47.     // 等待请求完成
  48.     cy.wait('@loginRequest')
  49.    
  50.     // 验证重定向到仪表板
  51.     cy.url().should('include', '/dashboard')
  52.    
  53.     // 验证用户信息显示
  54.     cy.get('[data-testid="user-name"]').should('contain', 'John Doe')
  55.   })
  56.   
  57.   it('should show error message for invalid credentials', () => {
  58.     // 拦截登录请求并模拟错误响应
  59.     cy.intercept('POST', '**/api/login', {
  60.       statusCode: 401,
  61.       body: {
  62.         message: 'Invalid email or password'
  63.       }
  64.     }).as('loginRequest')
  65.    
  66.     cy.get('[data-testid="email-input"]').type('john@example.com')
  67.     cy.get('[data-testid="password-input"]').type('wrong-password')
  68.     cy.get('[data-testid="submit-button"]').click()
  69.    
  70.     // 等待请求完成
  71.     cy.wait('@loginRequest')
  72.    
  73.     // 验证错误消息显示
  74.     cy.get('[data-testid="login-error"]').should('contain', 'Invalid email or password')
  75.    
  76.     // 验证仍在登录页面
  77.     cy.url().should('include', '/login')
  78.   })
  79. })
复制代码

1. 使用Cypress测试组件交互:
  1. // tests/e2e/user-list.cy.js
  2. describe('User List', () => {
  3.   beforeEach(() => {
  4.     // 拦截用户列表请求
  5.     cy.intercept('GET', '**/api/users', {
  6.       fixture: 'users.json'
  7.     }).as('getUsers')
  8.    
  9.     cy.visit('/users')
  10.     cy.wait('@getUsers')
  11.   })
  12.   
  13.   it('should display a list of users', () => {
  14.     cy.get('[data-testid="user-list"]').should('be.visible')
  15.     cy.get('[data-testid="user-item"]').should('have.length', 10)
  16.   })
  17.   
  18.   it('should filter users by search term', () => {
  19.     cy.get('[data-testid="search-input"]').type('John')
  20.    
  21.     cy.get('[data-testid="user-item"]').should('have.length', 2)
  22.     cy.get('[data-testid="user-item"]').first().should('contain', 'John')
  23.   })
  24.   
  25.   it('should sort users by name', () => {
  26.     // 获取初始用户名列表
  27.     const initialNames = []
  28.     cy.get('[data-testid="user-name"]').each($el => {
  29.       initialNames.push($el.text())
  30.     })
  31.    
  32.     // 点击排序按钮
  33.     cy.get('[data-testid="sort-button"]').click()
  34.    
  35.     // 获取排序后的用户名列表
  36.     const sortedNames = []
  37.     cy.get('[data-testid="user-name"]').each($el => {
  38.       sortedNames.push($el.text())
  39.     })
  40.    
  41.     // 验证列表已排序
  42.     expect(JSON.stringify(sortedNames)).to.equal(JSON.stringify(initialNames.sort()))
  43.   })
  44.   
  45.   it('should paginate users', () => {
  46.     // 验证分页控件存在
  47.     cy.get('[data-testid="pagination"]').should('be.visible')
  48.    
  49.     // 点击下一页
  50.     cy.get('[data-testid="next-page"]').click()
  51.    
  52.     // 验证URL包含页码
  53.     cy.url().should('include', 'page=2')
  54.    
  55.     // 拦截第二页请求
  56.     cy.intercept('GET', '**/api/users?page=2', {
  57.       fixture: 'users-page-2.json'
  58.     }).as('getUsersPage2')
  59.    
  60.     cy.wait('@getUsersPage2')
  61.    
  62.     // 验证用户列表已更新
  63.     cy.get('[data-testid="user-item"]').first().should('contain', 'User 11')
  64.   })
  65. })
复制代码

测试覆盖率与持续集成

1. 配置测试覆盖率:
  1. // vitest.config.js
  2. import { defineConfig } from 'vitest/config'
  3. import vue from '@vitejs/plugin-vue'
  4. import path from 'path'
  5. export default defineConfig({
  6.   plugins: [vue()],
  7.   test: {
  8.     // 启用测试覆盖率
  9.     coverage: {
  10.       provider: 'c8',
  11.       reporter: ['text', 'json', 'html'],
  12.       exclude: [
  13.         'node_modules/',
  14.         'src/main.js',
  15.         'src/router/index.js',
  16.         'src/stores/index.js'
  17.       ]
  18.     },
  19.     globals: true,
  20.     environment: 'jsdom'
  21.   },
  22.   resolve: {
  23.     alias: {
  24.       '@': path.resolve(__dirname, 'src')
  25.     }
  26.   }
  27. })
复制代码

1. GitHub Actions测试工作流:
  1. # .github/workflows/test.yml
  2. name: Tests
  3. on:
  4.   push:
  5.     branches: [ main, develop ]
  6.   pull_request:
  7.     branches: [ main, develop ]
  8. jobs:
  9.   test:
  10.     runs-on: ubuntu-latest
  11.    
  12.     steps:
  13.     - name: Checkout code
  14.       uses: actions/checkout@v3
  15.       
  16.     - name: Setup Node.js
  17.       uses: actions/setup-node@v3
  18.       with:
  19.         node-version: '16'
  20.         cache: 'npm'
  21.         
  22.     - name: Install dependencies
  23.       run: npm ci
  24.       
  25.     - name: Run unit tests
  26.       run: npm run test:unit
  27.       
  28.     - name: Upload coverage reports
  29.       uses: codecov/codecov-action@v3
  30.       with:
  31.         file: ./coverage/lcov.info
  32.         flags: unittests
  33.         name: codecov-umbrella
  34.         
  35.     - name: Run e2e tests
  36.       run: npm run test:e2e
  37.       continue-on-error: true
  38.       
  39.     - name: Upload e2e test artifacts
  40.       uses: actions/upload-artifact@v3
  41.       if: failure()
  42.       with:
  43.         name: cypress-screenshots
  44.         path: cypress/screenshots/
  45.         
  46.     - name: Upload e2e test videos
  47.       uses: actions/upload-artifact@v3
  48.       if: always()
  49.       with:
  50.         name: cypress-videos
  51.         path: cypress/videos/
复制代码

持续集成与部署

CI/CD流水线配置

1. GitHub Actions部署工作流:
  1. # .github/workflows/deploy.yml
  2. name: Deploy to Production
  3. on:
  4.   push:
  5.     branches: [ main ]
  6.   workflow_dispatch:
  7. jobs:
  8.   build-and-deploy:
  9.     runs-on: ubuntu-latest
  10.     environment: production
  11.    
  12.     steps:
  13.     - name: Checkout code
  14.       uses: actions/checkout@v3
  15.       
  16.     - name: Setup Node.js
  17.       uses: actions/setup-node@v3
  18.       with:
  19.         node-version: '16'
  20.         cache: 'npm'
  21.         
  22.     - name: Install dependencies
  23.       run: npm ci
  24.       
  25.     - name: Run linting
  26.       run: npm run lint
  27.       
  28.     - name: Run tests
  29.       run: npm run test:unit
  30.       
  31.     - name: Build application
  32.       run: npm run build
  33.       env:
  34.         VITE_API_BASE_URL: ${{ secrets.PROD_API_BASE_URL }}
  35.         VITE_APP_TITLE: ${{ secrets.PROD_APP_TITLE }}
  36.         
  37.     - name: Deploy to Netlify
  38.       uses: netlify/actions/cli@master
  39.       with:
  40.         args: deploy --dir=dist --prod
  41.       env:
  42.         NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
  43.         NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
复制代码

1. Docker化Vue应用:
  1. # Dockerfile
  2. # 构建阶段
  3. FROM node:16-alpine AS build
  4. # 设置工作目录
  5. WORKDIR /app
  6. # 复制package.json和package-lock.json
  7. COPY package*.json ./
  8. # 安装依赖
  9. RUN npm ci
  10. # 复制源代码
  11. COPY . .
  12. # 构建应用
  13. RUN npm run build
  14. # 生产阶段
  15. FROM nginx:alpine AS production
  16. # 复制nginx配置
  17. COPY docker/nginx.conf /etc/nginx/nginx.conf
  18. # 从构建阶段复制构建结果
  19. COPY --from=build /app/dist /usr/share/nginx/html
  20. # 暴露端口
  21. EXPOSE 80
  22. # 启动nginx
  23. CMD ["nginx", "-g", "daemon off;"]
复制代码
  1. # docker/nginx.conf
  2. worker_processes  1;
  3. events {
  4.     worker_connections  1024;
  5. }
  6. http {
  7.     include       /etc/nginx/mime.types;
  8.     default_type  application/octet-stream;
  9.     sendfile        on;
  10.     keepalive_timeout  65;
  11.     server {
  12.         listen 80;
  13.         server_name localhost;
  14.         root /usr/share/nginx/html;
  15.         index index.html index.htm;
  16.         location / {
  17.             try_files $uri $uri/ /index.html;
  18.         }
  19.         # API代理配置
  20.         location /api/ {
  21.             proxy_pass http://backend:3000/;
  22.             proxy_set_header Host $host;
  23.             proxy_set_header X-Real-IP $remote_addr;
  24.             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  25.             proxy_set_header X-Forwarded-Proto $scheme;
  26.         }
  27.         # 静态资源缓存
  28.         location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
  29.             expires 1y;
  30.             add_header Cache-Control "public, immutable";
  31.         }
  32.         error_page   500 502 503 504  /50x.html;
  33.         location = /50x.html {
  34.             root   /usr/share/nginx/html;
  35.         }
  36.     }
  37. }
复制代码

环境配置与部署策略

1. 多环境配置:
  1. // vite.config.js
  2. import { defineConfig, loadEnv } from 'vite'
  3. import vue from '@vitejs/plugin-vue'
  4. import path from 'path'
  5. export default defineConfig(({ mode }) => {
  6.   // 加载环境变量
  7.   const env = loadEnv(mode, process.cwd(), '')
  8.   
  9.   return {
  10.     plugins: [vue()],
  11.     resolve: {
  12.       alias: {
  13.         '@': path.resolve(__dirname, 'src')
  14.       }
  15.     },
  16.     // 根据环境配置不同的构建选项
  17.     build: {
  18.       minify: mode === 'production' ? 'esbuild' : false,
  19.       sourcemap: mode !== 'production',
  20.       rollupOptions: {
  21.         output: {
  22.           // 生产环境使用文件名哈希
  23.           entryFileNames: mode === 'production' ? 'assets/[name].[hash].js' : 'assets/[name].js',
  24.           chunkFileNames: mode === 'production' ? 'assets/[name].[hash].js' : 'assets/[name].js',
  25.           assetFileNames: mode === 'production' ? 'assets/[name].[hash].[ext]' : 'assets/[name].[ext]'
  26.         }
  27.       }
  28.     },
  29.     // 环境变量前缀
  30.     envPrefix: 'VITE_'
  31.   }
  32. })
复制代码

1. 蓝绿部署策略:
  1. # .github/workflows/blue-green-deploy.yml
  2. name: Blue-Green Deploy
  3. on:
  4.   push:
  5.     branches: [ main ]
  6. jobs:
  7.   build:
  8.     runs-on: ubuntu-latest
  9.    
  10.     steps:
  11.     - name: Checkout code
  12.       uses: actions/checkout@v3
  13.       
  14.     - name: Setup Node.js
  15.       uses: actions/setup-node@v3
  16.       with:
  17.         node-version: '16'
  18.         cache: 'npm'
  19.         
  20.     - name: Install dependencies
  21.       run: npm ci
  22.       
  23.     - name: Build application
  24.       run: npm run build
  25.       env:
  26.         VITE_API_BASE_URL: ${{ secrets.PROD_API_BASE_URL }}
  27.         
  28.     - name: Upload build artifacts
  29.       uses: actions/upload-artifact@v3
  30.       with:
  31.         name: dist
  32.         path: dist/
  33.   deploy:
  34.     needs: build
  35.     runs-on: ubuntu-latest
  36.     environment: production
  37.    
  38.     steps:
  39.     - name: Download build artifacts
  40.       uses: actions/download-artifact@v3
  41.       with:
  42.         name: dist
  43.         path: dist/
  44.         
  45.     - name: Deploy to staging environment (blue)
  46.       run: |
  47.         # 这里替换为实际的部署命令
  48.         # 例如:scp -r dist/* user@staging-server:/var/www/blue/
  49.         
  50.     - name: Run smoke tests on blue environment
  51.       run: |
  52.         # 运行冒烟测试验证蓝色环境
  53.         # 例如:npm run test:smoke -- --baseUrl=https://blue.example.com
  54.         
  55.     - name: Switch traffic to blue environment
  56.       run: |
  57.         # 切换流量到蓝色环境
  58.         # 例如:使用负载均衡器API或DNS切换
  59.         
  60.     - name: Run full tests on production
  61.       run: |
  62.         # 运行完整的生产环境测试
  63.         # 例如:npm run test:e2e -- --baseUrl=https://example.com
  64.         
  65.     - name: Finalize deployment
  66.       run: |
  67.         # 部署成功,将蓝色环境标记为生产环境
  68.         # 可以更新标签或配置
复制代码

总结与展望

Vue3作为现代前端框架的代表,其强大的功能和灵活的架构为构建可维护、高性能的应用提供了坚实基础。通过本文介绍的代码规范与最佳实践,开发者可以:

1. 构建结构清晰的项目:合理的项目结构和组件设计是可维护项目的基础。
2. 编写高质量的代码:遵循代码规范、使用Composition API、合理设计Props和Emits,可以显著提高代码质量。
3. 优化应用性能:通过组件优化、列表渲染优化和资源加载优化,可以提升用户体验。
4. 加强团队协作:统一的代码规范、Git工作流和代码审查流程,有助于团队高效协作。
5. 确保应用稳定性:完善的测试策略和持续集成/部署流程,可以确保应用的稳定性和可靠性。

构建结构清晰的项目:合理的项目结构和组件设计是可维护项目的基础。

编写高质量的代码:遵循代码规范、使用Composition API、合理设计Props和Emits,可以显著提高代码质量。

优化应用性能:通过组件优化、列表渲染优化和资源加载优化,可以提升用户体验。

加强团队协作:统一的代码规范、Git工作流和代码审查流程,有助于团队高效协作。

确保应用稳定性:完善的测试策略和持续集成/部署流程,可以确保应用的稳定性和可靠性。

随着Vue生态系统的发展,我们期待看到更多创新和改进。作为前端开发者,持续学习和实践这些最佳实践,将有助于我们构建更加优秀的产品,提升用户体验,并为团队带来更高的效率。

通过遵循本文提供的指南,开发者可以成为更加优秀的前端工程师,为团队和项目带来更大的价值。记住,优秀的代码不仅在于它能工作,更在于它易于理解、维护和扩展。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

手机版|联系我们|小黑屋|TG频道|RSS |网站地图

Powered by Pixtech

© 2025-2026 Pixtech Team.

>