vue编码风格指南

16 阅读10分钟

vue官方提供了一些在使用vue开发时的编码风格,有助于写出符合团队协作,便于阅读的代码。详见

按照重要程度分成四个等级

必须遵守

组件名称使用多个单词组合

因为所有 HTML 元素都是一个单词,所以自己定义的组件名称使用多个单词,可以避免与现在或是未来的 HTML 元素发生冲突。

<!-- Bad -->
<Item />
<item></item>

<!-- Good -->
<TodoItem />
<todo-item></todo-item>

定义 props 要详细

在组件中,定义 prop 要尽可能的详细,比如提供类型,默认值或者校验等。

// Bad
const props = defineProps(['status'])

// Good
const props = defineProps({
  status: String
})

// very good
const props = defineProps({
  status: {
    type: String,
    required: true,
    validator: (value) => {
      return ['syncing', 'synced', 'version-conflict', 'error'].includes(
        value
      )
    }
  }
})

模板中 v-for 必须指定 key

需要指定 key,且保证 key 唯一。

加上key可以提高dom更新,可以避免删除中间某一项时引发的问题。

如果不加, eslint 会给出提示。

<!-- Bad -->
<ul>
  <li v-for="todo in todos">
    {{ todo.text }}
  </li>
</ul>

<!-- Good -->
<ul>
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

避免 v-if 和 v-for 用在同一个元素上

在 vue2 中,如果 v-for 和 v-if 同时使用, v-for 的优先级更高一些。

在 vue3 中, v-if 优先级高于 v-for, 如果两个同时使用,就会导致报错, 比如下面的例子,在 vue3 中, v-if 条件使用到了 item, 但是此时 item 还不存在,导致报错。

<ul>
  <li
    v-for="item in items"
    v-if="item.status !== 'disabled'"
    :key="item.file_id"
  >
    {{ item.file_name }}
  </li>
</ul>

如果遇到了要同时使用 v-if 和 v-for 的场景,可以通过其他方式优化

比如上面的例子, 可以通过计算属性先将不符合条件的过滤掉。

有些情况可以通过调整 dom 结构将两者分布到不同的 dom 元素上。


<!-- Bad -->
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

<!-- Good -->
<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

<!-- Good -->
<ul>
  <template v-for="user in users" :key="user.id">
    <li v-if="user.isActive">
      {{ user.name }}
    </li>
  </template>
</ul>

使用组件范围的样式

在组件内部的样式应该只作用于当前组件, 这样可以避免样式冲突引起的问题,同时当有些地方需要覆盖该组件的样式的时候,可以精准的,最小影响的进行覆盖。

<!-- Bad -->
<template>
  <button class="btn btn-close">×</button>
</template>

<style>
.btn-close {
  background-color: red;
}
</style>
<!-- Good -->
<template>
  <button class="button button-close">×</button>
</template>

<style scoped>
.button {
  border: none;
  border-radius: 2px;
}
.button-close {
  background-color: red;
}
</style>

<template>
  <button :class="[$style.button, $style.buttonClose]">×</button>
</template>

<style module>
.button {
  border: none;
  border-radius: 2px;
}
.buttonClose {
  background-color: red;
}
</style>

<template>
  <button class="c-Button c-Button--close">×</button>
</template>

<!-- Using the BEM convention -->
<style>
.c-Button {
  border: none;
  border-radius: 2px;
}
.c-Button--close {
  background-color: red;
}
</style>

强烈推荐

使用单文件组件

每个组件都应该在单独的文件中。


// Bad
app.component('TodoList', {
  // ...
})

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

// Good
components/
|- TodoList.js
|- TodoItem.js

components/
|- TodoList.vue
|- TodoItem.vue

文件名使用统一格式命名

这个在公司中一般也会要求,

比如都使用 大驼峰(PascalCase)命名, 或者都使用短横线(kebab-case)命名

不要两者混用, 推荐使用 大驼峰方式。

<!-- Bad -->
components/
|- mycomponent.vue

components/
|- myComponent.vue


<!-- Good -->
components/
|- MyComponent.vue

