知海

强烈推荐规则

Vue风格指南

强烈推荐规则 {#priority-b-rules-strongly-recommended}

这些规则被证明在大多数项目中可以改善可读性和/或开发者体验。即使违反它们,你的代码仍然可以运行,但违规情况应该很少见,并且需要有充分的理由。

组件文件 {#component-files}

只要构建系统可用于拼接文件,每个组件都应放在单独的文件中。

这有助于你在需要编辑组件或查看其用法时更快地找到它。

Bad

js 复制代码
app.component('TodoList', {
  // ...
})

app.component('TodoItem', {
  // ...
})

Good

复制代码
components/
|- TodoList.js
|- TodoItem.js
复制代码
components/
|- TodoList.vue
|- TodoItem.vue

单文件组件文件名大小写 {#single-file-component-filename-casing}

单文件组件的文件名应该始终使用 PascalCase 或始终使用 kebab-case。

PascalCase 在代码编辑器中最适合自动补全,因为它与我们在 JS(X) 和模板中引用组件的方式保持一致。然而,混合大小写的文件名有时会在大小写不敏感的文件系统上引发问题,因此 kebab-case 也完全可以接受。

Bad

复制代码
components/
|- mycomponent.vue
复制代码
components/
|- myComponent.vue

Good

复制代码
components/
|- MyComponent.vue
复制代码
components/
|- my-component.vue

基础组件名称 {#base-component-names}

基础组件(即展示型、无状态或纯组件)用于应用特定样式和约定时,应全部以特定前缀开头,例如 BaseAppV

::: details 详细说明
这些组件为应用程序中一致的样式和行为奠定了基础。它们可能包含:

  • HTML 元素,
  • 其他基础组件,以及
  • 第三方 UI 组件。

但永远不会包含全局状态(例如来自 Pinia store 的状态)。

它们的名称通常包含其包装的元素名称(例如 BaseButtonBaseTable),除非没有适合其特定目的的元素(例如 BaseIcon)。如果你针对更具体的情境构建类似组件,这些组件几乎总是会使用这些基础组件(例如 BaseButton 可能被用在 ButtonSubmit 中)。

这个约定的一些优点:

  • 在编辑器中按字母顺序排列时,应用的基础组件会列在一起,更容易识别。

  • 由于组件名称应始终是多词的,此约定可避免你为简单组件包装器(例如 MyButtonVueButton)选择任意前缀。

  • 由于这些组件使用非常频繁,你可能希望直接将它们设为全局组件,而不是到处导入。使用前缀可以让这一点在 Vite 中实现:

    js 复制代码
    const modules = import.meta.glob('./src/**/Base*.vue', { eager: true })
    for (const path in modules) {
      const config = modules[path].default
      const name = config.name || path.match(/Base[A-Z]\w+/)[0]
      app.component(name, config)
    }

Bad

复制代码
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue

Good

复制代码
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
复制代码
components/
|- AppButton.vue
|- AppTable.vue
|- AppIcon.vue
复制代码
components/
|- VButton.vue
|- VTable.vue
|- VIcon.vue

紧耦合组件名称 {#tightly-coupled-component-names}

与父组件紧密耦合的子组件应包含父组件名称作为前缀。

如果某个组件仅在单个父组件的上下文中才有意义,那么这种关系应该从其名称中体现出来。由于编辑器通常按字母顺序组织文件,这也能让这些相关文件彼此相邻。 details 详细说明
你可能会试图通过将子组件嵌套在以其父组件命名的目录中来解决这个问题。例如:

复制代码
components/
|- TodoList/
   |- Item/
      |- index.vue
      |- Button.vue
   |- index.vue

或:

复制代码
components/
|- TodoList/
   |- Item/
      |- Button.vue
   |- Item.vue
|- TodoList.vue

不推荐这样做,因为它会导致:

  • 许多文件名相似,使代码编辑器中快速切换文件更加困难。
  • 许多嵌套子目录,增加了在编辑器侧边栏中浏览组件的时间。

Bad

复制代码
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
复制代码
components/
|- SearchSidebar.vue
|- NavigationForSearchSidebar.vue

Good

复制代码
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
复制代码
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue

组件名称中的单词顺序 {#order-of-words-in-component-names}

组件名称应以最高层级(通常是最一般的)单词开头,并以描述性修饰词结尾。 details 详细说明
你可能会疑惑:

“为什么我们要强制组件名称使用不那么自然的语言?”

在自然英语中,形容词和其他描述词通常出现在名词之前,而例外情况则使用连接词。例如:

  • 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.vue 组件)使得在代码编辑器中快速导航到特定组件更加困难。
  • 重构变得更加困难,因为查找替换通常不足以及时更新对已移动组件的相对引用。

Bad

复制代码
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue

Good

复制代码
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue

自闭合组件 {#self-closing-components}

没有内容的组件应在单文件组件、字符串模板和 JSX 中自闭合——但绝不能在 DOM 内模板中自闭合。

自闭合的组件不仅表明它们没有内容,还表明它们有意没有内容。这就像书中一张空白页和一张标有“此页有意留白”的区别。没有不必要的闭合标签,代码也更干净。

不幸的是,HTML 不允许自定义元素自闭合——只有官方的“void 元素”可以。因此,这种策略只有在 Vue 的模板编译器能够在 DOM 之前访问模板并输出符合 DOM 规范的 HTML 时才可能实现。

Bad

vue-html 复制代码
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
vue-html 复制代码
<!-- 在 DOM 内模板中 -->
<my-component/>

Good

vue-html 复制代码
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>
vue-html 复制代码
<!-- 在 DOM 内模板中 -->
<my-component></my-component>

模板中组件名称大小写 {#component-name-casing-in-templates}

在大多数项目中,组件名称在单文件组件和字符串模板中应始终使用 PascalCase——但在 DOM 内模板中应使用 kebab-case。

PascalCase 相比 kebab-case 有几个优点:

  • 编辑器可以在模板中自动补全组件名称,因为 JavaScript 中也使用 PascalCase。
  • <MyComponent> 与单词型 HTML 元素的视觉区别比 <my-component> 更明显,因为有两个字符差异(两个大写字母),而不仅仅是一个(连字符)。
  • 如果你在模板中使用任何非 Vue 自定义元素(例如 Web Component),PascalCase 可以确保你的 Vue 组件保持明显的可见性。

不幸的是,由于 HTML 对大小写不敏感,DOM 内模板必须使用 kebab-case。

另请注意,如果你已经在 kebab-case 上投入了大量精力,那么与 HTML 约定保持一致并在所有项目中使用相同的大小写可能比上述优点更重要。在这种情况下,全程使用 kebab-case 也是可以接受的。

Bad

vue-html 复制代码
<!-- 在单文件组件和字符串模板中 -->
<mycomponent/>
vue-html 复制代码
<!-- 在单文件组件和字符串模板中 -->
<myComponent/>
vue-html 复制代码
<!-- 在 DOM 内模板中 -->
<MyComponent></MyComponent>

Good

vue-html 复制代码
<!-- 在单文件组件和字符串模板中 -->
<MyComponent/>
vue-html 复制代码
<!-- 在 DOM 内模板中 -->
<my-component></my-component>

vue-html 复制代码
<!-- 所有位置 -->
<my-component></my-component>

JS/JSX 中组件名称大小写 {#component-name-casing-in-js-jsx}

JS/JSX 中的组件名称应始终使用 PascalCase,但在仅通过 app.component 使用全局组件注册的简单应用中,字符串内可以使用 kebab-case。 details 详细说明
在 JavaScript 中,PascalCase 是类和原型构造函数的约定——本质上,是任何可以有不同实例的东西。Vue 组件也有实例,因此使用 PascalCase 也很有意义。另一个好处是,在 JSX(和模板)中使用 PascalCase 可以让代码阅读者更容易区分组件和 HTML 元素。

然而,对于通过 app.component 使用全局组件定义的应用,我们建议改用 kebab-case。原因如下:

  • 全局组件很少在 JavaScript 中被引用,因此遵循 JavaScript 的约定意义不大。
  • 这些应用总是包含许多 DOM 内模板,其中必须使用 kebab-case

Bad

js 复制代码
app.component('myComponent', {
  // ...
})
js 复制代码
import myComponent from './MyComponent.vue'
js 复制代码
export default {
  name: 'myComponent'
  // ...
}
js 复制代码
export default {
  name: 'my-component'
  // ...
}

Good

js 复制代码
app.component('MyComponent', {
  // ...
})
js 复制代码
app.component('my-component', {
  // ...
})
js 复制代码
import MyComponent from './MyComponent.vue'
js 复制代码
export default {
  name: 'MyComponent'
  // ...
}

完整单词的组件名称 {#full-word-component-names}

组件名称应优先使用完整单词,而不是缩写。

编辑器中的自动补全功能让写更长的名字的成本很低,而它们带来的清晰度却是无价的。尤其应始终避免不常见的缩写。

Bad

复制代码
components/
|- SdSettings.vue
|- UProfOpts.vue

Good

复制代码
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue

Prop 名称大小写 {#prop-name-casing}

Prop 名称在声明时始终使用 camelCase。在 DOM 内模板中使用时,prop 应使用 kebab-case。单文件组件模板和 JSX 可以使用 kebab-case 或 camelCase 的 prop。大小写应保持一致——如果你选择使用 camelCase 的 prop,请确保在应用中不要使用 kebab-case 的 prop。

Bad

js 复制代码
props: {
  'greeting-text': String
}
js 复制代码
const props = defineProps({
  'greeting-text': String
})
vue-html 复制代码
// 对于 DOM 内模板
<welcome-message greetingText="hi"></welcome-message>

Good

js 复制代码
props: {
  greetingText: String
}
js 复制代码
const props = defineProps({
  greetingText: String
})
vue-html 复制代码
// 对于单文件组件 - 请确保项目中的大小写保持一致
// 你可以使用任何约定,但我们不建议混用两种不同的大小写风格
<WelcomeMessage greeting-text="hi"/>
// 或
<WelcomeMessage greetingText="hi"/>
vue-html 复制代码
// 对于 DOM 内模板
<welcome-message greeting-text="hi"></welcome-message>

多属性元素 {#multi-attribute-elements}

具有多个属性的元素应跨多行书写,每行一个属性。

在 JavaScript 中,将具有多个属性的对象跨多行书写被广泛认为是一个好约定,因为这样更容易阅读。我们的模板和 JSX 也应得到同样的考虑。

Bad

vue-html 复制代码
<img src="https://vuejs.org/images/logo.png" alt="Vue Logo">
vue-html 复制代码
<MyComponent foo="a" bar="b" baz="c"/>

Good

vue-html 复制代码
<img
  src="https://vuejs.org/images/logo.png"
  alt="Vue Logo"
>
vue-html 复制代码
<MyComponent
  foo="a"
  bar="b"
  baz="c"
/>

模板中的简单表达式 {#simple-expressions-in-templates}

组件模板应只包含简单的表达式,更复杂的表达式应重构为计算属性或方法。

模板中复杂的表达式会使其不够声明式。我们应该努力描述应该出现_什么_,而不是_如何_计算该值。计算属性和方法还允许代码被复用。

Bad

vue-html 复制代码
{{
  fullName.split(' ').map((word) => {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}

Good

vue-html 复制代码
<!-- 在模板中 -->
{{ normalizedFullName }}
js 复制代码
// 复杂表达式已被移到计算属性中
computed: {
  normalizedFullName() {
    return this.fullName.split(' ')
      .map(word => word[0].toUpperCase() + word.slice(1))
      .join(' ')
  }
}
js 复制代码
// 复杂表达式已被移到计算属性中
const normalizedFullName = computed(() =>
  fullName.value
    .split(' ')
    .map((word) => word[0].toUpperCase() + word.slice(1))
    .join(' ')
)

简单的计算属性 {#simple-computed-properties}

复杂的计算属性应尽可能拆分为多个更简单的属性。 details 详细说明
更简单、命名良好的计算属性:

  • 更容易测试

    当每个计算属性只包含一个非常简单的表达式,且依赖很少时,编写测试来确认其正确工作会容易得多。

  • 更容易阅读

    简化计算属性会迫使你为每个值提供一个描述性名称,即使它没有被复用。这使得其他开发人员(以及未来的你)更容易专注于他们关心的代码,并弄清楚发生了什么。

  • 更能适应不断变化的需求

    任何可以被命名的值都可能对视图有用。例如,我们可能决定显示一条消息,告诉用户他们节省了多少钱。我们也可能决定计算销售税,但也许将其单独显示,而不是作为最终价格的一部分。

    小而专注的计算属性对信息的使用方式做出更少的假设,因此在需求变化时需要的重构更少。

:::

Bad

js 复制代码
computed: {
  price() {
    const basePrice = this.manufactureCost / (1 - this.profitMargin)
    return (
      basePrice -
      basePrice * (this.discountPercent || 0)
    )
  }
}
js 复制代码
const price = computed(() => {
  const basePrice = manufactureCost.value / (1 - profitMargin.value)
  return basePrice - basePrice * (discountPercent.value || 0)
})

Good

js 复制代码
computed: {
  basePrice() {
    return this.manufactureCost / (1 - this.profitMargin)
  },

  discount() {
    return this.basePrice * (this.discountPercent || 0)
  },

  finalPrice() {
    return this.basePrice - this.discount
  }
}
js 复制代码
const basePrice = computed(
  () => manufactureCost.value / (1 - profitMargin.value)
)

const discount = computed(
  () => basePrice.value * (discountPercent.value || 0)
)

const finalPrice = computed(() => basePrice.value - discount.value)

带引号的属性值 {#quoted-attribute-values}

非空的 HTML 属性值应始终放在引号内(单引号或双引号,以 JS 中未使用的为准)。

虽然 HTML 中没有空格的属性值不需要引号,但这种做法通常会导致_避免_空格,使属性值更难读。

Bad

vue-html 复制代码
<input type=text>
vue-html 复制代码
<AppSidebar :style={width:sidebarWidth+'px'}>

Good

vue-html 复制代码
<input type="text">
vue-html 复制代码
<AppSidebar :style="{ width: sidebarWidth + 'px' }">

指令缩写 {#directive-shorthands}

指令缩写(: 代表 v-bind:@ 代表 v-on:# 代表 v-slot)应始终使用或始终不使用。

Bad

vue-html 复制代码
<input
  v-bind:value="newTodoText"
  :placeholder="newTodoInstructions"
>
vue-html 复制代码
<input
  v-on:input="onInput"
  @focus="onFocus"
>
vue-html 复制代码
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>

Good

vue-html 复制代码
<input
  :value="newTodoText"
  :placeholder="newTodoInstructions"
>
vue-html 复制代码
<input
  v-bind:value="newTodoText"
  v-bind:placeholder="newTodoInstructions"
>
vue-html 复制代码
<input
  @input="onInput"
  @focus="onFocus"
>
vue-html 复制代码
<input
  v-on:input="onInput"
  v-on:focus="onFocus"
>
vue-html 复制代码
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template v-slot:footer>
  <p>Here's some contact info</p>
</template>
vue-html 复制代码
<template #header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。