mcpField
V
vue-component-anatomy

vue-component-anatomy

github.com/basmilius开发092 次安装2 天前更新
代码仓库

关于 vue-component-anatomy

为 Vue 3.5+ 单文件组件提供组织与编写规范的技能,面向维护 Vue 组件的开发者。它涵盖单个 .vue 文件的区块顺序、宏、脚本、模板和样式,且不依赖特定组件库。

主要功能

  1. 01规定 SFC 区块与宏的顺序
  2. 02规范脚本声明与函数组织
  3. 03提供 props、emits 与 model 类型约定
  4. 04说明模板与样式写法
  5. 05统一空白行与排版规则

适用场景

  1. 01新建 Vue 单文件组件
  2. 02整理组件脚本与宏声明
  3. 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> 的顺序

从上到下排列代码主体,确保状态在使用前声明:

  1. 导入
  2. 宏(emits / models / props / slots)- 参见第 2 节
  3. 模块常量(ALL_CAPS:配置值、选项列表、查找映射)
  4. store(use*Store() - 共享状态)
  5. 响应式状态(ref、reactive、shallowRef 和模板 ref)
  6. 组合式函数(useRouter、useI18n、其他 use* 行为辅助函数)
  7. computed
  8. watch / watchEffect
  9. 生命周期钩子(onMounted、onUnmounted、...)
  10. 函数声明(事件处理函数和辅助函数)
  11. 最后是 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,**不要手动创建 modelValue prop,再配合 update:modelValue emit: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