components/
|- my-component.vue

组件名以特定前缀开头

一些基础组件或者通用组件可以按照此规则命名。

<!-- 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

紧耦合组件名称

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

如果一个组件只在一个父组件的上下文中有意义,那么这种关系应该在它的名称中很明显。由于编辑器通常按字母顺序组织文件,这也使这些相关文件彼此相邻。


<!-- 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

组件名称中的单词顺序

组件名称应该以最高级(通常是最通用的)单词开头,以描述性修饰词结尾。

<!-- 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

使用自闭合组件

在单文件组件、字符串模板和JSX中,没有内容的组件应该是自闭的——但在in- dom模板中绝不是这样。

自封闭的组件不仅没有内容,而且注定没有内容。这是书中的空白页和标有“此页故意留白”的页之间的区别。如果没有不必要的结束标记,代码也会更干净。

不幸的是,HTML不允许自定义元素自闭——只允许官方的“void”元素。这就是为什么只有当Vue的模板编译器能够在DOM之前到达模板,然后提供符合DOM规范的HTML时,这种策略才可行。

<!-- Bad -->

<!-- In Single-File Components, string templates, and JSX -->
<MyComponent></MyComponent>

<!-- In in-DOM templates -->
<my-component/>


<!-- Good -->

<!-- In Single-File Components, string templates, and JSX -->
<MyComponent/>

<!-- In in-DOM templates -->
<my-component></my-component>

模板中的组件名称大小写

在大多数项目中,单文件组件和字符串模板中的组件名应该总是PascalCase,而在dom模板中应该是kebab-case。

与kebab-case相比,PascalCase有几个优势:

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

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

还要注意,如果您已经在kebab-case方面投入了大量资金,那么与HTML惯例保持一致并能够在所有项目中使用相同的大小写可能比上面列出的优势更重要。在这些情况下,到处使用kebab-case也是可以接受的。


Bad
<!-- In Single-File Components and string templates -->
<mycomponent/>

<!-- In Single-File Components and string templates -->
<myComponent/>

<!-- In in-DOM templates -->
<MyComponent></MyComponent>

Good

<!-- In Single-File Components and string templates -->
<MyComponent/>

<!-- In in-DOM templates -->
<my-component></my-component>

<!-- Everywhere -->
<my-component></my-component>

JS/JSX中的组件名称大小写

JS/JSX中的组件名应该始终是PascalCase,尽管对于只通过app.component使用全局组件注册的简单应用程序来说,它们可能在字符串中是kebar -case。


// Bad
app.component('myComponent', {
  // ...
})

import myComponent from './MyComponent.vue'
export default {
  name: 'myComponent'
  // ...
}
export default {
  name: 'my-component'
  // ...
}

// Good
app.component('MyComponent', {
  // ...
})
app.component('my-component', {
  // ...
})

import MyComponent from './MyComponent.vue'

export default {
  name: 'MyComponent'
  // ...
}

组件名称要使用完整的单词

组件名称应该使用全称而不是缩写,特别要避免使用不常见的缩写

全称可以让我们更容易通过名称了解组件功能。

Bad
components/
|- SdSettings.vue
|- UProfOpts.vue

Good
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue

prop的大小写

在声明期间,prop 名应该始终使用小驼峰(camelCase)。

当在 in-DOM 中使用时,使用 kebab-case风格

单文件组件模板和JSX可以使用 kebab-case 或 camelCase 方式。

同样,整个项目应该使用统一的规则,两种风格不能混用。

Bad
const props = defineProps({
  'greeting-text': String
})

// for in-DOM templates
<welcome-message greetingText="hi"></welcome-message>

Good
const props = defineProps({
  greetingText: String
})

// SFC file
<WelcomeMessage greeting-text="hi"/>
// or
<WelcomeMessage greetingText="hi"/>

// for in-DOM templates
<welcome-message greeting-text="hi"></welcome-message>

多属性的元素换行

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

可以通过格式化工具来完成。


Bad
<img src="https://vuejs.org/images/logo.png" alt="Vue Logo">

