vue开发风格指南

255 阅读6分钟

前言

任何风格指南都不会完美适用于所有的团队或项目。因此建议开发者根据过去的经验、周边的技术栈或个人价值观来做出有意识的调整。

以下为 基于 Vue风格指南 的个人整理

规则类别

  1. 优先级A:必要的
  2. 优先级B:强烈推荐
  3. 优先级C:推荐
  4. 优先级D:谨慎使用

必要的(规避错误)

1. 组件名为多个单词

组件名应由多个单词组成,除了跟组件App

/* bad */
export default {
  name: 'Todo',
  // ...
}

/* good */
export default {
  name: 'TodoItem',
  // ...
}

2. prop定义应尽量详细

prop 定义应尽量详细,至少指定其类型

/* bad */
props: ['status']

/* good */
props: {
    status: String
}
/* better */
props: {
    status: {
        type: String,
        required: true,
        ...
    }
}

3. 为 v-for 设置 key

添加唯一key值,优化diff算法,维护内部组件及其子树的状态

/* 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>

4. 避免 v-ifv-for 一起使用

v-ifv-for 优先级高,可能会出问题

/* bad */
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :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>

5. 为组件样式设置作用域

为样式设置scope,或者CSS Modules,或者一个专属前缀(类似BEM的策略),可最大限度避免样式冲突

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

/* good */
// scope
<button class="btn btn-close">×</button>
<style scoped>
.btn-close {
  background-color: red;
}
</style>

// CSS modules
<button :class="[$style.buttonClose]">×</button>
<style module>
.buttonClose {
  background-color: red;
}
</style>

// BEM约定
<button class="c-Button--close">×</button>
<style>
.c-Button--close {
  background-color: red;
}
</style>

6. 私有 property 名称

使用模块作用域确保外部无法访问到私有函数。如果无法做到,则应添加前缀$_并附带命名空间($_yourPluginName),避免冲突

/* bad */
const myGreatMixin = {
  // ...
  methods: {
    // update() {
    // _update() {
    // $update() {
    $_update() {
      // ...
    }
  }
}

/* good */
const myGreatMixin = {
  // ...
  methods: {
    $_myGreatMixin_update() {
      // ...
    }
  }
}
/* better */
const myGreatMixin = {
  // ...
  methods: {
    publicMethod() {
      // ...
      myPrivateFunction()
    }
  }
}
function myPrivateFunction() {
  // ...
}
export default myGreatMixin

强烈推荐(增强代码可读性)

1. 组件文件

尽量把每个组件单独分成文件,更加细化

/* bad */
app.component('TodoList', {
  // ...
})
app.component('TodoItem', {
  // ...
})

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

2. 单文件组件文件大小写

要么始终单词大写开头(PascalCase),要么始终横线链接(kebab-case)

/* bad */
components/
|- mycomponent.vue
|- myComponent.vue

/* good */
components/
|- MyComponent.vue
|- my-component.vue

3. 基础组件名称

特定样式和基础组件(展示类、无逻辑或无状态的组件),应全部以一个特定前缀开头,比如 BaseAppV

/* bad */
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue

/* good */
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue

4. 单例组件名称

只应该拥有单个活跃实例的组件应该以 The 前缀命名,以示其唯一性。每个页面只能使用一次,不接受prop

/* bad */
components/
|- Heading.vue
|- MySidebar.vue

/* good */
components/
|- TheHeading.vue
|- TheSidebar.vue

5. 紧密耦合的组件名称

与父组件紧密耦合的子组件应该以父组件名作为前缀命名

/* bad */
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue

/* good */
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue

6. 组件名称中的单词顺序

组件名称应该以高阶的(通常时一般化描述的)单词开头,并以描述性的修饰词结尾

/* 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

7. 自闭合组件

在单文件组件、字符串模板和 JSX 中,有内容的组件应该时自闭合的,但在 DOM 模板里永远不要这样做

/* bad */
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
<!-- 在 DOM 模板中 -->
<my-component/>

/* good */
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>

8. 模板中组件名称大小写

