組件注冊
組件名
在注冊一個組件的時候,我們始終需要給它一個名字。
Vue.component('my-component-name',{ /* ... */ })
該組件名就是Vue.component
的第一個參數。
組件名大小寫
定義組件名的方式有兩種:
使用短橫線分隔命名
Vue.component('my-component-name', { /* ... */ })
當使用短橫線分隔命名定義一個組件時,必須在引用這個自定義元素時使用短橫線分隔命名,例如 <my-component-name>
。
使用駝峰式命名
Vue.component('MyComponentName', { /* ... */ })
當使用駝峰式命名定義一個組件時,引用這個自定義元素時兩種命名法都可以使用。也就是說<my-component-name>
和<MyComponentName>
都是可接受的。注意,盡管如此,直接在DOM(即非字符串的模板)中使用時只有短橫線分隔命名是有效的。
全局注冊
Vue.component('my-component-name',{
// ... 選項 ...
})
這些組件是全局注冊的。也就是說它們在注冊之后可以用在任何新創建的Vue
根實例 (new Vue
)的模板中。
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
new Vue({ el: '#app' })
<div id="app">
<component-a></component-a>
<component-b></component-b>
</div>
在所有子組件中也是如此,也就是說這兩個組件在各自內部也都可以相互使用。
局部注冊
通過一個普通的JavaScript對象來定義組件。
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
然后在components
選項中定義要使用的組件。
new Vue({
el: '#app'
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
對于components
對象中的每個屬性來說,其屬性名就是自定義元素的名字,其屬性值就是這個組件的選項對象。
注意局部注冊的組件在其子組件中不可用。例如,如果你希望ComponentA
在ComponentB
中可用,則你需要這樣寫:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a':ComponentA
},
// ...
}
或者通過Babel和webpack使用ES2015模塊。
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
在 ES2015+中,在對象中放一個類似ComponentA
的變量名其實是ComponentA:ComponentA
的縮寫,即這個變量名同時是:
- 用在模板中的自定義元素的名稱
- 包含了這個組件選項的變量名
模塊系統
在模塊系統中局部注冊
創建一個components
目錄,并將每個組件放置在其各自的文件中。
然后在局部注冊之前導入每個你想使用的組件。例如,在一個假設的ComponentB.js
或ComponentB.vue
文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}
現在ComponentA
和ComponentC
都可以在ComponentB
的模板中使用了。
基礎組件的自動化全局注冊
可能你的許多組件只是包裹了一個輸入框或按鈕之類的元素,是相對通用的。我們有時候會把它們稱為基礎組件,它們會在各個組件中被頻繁的用到。
所以會導致很多組件里都會有一個包含基礎組件的長列表。
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
而只是用于模板中的一小部分。
<BaseInput v-model="searchText" @keydown.enter="search"/>
<BaseButton @click="search">
<BaseIcon name="search"/>
</BaseButton>
如果你使用了webpack,那么就可以使用 require.context
只全局注冊這些非常通用的基礎組件。這里有一份可以讓你在應用入口文件 (比如 src/main.js
) 中全局導入基礎組件的示例代碼:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
'./components', // 其組件目錄的相對路徑
false, // 是否查詢其子目錄
// 匹配基礎組件文件名的正則表達式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 獲取組件配置
const componentConfig = requireComponent(fileName)
// 獲取組件的PascalCase命名
const componentName = upperFirst(
camelCase(
// 剝去文件名開頭的 `'./` 和結尾的擴展名
fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
)
)
// 全局注冊組件
Vue.component(
componentName,
// 如果這個組件選項是通過export default導出的,
// 那么就會優先使用.default,否則回退到使用模塊的根。
componentConfig.default || componentConfig
)
})
全局注冊的行為必須在根Vue
實例(通過new Vue
)創建之前發生。
Prop
Prop的大小寫
HTML中的特性名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符。這意味著當你使用DOM中的模板時,駝峰命名法的prop
名需要使用其等價的短橫線分隔命名。
Vue.component('blog-post', {
// 在JavaScript中是駝峰命名法的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML中是短橫線分隔命名的 -->
<blog-post post-title="hello!"></blog-post>
如果使用字符串模板,那么這個限制就不存在了。
靜態和動態的Prop
可以像這樣給prop
傳入一個靜態的值。
<blog-post title="My journey with Vue"></blog-post>
prop
還可以通過v-bind
動態賦值。
<blog-post v-bind:title="post.title"></blog-post>
任何類型的值都可以傳給一個prop
。
傳入一個數字
<!-- 即便42是靜態的,我們仍然需要v-bind來告訴Vue -->
<!-- 這是一個JavaScript表達式而不是一個字符串 -->
<blog-post v-bind:likes="42"></blog-post>
<!-- 用一個變量進行動態賦值 -->
<blog-post v-bind:likes="post.likes"></blog-post>
傳入一個布爾值
<!-- 包含該prop沒有值的情況在內,都意味著true -->
<blog-post favorited></blog-post>
<!-- 即便false是靜態的,我們仍然需要v-bind來告訴Vue -->
<!-- 這是一個JavaScript表達式而不是一個字符串。-->
<base-input v-bind:favorited="false">
<!-- 用一個變量進行動態賦值。-->
<base-input v-bind:favorited="post.currentUserFavorited">
傳入一個數組
<!-- 即便數組是靜態的,我們仍然需要v-bind來告訴Vue -->
<!-- 這是一個JavaScript表達式而不是一個字符串 -->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
<!-- 用一個變量進行動態賦值 -->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
傳入一個對象
<!-- 即便對象是靜態的,我們仍然需要v-bind來告訴Vue -->
<!-- 這是一個JavaScript表達式而不是一個字符串 -->
<blog-post v-bind:comments="{ id: 1, title: 'My Journey with Vue' }"></blog-post>
<!-- 用一個變量進行動態賦值 -->
<blog-post v-bind:post="post"></blog-post>
傳入一個對象的所有屬性
如果你想要將一個對象的所有屬性都作為prop
傳入,可以使用不帶參數的v-bind
(取代v-bind:prop-name
)。
post: {
id: 1,
title: 'My Journey with Vue'
}
//下面的模板:
<blog-post v-bind="post"></blog-post>
//等價于:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
單向數據流
所有的prop
都使得其父子prop
之間形成了一個單向下行綁定:父級prop
的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的數據流向難以理解。
額外的,每次父級組件發生更新時,子組件中所有的prop
都將會刷新為最新的值。這意味著你不應該在一個子組件內部改變prop
。如果你這樣做了,Vue
會在瀏覽器的控制臺中發出警告。
這里有兩種常見的試圖改變一個prop
的情形:
- 這個
prop
用來傳遞一個初始值;這個子組件接下來希望將其作為一個本地的prop
數據來使用。在這種情況下,最好定義一個本地的data
屬性并將這個prop
用作其初始值。
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 這個
prop
以一種原始的值傳入且需要進行轉換。在這種情況下,最好使用這個prop
的值來定義一個計算屬性。
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
注意在JavaScript中對象和數組是通過引用傳入的,所以對于一個數組或對象類型的prop
來說,在子組件中改變這個對象或數組本身將會影響到父組件的狀態。
Prop驗證
我們可以為組件的prop
指定需求。如果有一個需求沒有被滿足,則Vue
會在瀏覽器控制臺中警告你。
為了定制prop
的驗證方式,可以為props
中的值提供一個帶有驗證需求的對象,而不是一個字符串數組。
Vue.component('my-component', {
props: {
// 基礎的類型檢查 (`null` 匹配任何類型)
propA: Number,
// 多個可能的類型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 帶有默認值的數字
propD: {
type: Number,
default: 100
},
// 帶有默認值的對象
propE: {
type: Object,
// 對象或數組且一定會從一個工廠函數返回默認值
default: function () {
return { message: 'hello' }
}
},
// 自定義驗證函數
propF: {
validator: function (value) {
// 這個值必須匹配下列字符串中的一個
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
當prop
驗證失敗的時候,(開發環境構建版本的)Vue
將會產生一個控制臺的警告。
注意那些prop
會在一個組件實例創建之前進行驗證,所以實例的屬性 (如data
、computed
等) 在default
或validator
函數中是不可用的。
類型檢查
type
可以是下列原生構造函數中的一個:String
、Number
、Boolean
、Function
、Object
、Array
、Symbol
。
額外的,type
還可以是一個自定義的構造函數,并且通過instanceof
來進行檢查確認。例如,給定下列現成的構造函數:
function Person (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
你可以使用:
Vue.component('blog-post', {
props: {
author: Person
}
})
來驗證author prop
的值是否是通過new Person
創建的。
非Prop的特性
一個非prop
特性是指傳向一個組件,但是該組件并沒有相應prop
定義的特性。
因為顯式定義的prop
適用于向一個子組件傳入信息,然而組件庫的作者并不總能預見組件會被用于怎樣的場景。這也是為什么組件可以接受任意的特性,而這些特性會被添加到這個組件的根元素上。
例如,想象一下你通過一個Bootstrap插件使用了一個第三方的<bootstrap-data-input>
組件,這個插件需要在其<input>
上用到一個data-date-picker
特性。我們可以將這個特性添加到你的組件實例上:
<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
然后這個data-date-picker="activated"
特性就會自動添加到<bootstrap-date-input>
的根元素上。
替換/合并已有的特性
想象一下 <bootstrap-date-input>
的模板是這樣的:
<input type="date" class="form-control">
為了給我們的日期選擇器插件定制一個主題,我們可能需要像這樣添加一個特別的類名:
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>
在這種情況下,我們定義了兩個不同的 class
的值:
-
form-control
,這是在組件的模板內設置好的 -
date-picker-theme-dark
,這是從組件的父級傳入的
對于絕大多數特性來說,從外部提供給組件的值會替換掉組件內部設置好的值。所以如果傳入 type="text"
就會替換掉 type="date"
并把它破壞!慶幸的是,class
和 style
特性會稍微智能一些,即兩邊的值會被合并起來,從而得到最終的值:form-control date-picker-theme-dark
。
禁用特性繼承
如果你不希望組件的根元素繼承特性,你可以設置在組件的選項中設置inheritAttrs: false
。例如:
Vue.component('my-component', {
inheritAttrs: false,
// ...
})
這尤其適合配合實例的 $attrs
屬性使用,該屬性包含了傳遞給一個組件的特性名和特性值,例如:
{
class: 'username-input',
placeholder: 'Enter your username'
}
有了 inheritAttrs: false
和 $attrs
,你就可以手動決定這些特性會被賦予哪個元素。在撰寫基礎組件的時候是常會用到的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})
這個模式允許你在使用基礎組件的時候更像是使用原始的HTML元素,而不會擔心哪個元素是真正的根元素:
<base-input
v-model="username"
class="username-input"
placeholder="Enter your username"
></base-input>
插槽
插槽內容
Vue
實現了一套內容分發的API,將<slot>
元素作為承載分發內容的出口。
它允許你像這樣合成組件:
<navigation-link url="/profile">
Your Profile
</navigation-link>
然后你在<navigation-link>
的模板中可能會寫為:
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>
當組件渲染的時候,這個<slot>
元素將會被替換為Your Profile
。插槽內可以包含任何模板代碼,包括 HTML。
<navigation-link url="/profile">
<!-- 添加一個 Font Awesome 圖標 -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>
甚至其它的組件。
<navigation-link url="/profile">
<!-- 添加一個圖標的組件 -->
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>
如果<navigation-link>
沒有包含一個<slot>
元素,則任何傳入它的內容都會被拋棄。
具名插槽
有些時候我們需要多個插槽。例如,一個假設的<base-layout>
組件多模板如下:
<div class="container">
<header>
<!-- 我們希望把頁頭放這里 -->
</header>
<main>
<!-- 我們希望把主要內容放這里 -->
</main>
<footer>
<!-- 我們希望把頁腳放這里 -->
</footer>
</div>
對于這樣的情況,<slot>
元素有一個特殊的特性:name
。這個特性可以用來定義額外的插槽。
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
在向具名插槽提供內容的時候,我們可以在一個父組件的<template>
元素上使用slot
特性。
<base-layout>
<template slot="header">
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>
另一種slot
特性的用法是直接用在一個普通的元素上:
<base-layout>
<h1 slot="header">Here might be a page title</h1>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<p slot="footer">Here's some contact info</p>
</base-layout>
我們還是可以保留一個未命名插槽,這個插槽是默認插槽,也就是說它會作為所有未匹配到插槽的內容的統一出口。上述兩個示例渲染出來的HTML都將會是:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
插槽的默認內容
有的時候為插槽提供默認的內容是很有用的。例如,一個<submit-button>
組件可能希望這個按鈕的默認內容是“Submit”,但是同時允許用戶覆寫為“Save”、“Upload”或別的內容。
可以在<slot>
標簽內部指定默認的內容來做到這一點。
<button type="submit">
<slot>Submit</slot>
</button>
如果父組件為這個插槽提供了內容,則默認的內容會被替換掉。
自定義事件
事件名
跟組件和prop
不同,事件名不存在任何自動化的大小寫轉換。而是觸發的事件名需要完全匹配監聽這個事件所用的名稱。如果觸發一個駝峰式命名名字的事件:
this.$emit('myEvent')
則監聽這個名字的短橫線分隔命名版本是不會有任何效果的:
<my-component v-on:my-event="doSomething"></my-component>
跟組件和prop
不同,事件名不會被用作一個JavaScript變量名或屬性名,所以就沒有理由使用駝峰式命名了。并且v-on
事件監聽器在DOM模板中會被自動轉換為全小寫 (因為HTML是大小寫不敏感的),所以v-on:myEvent
將會變成v-on:myevent
——導致myEvent
不可能被監聽到。
因此,我們推薦你始終使用短橫線分隔命名的事件名。
自定義組件的v-model
2.2.0+ 新增
一個組件上的v-model
默認會利用名為 value
的prop
和名為input
的事件,但是像單選框、復選框等類型的輸入控件可能會將value
特性用于不同的目的。model
選項可以用來避免這樣的沖突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
現在在這個組件上使用v-model
的時候:
<base-checkbox v-model="lovingVue"></base-checkbox>
這里的lovingVue
的值將會傳入這個名為checked
的prop
。同時當<base-checkbox>
觸發一個change
事件并附帶一個新的值的時候,這個lovingVue
的屬性將會被更新。
注意你仍然需要在組件的props
選項里聲明checked
這個prop
。
將原生事件綁定到組件
要在一個組件的根元素上直接監聽一個原生事件。可以使用-on
的.native
修飾符:
<base-input v-on:focus.native="onFocus"></base-input>
有的時候這是很有用的,不過在你嘗試監聽一個類似<input>
的非常特定的元素時,這并不是個好主意。比如上述<base-input>
組件可能做了如下重構,所以根元素實際上是一個<label>
元素:
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
這時,父級的.native
監聽器將靜默失敗。它不會產生任何報錯,但是onFocus
處理函數不會被調用。
為了解決這個問題,Vue提供了一個$listeners
屬性,它是一個對象,里面包含了作用在這個組件上的所有監聽器。
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}
有了這個$listeners
屬性,你就可以配合v-on="$listeners"
將所有的事件監聽器指向這個組件的某個特定的子元素。對于類似<input>
的你希望它也可以配合v-model
工作的組件來說,為這些監聽器創建一個類似下述inputListeners
的計算屬性通常是非常有用的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 將所有的對象合并為一個新對象
return Object.assign({},
// 我們從父級添加所有的監聽器
this.$listeners,
// 然后我們添加自定義監聽器,
// 或覆寫一些監聽器的行為
{
// 這里確保組件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
現在<base-input>
組件是一個完全透明的包裹器了,也就是說它可以完全像一個普通的<input>
元素一樣使用了:所有跟它相同的特性和監聽器的都可以工作。
.sync修飾符
2.3.0+ 新增
在有些情況下,我們可能需要對一個 prop 進行“雙向綁定”。不幸的是,真正的雙向綁定會帶來維護上的問題,因為子組件可以修改父組件,且在父組件和子組件都沒有明顯的改動來源。
我們推薦以 update:my-prop-name
的模式觸發事件取而代之。舉個例子,在一個包含 title
prop 的假設的組件中,我們可以用以下方法表達對其賦新值的意圖:
this.$emit('update:title', newTitle)
然后父組件可以監聽那個事件并根據需要更新一個本地的數據屬性。例如:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
為了方便起見,我們為這種模式提供一個縮寫,即 .sync
修飾符:
<text-document v-bind:title.sync="doc.title"></text-document>
當我們用一個對象同時設置多個 prop 的時候,也可以將這個 .sync
修飾符和 v-bind
配合使用:
<text-document v-bind.sync="doc"></text-document>
這樣會把 doc
對象中的每一個屬性 (如 title
) 都作為一個獨立的 prop 傳進去,然后各自添加用于更新的 v-on
監聽器。
將 v-bind.sync
用在一個字面量的對象上,例如 v-bind.sync=”{ title: doc.title }”
,是無法正常工作的,因為在解析一個像這樣的復雜表達式的時候,有很多邊緣情況需要考慮。