title: 風格指南
type: style-guide
這里是官方的 Vue 特有代碼的風格指南。如果在工程中使用 Vue,為了回避錯誤、小糾結和反模式,該指南是份不錯的參考。不過我們也不確信風格指南的所有內容對于所有的團隊或工程都是理想的。所以根據過去的經驗、周圍的技術棧、個人價值觀做出有意義的偏差是可取的。
對于其絕大部分,我們也總體上避免就 JavaScript 或 HTML 的本身提出建議。我們不介意你是否使用分號或結尾的逗號。我們不介意你在 HTML 特性中使用單引號還是雙引號。不過當我們發現在 Vue 的情景下有幫助的特定模式時,也會存在例外。
不久之后,我們還會提供操作層面的技巧。有的時候你只需要遵守規則,而我們會盡可能向你展示如何使用 ESLint 及其它自動化程序把操作層面弄得更簡單。
最終,我們把所有的規則歸為了四個大類:
規則歸類
優先級 A:必要的
這些規則會幫你規避錯誤,所以學習并接受它們帶來的全部代價吧。這里面可能存在例外,但應該非常少,且只有你同時精通 JavaScript 和 Vue 才可以這樣做。
優先級 B:強烈推薦
這些規則能夠在絕大多數工程中改善可讀性和開發體驗。即使你違反了,代碼還是能照常運行,但例外應該盡可能少且有合理的理由。
優先級 C:推薦
當存在多個同樣好的選項,選任意一個都可以確保一致性。在這些規則里,我們描述了每個選項并建議一個默認的選擇。也就是說只要保持一致且理由充分,你可以隨意在你的代碼庫中做出不同的選擇。請務必給出一個好的理由!通過接受社區的標準,你將會:
- 訓練你的大腦,以便更容易的處理你在社區遇到的代碼;
- 不做修改就可以直接復制粘貼社區的代碼示例;
- 能夠經常招聘到和你編碼習慣相同的新人,至少跟 Vue 相關的東西是這樣的。
優先級 D:謹慎使用
有些 Vue 特性的存在是為了照顧極端情況或幫助老代碼的平穩遷移。當被過度使用時,這些特性會讓你的代碼難于維護甚至變成 bug 的來源。這些規則是為了給有潛在風險的特性敲個警鐘,并說明它們什么時候不應該使用以及為什么。
優先級 A 的規則:必要的 (規避錯誤)
組件名為多個單詞 <sup data-p="a">必要
組件名應該始終是多個單詞的,根組件 App
除外。
這樣做可以避免跟現有的以及未來的 HTML 元素相沖突,因為所有的 HTML 元素名稱都是單個單詞的。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
Vue.component('todo', {
// ...
})
export default {
name: 'Todo',
// ...
}
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
Vue.component('todo-item', {
// ...
})
export default {
name: 'TodoItem',
// ...
}
{% raw %}</div>{% endraw %}
組件數據 <sup data-p="a">必要
組件的 data
必須是一個函數。
當在組件中使用 data
屬性的時候 (除了 new Vue
外的任何地方),它的值必須是返回一個對象的函數。
此處:返回一個唯一的對象,不要和其他組件共用一個對象進行返回(這樣就不會處理同一個對象)
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
當 data
的值是一個對象時,它會在這個組件的所有實例之間共享。想象一下,假如一個 TodoList
組件的數據是這樣的:
data: {
listTitle: '',
todos: []
}
我們可能希望重用這個組件,允許用戶維護多個列表 (比如分為購物、心愿單、日常事務等)。這時就會產生問題。因為每個組件的實例都引用了相同的數據對象,更改其中一個列表的標題就會改變其它每一個列表的標題。增刪改一個待辦事項的時候也是如此。
取而代之的是,我們希望每個組件實例都管理其自己的數據。為了做到這一點,每個實例必須生成一個獨立的數據對象。在 JavaScript 中,在一個函數中返回這個對象就可以了:
data: function () {
return {
listTitle: '',
todos: []
}
}
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
Vue.component('some-comp', {
data: {
foo: 'bar'
}
})
export default {
data: {
foo: 'bar'
}
}
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
Vue.component('some-comp', {
data: function () {
return {
foo: 'bar'
}
}
})
// In a .vue file
export default {
data () {
return {
foo: 'bar'
}
}
}
// 在一個 Vue 的根實例上直接使用對象是可以的,
// 因為只存在一個這樣的實例。
new Vue({
data: {
foo: 'bar'
}
})
{% raw %}</div>{% endraw %}
Prop 定義 <sup data-p="a">必要
Prop 定義應該盡量詳細。
在你提交的代碼中,prop 的定義應該盡量詳細,至少需要指定其類型。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
細致的 prop 定義有兩個好處:
- 它們寫明了組件的 API,所以很容易看懂組件的用法;
- 在開發環境下,如果向一個組件提供格式不正確的 prop,Vue 將會告警,以幫助你捕獲潛在的錯誤來源。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
// 這樣做只有開發原型系統時可以接受
props: ['status']
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
props: {
status: String
}
// 更好的做法!
props: {
status: {
type: String,
required: true,
validator: function (value) {
return [
'syncing',
'synced',
'version-conflict',
'error'
].indexOf(value) !== -1
}
}
}
{% raw %}</div>{% endraw %}
為 v-for
設置鍵值 <sup data-p="a">必要
總是用 key
配合 v-for
。
在組件上總是必須用 key
配合 v-for
,以便維護內部組件及其子樹的狀態。甚至在元素上維護可預測的行為,比如動畫中的對象固化 (object constancy),也是一種好的做法。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
假設你有一個待辦事項列表:
data: function () {
return {
todos: [
{
id: 1,
text: '學習使用 v-for'
},
{
id: 2,
text: '學習使用 key'
}
]
}
}
然后你把它們按照字母順序排序。在更新 DOM 的時候,Vue 將會優化渲染把可能的 DOM 變動降到最低。即可能刪掉第一個待辦事項元素,然后把它重新加回到列表的最末尾。
這里的問題在于,不要刪除仍然會留在 DOM 中的元素。比如你想使用 <transition-group>
給列表加過渡動畫,或想在被渲染元素是 <input>
時保持聚焦。在這些情況下,為每一個項目添加一個唯一的鍵值 (比如 :key="todo.id"
) 將會讓 Vue 知道如何使行為更容易預測。
根據我們的經驗,最好始終添加一個唯一的鍵值,以便你和你的團隊永遠不必擔心這些極端情況。也在少數對性能有嚴格要求的情況下,為了避免對象固化,你可以刻意做一些非常規的處理。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<ul>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ul>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<ul>
<li
v-for="todo in todos"
:key="todo.id"
>
{{ todo.text }}
</li>
</ul>
{% raw %}</div>{% endraw %}
避免 v-if
和 v-for
用在一起 <sup data-p="a">必要
永遠不要把 v-if
和 v-for
同時用在同一個元素上。
一般我們在兩種常見的情況下會傾向于這樣做:
為了過濾一個列表中的項目 (比如
v-for="user in users" v-if="user.isActive"
)。在這種情形下,請將users
替換為一個計算屬性 (比如activeUsers
),讓其返回過濾后的列表。為了避免渲染本應該被隱藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。這種情形下,請將v-if
移動至容器元素上 (比如ul
,ol
)。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
當 Vue 處理指令時,v-for
比 v-if
具有更高的優先級,所以這個模板:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
將會經過如下運算:
this.users.map(function (user) {
if (user.isActive) {
return user.name
}
})
因此哪怕我們只渲染出一小部分用戶的元素,也得在每次重渲染的時候遍歷整個列表,不論活躍用戶是否發生了變化。
通過將其更換為在如下的一個計算屬性上遍歷:
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
我們將會獲得如下好處:
- 過濾后的列表只會在
users
數組發生相關變化時才被重新運算,過濾更高效。 - 使用
v-for="user in activeUsers"
之后,我們在渲染的時候只遍歷活躍用戶,渲染更高效。 - 解藕渲染層的邏輯,可維護性 (對邏輯的更改和擴展) 更強。
為了獲得同樣的好處,我們也可以把:
<ul>
<li
v-for="user in users"
v-if="shouldShowUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
更新為:
<ul v-if="shouldShowUsers">
<li
v-for="user in users"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
通過將 v-if
移動到容器元素,我們不會再對列表中的每個用戶檢查 shouldShowUsers
。取而代之的是,我們只檢查它一次,且不會在 shouldShowUsers
為否的時候運算 v-for
。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
<ul>
<li
v-for="user in users"
v-if="shouldShowUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
<ul v-if="shouldShowUsers">
<li
v-for="user in users"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
{% raw %}</div>{% endraw %}
為組件樣式設置作用域 <sup data-p="a">必要
對于應用來說,頂級 App
組件和布局組件中的樣式可以是全局的,但是其它所有組件都應該是有作用域的。
這條規則只和單文件組件有關。你不一定要使用 scoped
特性。設置作用域也可以通過 CSS Modules,那是一個基于 class 的類似 BEM 的策略,當然你也可以使用其它的庫或約定。
不管怎樣,對于組件庫,我們應該更傾向于選用基于 class 的策略而不是 scoped
特性。
這讓覆寫內部樣式更容易:使用了常人可理解的 class 名稱且沒有太高的選擇器優先級,而且不太會導致沖突。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
如果你和其他開發者一起開發一個大型工程,或有時引入三方 HTML/CSS (比如來自 Auth0),設置一致的作用域會確保你的樣式只會運用在它們想要作用的組件上。
不止要使用 scoped
特性,使用唯一的 class 名可以幫你確保那些三方庫的 CSS 不會運用在你自己的 HTML 上。比如許多工程都使用了 button
、btn
或 icon
class 名,所以即便你不使用類似 BEM 的策略,添加一個 app 專屬或組件專屬的前綴 (比如 ButtonClose-icon
) 也可以提供很多保護。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<template>
<button class="btn btn-close">X</button>
</template>
<style>
.btn-close {
background-color: red;
}
</style>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<template>
<button class="button button-close">X</button>
</template>
<!-- 使用 `scoped` 特性 -->
<style scoped>
.button {
border: none;
border-radius: 2px;
}
.button-close {
background-color: red;
}
</style>
<template>
<button :class="[$style.button, $style.buttonClose]">X</button>
</template>
<!-- 使用 CSS Modules -->
<style module>
.button {
border: none;
border-radius: 2px;
}
.buttonClose {
background-color: red;
}
</style>
<template>
<button class="c-Button c-Button--close">X</button>
</template>
<!-- 使用 BEM 約定 -->
<style>
.c-Button {
border: none;
border-radius: 2px;
}
.c-Button--close {
background-color: red;
}
</style>
{% raw %}</div>{% endraw %}
私有屬性名 <sup data-p="a">必要
在插件、混入等擴展中始終為自定義的私有屬性使用 $_
前綴。并附帶一個命名空間以回避和其它作者的沖突 (比如 $_yourPluginName_
)。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
Vue 使用 _
前綴來定義其自身的私有屬性,所以使用相同的前綴 (比如 _update
) 有覆寫實例屬性的風險。即便你檢查確認 Vue 當前版本沒有用到這個屬性名,也不能保證和將來的版本沒有沖突。
對于 $
前綴來說,其在 Vue 生態系統中的目的是暴露給用戶的一個特殊的實例屬性,所以把它用于私有屬性并不合適。
不過,我們推薦把這兩個前綴結合為 $_
,作為一個用戶定義的私有屬性的約定,以確保不會和 Vue 自身相沖突。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
var myGreatMixin = {
// ...
methods: {
update: function () {
// ...
}
}
}
var myGreatMixin = {
// ...
methods: {
_update: function () {
// ...
}
}
}
var myGreatMixin = {
// ...
methods: {
$update: function () {
// ...
}
}
}
var myGreatMixin = {
// ...
methods: {
$_update: function () {
// ...
}
}
}
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
var myGreatMixin = {
// ...
methods: {
$_myGreatMixin_update: function () {
// ...
}
}
}
{% raw %}</div>{% endraw %}
優先級 B 的規則:強烈推薦 (增強可讀性)
組件文件 <sup data-p="b">強烈推薦
只要有能夠拼接文件的構建系統,就把每個組件單獨分成文件。
當你需要編輯一個組件或查閱一個組件的用法時,可以更快速的找到它。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
Vue.component('TodoList', {
// ...
})
Vue.component('TodoItem', {
// ...
})
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- TodoList.js
|- TodoItem.js
components/
|- TodoList.vue
|- TodoItem.vue
{% raw %}</div>{% endraw %}
單文件組件文件的大小寫 <sup data-p="b">強烈推薦
單文件組件的文件名應該要么始終是單詞大寫開頭 (PascalCase),要么始終是橫線連接 (kebab-case)。
單詞大寫開頭對于代碼編輯器的自動補全最為友好,因為這使得我們在 JS(X) 和模板中引用組件的方式盡可能的一致。然而,混用文件命名方式有的時候會導致大小寫不敏感的文件系統的問題,這也是橫線連接命名同樣完全可取的原因。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
components/
|- mycomponent.vue
components/
|- myComponent.vue
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- MyComponent.vue
components/
|- my-component.vue
{% raw %}</div>{% endraw %}
基礎組件名 <sup data-p="b">強烈推薦
應用特定樣式和約定的基礎組件 (也就是展示類的、無邏輯的或無狀態的組件) 應該全部以一個特定的前綴開頭,比如 Base
、App
或 V
。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
這些組件為你的應用奠定了一致的基礎樣式和行為。它們可能只包括:
- HTML 元素
- 其它帶
Base
前綴的組件 - 第三方 UI 組件庫
但是它們絕不會包括全局狀態 (比如來自 Vuex store)。
它們的名字通常包含所包裹元素的名字 (比如 BaseButton
、BaseTable
),除非沒有現成的對應功能的元素 (比如 BaseIcon
)。如果你為特定的上下文構建類似的組件,那它們幾乎總會消費這些組件 (比如 BaseButton
可能會用在 ButtonSubmit
上)。
這樣做的幾個好處:
當你在編輯器中以字母順序排序時,你的應用的基礎組件會全部列在一起,這樣更容易識別。
因為組件名應該始終是多個單詞,所以這樣做可以避免你在包裹簡單組件時隨意選擇前綴 (比如
MyButton
、VueButton
)。-
因為這些組件會被頻繁使用,所以你可能想把它們放到全局而不是在各處分別導入它們。使用相同的前綴可以讓 webpack 這樣工作:
var requireComponent = require.context("./src", true, /^Base[A-Z]/) requireComponent.keys().forEach(function (fileName) { var baseComponentConfig = requireComponent(fileName) baseComponentConfig = baseComponentConfig.default || baseComponentConfig var baseComponentName = baseComponentConfig.name || ( fileName .replace(/^.+\//, '') .replace(/\.\w+$/, '') ) Vue.component(baseComponentName, baseComponentConfig) })
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
components/
|- AppButton.vue
|- AppTable.vue
|- AppIcon.vue
components/
|- VButton.vue
|- VTable.vue
|- VIcon.vue
{% raw %}</div>{% endraw %}
單例組件名 <sup data-p="b">強烈推薦
只應該擁有單個活躍實例的組件應該以 The
前綴命名,以示其唯一性。
這不意味著組件只可用于一個單頁面,而是每個頁面只使用一次。這些組件永遠不接受任何 prop,因為它們是為你的應用定制的,而不是它們在你的應用中的上下文。如果你發現有必要添加 prop,那就表明這實際上是一個可復用的組件,只是目前在每個頁面里只使用一次。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
components/
|- Heading.vue
|- MySidebar.vue
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- TheHeading.vue
|- TheSidebar.vue
{% raw %}</div>{% endraw %}
緊密耦合的組件名 <sup data-p="b">強烈推薦
和父組件緊密耦合的子組件應該以父組件名作為前綴命名。
如果一個組件只在某個父組件的場景下有意義,這層關系應該體現在其名字上。因為編輯器通常會按字母順序組織文件,所以這樣做可以把相關聯的文件排在一起。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
你可以試著通過在其父組件命名的目錄中嵌套子組件以解決這個問題。比如:
components/
|- TodoList/
|- Item/
|- index.vue
|- Button.vue
|- index.vue
或:
components/
|- TodoList/
|- Item/
|- Button.vue
|- Item.vue
|- TodoList.vue
但是這種方式并不推薦,因為這會導致:
- 許多文件的名字相同,使得在編輯器中快速切換文件變得困難。
- 過多嵌套的子目錄增加了在編輯器側邊欄中瀏覽組件所花的時間。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
components/
|- SearchSidebar.vue
|- NavigationForSearchSidebar.vue
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue
{% raw %}</div>{% endraw %}
組件名中的單詞順序 <sup data-p="b">強烈推薦
組件名應該以高級別的 (通常是一般化描述的) 單詞開頭,以描述性的修飾詞結尾。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
你可能會疑惑:
“為什么我們給組件命名時不多遵從自然語言呢?”
在自然的英文里,形容詞和其它描述語通常都出現在名詞之前,否則需要使用連接詞。比如:
- 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
組件) 的時候在編輯器里更難快速定位。 - 讓重構變得更難,因為為一個移動了的組件更新相關引用時,查找/替換通常并不高效。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue
{% raw %}</div>{% endraw %}
自閉合組件 <sup data-p="b">強烈推薦
在單文件組件、字符串模板和 JSX 中沒有內容的組件應該是自閉合的——但在 DOM 模板里永遠不要這樣做。
自閉合組件表示它們不僅沒有內容,而且刻意沒有內容。其不同之處就好像書上的一頁白紙對比貼有“本頁有意留白”標簽的白紙。而且沒有了額外的閉合標簽,你的代碼也更簡潔。
不幸的是,HTML 并不支持自閉合的自定義元素——只有官方的“空”元素。所以上述策略僅適用于進入 DOM 之前 Vue 的模板編譯器能夠觸達的地方,然后再產出符合 DOM 規范的 HTML。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<!-- 在單文件組件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
<!-- 在 DOM 模板中 -->
<my-component/>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<!-- 在單文件組件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
{% raw %}</div>{% endraw %}
模板中的組件名大小寫 <sup data-p="b">強烈推薦
對于絕大多數項目來說,在單文件組件和字符串模板中組件名應該總是 PascalCase 的——但是在 DOM 模板中總是 kebab-case 的。
PascalCase 相比 kebab-case 有一些優勢:
- 編輯器可以在模板里自動補全組件名,因為 PascalCase 同樣適用于 JavaScript。
-
<MyComponent>
視覺上比<my-component>
更能夠和單個單詞的 HTML 元素區別開來,因為前者的不同之處有兩個大寫字母,后者只有一個橫線。 - 如果你在模板中使用任何非 Vue 的自定義元素,比如一個 Web Component,PascalCase 確保了你的 Vue 組件在視覺上仍然是易識別的。
不幸的是,由于 HTML 是大小寫不敏感的,在 DOM 模板中必須仍使用 kebab-case。
還請注意,如果你已經是 kebab-case 的重度用戶,那么與 HTML 保持一致的命名約定且在多個項目中保持相同的大小寫規則就可能比上述優勢更為重要了。在這些情況下,在所有的地方都使用 kebab-case 同樣是可以接受的。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<!-- 在單文件組件和字符串模板中 -->
<mycomponent/>
<!-- 在單文件組件和字符串模板中 -->
<myComponent/>
<!-- 在 DOM 模板中 -->
<MyComponent></MyComponent>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<!-- 在單文件組件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
或者
<!-- 在所有地方 -->
<my-component></my-component>
{% raw %}</div>{% endraw %}
JS/JSX 中的組件名大小寫 <sup data-p="b">強烈推薦
JS/JSX 中的組件名應該始終是 PascalCase 的,盡管在較為簡單的應用中只使用 Vue.component
進行全局組件注冊時,可以使用 kebab-case 字符串。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
在 JavaScript 中,PascalCase 是類和構造函數 (本質上任何可以產生多份不同實例的東西) 的命名約定。Vue 組件也有多份實例,所以同樣使用 PascalCase 是有意義的。額外的好處是,在 JSX (和模板) 里使用 PascalCase 使得代碼的讀者更容易分辨 Vue 組件和 HTML 元素。
然而,對于只通過 Vue.component
定義全局組件的應用來說,我們推薦 kebab-case 作為替代。原因是:
- 全局組件很少被 JavaScript 引用,所以遵守 JavaScript 的命名約定意義不大。
- 這些應用往往包含許多 DOM 內的模板,這種情況下是必須使用 kebab-case 的。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
Vue.component('myComponent', {
// ...
})
import myComponent from './MyComponent.vue'
export default {
name: 'myComponent',
// ...
}
export default {
name: 'my-component',
// ...
}
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
Vue.component('MyComponent', {
// ...
})
Vue.component('my-component', {
// ...
})
import MyComponent from './MyComponent.vue'
export default {
name: 'MyComponent',
// ...
}
{% raw %}</div>{% endraw %}
完整單詞的組件名 <sup data-p="b">強烈推薦
組件名應該傾向于完整單詞而不是縮寫。
編輯器中的自動補全已經讓書寫長命名的代價非常之低了,而其帶來的明確性卻是非常寶貴的。不常用的縮寫尤其應該避免。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
components/
|- SdSettings.vue
|- UProfOpts.vue
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue
{% raw %}</div>{% endraw %}
Prop 名大小寫 <sup data-p="b">強烈推薦
在聲明 prop 的時候,其命名應該始終使用 camelCase,而在模板和 JSX 中應該始終使用 kebab-case。
我們單純的遵循每個語言的約定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中則是 kebab-case。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
props: {
'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
props: {
greetingText: String
}
<WelcomeMessage greeting-text="hi"/>
{% raw %}</div>{% endraw %}
多個特性的元素 <sup data-p="b">強烈推薦
多個特性的元素應該分多行撰寫,每個特性一行。
在 JavaScript 中,用多行分隔對象的多個屬性是很常見的最佳實踐,因為這樣更易讀。模板和 JSX 值得我們做相同的考慮。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<img src="https://vuejs.org/images/logo.png" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<img
src="https://vuejs.org/images/logo.png"
alt="Vue Logo"
>
<MyComponent
foo="a"
bar="b"
baz="c"
/>
{% raw %}</div>{% endraw %}
模板中簡單的表達式 <sup data-p="b">強烈推薦
組件模板應該只包含簡單的表達式,復雜的表達式則應該重構為計算屬性或方法。
復雜表達式會讓你的模板變得不那么聲明式。我們應該盡量描述應該出現的是什么,而非如何計算那個值。而且計算屬性和方法使得代碼可以重用。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
{{
fullName.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}}
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<!-- 在模板中 -->
{{ normalizedFullName }}
// 復雜表達式已經移入一個計算屬性
computed: {
normalizedFullName: function () {
return this.fullName.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}
}
{% raw %}</div>{% endraw %}
簡單的計算屬性 <sup data-p="b">強烈推薦
應該把復雜計算屬性分割為盡可能多的更簡單的屬性。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
更簡單、命名得當的計算屬性是這樣的:
-
易于測試
當每個計算屬性都包含一個非常簡單且很少依賴的表達式時,撰寫測試以確保其正確工作就會更加容易。
-
易于閱讀
簡化計算屬性要求你為每一個值都起一個描述性的名稱,即便它不可復用。這使得其他開發者 (以及未來的你) 更容易專注在他們關心的代碼上并搞清楚發生了什么。
-
更好的“擁抱變化”
任何能夠命名的值都可能用在視圖上。舉個例子,我們可能打算展示一個信息,告訴用戶他們存了多少錢;也可能打算計算稅費,但是可能會分開展現,而不是作為總價的一部分。
小的、專注的計算屬性減少了信息使用時的假設性限制,所以需求變更時也用不著那么多重構了。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
computed: {
price: function () {
var basePrice = this.manufactureCost / (1 - this.profitMargin)
return (
basePrice -
basePrice * (this.discountPercent || 0)
)
}
}
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
computed: {
basePrice: function () {
return this.manufactureCost / (1 - this.profitMargin)
},
discount: function () {
return this.basePrice * (this.discountPercent || 0)
},
finalPrice: function () {
return this.basePrice - this.discount
}
}
{% raw %}</div>{% endraw %}
帶引號的特性值 <sup data-p="b">強烈推薦
非空 HTML 特性值應該始終帶引號 (單引號或雙引號,選你 JS 里不用的那個)。
在 HTML 中不帶空格的特性值是可以沒有引號的,但這樣做常常導致帶空格的特征值被回避,導致其可讀性變差。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">
{% raw %}</div>{% endraw %}
指令縮寫 <sup data-p="b">強烈推薦
指令縮寫 (用 :
表示 v-bind:
和用 @
表示 v-on:
) 應該要么都用要么都不用。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<input
v-bind:value="newTodoText"
:placeholder="newTodoInstructions"
>
<input
v-on:input="onInput"
@focus="onFocus"
>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<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"
>
{% raw %}</div>{% endraw %}
優先級 C 的規則:推薦 (將選擇和認知成本最小化)
組件/實例的選項的順序 <sup data-p="c">推薦
組件/實例的選項應該有統一的順序。
這是我們推薦的組件選項默認順序。它們被劃分為幾大類,所以你也能知道從插件里添加的新屬性應該放到哪里。
- 副作用 (觸發組件外的影響)
el
- 全局感知 (要求組件以外的知識)
name
parent
- 組件類型 (更改組件的類型)
functional
- 模板修改器 (改變模板的編譯方式)
delimiters
comments
- 模板依賴 (模板內使用的資源)
components
directives
filters
- 組合 (向選項里合并屬性)
extends
mixins
- 接口 (組件的接口)
inheritAttrs
model
-
props
/propsData
- 本地狀態 (本地的響應式屬性)
data
computed
- 事件 (通過響應式事件觸發的回調)
watch
- 生命周期鉤子 (按照它們被調用的順序)
- 非響應式的屬性 (不依賴響應系統的實例屬性)
methods
- 渲染 (組件輸出的聲明式描述)
-
template
/render
renderError
元素特性的順序 <sup data-p="c">推薦
元素 (包括組件) 的特性應該有統一的順序。
這是我們為組件選項推薦的默認順序。它們被劃分為幾大類,所以你也能知道新添加的自定義特性和指令應該放到哪里。
- 定義 (提供組件的選項)
is
- 列表渲染 (創建多個變化的相同元素)
v-for
- 條件渲染 (元素是否渲染/顯示)
v-if
v-else-if
v-else
v-show
v-cloak
- 渲染方式 (改變元素的渲染方式)
v-pre
v-once
- 全局感知 (需要超越組件的知識)
id
- 唯一的特性 (需要唯一值的特性)
ref
key
slot
- 雙向綁定 (把綁定和事件結合起來)
v-model
其它特性 (所有普通的綁定或未綁定的特性)
事件 (組件事件監聽器)
v-on
- 內容 (覆寫元素的內容)
v-html
v-text
組件/實例選項中的空行 <sup data-p="c">推薦
你可能想在多個屬性之間增加一個空行,特別是在這些選項一屏放不下,需要滾動才能都看到的時候。
當你的組件開始覺得密集或難以閱讀時,在多個屬性之間添加空行可以讓其變得容易。在一些諸如 Vim 的編輯器里,這樣格式化后的選項還能通過鍵盤被快速導航。
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
props: {
value: {
type: String,
required: true
},
focused: {
type: Boolean,
default: false
},
label: String,
icon: String
},
computed: {
formattedValue: function () {
// ...
},
inputClasses: function () {
// ...
}
}
// 沒有空行在組件易于閱讀和導航時也沒問題。
props: {
value: {
type: String,
required: true
},
focused: {
type: Boolean,
default: false
},
label: String,
icon: String
},
computed: {
formattedValue: function () {
// ...
},
inputClasses: function () {
// ...
}
}
{% raw %}</div>{% endraw %}
單文件組件的頂級元素的順序 <sup data-p="c">推薦
單文件組件應該總是讓 <script>
、<template>
和 <style>
標簽的順序保持一致。且 <style>
要放在最后,因為另外兩個標簽至少要有一個。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>
<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>
<!-- ComponentB.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>
<!-- ComponentA.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
{% raw %}</div>{% endraw %}
優先級 D 的規則:謹慎使用 (有潛在危險的模式)
沒有在 v-if
/v-if-else
/v-else
中使用 key
<sup data-p="d">謹慎使用
如果一組 v-if
+ v-else
的元素類型相同,最好使用 key
(比如兩個 <div>
元素)。
默認情況下,Vue 會盡可能高效的更新 DOM。這意味著其在相同類型的元素之間切換時,會修補已存在的元素,而不是將舊的元素移除然后在同一位置添加一個新元素。如果本不相同的元素被識別為相同,則會出現意料之外的副作用。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<div v-if="error">
錯誤:{{ error }}
</div>
<div v-else>
{{ results }}
</div>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<div
v-if="error"
key="search-status"
>
錯誤:{{ error }}
</div>
<div
v-else
key="search-results"
>
{{ results }}
</div>
<p v-if="error">
錯誤:{{ error }}
</p>
<div v-else>
{{ results }}
</div>
{% raw %}</div>{% endraw %}
scoped
中的元素選擇器 <sup data-p="d">謹慎使用
元素選擇器應該避免在 scoped
中出現。
在 scoped
樣式中,類選擇器比元素選擇器更好,因為大量使用元素選擇器是很慢的。
{% raw %}
<details>
<summary>
<h4>詳解</h4>
</summary>
{% endraw %}
為了給樣式設置作用域,Vue 會為元素添加一個獨一無二的特性,例如 data-v-f3f3eg9
。然后修改選擇器,使得在匹配選擇器的元素中,只有帶這個特性才會真正生效 (比如 button[data-v-f3f3eg9]
)。
問題在于大量的元素和特性組合的選擇器 (比如 button[data-v-f3f3eg9]
) 會比類和特性組合的選擇器 慢,所以應該盡可能選用類選擇器。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
<template>
<button>X</button>
</template>
<style scoped>
button {
background-color: red;
}
</style>
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
<template>
<button class="btn btn-close">X</button>
</template>
<style scoped>
.btn-close {
background-color: red;
}
</style>
{% raw %}</div>{% endraw %}
隱性的父子組件通信 <sup data-p="d">謹慎使用
應該優先通過 prop 和事件進行父子組件之間的通信,而不是 this.$parent
或改變 prop。
一個理想的 Vue 應用是 prop 向下傳遞,事件向上傳遞的。遵循這一約定會讓你的組件更易于理解。然而,在一些邊界情況下 prop 的變更或 this.$parent
能夠簡化兩個深度耦合的組件。
問題在于,這種做法在很多簡單的場景下可能會更方便。但請當心,不要為了一時方便 (少寫代碼) 而犧牲數據流向的簡潔性 (易于理解)。
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
Vue.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
template: '<input v-model="todo.text">'
})
Vue.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
methods: {
removeTodo () {
var vm = this
vm.$parent.todos = vm.$parent.todos.filter(function (todo) {
return todo.id !== vm.todo.id
})
}
},
template: `
<span>
{{ todo.text }}
<button @click="removeTodo">
X
</button>
</span>
`
})
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
Vue.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
template: `
<input
:value="todo.text"
@input="$emit('input', $event.target.value)"
>
`
})
Vue.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
template: `
<span>
{{ todo.text }}
<button @click="$emit('delete')">
X
</button>
</span>
`
})
{% raw %}</div>{% endraw %}
非 Flux 的全局狀態管理 <sup data-p="d">謹慎使用
應該優先通過 Vuex 管理全局狀態,而不是通過 this.$root
或一個全局事件總線。
通過 this.$root
和/或全局事件總線管理狀態在很多簡單的情況下都是很方便的,但是并不適用于絕大多數的應用。Vuex 提供的不僅是一個管理狀態的中心區域,還是組織、追蹤和調試狀態變更的好工具。
{% raw %}</details>{% endraw %}
{% raw %}<div class="style-example example-bad">{% endraw %}
反例
// main.js
new Vue({
data: {
todos: []
},
created: function () {
this.$on('remove-todo', this.removeTodo)
},
methods: {
removeTodo: function (todo) {
var todoIdToRemove = todo.id
this.todos = this.todos.filter(function (todo) {
return todo.id !== todoIdToRemove
})
}
}
})
{% raw %}</div>{% endraw %}
{% raw %}<div class="style-example example-good">{% endraw %}
好例子
// 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>
{% raw %}</div>{% endraw %}
{% raw %}
<script>
(function () {
var enforcementTypes = {
none: '<span title="這一規則無法強制執行">自律</span>',
runtime: '運行時錯誤',
linter: '<a target="_blank">plugin:vue/recommended</a>'
}
Vue.component('sg-enforcement', {
template: '
<span>
<strong>強制執行</strong>:
<span class="style-rule-tag" v-html="humanType"/>
</span>
',
props: {
type: {
type: String,
required: true,
validate: function (value) {
Object.keys(enforcementTypes).indexOf(value) !== -1
}
}
},
computed: {
humanType: function () {
return enforcementTypes[this.type]
}
}
})
// new Vue({
// el: '#main'
// })
})()
</script>
{% endraw %}