大多数项目来说,单文件组件和字符串模板中组件名应该始终是 PascalCase 的,但是在DOM模板中是 kebab-case

/* bad */
<!-- 在单文件组件和字符串模板中 -->
<mycomponent/>
<myComponent/>
<!-- 在 DOM 模板中 -->
<MyComponent></MyComponent>

/* good */
<!-- 在单文件组件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
/* 或者 */
<!-- 在所有地方 -->
<my-component></my-component>

9. JS/JSX中使用的组件名称

JS/JSX中使用的组件名称应该始终是 PascalCase 的。在较为简单的项目中,使用 app.component 进行全局组件注册时,可使用 kebab-case

/* bad */
app.component('myComponent', {
  // ...
})
import myComponent from './MyComponent.vue'
export default {
    // name: 'myComponent',
    name: 'my-component',
}

/* good */
app.component('MyComponent', {
  // ...
})
app.component('my-component', {
  // ...
})
import MyComponent from './MyComponent.vue'
export default {
    name: 'MyComponent',
}

10. 完整单词的组件名称

组件名称应倾向于完整单词,而不是缩写

/* bad */
components/
| - SdSettings.vue
| - UProfOpts.vue

/* good */
components/
| - StudentDashboardSettings.vue
| - UserProfileOptions.vue

11. prop命名

声明 prop 时,命名应该始终使用 camelCase,而在模板和JSX中应该始终使用 kebab-case

