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 学习资源。