前言
任何风格指南都不会完美适用于所有的团队或项目。因此建议开发者根据过去的经验、周边的技术栈或个人价值观来做出有意识的调整。
以下为 基于 Vue风格指南 的个人整理
规则类别
- 优先级A:必要的
- 优先级B:强烈推荐
- 优先级C:推荐
- 优先级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-if
和 v-for
一起使用
v-if
比v-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. 基础组件名称
特定样式和基础组件(展示类、无逻辑或无状态的组件),应全部以一个特定前缀开头,比如
Base
、App
或V
/* 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. 组件/实例选项的顺序
组件/实例的选项应该有统一的顺序
-
全局感知 (要求在组件以外被感知)
name
-
模板编译选项 (改变模板编译的方式)
compilerOptions
-
模板依赖 (模板内使用的资源)
components
directives
-
组合 (合并 property 至选项内)
extends
mixins
provide
/inject
-
接口 (组件的接口)
inheritAttrs
props
emits
-
组合式 API (使用组合式 API 的入口点)
setup
-
本地状态 (本地的响应式 property)
data
computed
-
事件 (通过响应式事件触发的回调)
-
watch
-
生命周期事件 (按照它们被调用的顺序)
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeUnmount
unmounted
errorCaptured
renderTracked
renderTriggered
-
-
非响应式的 property (不依赖响应性系统的实例 property)
methods
-
渲染 (组件输出的声明式描述)
2. 元素 attribute
的顺序
元素 (包括组件) 的
attribute
应该有统一的顺序
-
定义 (提供组件的选项)
is
-
列表渲染 (创建相同元素的多个变体)
v-for
-
条件 (元素是否渲染/显示)
v-if
v-else-if
v-else
v-show
v-cloak
-
渲染修饰符 (改变元素的渲染方式)
v-pre
v-once
-
全局感知 (要求在组件以外被感知)
id
-
唯一性 Attribute (需要唯一值的 attribute)
ref
key
-
双向绑定 (结合了绑定与事件)
v-model
-
其他 Attribute (所有普通的、绑定或未绑定的 attribute)
-
事件 (组件事件监听器)
v-on
-
内容 (覆写元素的内容)
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>