/* bad */
props: {
  'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>

/* good */
props: {
  greetingText: String
}
<WelcomeMessage greeting-text="hi"/>

12. 多个 attribute 的元素

多个 attribute 的元素应该分多行撰写,每个 attribute 一行

/* 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"
/>

13. 模板中的简单表达式

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

/* bad */
{{
  fullName.split(' ').map((word) => {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}

/* good */
// 在模板中
{{ normalizedFullName }}
// 复杂表达式已经转为了一个计算属性
computed: {
  normalizedFullName() {
    return this.fullName.split(' ')
      .map(word => word[0].toUpperCase() + word.slice(1))
      .join(' ')
  }
}

14. 简单的计算属性

应该把复杂的计算属性尽可能多地分割为更简单的计算属性

/* bad */
computed: {
  price() {
    const basePrice = this.manufactureCost / (1 - this.profitMargin)
    return (
      basePrice -
      basePrice * (this.discountPercent || 0)
    )
  }
}

/* good */
computed: {
  basePrice() {
    return this.manufactureCost / (1 - this.profitMargin)
  },
  discount() {
    return this.basePrice * (this.discountPercent || 0)
  },
  finalPrice() {
    return this.basePrice - this.discount
  }
}

15. 带引号的 attribute 值

非空 HTML attribute 的值应该始终带有引号(单引号或双引号,选择未在js里面使用的那个)

/* bad */
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>

/* good */
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">

16. 指令缩写

v-on => @v-bind => :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>
    <h1>Here's some contact info</h1>
</template>

/* good */
<input
  :value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  @input="onInput"
  @focus="onFocus"
>
<template v-slot:header>
    <h1>Here might be a page title</h1>
</template>
<template v-slot:footer>
    <h1>Here's some contact info</h1>
</template>

推荐(将选择和认知成本最小化)

1. 组件/实例选项的顺序

组件/实例的选项应该有统一的顺序

  1. 全局感知 (要求在组件以外被感知)

    • name
  2. 模板编译选项 (改变模板编译的方式)

    • compilerOptions
  3. 模板依赖 (模板内使用的资源)

    • components
    • directives
  4. 组合 (合并 property 至选项内)

    • extends
    • mixins
    • provide/inject
  5. 接口 (组件的接口)

    • inheritAttrs
    • props
    • emits
  6. 组合式 API (使用组合式 API 的入口点)

    • setup
  7. 本地状态 (本地的响应式 property)

    • data
    • computed
  8. 事件 (通过响应式事件触发的回调)

    • watch

    • 生命周期事件 (按照它们被调用的顺序)

      • beforeCreate
      • created
      • beforeMount
      • mounted
      • beforeUpdate
      • updated
      • activated
      • deactivated
      • beforeUnmount
      • unmounted
      • errorCaptured
      • renderTracked
      • renderTriggered
  9. 非响应式的 property (不依赖响应性系统的实例 property)

    • methods
  10. 渲染 (组件输出的声明式描述)

2. 元素 attribute 的顺序

元素 (包括组件) 的 attribute 应该有统一的顺序

  1. 定义 (提供组件的选项)

    • is
  2. 列表渲染 (创建相同元素的多个变体)

    • v-for
  3. 条件 (元素是否渲染/显示)

    • v-if
    • v-else-if
    • v-else
    • v-show
    • v-cloak
  4. 渲染修饰符 (改变元素的渲染方式)

    • v-pre
    • v-once
  5. 全局感知 (要求在组件以外被感知)

    • id
  6. 唯一性 Attribute (需要唯一值的 attribute)

    • ref
    • key
  7. 双向绑定 (结合了绑定与事件)

    • v-model
  8. 其他 Attribute (所有普通的、绑定或未绑定的 attribute)

  9. 事件 (组件事件监听器)

    • v-on
  10. 内容 (覆写元素的内容)

    • v-html
    • v-text

3. 组件/实例选项中的空行

多个 property 之间增加一个空行,特别是这些选项很多的时候

/* good */
props: {
  value: {
    type: String,
    required: true
  },

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

  label: String,
  icon: String
},

computed: {
  formattedValue() {
    // ...
  },

  inputClasses() {
    // ...
  }
}

// 在组件仍然能够被轻松阅读与定位时,
// 没有空行也挺好
props: {
  value: {
    type: String,
    required: true
  },
  focused: {
    type: Boolean,
    default: false
  },
  label: String,
  icon: String
},
computed: {
  formattedValue() {
    // ...
  },
  inputClasses() {
    // ...
  }
}

4. 单文件组件的顶级元素顺序

单文件组件应始终保持 <script><template><style>标签的顺序一致,且 <style> 要放在最后

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

/* good */
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
// template 和 script也可调换顺序

谨慎使用(潜在风险)

1. scope 中的元素选择器

元素选择器应该避免在 scope 中出现

/* 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>

2. 隐性的父子组件通信

应该优先通过 prop 和事件进行父子组件之间的通信,而不是通过 this.$parent 或对 prop 做出变更

/* bad */
app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  template: '<input v-model="todo.text">'
})

app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  methods: {
    removeTodo() {
      this.$parent.todos = this.$parent.todos.filter(todo => todo.id !== vm.todo.id)
    }
  },

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

/* good */
app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  emits: ['input'],

  template: `
    <input
      :value="todo.text"
      @input="$emit('input', $event.target.value)"
    >
  `
})

app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  emits: ['delete'],

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

3. 非 Flux 的全局状态管理

应该优先通过 Vuex 管理全局状态,而不是通过 this.$root 或一个全局事件总线

/* bad */
// main.js
import { createApp } from 'vue'
import mitt from 'mitt'
const app = createApp({
  data() {
    return {
      todos: [],
      emitter: mitt()
    }
  },

  created() {
    this.emitter.on('remove-todo', this.removeTodo)
  },

  methods: {
    removeTodo(todo) {
      const todoIdToRemove = todo.id
      this.todos = this.todos.filter(todo => todo.id !== todoIdToRemove)
    }
  }
})

/* good */
// store/modules/todos.js
export default {
  state: {
    list: []
  },

  mutations: {
    REMOVE_TODO (state, todoId) {
      state.list = state.list.filter(todo => todo.id !== todoId)
    }
  },

  actions: {
    removeTodo ({ commit, state }, todo) {
      commit('REMOVE_TODO', todo.id)
    }
  }
}
<!-- TodoItem.vue -->
<template>
  <span>
    {{ todo.text }}
    <button @click="removeTodo(todo)">
      X
    </button>
  </span>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  methods: mapActions(['removeTodo'])
}
</script>