Skip to content

Vue.js 最佳实践

代码组织

1. 使用组合式 API

vue
<!-- ✅ 推荐:组合式 API -->
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const doubleCount = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<!-- ❌ 不推荐:选项式 API(除非维护旧项目) -->
<script>
export default {
  data() {
    return { count: 0 }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  }
}
</script>

2. 组件命名

vue
<!-- ✅ 推荐:PascalCase -->
<script setup>
import UserProfile from './UserProfile.vue'
import TodoList from './TodoList.vue'
</script>

<template>
  <UserProfile />
  <TodoList />
</template>

<!-- ❌ 不推荐:kebab-case 或单词 -->
<template>
  <user-profile />
  <profile />
</template>

3. 文件结构

src/
├── assets/          # 静态资源
├── components/      # 通用组件
│   ├── common/      # 基础组件
│   ├── layout/      # 布局组件
│   └── business/    # 业务组件
├── composables/     # 组合式函数
├── stores/          # Pinia stores
├── router/          # 路由配置
├── views/           # 页面组件
├── utils/           # 工具函数
├── api/             # API 接口
├── types/           # TypeScript 类型
└── App.vue

组件设计

1. 单一职责原则

vue
<!-- ✅ 推荐:职责单一 -->
<script setup>
// UserAvatar.vue - 只负责显示头像
defineProps({
  src: String,
  size: { type: String, default: 'medium' }
})
</script>

<!-- ❌ 不推荐:职责过多 -->
<script setup>
// UserCard.vue - 包含太多功能
// 头像、个人信息、操作按钮、统计数据...
</script>

2. Props 验证

vue
<script setup>
// ✅ 推荐:完整的 props 验证
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value) => value >= 0
  },
  status: {
    type: String,
    default: 'pending',
    validator: (value) => ['pending', 'success', 'error'].includes(value)
  }
})

// ❌ 不推荐:没有验证
defineProps(['title', 'count', 'status'])
</script>

3. 事件命名

vue
<script setup>
// ✅ 推荐:使用 kebab-case
const emit = defineEmits(['update:modelValue', 'item-selected', 'form-submit'])

// ❌ 不推荐:使用 camelCase
const emit = defineEmits(['updateModelValue', 'itemSelected'])
</script>

响应式数据

1. 使用 ref 和 reactive

vue
<script setup>
import { ref, reactive } from 'vue'

// ✅ 推荐:基本类型用 ref
const count = ref(0)
const message = ref('Hello')

// ✅ 推荐:对象用 reactive
const user = reactive({
  name: '张三',
  age: 25
})

// ❌ 不推荐:对象用 ref(需要 .value.property)
const user = ref({
  name: '张三',
  age: 25
})
// user.value.name = '李四'  // 繁琐
</script>

2. 避免直接修改 props

vue
<script setup>
const props = defineProps(['count'])

// ❌ 不推荐:直接修改 props
function increment() {
  props.count++  // 错误!
}

// ✅ 推荐:使用本地状态
const localCount = ref(props.count)
function increment() {
  localCount.value++
}

// ✅ 推荐:触发事件
const emit = defineEmits(['update:count'])
function increment() {
  emit('update:count', props.count + 1)
}
</script>

计算属性和侦听器

1. 计算属性应该是纯函数

vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([1, 2, 3, 4, 5])

// ✅ 推荐:纯函数,无副作用
const evenItems = computed(() => {
  return items.value.filter(item => item % 2 === 0)
})

// ❌ 不推荐:有副作用
const evenItems = computed(() => {
  console.log('Computing...')  // 副作用
  someGlobalVariable = 123     // 副作用
  return items.value.filter(item => item % 2 === 0)
})
</script>

2. 避免在侦听器中修改被侦听的值

vue
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

// ❌ 不推荐:可能导致无限循环
watch(count, (newValue) => {
  count.value = newValue + 1  // 危险!
})

// ✅ 推荐:修改其他值
const doubleCount = ref(0)
watch(count, (newValue) => {
  doubleCount.value = newValue * 2
})
</script>

模板最佳实践

1. 使用 v-for 时提供 key

vue
<template>
  <!-- ✅ 推荐:使用唯一 key -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- ❌ 不推荐:使用索引作为 key(列表会变化时) -->
  <div v-for="(item, index) in items" :key="index">
    {{ item.name }}
  </div>
</template>

2. 避免 v-if 和 v-for 同时使用

vue
<template>
  <!-- ❌ 不推荐:v-if 和 v-for 在同一元素 -->
  <div v-for="item in items" v-if="item.isActive" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- ✅ 推荐:使用计算属性过滤 -->
  <div v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

<script setup>
import { computed } from 'vue'

const activeItems = computed(() => {
  return items.value.filter(item => item.isActive)
})
</script>

3. 使用简洁的模板表达式

vue
<template>
  <!-- ❌ 不推荐:复杂的模板表达式 -->
  <div>
    {{ user.firstName + ' ' + user.lastName + ' (' + user.age + ' years old)' }}
  </div>
  
  <!-- ✅ 推荐:使用计算属性 -->
  <div>{{ userDisplayName }}</div>
</template>

<script setup>
import { computed } from 'vue'

const userDisplayName = computed(() => {
  return `${user.firstName} ${user.lastName} (${user.age} years old)`
})
</script>

性能优化

1. 使用 v-show vs v-if

vue
<template>
  <!-- ✅ 频繁切换使用 v-show -->
  <div v-show="isVisible">频繁切换的内容</div>
  
  <!-- ✅ 很少改变使用 v-if -->
  <div v-if="isLoggedIn">登录后才显示</div>
