這里是官方的 Vue 特有代碼的風(fēng)格指南。如果在工程中使用 Vue,為了回避錯(cuò)誤、小糾結(jié)和反模式,該指南是份不錯(cuò)的參考。不過我們也不確信風(fēng)格指南的所有內(nèi)容對于所有的團(tuán)隊(duì)或工程都是理想的。所以根據(jù)過去的經(jīng)驗(yàn)、周邊的技術(shù)棧、個(gè)人價(jià)值觀做出有意義的偏差是可取的。
對于其絕大部分,我們也總體上避免就 JavaScript 或 HTML 的本身提出建議。我們不介意你是否使用分號或結(jié)尾的逗號。我們不介意你在 HTML attribute 中使用單引號還是雙引號。不過當(dāng)我們發(fā)現(xiàn)在 Vue 的情景下有幫助的特定模式時(shí),也會存在例外。
最終,我們把所有的規(guī)則歸為了四個(gè)大類:
這些規(guī)則會幫你規(guī)避錯(cuò)誤,所以學(xué)習(xí)并接受它們帶來的全部代價(jià)吧。這里面可能存在例外,但應(yīng)該非常少,且只有你同時(shí)精通 JavaScript 和 Vue 才可以這樣做。
這些規(guī)則能夠在絕大多數(shù)工程中改善可讀性和開發(fā)體驗(yàn)。即使你違反了,代碼還是能照常運(yùn)行,但例外應(yīng)該盡可能少且有合理的理由。
當(dāng)存在多個(gè)同樣好的選項(xiàng),選任意一個(gè)都可以確保一致性。在這些規(guī)則里,我們描述了每個(gè)選項(xiàng)并建議一個(gè)默認(rèn)的選擇。也就是說只要保持一致且理由充分,你可以隨意在你的代碼庫中做出不同的選擇。請務(wù)必給出一個(gè)好的理由!通過接受社區(qū)的標(biāo)準(zhǔn),你將會:
有些 Vue 特性的存在是為了照顧極端情況或幫助老代碼的平穩(wěn)遷移。當(dāng)被過度使用時(shí),這些特性會讓你的代碼難于維護(hù)甚至變成 bug 的來源。這些規(guī)則是為了給有潛在風(fēng)險(xiǎn)的特性敲個(gè)警鐘,并說明它們什么時(shí)候不應(yīng)該使用以及為什么。
組件名應(yīng)該始終是多個(gè)單詞的,根組件 App
以及 <transition>
、<component>
之類的 Vue 內(nèi)置組件除外。
這樣做可以避免跟現(xiàn)有的以及未來的 HTML 元素相沖突,因?yàn)樗械?HTML 元素名稱都是單個(gè)單詞的。
app.component('todo', {
// ...
})
export default {
name: 'Todo',
// ...
}
app.component('todo-item', {
// ...
})
export default {
name: 'TodoItem',
// ...
}
Prop 定義應(yīng)盡量詳細(xì)
在你提交的代碼中,prop 的定義應(yīng)該盡量詳細(xì),至少需要指定其類型。
詳解
細(xì)致的 prop 定義有兩個(gè)好處:
它們寫明了組件的 API,所以很容易看懂組件的用法;
在開發(fā)環(huán)境下,如果向一個(gè)組件提供格式不正確的 prop,Vue 將會告警,以幫助你捕獲潛在的錯(cuò)誤來源。
// 這樣做只有開發(fā)原型系統(tǒng)時(shí)可以接受
props: ['status']
props: {
status: String
}
// 更好的例子
props: {
status: {
type: String,
required: true,
validator: value => {
return [
'syncing',
'synced',
'version-conflict',
'error'
].includes(value)
}
}
}
v-for
設(shè)置 key 值必要
總是用 key
配合 v-for
在組件上總是必須用 key
配合 v-for
,以便維護(hù)內(nèi)部組件及其子樹的狀態(tài)。甚至在元素上維護(hù)可預(yù)測的行為,比如動(dòng)畫中的對象固化 (object constancy) ,也是一種好的做法。
詳解 假設(shè)你有一個(gè)待辦事項(xiàng)列表:
data() {
return {
todos: [
{
id: 1,
text: 'Learn to use v-for'
},
{
id: 2,
text: 'Learn to use key'
}
]
}
}
然后你把它們按照字母順序排序。在更新 DOM 的時(shí)候,Vue 將會優(yōu)化渲染把可能的 DOM 變更降到最低。即可能刪掉第一個(gè)待辦事項(xiàng)元素,然后把它重新加回到列表的最末尾。 這里的問題在于,不要?jiǎng)h除仍然會留在 DOM 中的元素。比如你想使用 <transition-group> 給列表加過渡動(dòng)畫,或想在被渲染元素是 <input> 時(shí)保持聚焦。在這些情況下,為每一個(gè)項(xiàng)目添加一個(gè)唯一的鍵值 (比如 :key="todo.id") 將會讓 Vue 知道如何使行為更容易預(yù)測。
詳解
假設(shè)你有一個(gè)待辦事項(xiàng)列表:JavaScript
data() {
return {
todos: [
{
id: 1,
text: 'Learn to use v-for'
},
{
id: 2,
text: 'Learn to use key'
}
]
}
}
然后你把它們按照字母順序排序。在更新 DOM 的時(shí)候,Vue 將會優(yōu)化渲染把可能的 DOM 變更降到最低。即可能刪掉第一個(gè)待辦事項(xiàng)元素,然后把它重新加回到列表的最末尾。
這里的問題在于,不要?jiǎng)h除仍然會留在 DOM 中的元素。比如你想使用 <transition-group> 給列表加過渡動(dòng)畫,或想在被渲染元素是 <input> 時(shí)保持聚焦。在這些情況下,為每一個(gè)項(xiàng)目添加一個(gè)唯一的鍵值 (比如 :key="todo.id") 將會讓 Vue 知道如何使行為更容易預(yù)測。
根據(jù)我們的經(jīng)驗(yàn),最好始終添加一個(gè)唯一的鍵值,以便你和你的團(tuán)隊(duì)永遠(yuǎn)不必?fù)?dān)心這些極端情況。也在少數(shù)對性能有嚴(yán)格要求的情況下,為了避免對象固化,你可以刻意做一些非常規(guī)的處理。根據(jù)我們的經(jīng)驗(yàn),最好始終添加一個(gè)唯一的鍵值,以便你和你的團(tuán)隊(duì)永遠(yuǎn)不必?fù)?dān)心這些極端情況。也在少數(shù)對性能有嚴(yán)格要求的情況下,為了避免對象固化,你可以刻意做一些非常規(guī)的處理。
<ul>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ul>
<ul>
<li
v-for="todo in todos"
:key="todo.id"
>
{{ todo.text }}
</li>
</ul>
v-if
和 v-for
一起使用必要
永遠(yuǎn)不要把 v-if
和 v-for
同時(shí)用在同一個(gè)元素上。
一般我們在兩種常見的情況下會傾向于這樣做:
v-for="user in users" v-if="user.isActive"
)。在這種情形下,請將 users
替換為一個(gè)計(jì)算屬性 (比如 activeUsers
),讓其返回過濾后的列表。v-for="user in users" v-if="shouldShowUsers"
)。這種情形下,請將 v-if
移動(dòng)至容器元素上 (比如 ul
、ol
詳解
當(dāng) Vue 處理指令時(shí),v-for
比 v-if
具有更高的優(yōu)先級,所以這個(gè)模板:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
將會經(jīng)過如下運(yùn)算:
this.users.map(user => {
if (user.isActive) {
return user.name
}
})
因此哪怕我們只渲染出一小部分用戶的元素,也得在每次重渲染的時(shí)候遍歷整個(gè)列表,不論活躍用戶是否發(fā)生了變化。 通過將其更換為在如下的一個(gè)計(jì)算屬性上遍歷:
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
我們將會獲得如下好處:
users
數(shù)組發(fā)生相關(guān)變化時(shí)才被重新運(yùn)算,過濾更高效。v-for="user in activeUsers"
之后,我們在渲染的時(shí)候只遍歷活躍用戶,渲染更高效。<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>
詳解
當(dāng) Vue 處理指令時(shí),v-for
比v-if
具有更高的優(yōu)先級,所以這個(gè)模板:html
lt;ul&
lt;li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
{{ user.name }}
lt;/li&
lt;/ul&
將會經(jīng)過如下運(yùn)算:js
this.users.map(user => {
if (user.isActive) {
return user.name
}
})
因此哪怕我們只渲染出一小部分用戶的元素,也得在每次重渲染的時(shí)候遍歷整個(gè)列表,不論活躍用戶是否發(fā)生了變化。
通過將其更換為在如下的一個(gè)計(jì)算屬性上遍歷:js
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}html
lt;ul&
lt;li
v-for="user in activeUsers"
:key="user.id"
{{ user.name }}
lt;/li&
lt;/ul&
我們將會獲得如下好處:
users
數(shù)組發(fā)生相關(guān)變化時(shí)才被重新運(yùn)算,過濾更高效。v-for="user in activeUsers"
之后,我們在渲染的時(shí)候只遍歷活躍用戶,渲染更高效。<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
移動(dòng)到容器元素,我們不會再對列表中的每個(gè)用戶檢查 shouldShowUsers
。取而代之的是,我們只檢查它一次,且不會在 shouldShowUsers
為否的時(shí)候運(yùn)算 v-for
。通過將 v-if
移動(dòng)到容器元素,我們不會再對列表中的每個(gè)用戶檢查 shouldShowUsers
。取而代之的是,我們只檢查它一次,且不會在 shouldShowUsers
為否的時(shí)候運(yùn)算 v-for
。
<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>
<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>
對于應(yīng)用來說,頂級 App
組件和布局組件中的樣式可以是全局的,但是其它所有組件都應(yīng)該是有作用域的。
這條規(guī)則只和單文件組件有關(guān)。你不一定要使用 scoped
attribute。設(shè)置作用域也可以通過 CSS Modules,那是一個(gè)基于 class 的類似 BEM 的策略,當(dāng)然你也可以使用其它的庫或約定。
不管怎樣,對于組件庫,我們應(yīng)該更傾向于選用基于 class 的策略而不是 scoped
attribute。
這讓覆寫內(nèi)部樣式更容易:使用了人類可理解的 class 名稱且沒有太高的選擇器優(yōu)先級,而且不太會導(dǎo)致沖突。
詳解
如果你和其他開發(fā)者一起開發(fā)一個(gè)大型工程,或有時(shí)引入三方 HTML/CSS (比如來自 Auth0),設(shè)置一致的作用域會確保你的樣式只會運(yùn)用在它們想要作用的組件上。
不止要使用scoped
attribute,使用唯一的 class 名可以幫你確保那些三方庫的 CSS 不會運(yùn)用在你自己的 HTML 上。比如許多工程都使用了button
、btn
或icon
class 名,所以即便你不使用類似 BEM 的策略,添加一個(gè) app 專屬或組件專屬的前綴 (比如ButtonClose-icon
) 也可以提供很多保護(hù)。
<template>
<button class="btn btn-close">×</button>
</template>
<style>
.btn-close {
background-color: red;
}
</style>
<template>
<button class="button button-close">×</button>
</template>
<!-- 使用 `scoped` attribute -->
<style scoped>
.button {
border: none;
border-radius: 2px;
}
.button-close {
background-color: red;
}
</style>
<template>
<button :class="[$style.button, $style.buttonClose]">×</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">×</button>
</template>
<!-- 使用 BEM 約定 -->
<style>
.c-Button {
border: none;
border-radius: 2px;
}
.c-Button--close {
background-color: red;
}
</style>
使用模塊作用域保持不允許外部訪問的函數(shù)的私有性。如果無法做到這一點(diǎn),就始終為插件、混入等不考慮作為對外公共 API 的自定義私有 property 使用 $_
前綴。并附帶一個(gè)命名空間以回避和其它作者的沖突 (比如 $_yourPluginName_
)。
::: 詳細(xì)
Vue 使用 _
前綴來定義其自身的私有 property,所以使用相同的前綴 (比如 _update
) 有覆寫實(shí)例 property 的風(fēng)險(xiǎn)。即便你檢查確認(rèn) Vue 當(dāng)前版本沒有用到這個(gè) property 名,也不能保證和將來的版本沒有沖突。
對于 $
前綴來說,其在 Vue 生態(tài)系統(tǒng)中的目的是暴露給用戶的一個(gè)特殊的實(shí)例 property,所以把它用于私有 property 并不合適。
不過,我們推薦把這兩個(gè)前綴結(jié)合為 $_
,作為一個(gè)用戶定義的私有 property 的約定,以確保不會和 Vue 自身相沖突。
:::
const myGreatMixin = {
// ...
methods: {
update() {
// ...
}
}
}
const myGreatMixin = {
// ...
methods: {
_update() {
// ...
}
}
}
const myGreatMixin = {
// ...
methods: {
$update() {
// ...
}
}
}
const myGreatMixin = {
// ...
methods: {
$_update() {
// ...
}
}
}
const myGreatMixin = {
// ...
methods: {
$_myGreatMixin_update() {
// ...
}
}
}
// Even better!
const myGreatMixin = {
// ...
methods: {
publicMethod() {
// ...
myPrivateFunction()
}
}
}
function myPrivateFunction() {
// ...
}
export default myGreatMixin
只要有能夠拼接文件的構(gòu)建系統(tǒng),就把每個(gè)組件單獨(dú)分成文件。
當(dāng)你需要編輯一個(gè)組件或查閱一個(gè)組件的用法時(shí),可以更快速的找到它。
app.component('TodoList', {
// ...
})
app.component('TodoItem', {
// ...
})
components/
|- TodoList.js
|- TodoItem.js
components/
|- TodoList.vue
|- TodoItem.vue
單文件組件的文件名應(yīng)該要么始終是單詞大寫開頭 (PascalCase),要么始終是橫線連接 (kebab-case)。
單詞大寫開頭對于代碼編輯器的自動(dòng)補(bǔ)全最為友好,因?yàn)檫@使得我們在 JS(X) 和模板中引用組件的方式盡可能的一致。然而,混用文件命名方式有的時(shí)候會導(dǎo)致大小寫不敏感的文件系統(tǒng)的問題,這也是橫線連接命名同樣完全可取的原因。
components/
|- mycomponent.vue
components/
|- myComponent.vue
components/
|- MyComponent.vue
components/
|- my-component.vue
應(yīng)用特定樣式和約定的基礎(chǔ)組件 (也就是展示類的、無邏輯的或無狀態(tài)的組件) 應(yīng)該全部以一個(gè)特定的前綴開頭,比如 Base
、App
或 V
。
詳解
這些組件為你的應(yīng)用奠定了一致的基礎(chǔ)樣式和行為。它們可能只包括:
- HTML 元素
- 其它基礎(chǔ)組件
- 第三方 UI 組件庫
但是它們絕不會包括全局狀態(tài) (比如來自 Vuex store)。
它們的名字通常包含所包裹元素的名字 (比如BaseButton
、BaseTable
),除非沒有現(xiàn)成的對應(yīng)功能的元素 (比如BaseIcon
)。如果你為特定的上下文構(gòu)建類似的組件,那它們幾乎總會消費(fèi)這些組件 (比如BaseButton
可能會用在ButtonSubmit
上)。
這樣做的幾個(gè)好處:
- 當(dāng)你在編輯器中以字母順序排序時(shí),你的應(yīng)用的基礎(chǔ)組件會全部列在一起,這樣更容易識別。
- 因?yàn)榻M件名應(yīng)該始終是多個(gè)單詞,所以這樣做可以避免你在包裹簡單組件時(shí)隨意選擇前綴 (比如MyButton
、VueButton
)。
- 因?yàn)檫@些組件會被頻繁使用,所以你可能想把它們放到全局而不是在各處分別導(dǎo)入它們。使用相同的前綴可以讓 webpack 這樣工作:js
const requireComponent = require.context("./src", true, /Base[A-Z]\w+\.(vue|js)$/)
requireComponent.keys().forEach(function (fileName) {
let baseComponentConfig = requireComponent(fileName)
baseComponentConfig = baseComponentConfig.default || baseComponentConfig
const baseComponentName = baseComponentConfig.name || (
fileName
.replace(/^.+\//, '')
.replace(/\.\w+$/, '')
)
app.component(baseComponentName, baseComponentConfig)
})
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
components/
|- AppButton.vue
|- AppTable.vue
|- AppIcon.vue
components/
|- VButton.vue
|- VTable.vue
|- VIcon.vue
只應(yīng)該擁有單個(gè)活躍實(shí)例的組件應(yīng)該以 The
前綴命名,以示其唯一性。
這不意味著組件只可用于一個(gè)單頁面,而是每個(gè)頁面只使用一次。這些組件永遠(yuǎn)不接受任何 prop,因?yàn)樗鼈兪菫槟愕膽?yīng)用定制的,而不是它們在你的應(yīng)用中的上下文。如果你發(fā)現(xiàn)有必要添加 prop,那就表明這實(shí)際上是一個(gè)可復(fù)用的組件,只是目前在每個(gè)頁面里只使用一次。
components/
|- Heading.vue
|- MySidebar.vue
components/
|- TheHeading.vue
|- TheSidebar.vue
和父組件緊密耦合的子組件應(yīng)該以父組件名作為前綴命名。
如果一個(gè)組件只在某個(gè)父組件的場景下有意義,這層關(guān)系應(yīng)該體現(xiàn)在其名字上。因?yàn)榫庉嬈魍ǔ醋帜疙樞蚪M織文件,所以這樣做可以把相關(guān)聯(lián)的文件排在一起。
::: 詳情 你可以試著通過在其父組件命名的目錄中嵌套子組件以解決這個(gè)問題。比如:
components/
|- TodoList/
|- Item/
|- index.vue
|- Button.vue
|- index.vue
或:
components/
|- TodoList/
|- Item/
|- Button.vue
|- Item.vue
|- TodoList.vue
但是這種方式并不推薦,因?yàn)檫@會導(dǎo)致:
:::
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
components/
|- SearchSidebar.vue
|- NavigationForSearchSidebar.vue
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue
組件名稱應(yīng)該以高階的 (通常是一般化描述的) 單詞開頭,以描述性的修飾詞結(jié)尾。
詳解 你可能會疑惑:
“為什么我們給組件命名時(shí)不多遵從自然語言呢?”
在自然的英文里,形容詞和其它描述語通常都出現(xiàn)在名詞之前,否則需要使用連接詞。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在組件名里包含這些連接詞,但是單詞的順序很重要。
同樣要注意在你的應(yīng)用中所謂的“高階”是跟語境有關(guān)的。比如對于一個(gè)帶搜索表單的應(yīng)用來說,它可能包含這樣的組件:bash
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我們很難看出來哪些組件是針對搜索的?,F(xiàn)在我們來根據(jù)規(guī)則給組件重新命名:bash
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因?yàn)榫庉嬈魍ǔ醋帜疙樞蚪M織文件,所以現(xiàn)在組件之間的重要關(guān)系一目了然。
你可能想換成多級目錄的方式,把所有的搜索組件放到“search”目錄,把所有的設(shè)置組件放到“settings”目錄。我們只推薦在非常大型 (如有 100+ 個(gè)組件) 的應(yīng)用下才考慮這么做,因?yàn)椋?
- 在多級目錄間找來找去,要比在單個(gè)components
目錄下滾動(dòng)查找要花費(fèi)更多的精力。
- 存在組件重名 (比如存在多個(gè)ButtonDelete.vue
組件) 的時(shí)候在編輯器里更難快速定位。
詳解
你可能會疑惑:
“為什么我們給組件命名時(shí)不多遵從自然語言呢?”
在自然的英文里,形容詞和其它描述語通常都出現(xiàn)在名詞之前,否則需要使用連接詞。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在組件名里包含這些連接詞,但是單詞的順序很重要。
同樣要注意在你的應(yīng)用中所謂的“高階”是跟語境有關(guān)的。比如對于一個(gè)帶搜索表單的應(yīng)用來說,它可能包含這樣的組件:bash
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我們很難看出來哪些組件是針對搜索的?,F(xiàn)在我們來根據(jù)規(guī)則給組件重新命名:bash
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因?yàn)榫庉嬈魍ǔ醋帜疙樞蚪M織文件,所以現(xiàn)在組件之間的重要關(guān)系一目了然。
你可能想換成多級目錄的方式,把所有的搜索組件放到“search”目錄,把所有的設(shè)置組件放到“settings”目錄。我們只推薦在非常大型 (如有 100+ 個(gè)組件) 的應(yīng)用下才考慮這么做,因?yàn)椋?
- 在多級目錄間找來找去,要比在單個(gè)components
目錄下滾動(dòng)查找要花費(fèi)更多的精力。
- 存在組件重名 (比如存在多個(gè)ButtonDelete.vue
組件) 的時(shí)候在編輯器里更難快速定位。
- 讓重構(gòu)變得更難,因?yàn)闉橐粋€(gè)移動(dòng)了的組件更新相關(guān)引用時(shí),查找/替換通常并不高效。- 讓重構(gòu)變得更難,因?yàn)闉橐粋€(gè)移動(dòng)了的組件更新相關(guān)引用時(shí),查找/替換通常并不高效。
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue
在單文件組件、字符串模板和 JSX 中沒有內(nèi)容的組件應(yīng)該是自閉合的——但在 DOM 模板里永遠(yuǎn)不要這樣做。
自閉合組件表示它們不僅沒有內(nèi)容,而且刻意沒有內(nèi)容。其不同之處就好像書上的一頁白紙對比貼有“本頁有意留白”標(biāo)簽的白紙。而且沒有了額外的閉合標(biāo)簽,你的代碼也更簡潔。
不幸的是,HTML 并不支持自閉合的自定義元素——只有官方的“空”元素。所以上述策略僅適用于進(jìn)入 DOM 之前 Vue 的模板編譯器能夠觸達(dá)的地方,然后再產(chǎn)出符合 DOM 規(guī)范的 HTML。
<!-- 在單文件組件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
<!-- 在 DOM 模板中 -->
<my-component/>
<!-- 在單文件組件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
對于絕大多數(shù)項(xiàng)目來說,在單文件組件和字符串模板中組件名稱應(yīng)該總是 PascalCase 的——但是在 DOM 模板中總是 kebab-case 的。
PascalCase 相比 kebab-case 有一些優(yōu)勢:
MyComponent>
視覺上比 <my-component>
更能夠和單個(gè)單詞的 HTML 元素區(qū)別開來,因?yàn)榍罢叩牟煌幱袃蓚€(gè)大寫字母,后者只有一個(gè)橫線。不幸的是,由于 HTML 是大小寫不敏感的,在 DOM 模板中必須仍使用 kebab-case。
還請注意,如果你已經(jīng)是 kebab-case 的重度用戶,那么與 HTML 保持一致的命名約定且在多個(gè)項(xiàng)目中保持相同的大小寫規(guī)則就可能比上述優(yōu)勢更為重要了。在這些情況下,在所有的地方都使用 kebab-case 同樣是可以接受的。
<!-- 在單文件組件和字符串模板中 -->
<mycomponent/>
<!-- 在單文件組件和字符串模板中 -->
<myComponent/>
<!-- 在 DOM 模板中 -->
<MyComponent></MyComponent>
<!-- 在單文件組件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
或者
<!-- 在所有地方 -->
<my-component></my-component>
JS/JSX 中的組件名應(yīng)該始終是 PascalCase 的,盡管在較為簡單的應(yīng)用中只使用 app.component
進(jìn)行全局組件注冊時(shí),可以使用 kebab-case 字符串。
詳解
在 JavaScript 中,PascalCase 是類和構(gòu)造函數(shù) (本質(zhì)上任何可以產(chǎn)生多份不同實(shí)例的東西) 的命名約定。Vue 組件也有多份實(shí)例,所以同樣使用 PascalCase 是有意義的。額外的好處是,在 JSX (和模板) 里使用 PascalCase 使得代碼的讀者更容易分辨 Vue 組件和 HTML 元素。
然而,對于只通過app.component
定義全局組件的應(yīng)用來說,我們推薦 kebab-case 作為替代。原因是:
- 全局組件很少被 JavaScript 引用,所以遵守 JavaScript 的命名約定意義不大。
- 這些應(yīng)用往往包含許多 DOM 內(nèi)的模板,這種情況下是必須使用 kebab-case 的。
app.component('myComponent', {
// ...
})
import myComponent from './MyComponent.vue'
export default {
name: 'myComponent',
// ...
}
export default {
name: 'my-component',
// ...
}
app.component('MyComponent', {
// ...
})
app.component('my-component', {
// ...
})
import MyComponent from './MyComponent.vue'
export default {
name: 'MyComponent',
// ...
}
組件名稱應(yīng)該傾向于完整單詞而不是縮寫。
編輯器中的自動(dòng)補(bǔ)全已經(jīng)讓書寫長命名的代價(jià)非常之低了,而其帶來的明確性卻是非常寶貴的。不常用的縮寫尤其應(yīng)該避免。
components/
|- SdSettings.vue
|- UProfOpts.vue
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue
在聲明 prop 的時(shí)候,其命名應(yīng)該始終使用 camelCase,而在模板和 JSX 中應(yīng)該始終使用 kebab-case。
我們單純的遵循每個(gè)語言的約定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中則是 kebab-case。
props: {
'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>
props: {
greetingText: String
}
<WelcomeMessage greeting-text="hi"/>
多個(gè) attribute 的元素應(yīng)該分多行撰寫,每個(gè) attribute 一行。
在 JavaScript 中,用多行分隔對象的多個(gè) property 是很常見的最佳實(shí)踐,因?yàn)檫@樣更易讀。模板和 JSX 值得我們做相同的考慮。
<img src="https://vuejs.org/images/logo.png" rel="external nofollow" rel="external nofollow" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
<img
src="https://vuejs.org/images/logo.png" rel="external nofollow" rel="external nofollow"
alt="Vue Logo"
>
<MyComponent
foo="a"
bar="b"
baz="c"
/>
組件模板應(yīng)該只包含簡單的表達(dá)式,復(fù)雜的表達(dá)式則應(yīng)該重構(gòu)為計(jì)算屬性或方法。
復(fù)雜表達(dá)式會讓你的模板變得不那么聲明式。我們應(yīng)該盡量描述應(yīng)該出現(xiàn)的是什么,而非如何計(jì)算那個(gè)值。而且計(jì)算屬性和方法使得代碼可以重用。
{{
fullName.split(' ').map((word) => {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}}
<!-- 在模板中 -->
{{ normalizedFullName }}
// 復(fù)雜表達(dá)式已經(jīng)移入一個(gè)計(jì)算屬性
computed: {
normalizedFullName() {
return this.fullName.split(' ')
.map(word => word[0].toUpperCase() + word.slice(1))
.join(' ')
}
}
應(yīng)該把復(fù)雜計(jì)算屬性分割為盡可能多的更簡單的 property。
詳解
更簡單、命名得當(dāng)?shù)挠?jì)算屬性是這樣的:
- 易于測試
當(dāng)每個(gè)計(jì)算屬性都包含一個(gè)非常簡單且很少依賴的表達(dá)式時(shí),撰寫測試以確保其正確工作就會更加容易。
- 易于閱讀
簡化計(jì)算屬性要求你為每一個(gè)值都起一個(gè)描述性的名稱,即便它不可復(fù)用。這使得其他開發(fā)者 (以及未來的你) 更容易專注在他們關(guān)心的代碼上并搞清楚發(fā)生了什么。
- 更好的“擁抱變化”
任何能夠命名的值都可能用在視圖上。舉個(gè)例子,我們可能打算展示一個(gè)信息,告訴用戶他們存了多少錢;也可能打算計(jì)算稅費(fèi),但是可能會分開展現(xiàn),而不是作為總價(jià)的一部分。
小的、專注的計(jì)算屬性減少了信息使用時(shí)的假設(shè)性限制,所以需求變更時(shí)也用不著那么多重構(gòu)了。
computed: {
price() {
const basePrice = this.manufactureCost / (1 - this.profitMargin)
return (
basePrice -
basePrice * (this.discountPercent || 0)
)
}
}
computed: {
basePrice() {
return this.manufactureCost / (1 - this.profitMargin)
},
discount() {
return this.basePrice * (this.discountPercent || 0)
},
finalPrice() {
return this.basePrice - this.discount
}
}
非空 HTML attribute 值應(yīng)該始終帶引號 (單引號或雙引號,選你 JS 里不用的那個(gè))。
在 HTML 中不帶空格的 attribute 值是可以沒有引號的,但這鼓勵(lì)了大家在特征值里不寫空格,導(dǎo)致可讀性變差。
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">
指令縮寫 (用 :
表示 v-bind:
,@
表示 v-on:
和用 #
表示 v-slot
) 應(yīng)該要么都用要么都不用。
<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>
<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>
組件/實(shí)例的選項(xiàng)應(yīng)該有統(tǒng)一的順序。
這是我們推薦的組件選項(xiàng)默認(rèn)順序。它們被劃分為幾大類,所以你也能知道從插件里添加的新 property 應(yīng)該放到哪里。
name
components
directives
extends
mixins
provide
/inject
inheritAttrs
props
emits
setup
data
computed
watch
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeUnmount
unmounted
errorCaptured
renderTracked
renderTriggered
methods
template
/render
元素 (包括組件) 的 attribute 應(yīng)該有統(tǒng)一的順序。
這是我們?yōu)榻M件選項(xiàng)推薦的默認(rèn)順序。它們被劃分為幾大類,所以你也能知道新添加的自定義 attribute 和指令應(yīng)該放到哪里。
is
v-for
v-if
v-else-if
v-else
v-show
v-cloak
v-pre
v-once
id
ref
key
v-model
v-on
v-html
v-text
你可能想在多個(gè) property 之間增加一個(gè)空行,特別是在這些選項(xiàng)一屏放不下,需要滾動(dòng)才能都看到的時(shí)候。
當(dāng)你的組件開始覺得密集或難以閱讀時(shí),在多個(gè) property 之間添加空行可以讓其變得容易。在一些諸如 Vim 的編輯器里,這樣格式化后的選項(xiàng)還能通過鍵盤被快速導(dǎo)航。
props: {
value: {
type: String,
required: true
},
focused: {
type: Boolean,
default: false
},
label: String,
icon: String
},
computed: {
formattedValue() {
// ...
},
inputClasses() {
// ...
}
}
// 沒有空行在組件易于閱讀和導(dǎo)航時(shí)也沒問題。
props: {
value: {
type: String,
required: true
},
focused: {
type: Boolean,
default: false
},
label: String,
icon: String
},
computed: {
formattedValue() {
// ...
},
inputClasses() {
// ...
}
}
單文件組件應(yīng)該總是讓 <script>
、<template>
和 <style>
標(biāo)簽的順序保持一致。且 <style>
要放在最后,因?yàn)榱硗鈨蓚€(gè)標(biāo)簽至少要有一個(gè)。
<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>
<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
<!-- 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>
scoped
中的元素選擇器謹(jǐn)慎使用
元素選擇器應(yīng)該避免在 scoped
中出現(xiàn)。
在 scoped
樣式中,類選擇器比元素選擇器更好,因?yàn)榇罅渴褂迷剡x擇器是很慢的。
詳解
為了給樣式設(shè)置作用域,Vue 會為元素添加一個(gè)獨(dú)一無二的 attribute,例如 data-v-f3f3eg9。然后修改選擇器,使得在匹配選擇器的元素中,只有帶這個(gè) attribute 才會真正生效 (比如 button[data-v-f3f3eg9])。
問題在于大量的元素和 attribute 組合的選擇器 (比如 button[data-v-f3f3eg9]) 會比類和 attribute 組合的選擇器慢,所以應(yīng)該盡可能選用類選擇器。
<template>
<button>×</button>
</template>
<style scoped>
button {
background-color: red;
}
</style>
<template>
<button class="btn btn-close">×</button>
</template>
<style scoped>
.btn-close {
background-color: red;
}
</style>
應(yīng)該優(yōu)先通過 prop 和事件進(jìn)行父子組件之間的通信,而不是 this.$parent
或變更 prop。
一個(gè)理想的 Vue 應(yīng)用是 prop 向下傳遞,事件向上傳遞的。遵循這一約定會讓你的組件更易于理解。然而,在一些邊界情況下 prop 的變更或 this.$parent
能夠簡化兩個(gè)深度耦合的組件。
問題在于,這種做法在很多簡單的場景下可能會更方便。但請當(dāng)心,不要為了一時(shí)方便 (少寫代碼) 而犧牲數(shù)據(jù)流向的簡潔性 (易于理解)。
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>
`
})
app.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
template: `
<input
:value="todo.text"
@input="$emit('input', $event.target.value)"
>
`
})
app.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
template: `
<span>
{{ todo.text }}
<button @click="$emit('delete')">
×
</button>
</span>
`
})
應(yīng)該優(yōu)先通過 Vuex 管理全局狀態(tài),而不是通過 this.$root
或一個(gè)全局事件總線。
通過 this.$root
和/或全局事件總線管理狀態(tài)在很多簡單的情況下都是很方便的,但是并不適用于絕大多數(shù)的應(yīng)用。
Vuex 是 Vue 的官方類 flux 實(shí)現(xiàn),其提供的不僅是一個(gè)管理狀態(tài)的中心區(qū)域,還是組織、追蹤和調(diào)試狀態(tài)變更的好工具。它很好地集成在了 Vue 生態(tài)系統(tǒng)之中 (包括完整的 Vue DevTools 支持)。
// 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)
}
}
})
// 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>
更多建議: