优先级 B 的规则:强烈推荐
组件的编写方式
- 必须使用 setup 语法糖编写组件,统一使用 TypeScript 编写。
- 组件不要直接放在
components
目录下,而是要根据独立性区分目录(App.vue
、Main.vue
除外),目录名始终是横线连接 (kebab-case),对应的专用类型接口放置在同目录下的types.ts
中。正例:
components/ |- todo-list/ | |- TodoList.vue | |- TodoListItem.vue | |- types.ts
- 复杂逻辑的功能适当拆成多文件组件,将部分方法拆成独立的 TS 文件,TS 文件名始终是横线连接 (kebab-case)
正例:
components/ |- todo-list/ | |- TodoList.vue | |- TodoListItem.vue | |- todo-list.js | |- types.ts
组件文件
只要有能够拼接文件的构建系统,就把每个组件单独分成文件。当你需要编辑一个组件或查阅一个组件的用法时,可以更快速的找到它。
正例:
components/ |- TodoList.vue |- TodoListItem.vue
文件和目录的命名规范
- 单文件组件的文件名应该始终是单词大写开头 (PascalCase)。
- 目录名始终是横线连接 (kebab-case)
- TS 文件名始终是横线连接 (kebab-case)
- 声明文件需要使用
.d.ts
结尾
正例:
components/
|- todo-list/
| |- TodoList.vue
| |- TodoListItem.vue
| |- todo-list.js
| |- types.ts
基础组件名
应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 Base
、App
或 V
。
详解:
这些组件为你的应用奠定了一致的基础样式和行为。它们可能只包括:
- HTML 元素
- 其它基础组件
- 第三方 UI 组件库
但是它们绝不会包括全局状态 (比如来自 Pinia)。
它们的名字通常包含所包裹元素的名字 (比如
BaseButton
、BaseTable
),除非没有现成的对应功能的元素 (比如BaseIcon
)。如果你为特定的上下文构建类似的组件,那它们几乎总会消费这些组件 ( 比如BaseButton
可能会用在ButtonSubmit
上)。这样做的几个好处:
- 当你在编辑器中以字母顺序排序时,你的应用的基础组件会全部列在一起,这样更容易识别。
- 因为组件名应该始终是多个单词,所以这样做可以避免你在包裹简单组件时随意选择前缀 (比如
MyButton
、VueButton
)。
反例:
MyButton.vue、VueTable.vue、Icon.vue
正例:
BaseButton.vue、BaseTable.vue、BaseIcon.vue、AppButton.vue、AppTable.vue、AppIcon.vue、VButton.vue、VTable.vue、VIcon.vue
单例组件名
只应该拥有单个活跃实例的组件应该以 The
前缀命名,以示其唯一性。
这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。如果你发现有必要添加 prop,那就表明这实际上是一个可复用的组件,只是目前在每个页面里只使用一次。
正例:
TheHeading.vue、TheSidebar.vue
反例:
Heading.vue、MySidebar.vue
紧密耦合的组件名
和父组件紧密耦合的子组件应该以父组件名作为前缀命名。
如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。
正例:
components/ |- TodoList.vue |- TodoListItem.vue |- TodoListItemButton.vue
反例:
components/ |- TodoList.vue |- TodoItem.vue |- TodoButton.vue
组件名中的单词顺序
组件名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。
详解
为什么我们给组件命名时不多遵从自然语言呢?
在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在组件名里包含这些连接词,但是单词的顺序很重要。
同样要注意在你的应用中所谓的“高级别”是跟语境有关的。比如对于一个带搜索表单的应用来说,它可能包含这样的组件:
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我们很难看出来哪些组件是针对搜索的。现在我们来根据规则给组件重新命名:
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因为编辑器通常会按字母顺序组织文件,所以现在组件之间的重要关系一目了然。
你可能想换成多级目录的方式,把所有的搜索组件放到“search”目录,把所有的设置组件放到“settings”目录。我们只推荐在非常大型 (如有 100+ 个组件) 的应用下才考虑这么做,因为:
- 在多级目录间找来找去,要比在单个
components
目录下滚动查找要花费更多的精力。 - 存在组件重名 (比如存在多个
ButtonDelete
组件) 的时候在编辑器里更难快速定位。 - 让重构变得更难,因为为一个移动了的组件更新相关引用时,查找/替换通常并不高效。
正例:
components/ |- SearchButtonClear.vue |- SearchButtonRun.vue |- SearchInputExcludeGlob.vue |- SearchInputQuery.vue |- SettingsCheckboxLaunchOnStartup.vue |- SettingsCheckboxTerms.vue
反例:
components/ |- ClearSearchButton.vue |- ExcludeFromSearchInput.vue |- LaunchOnStartupCheckbox.vue |- RunSearchButton.vue |- SearchInput.vue |- TermsCheckbox.vue
自闭合组件
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。
自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。
不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。
正例:
<!-- 在单文件组件、字符串模板和 JSX 中 --> <MyComponent /> <!-- 在 DOM 模板中 --> <MyComponent></MyComponent>
反例:
<!-- 在单文件组件、字符串模板和 JSX 中 --> <MyComponent></MyComponent> <!-- 在 DOM 模板中 --> <MyComponent />
模板中的组件名大小写
对于绝大多数项目来说,在单文件组件、字符串模板、DOM 模板中组件名应该总是 PascalCase 的。
正例:
<!-- 在所有地方 --> <MyComponent />
完整单词的组件名
组件名应该倾向于完整单词而不是缩写。
编辑器中的自动补全已经让书写长命名的代价非常之低了,而其带来的明确性却是非常宝贵的。不常用的缩写尤其应该避免。
正例:
StudentDashboardSettings.vue、UserProfileOptions.vue
反例:
SdSettings.vue、UProfOpts.vue
Prop 名大小写
在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX 中应该始终使用 kebab-case。
正例:
defineProps({ greetingText: String, });
<MyComponent greeting-text="hi" />
反例:
defineProps({ greetingText: String, });
<MyComponent greetingText="hi" />
模板中简单的表达式
组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。
复杂表达式会让你的模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。
正例:
<!-- 在模板中 --> {{ normalizedFullName }}
// 复杂表达式已经移入一个计算属性 const normalizedFullName = computed(() => { return this.fullName .split(' ') .map(function (word) { return word[0].toUpperCase() + word.slice(1); }) .join(' '); });
反例:
{ { fullName .split(' ') .map(function (word) { return word[0].toUpperCase() + word.slice(1); }) .join(' '); } }
带引号的 attribute 值
非空 HTML attribute 值应该始终带引号 (单引号或双引号,以 JS 中未使用的为准)。
在 HTML 中不带空格的 attribute 值是可以没有引号的,但这鼓励了大家在特征值里不写空格,导致可读性变差。
正例:
<input type="text" /> <app-sidebar :style="{ width: sidebarWidth + 'px' }" />
反例
<input type="text" /> <app-sidebar :style={ width: sidebarWidth + 'px' } />
指令缩写
指令缩写 (用 :
表示 v-bind:
、用 @
表示 v-on:
和用 #
表示 v-slot:
) 应该要么都用要么都不用。
Pinia状态管理的命名
- TS 文件统一放在
store
目录下,文件名为使用 store 的名字(比如:user.ts
、cart.ts
)。 - 对
defineStore()
的返回值的命名规则为:使用 store 的名字,同时以use
开头且以Store
结尾,其命名应该始终使用 camelCase(比如useUserStore
,useCartStore
,useProductStore
)。
正例:
store/
|- user.ts
export const useUserStore = defineStore('user', () => {
return {};
});