<MyComponent foo="a" bar="b" baz="c"/>

Good
<img
  src="https://vuejs.org/images/logo.png"
  alt="Vue Logo"
>

<MyComponent
  foo="a"
  bar="b"
  baz="c"
/>

模板中只应该有简单表达式

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

模板中复杂的表达式不利于阅读。

// Bad
{{
  fullName.split(' ').map((word) => {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}
<!-- Good -->

<!-- In a template -->
{{ normalizedFullName }}


// The complex expression has been moved to a computed property
const normalizedFullName = computed(() =>
  fullName.value
    .split(' ')
    .map((word) => word[0].toUpperCase() + word.slice(1))
    .join(' ')
)

计算属性尽量简单

复杂的计算属性应该被分割成尽可能多的简单属性。

// Bad
const price = computed(() => {
  const basePrice = manufactureCost.value / (1 - profitMargin.value)
  return basePrice - basePrice * (discountPercent.value || 0)
})
// Good
const basePrice = computed(
  () => manufactureCost.value / (1 - profitMargin.value)
)
const discount = computed(
  () => basePrice.value * (discountPercent.value || 0)
)
const finalPrice = computed(() => basePrice.value - discount.value)

引用属性值

非空的HTML属性值应该总是在引号内(单引号或双引号,JS中不使用的那种)。

虽然不带空格的属性值在HTML中不需要加引号,但这种做法通常会导致避免使用空格,从而降低属性值的可读性。

<!-- Bad -->
<input type=text>

<AppSidebar :style={width:sidebarWidth+'px'}>


<!-- Good -->
<input type="text">

<AppSidebar :style="{ width: sidebarWidth + 'px' }">

指令简写

指令简写(对于v-bind::,对于v-on: @,对于v-slot: #)应该总是或从不使用。

<!-- Bad -->
<input
  v-bind:value="newTodoText"
  :placeholder="newTodoInstructions"
>

<input
  v-on:input="onInput"
  @focus="onFocus"
>

<!-- 两种插槽写法都用不推荐 -->
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>
<!-- Good -->
<input
  :value="newTodoText"
  :placeholder="newTodoInstructions"
>

<input
  v-bind:value="newTodoText"
  v-bind:placeholder="newTodoInstructions"
>

<input
  @input="onInput"
  @focus="onFocus"
>

<input
  v-on:input="onInput"
  v-on:focus="onFocus"
>


<!-- 都是用非简写 -->
<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>

<!-- 都是用简写 -->
<template #header>
  <h1>Here might be a page title</h1>
</template>
<template #footer>
  <p>Here's some contact info</p>
</template>

一般推荐

当存在多个同样好的选择时,可以做出任意选择以确保一致性。在这些规则中,我们描述每个可接受的选项,并建议一个默认选择。这意味着您可以自由地在自己的代码库中做出不同的选择,只要您保持一致并且有充分的理由。不过,请给出一个充分的理由!通过适应社区标准,您将:

组件/实例选项顺序

组件/实例选项的排序应该一致。

这是我们为组件选项推荐的默认顺序。它们被分成不同的类别,这样你就知道在哪里添加插件的新属性。

  1. Global Awareness (requires knowledge beyond the component)

    • name
  2. Template Compiler Options (changes the way templates are compiled)

    • compilerOptions
  3. Template Dependencies (assets used in the template)

    • components
    • directives
  4. Composition (merges properties into the options)

    • extends
    • mixins
    • provide/inject
  5. Interface (the interface to the component)

    • inheritAttrs
    • props
    • emits
  6. Composition API (the entry point for using the Composition API)

    • setup
  7. Local State (local reactive properties)

    • data
    • computed

Events (callbacks triggered by reactive events)

  • watch

  • Lifecycle Events (in the order they are called)

    • beforeCreate
    • created
    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • activated
    • deactivated
    • beforeUnmount
    • unmounted
    • errorCaptured
    • renderTracked
    • renderTriggered
  1. Non-Reactive Properties (instance properties independent of the reactivity system)

    • methods
  2. Rendering (the declarative description of the component output)

    • template/render

元素属性顺序

元素(包括组件)的属性应该保持一致。

这是我们为组件选项推荐的默认顺序。它们被分成不同的类别,这样你就知道在哪里添加自定义属性和指令。

  1. Definition (provides the component options)

    • is
  2. List Rendering (creates multiple variations of the same element)

    • v-for
  3. Conditionals (whether the element is rendered/shown)

    • v-if
    • v-else-if
    • v-else
    • v-show
    • v-cloak
  4. Render Modifiers (changes the way the element renders)

    • v-pre
    • v-once
  5. Global Awareness (requires knowledge beyond the component)

    • id
  6. Unique Attributes (attributes that require unique values)

    • ref
    • key
  7. Two-Way Binding (combining binding and events)

    • v-model
  8. Other Attributes (all unspecified bound & unbound attributes)

  9. Events (component event listeners)

    • v-on
  10. Content (overrides the content of the element)

    • v-html
    • v-text

组件/实例选项中的空行

您可能希望在多行属性之间添加一个空行,特别是如果不滚动这些选项就无法在屏幕上显示时。

当组件开始感到狭窄或难以阅读时,在多行属性之间添加空格可以使它们更容易再次浏览。在一些编辑器(如Vim)中,这样的格式化选项也可以使它们更容易使用键盘导航。

// Bad
defineProps({
  value: {
    type: String,
    required: true
  },
  focused: {
    type: Boolean,
    default: false
  },
  label: String,
  icon: String
})
const formattedValue = computed(() => {
  // ...
})
const inputClasses = computed(() => {
  // ...
})
// Good

defineProps({
  value: {
    type: String,
    required: true
  },

  focused: {
    type: Boolean,
    default: false
  },

  label: String,
  icon: String
})

const formattedValue = computed(() => {
  // ...
})

const inputClasses = computed(() => {
  // ...
})

单文件组件顶级元素顺序

单文件组件应该始终按照<script><template><style>标签的顺序排列,并将<style>放在最后,因为其他两个标签中至少有一个是必需的。

// Bad
<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>

// Bad
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
// Good
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>


// Good
<!-- ComponentA.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

谨慎使用

Vue的一些特性是为了适应罕见的边缘情况或从遗留代码库平稳迁移而存在的。但是,如果过度使用,它们会使代码更难维护,甚至成为bug的来源。这些规则揭示了潜在的风险特征,描述了何时以及为什么应该避免这些特征。

具有作用域的元素选择器

元素选择器应该避免使用作用域。

在作用域样式中,首选类选择器而不是元素选择器,因为大量的元素选择器很慢。

Bad

<template>
  <button>×</button>
</template>

<style scoped>
button {
  background-color: red;
}
</style>

Good

<template>
  <button class="btn btn-close">×</button>
</template>

<style scoped>
.btn-close {
  background-color: red;
}
</style>

隐性通信

对于父子组件通信,应该首选 Props 和事件,而不是this.$parentmutating props

理想的Vue应用程序是向下支持props,向上支持事件。坚持这种约定会使您的组件更容易理解。然而,也有一些边缘情况比如mutating props或者this.$parent可以简化两个已经深度耦合的组件。

Bad

<script setup>
defineProps({
  todo: {
    type: Object,
    required: true
  }
})
</script>

<template>
  <input v-model="todo.text" />
</template>
<script setup>
import { getCurrentInstance } from 'vue'

const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

const instance = getCurrentInstance()

function removeTodo() {
  const parent = instance.parent
  if (!parent) return

  parent.props.todos = parent.props.todos.filter((todo) => {
    return todo.id !== props.todo.id
  })
}
</script>

<template>
  <span>
    {{ todo.text }}
    <button @click="removeTodo">×</button>
  </span>
</template>

Good

<script setup>
defineProps({
  todo: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['input'])
</script>

<template>
  <input :value="todo.text" @input="emit('input', $event.target.value)" />
</template>
<script setup>
defineProps({
  todo: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['delete'])
</script>

<template>
  <span>
    {{ todo.text }}
    <button @click="emit('delete')">×</button>
  </span>
</template>