Vue3.0带来了组合式API,使得组件逻辑可以按照功能维度自由组织,但自由度过高也会带来维护负担。官方风格指南把规范分成四个优先级:必要、强烈推荐、推荐、谨慎使用。实际项目中不用逐条背诵,抓住几个高频冲突点就能显著改善代码可读性。本文从命名规范、Props与事件约束、组合式API取舍三个角度,结合常见问题给出可以直接落地的建议。

一、组件命名与单文件组件结构规范
组件名应该始终由多个单词组成,根组件App和Vue内置组件除外。这么做主要是为了避免与现有或未来的HTML元素冲突,因为HTML元素都是单个单词。例如使用TodoItem而不是Todo,使用UserCard而不是Card。在单文件组件中,文件名建议采用PascalCase,模板中使用时可以用PascalCase或kebab-case,但在DOM模板里必须使用kebab-case。
对于项目中的公用基础组件,可以统一使用特定前缀,让它们在文件树和代码检索中更容易被识别。比如按钮、输入框、弹窗这类组件分别命名为BaseButton、BaseInput、BaseModal。单例组件通常每个页面只使用一次,例如站点头部、侧边栏,推荐使用The前缀,如TheHeader、TheSidebar。与父组件紧密耦合的子组件,则建议以父组件名开头,例如TodoList的子项命名为TodoListItem,子项中的编辑组件命名为TodoListItemEditor。
单文件组件内部结构也有推荐顺序。常规顺序是先写<template>,再写<script>,最后写<style>。使用<script setup>时,可以在同一文件中补充一个普通<script>块来导出组件名称等选项。样式建议使用scoped特性限制作用域,避免样式污染。下面是一个符合规范的组件骨架示例:
<template>
<div class="todo-item">
<span class="todo-item__title">{{ todo.title }}</span>
<button type="button" @click="$emit('remove')">删除</button>
</div>
</template>
<script setup>
defineProps({
todo: {
type: Object,
required: true
}
})
defineEmits(['remove'])
</script>
<style scoped>
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
这里组件名显示为TodoItem,文件名对应TodoItem.vue。脚本中通过defineProps和defineEmits显式声明组件对外接口,这比隐式使用属性更利于维护。模板中事件名使用@click="$emit('remove')",父组件中监听时写成@remove,保持事件名与DOM事件风格的差异,后续小节会继续展开。
二、Props、事件与模板表达式的风格约束
Props定义时优先使用对象语法而不是数组语法。对象语法可以同时声明类型、默认值、必填性和自定义校验,让组件接口一目了然。数组语法虽然简洁,但时间一长就很难判断某个prop到底接受什么类型,后续改动也容易破坏调用方。下面两种写法中,第一种仅适合快速原型,正式项目建议统一使用第二种。
// 不推荐:数组语法
props: ['title', 'count', 'status']
// 推荐:对象语法
props: {
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
status: {
type: String,
default: 'pending',
validator(value) {
return ['pending', 'done', 'failed'].includes(value)
}
}
}
使用<script setup>时,defineProps也能接收同样的对象配置。如果使用TypeScript,可以在泛型中定义类型,但运行时校验仍然建议保留。对于复杂对象和数组类型的prop,默认值需要通过工厂函数返回,避免多个组件实例共享同一个引用。例如default: () => []而不是default: []。
自定义事件推荐使用kebab-case命名。虽然JavaScript中声明事件时通常写成驼峰形式,但在模板中监听时HTML属性会被浏览器自动转为小写,如果事件名为updateValue,DOM模板中写成@update-value才能正确匹配。为了消除大小写歧义,声明和监听统一使用kebab-case最稳妥。例如组件内部emit('update-value', value),父组件中写成@update-value="handleUpdate"。
模板中应该避免出现复杂的JavaScript表达式。模板的作用是描述结构,不适合承载业务逻辑。如果出现嵌套三元运算、长条件链或复杂计算,应该提取到计算属性或方法中。比如下面的写法虽然能运行,但可读性很差:
<template>
<div>
<span>{{ user.status === 'active' ? (user.role === 'admin' ? '管理员在线' : '用户在线') : '离线' }}</span>
<ul>
<li v-for="item in list.filter(i => i.visible).slice(0, 5)" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
改成计算属性后,逻辑集中且可测试:
import { computed } from 'vue'
const props = defineProps({
user: Object,
list: Array
})
const statusText = computed(() => {
if (props.user.status !== 'active') return '离线'
return props.user.role === 'admin' ? '管理员在线' : '用户在线'
})
const visibleList = computed(() =>
props.list.filter(item => item.visible).slice(0, 5)
)
另外v-for必须始终配合key使用。key帮助Vue精准复用和重排DOM元素,缺少key时同类型节点可能被错误复用,导致状态串位。key值优先使用唯一稳定ID,避免使用索引,因为列表增删时索引不稳定。对于只渲染一次的静态列表,使用索引影响不大,但涉及输入框、动画或子组件状态时,索引会导致错乱。
三、组合式API中的响应式选择与常见问题解答
组合式API提供了ref和reactive两种响应式入口。官方风格指南没有强制要求使用哪一种,但社区实践更倾向于用ref处理基础类型和需要重新赋值的变量,用reactive处理一组相关状态。一个常见误区是在reactive对象上解构属性,解构会丢失响应式连接。下面这段代码中title不会跟着更新:
import { reactive } from 'vue'
const state = reactive({
title: '初始标题',
count: 0
})
// 错误:解构后丢失响应性
const { title, count } = state
正确的做法是使用toRefs保持响应式,或者直接使用多个ref组织状态。对于需要整体替换对象的情况,reactive也不适合,因为重新赋值会切断引用。此时用ref包一层对象更灵活。
监听器方面,watch和watchEffect的选择经常让人困惑。watchEffect会自动收集同步执行期间访问到的响应式依赖,适合不需要显式指定来源、只关心副作用的场景。watch更明确,可以拿到旧值和新值,适合需要比较变化前后状态的场景。混用不会出错,但团队内部建议约定:默认使用watch,只有简单副作用或需要自动收集依赖时才用watchEffect。
常见问题中,组件名冲突是一个高频问题。如果两个组件都命名为Button,在模板解析时可能出现不可预期的引用。解决方式是通过多单词命名和目录约定区分,必要时在导入时使用as重命名。响应式丢失则通常发生在解构reactive、传递props时直接解构、或使用全局变量未声明为响应式。另一个需要注意的是,不要在computed内部执行副作用,比如修改其他响应式状态或发起请求,这会让数据流变得难以追踪。计算属性应当是无副作用的纯函数。
生命周期钩子在<script setup>中直接使用即可,但注册顺序不影响执行顺序,执行始终由组件实例生命周期决定。建议把钩子放在相关逻辑附近,而不是集中放在顶部或底部。提取可组合函数时,函数命名以use开头,例如useFetchData、useUserStore,这样既符合社区习惯,也便于区分普通工具函数和包含响应式状态的逻辑单元。

