关于 vue-component-anatomy
为 Vue 3.5+ 单文件组件提供组织与编写规范的技能,面向维护 Vue 组件的开发者。它涵盖单个 .vue 文件的区块顺序、宏、脚本、模板和样式,且不依赖特定组件库。
主要功能
- 01规定 SFC 区块与宏的顺序
- 02规范脚本声明与函数组织
- 03提供 props、emits 与 model 类型约定
- 04说明模板与样式写法
- 05统一空白行与排版规则
适用场景
- 01新建 Vue 单文件组件
- 02整理组件脚本与宏声明
- 03为 props、事件和双向绑定确定写法
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ basmilius/ skills --skill vue-component-anatomy安装后在 Claude Code 中输入/vue-component-anatomy确认已加载。
详细介绍
Vue 组件结构
深入了解一个 Vue 3 单文件组件的内部构成。本文介绍单个 .vue 文件的结构:各区块的顺序、宏、脚本和样式。本文刻意不绑定任何库。若要了解如何将一个功能拆分到多个组件中,请参阅 vue-build-feature;若要选择 UI 组件(项目使用 Flux 时),请参阅 flux-ui;项目特定规则见仓库中的 CLAUDE.md / AGENTS.md。
除非仓库另有说明,否则假设使用 <script setup lang="ts"> 和 Vue 3.5+。
1. SFC 骨架与模板根节点
- 区块顺序: 先写
<template>,然后是<script setup lang="ts">,接着是<style>,最后可选地添加<i18n>区块。在整个代码库中保持一致。 <i18n>区块(使用 vue-i18n SFC 消息的项目)始终放在<style>之后。遵循现有组件的格式:编写前检查代码库使用的是<i18n lang="yaml">还是 JSON,并在所有地方使用相同的lang属性。- 模板中优先使用单个根元素: 这样可以让属性透传行为更可预测(
class、@click、v-bind="$attrs"会落在同一个节点上),而且许多<Transition>配置都要求单个根元素。Vue 3 允许使用片段(多个根节点),但应有意识地使用,而不是无意间采用:根节点多于一个时,$attrs不会自动继承,必须显式绑定。 - 将传送的子元素/覆盖层(对话框、工具提示)放在根元素内部,而不是作为它的同级元素,这样可以保持过渡和属性透传正常工作。
2. 宏的顺序(<script setup> 顶部)
先声明编译器宏,并按以下顺序排列,以便一眼看清组件的契约:
defineOptions(少见) -> defineEmits -> defineModel -> defineProps -> defineSlots。
defineExpose 是例外:它放在脚本最末尾(函数声明之后,见第 3 节),且仅当父组件需要通过 ref 访问句柄时才使用。它描述对外暴露的接口,因此应在所有要暴露的内容定义完毕后最后编写。
- 为每个宏指定类型;若使用类型参数更合适,就不要传入运行时选项对象。
3. <script setup> 的顺序
从上到下排列代码主体,确保状态在使用前声明:
- 导入
- 宏(emits / models / props / slots)- 参见第 2 节
- 模块常量(
ALL_CAPS:配置值、选项列表、查找映射) - store(
use*Store()- 共享状态) - 响应式状态(
ref、reactive、shallowRef和模板 ref) - 组合式函数(
useRouter、useI18n、其他use*行为辅助函数) - computed
- watch / watchEffect
- 生命周期钩子(
onMounted、onUnmounted、...) - 函数声明(事件处理函数和辅助函数)
- 最后是
defineExpose,仅当父组件需要通过 ref 访问句柄时使用
- 模块常量应放在宏之后,而不是之前。 宏是组件的公共契约,应先阅读;常量属于实现细节。需要
t()或其他响应式输入的值不是常量:应在第 7 步将其设为computed。 - store 放在响应式状态之前,组合式函数放在其后。 store 是共享状态,应与组件自身状态一起阅读;组合式函数提供行为或派生数据,并可能基于该状态构建。经验法则:来自 store 层的
use*Store()是 store,其他所有use*调用都是组合式函数。 - 模板 ref 使用
useTemplateRef('name')(Vue 3.5+),并与响应式状态放在一起;字符串参数应与模板中的ref="name"相匹配。 - **导入风格(别名还是相对路径)**遵循
vue-build-feature:跨层使用路径别名(@/...),仅同一功能中的同级文件使用相对路径。 - 将副作用(启动数据获取、设置标题)放在生命周期步骤中,不要散落在各个声明之间。
4. function 与箭头函数
- 具名处理器和辅助函数使用
function声明 (function onSubmit(): void { ... }):它们会提升,在模板中易于阅读,且堆栈跟踪中会显示名称。 - 箭头函数仅用于内联回调和闭包(监听器、
array.map、.filter、事件选项)。 - 生命周期函数体保持内联:
onMounted(async () => { ... })可将副作用放在实际执行的位置。只有在第二个调用方需要它(监听器、重试按钮、模板),或函数体超过几条语句时,才将它提取为具名函数——不要仅仅为了给它命名而提取。
5. Props 及其类型
- 使用泛型形式为 props 添加类型:
defineProps<{ readonly id: number }>()。 将字段标记为readonly——props 是不可变的。 - **默认值:**使用带默认值的响应式 props 解构:
const { size = 'md' } = defineProps<{ size?: Size }>()。解构后的绑定仍保持响应性;只解构需要默认值的 props。 - prop 和数据结构使用
type而非interface。仅在需要声明合并时使用interface(模块扩充、declare global)。 - 如果 props 类型只在
defineProps<{ ... }>()中使用,就将类型内联。仅在其他地方也会复用时(例如传给另一个泛型),才将它提取为具名导出的type Props。 - 如果 props 只在模板中读取,不要在
<script>中解构;仅当需要默认值或局部绑定时,才在脚本中解构。
6. Emits 和模型
- 为 emits 添加类型:
const emit = defineEmits<{ submit: [id: number]; cancel: [] }>()。 - **双向绑定使用
defineModel,**不要手动创建modelValueprop,再配合update:modelValueemit:const value = defineModel<string>()。具名模型:defineModel<number>('page')与父组件上的v-model:page配对。 - 模型默认值使用运行时选项对象(这是第 2 节的例外):
defineModel<string>({ default: '' })。为数组或对象模型提供工厂默认值,确保其值不会是undefined:defineModel<string[]>({ default: () => [] })。 - 让子组件保持简单:触发事件或写入模型;由父组件负责决策。
7. 模板约定
v-for始终要有稳定的:key(使用 id;当列表会重新排序时,不要使用数组索引)。- **模板中不要编写复杂逻辑。**将超出简单表达式的内容移入
computed或具名函数。 - 使用
v-model:x绑定具名模型;当父元素也处理事件时,在嵌套的交互元素上阻止事件冒泡(@click.stop)。
8. 样式
- 默认使用
<style module>($style.someClass),类名采用 camelCase。类名应以组件命名:基础类与组件名称一致($style.tagInput),子部分的类名为「基础类 + 后缀」($style.tagInputChip、$style.tagInputField),状态则在同一元素上使用独立的is*修饰符(.tagInput.isDisabled)。此前缀已隔离这些类,因此作用域样式基本多余;只有项目已有此做法时,才使用<style scoped>。 - 使用数组/对象绑定切换修饰符:
:class="[$style.tagInput, { [$style.isDisabled]: disabled }]"。 - 自定义/全局/深层选择器 CSS 是最后手段。 尽量精简,并留下一行注释说明为何无法使用模块方案。
- 覆盖组件库的内部样式(当 props、插槽和设计令牌确实无法实现需求时),应在自己的基础类下嵌套使用
:local(.pane)。从功能上说,它只是一个本地类引用;显式写出:local()是为了标明该类来自外部,并且是有意深入访问的。只有当应用和组件库使用相同的基于名称的 CSS 模块混淆方式时(例如,二者都通过@basmilius/vite-preset构建),这才会解析到组件库的类;仍须遵循上文的一行注释规则。 - 通过 CSS 自定义属性(设计令牌)设置主题,而不是覆盖其他组件的内部选择器。如果项目没有定义令牌,独立组件可以公开自己的
var(--x, fallback)属性,供使用者覆盖。
9. 空白行
<script setup>中顶层分组之间留一个空白行(导入、每个宏块、常量、存储、状态、组合式函数、计算属性、侦听器、生命周期)。 每个function声明之间也留一个空白行。- 同一分组中相邻的单行声明之间不留空白行(一组 refs 或组合式函数应连续排列)。
- 函数内部的逻辑步骤之间留一个空白行:守卫/提前返回代码块之后,以及设置步骤与后续操作之间。
- 模板中,逻辑上独立的元素组之间留一个空白行。
<template>、<script>、<style>和<i18n>块之间各留一个空白行。
相关技能
vue-build-feature- 将一项功能拆分到视图、多个组件、组合式函数、数据访问层和路由中。flux-ui- 项目使用 Flux 时,选择并组合组件。- 仓库中的
CLAUDE.md/AGENTS.md- 项目特定的样式、i18n 和工具规则;这些规则优先于此处的通用默认值。
评论
支持 Markdown