</template>

2. 懒加载组件

vue
<script setup>
import { defineAsyncComponent } from 'vue'

// ✅ 推荐:懒加载大型组件
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// ❌ 不推荐:同步导入所有组件
import HeavyComponent from './HeavyComponent.vue'
</script>

3. 使用 v-memo

vue
<template>
  <!-- ✅ 缓存不常变化的列表项 -->
  <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
    <!-- 只有 item.selected 变化时才重新渲染 -->
    {{ item.name }}
  </div>
</template>

组合式函数

1. 命名约定

javascript
// ✅ 推荐:use 开头
export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

// ❌ 不推荐:没有 use 前缀
export function counter() {
  // ...
}

2. 返回响应式对象

javascript
// ✅ 推荐:返回 ref
export function useCounter() {
  const count = ref(0)
  return { count }
}

// ❌ 不推荐:返回普通值
export function useCounter() {
  let count = 0
  return { count }  // 不是响应式的
}

错误处理

1. 全局错误处理

javascript
// main.js
app.config.errorHandler = (err, instance, info) => {
  console.error('Global error:', err)
  // 发送到错误监控服务
}

2. 组件错误边界

vue
<script setup>
import { onErrorCaptured } from 'vue'

onErrorCaptured((err, instance, info) => {
  console.error('Component error:', err)
  // 返回 false 阻止错误继续传播
  return false
})
</script>

TypeScript 最佳实践

1. 为 props 定义类型

vue
<script setup lang="ts">
interface Props {
  title: string
  count?: number
  items: Array<{ id: number; name: string }>
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

2. 为 emits 定义类型

vue
<script setup lang="ts">
interface Emits {
  (e: 'update:modelValue', value: string): void
  (e: 'submit', data: FormData): void
}

const emit = defineEmits<Emits>()
</script>

样式最佳实践

1. 使用 scoped 样式

vue
<style scoped>
/* ✅ 推荐:scoped 样式不会影响其他组件 */
.button {
  background-color: blue;
}
</style>

2. 使用 CSS 变量

vue
<style scoped>
.component {
  --primary-color: #42b983;
  --spacing: 16px;
  
  color: var(--primary-color);
  padding: var(--spacing);
}
</style>

3. 避免深度选择器滥用

vue
<style scoped>
/* ❌ 不推荐:过度使用深度选择器 */
:deep(.child .grandchild .great-grandchild) {
  color: red;
}

/* ✅ 推荐:在子组件中定义样式 */
.child {
  color: red;
}
</style>

测试最佳实践

1. 测试用户行为

javascript
// ✅ 推荐:测试用户行为
it('should increment count when button is clicked', async () => {
  const wrapper = mount(Counter)
  await wrapper.find('button').trigger('click')
  expect(wrapper.text()).toContain('Count: 1')
})

// ❌ 不推荐:测试实现细节
it('should call increment method', () => {
  const wrapper = mount(Counter)
  const spy = vi.spyOn(wrapper.vm, 'increment')
  expect(spy).toHaveBeenCalled()
})

2. 使用测试 ID

vue
<template>
  <button data-testid="submit-btn">提交</button>
</template>
javascript
it('should submit form', async () => {
  const wrapper = mount(Form)
  await wrapper.find('[data-testid="submit-btn"]').trigger('click')
  // ...
})

安全最佳实践

1. 避免 XSS 攻击

vue
<template>
  <!-- ✅ 推荐:自动转义 -->
  <div>{{ userInput }}</div>
  
  <!-- ❌ 危险:v-html 可能导致 XSS -->
  <div v-html="userInput"></div>
  
  <!-- ✅ 如果必须使用 v-html,先清理内容 -->
  <div v-html="sanitizedInput"></div>
</template>

<script setup>
import DOMPurify from 'dompurify'

const sanitizedInput = computed(() => {
  return DOMPurify.sanitize(userInput.value)
})
</script>

2. 环境变量安全

javascript
// ❌ 不推荐:在客户端暴露敏感信息
const API_KEY = 'secret-key-123'

// ✅ 推荐:敏感信息放在服务端
// 客户端只使用公开的环境变量
const API_URL = import.meta.env.VITE_API_URL

代码审查清单

组件

  • ✅ 组件名称使用 PascalCase
  • ✅ Props 有完整的类型和验证
  • ✅ 事件使用 kebab-case 命名
  • ✅ 组件职责单一
  • ✅ 使用 scoped 样式

性能

  • ✅ 大型组件使用懒加载
  • ✅ 列表使用唯一 key
  • ✅ 避免 v-if 和 v-for 同时使用
  • ✅ 合理使用计算属性缓存
  • ✅ 图片使用懒加载

代码质量

  • ✅ 没有 console.log
  • ✅ 没有未使用的变量
  • ✅ 代码格式统一
  • ✅ 有必要的注释
  • ✅ 通过 ESLint 检查

测试

  • ✅ 关键功能有测试覆盖
  • ✅ 测试用户行为而非实现
  • ✅ 测试边界情况
  • ✅ Mock 外部依赖

总结

  • 使用组合式 API 和 <script setup>
  • 遵循命名约定和文件结构
  • Props 验证和类型定义
  • 合理使用计算属性和侦听器
  • 注意性能优化
  • 编写可测试的代码
  • 注意安全问题
  • 保持代码简洁和可维护

下一步

最后学习 学习资源,了解更多 Vue 学习资源。