參考書籍:《Vue.js 實戰》
元件與複用
為什麼使用元件
Vue 的元件就是提高複用性,讓代碼可複用。
元件用法
<div id="app">
<my-component></my-component>
</div>
元件需要注冊後才可以使用,注冊有全局注冊和局部注冊兩種方式。
- 全局注冊後,任何 Vue 執行個體都可以使用。
// 要在父執行個體中使用這個元件,必須要在執行個體建立前注冊。 Vue.component('my-component', { template: '<div>my component</div>' }); var app = new Vue({ el: '#app' });
- 在 Vue 執行個體中,使用
選項可以局部注冊元件,注冊後的元件隻有在該執行個體作用域下有效。components
var Child = { template: '<div>my component</div>' }; var app = new Vue({ el: '#app', components: { 'my-component': Child } });
渲染後的結果是:
<div id="app">
<div>my component</div>
</div>
Vue 元件的模闆在某些情況下會收到
HTML
的限制,比如
<table>
内規定隻允許是
<tr>
、
<td>
等這些表格元素,是以在
<table>
内直接使用元件是無效的,這種情況下可以使用特殊的
is
屬性來挂載元件。
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
Vue.component('my-component', {
template: '<div>my component</div>'
});
var app = new Vue({
el: '#app'
});
渲染後的結果是:
<div id="app">
<table>
<div>my component</div>
</table>
</div>
在元件中使用
data
時,必須是函數,然後将資料 return 出去。
Vue.component('my-component', {
template: '<div>my component</div>',
data() {
return {
message: 'message'
}
}
});
使用 props
傳遞資料
props
基本用法
元件不僅僅是要把模闆的内容進行複用,更重要的是元件間要進行通信。通常父元件的模闆中包含子元件,父元件要正向地向子元件傳遞資料或參數,子元件接收到後根據參數的不同來渲染不同的内容或執行操作。這個正向傳遞資料的過程就是通過
props
來實作的。
props
中聲明的資料與元件
data
函數内的資料主要差別就是
props
的資料來自父級,而
data
的資料是元件自己的資料,這兩種資料都可以在模闆
template
、計算屬性
computed
和方法
methods
中使用。
通常,傳遞的資料并不是直接寫死的,而是來自父級的動态資料,這時可以使用指令
v-bind
來動态綁定
props
的值,當父元件的資料變化時,也會傳遞給子元件。
由于
HTML
特性不區分大小寫,當時用
DOM
模闆時,駝峰命名(camelCase)的
props
名稱要轉為短橫分隔命名(kebab-case)。
<div id="app">
<input type="text" v-model="parentMessage" />
<my-component :my-message="parentMessage"></my-component>
</div>
Vue.component('my-component', {
props: ['myMessage'],
template: '<div>{{ myMessage}}</div>'
});
var app = new Vue({
el: '#app',
data: {
parentMessage: ''
}
});
渲染後的結果是:
<div id="app">
<div>dataMes</div>
</div>
這裡用
v-model
綁定了父級的資料
parentMessage
,當通過輸入框任意輸入時,子元件接收到的
props
也會實時響應,并更新元件模闆。
單向資料流
Vue 通過
props
傳遞資料是單向的,也就是父元件資料變化時會傳遞給子元件,但是反過來不行。之是以這樣設計,是盡可能将父子元件解耦,避免子元件無意中修改了父元件的狀态。
業務中會經常遇到兩種需要改變
prop
的情況。
- 一種是父元件傳遞初始值進來,子元件将它作為初始值儲存起來,在自己的作用域下可以随意使用和修改。
<div id="app"> <my-component :init-count="1"></my-component> </div>
元件中聲明了資料Vue.component('my-component', { props: ['initCount'], template: '<div>{{ count }}</div>', data() { return { count: this.initCount } } }); var app = new Vue({ el: '#app' });
,它在元件初始化時會擷取來自父元件的count
,之後就與之無關了,隻用維護initCount
,這樣就可以避免直接操作count
。initCount
- 另一種情況就是
作為需要被轉變的原始值傳入。prop
<div id="app"> <my-component :width="100"></my-component> </div>
因為用Vue.component('my-component', { props: ['width'], template: '<div :style="style">元件内容</div>', computed: { style() { return { width: this.width + 'px' } } } }); var app = new Vue({ el: '#app' });
傳遞寬度要帶機關(px),但是每次都寫太麻煩了,而且數值計算一般是不帶機關的,是以統一在元件内使用計算屬性就可以了。CSS
資料驗證
Vue.component('my-component', {
props: {
propA: Number,
propB: [String, Number],
propC: {
type: Boolean,
default: true
},
propD: {
type: Number,
required: true
},
// 如果是數組或對象,預設值必須是一個函數來傳回
propE: {
type: Array,
default() {
return [];
}
},
propF: {
validator(value) {
return value > 10;
}
}
}
});
元件通信
元件關系可分為父子元件通信、兄弟元件通信和跨級元件通信。
自定義事件
當子元件需要向父元件傳遞資料時,就要用到自定義事件。
子元件用
$emit()
來觸發事件,父元件用
$on
來監聽子元件的事件。
父元件也可以直接在子元件的自定義标簽上使用
v-on
來監聽子元件觸發的自定義事件。
<div id="app">
<p>總數:{{ total }}</p>
<my-component
@increase="handleGetTotal"
@reduce="handleGetTotal">
</my-component>
</div>
Vue.component('my-component', {
template: `
<div>
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">-1</button>
</div>
`,
data() {
return {
counter: 0
}
},
methods: {
handleIncrease() {
this.counter++;
this.$emit('increase', this.counter);
},
handleReduce() {
this.counter--;
this.$emit('reduce', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal(total) {
this.total = total;
}
}
});
上面示例中,在改變元件的
counter
後,通過
$emit()
再把它傳遞給父元件。
$emit()
方法的第一個參數是自定義事件的名稱,後面的參數是要傳遞的資料,可以不填或填寫多個。
除了用
v-on
在元件上監聽自定義事件外,也可以監聽
DOM
事件,這時可以用
.native
修飾符表示監聽的是一個原生事件,監聽的是該元件的根元素。
<my-component v-on:click.native="handleClick"></my-component>
使用 v-model
v-model
Vue 可以在自定義元件上使用
v-model
指令。
<div id="app">
<p>總數:{{ total }}</p>
<my-component v-model="total"></my-component>
</div>
Vue.component('my-component', {
template: '<button @click="handleIncrease">+1</button>',
data() {
return {
counter: 0
}
},
methods: {
handleClick() {
this.counter++;
this.$emit('input', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
}
});
在使用元件的父級,并沒有在
<my-component>
使用
@input="handler"
,而是直接用了
v-model
綁定的一個資料
total
。這也可以稱作是一個文法糖,因為上面的示例可以間接地用自定義事件來實作:
<div id="app">
<p>總數:{{ total }}</p>
<my-component @input="handleGetTotal"></my-component>
</div>
// 省略元件代碼
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal() {
this.total = total;
}
}
});
v-model
還可以用來建立自定義的表單輸入元件,進行資料雙向綁定。
<div id="app">
<p>總數:{{ total }}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
</div>
Vue.component('my-component', {
props: ['value'],
template: '<input :value="value" @input="updateValue" />',
methods: {
updateValue(event) {
this.$emit('input', event.target.value);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleReduce() {
this.total--;
}
}
});
實作這樣一個具有雙向綁定的
v-model
元件要滿足下面兩個條件:
- 接收一個
屬性。value
- 在有新的
時觸發value
事件。input
非父子元件通信
非父子元件一般有兩種,兄弟元件和跨多級元件。
在 Vue 中,推薦使用一個空的 Vue 執行個體作為中央事件總線(bus),也就是一個中介。
<div id="app">
{{ message }}
<component-a></component-a>
</div>
var bus = new Vue();
Vue.component('component-a', {
template: '<button @click="handleEvent">傳遞事件</button>',
methods: {
handleEvent() {
bus.$emit('on-message', '來自元件 component-a 的内容');
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
},
mounted() {
var _this = this;
// 在執行個體初始化時,監聽來自 bus 執行個體的事件
bus.$on('on-message', function(msg) {
_this.message = msg;
});
}
});
首先建立一個名為
bus
的空 Vue 執行個體,然後定義全局元件
component-a
,最後建立 Vue 執行個體
app
。在
app
初始化時,監聽了來自
bus
的事件
on-message
,而在元件
component-a
中,點選按鈕會通過
bus
把事件
on-message
發出去,此時
app
就會接收到來自
bus
的事件,進而在回調裡完成自己的業務邏輯。
這種方法巧妙而輕量地實作了任何元件間的通信,包括父子、兄弟和跨級。如果深入使用,可以擴充
bus
執行個體,給它添加
data
、
methods
和
computed
等選項,這些都是可以共用的,在業務中,尤其是協同開發時非常有用,因為經常需要共享一些通用的資訊,比如使用者登入的昵稱、性别、郵箱和授權等。隻需子安初始化時讓
bus
擷取一次,任何時間、任何元件就可以從中直接使用,在單頁面富應用(SPA)中會很實用。
除了中央事件總線
bus
外,還有兩種方法可以實作元件間通信:父鍊和子元件索引。
父鍊
在子元件中,使用
this.$parent
可以直接通路該元件的父執行個體或元件,父元件也可以通過
this.$children
通路它所有的子元件,而且可以遞歸向上或向下無限通路,直到根執行個體或最内層的元件。
<div id="app">
{{ message }}
<component-a></component-a>
</div>
Vue.component('component-a', {
template: '<button @click="handleEvent">通過父鍊直接修改資料</button>',
methods: {
handleEvent() {
// 通路到父鍊後,可以做任何操作,比如直接修改資料
this.$parent.message = '來自元件 component-a 的内容'
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
}
});
盡管 Vue 允許這樣操作,但在業務中,子元件應該盡可能避免依賴父元件的資料,更不應該去主動修改它的資料,因為這樣使得父子元件耦合,隻看父元件,很難了解父元件的狀态,因為它可能被任意元件修改,理想情況下,隻有元件自己能修改它的狀态。父子元件最好還是通過
props
和
$emit()
來通信。
子元件索引
當子元件較多時,通過
this.$children
來一一周遊出我們需要的一個元件執行個體時比較困難的,尤其是元件動态渲染時,它們的序列是不固定的。Vue 提供了子元件索引的方法,用特殊的屬性
ref
來為子元件指定一個索引名稱。
<div id="app">
<button @click="handleRef">通過 ref 擷取子元件執行個體</button>
<component-a ref="comA"></component-a>
</div>
Vue.component('component-a', {
template: '<div>子元件</div>',
data() {
return {
message: '子元件内容'
}
}
});
var app = new Vue({
el: '#app',
methods: {
handleRef() {
// 通過 $refs 來通路指定的執行個體
var msg = this.$refs.comA.message;
console.log(msg);
}
}
});
提示:
$refs
隻在元件渲染完成後才填充,并且它是非響應式的。它僅僅作為一個直接通路子元件的應急方案,應當避免在模闆或計算屬性中使用
$refs
。
使用 slot
分發内容
slot
什麼是 slot
slot
下面是一個正常的網站布局元件化後的機構:
<app>
<menu-main></menu-main>
<menu-sub></menu-sub>
<div class="container">
<menu-left></menu-left>
<container></container>
</div>
<app-footer><app-footer>
</app>
當需要讓元件組合使用,混合父元件的内容與子元件的模闆時,就會用到
slot
,這個過程叫做内容分發(transclusion)。以
<app>
為例,它有兩個特點:
-
元件不知道它的挂載點會有什麼内容。挂載點的内容由<app>
的父元件決定。<app>
-
元件很可能有它自己的模闆。<app>
props
傳遞資料、
events
觸發事件和
slot
内容分發就構成了 Vue 元件的 3 個 API來源,再複雜的元件也是由這 3 部分構成的。
作用域
父元件模闆的内容是在父元件作用域内編譯,子元件模闆的内容是在子元件作用域内編譯。
<div id="app">
<child-component v-show="showChild"></child-component>
</div>
Vue.component('child-component', {
template: '<div>子元件</div>'
});
var app = new Vue({
el: '#app',
data: {
showChild: true
}
});
這裡的狀态
showChild
綁定的是父元件的資料,如果想在子元件上綁定,應該是:
<div id="app">
<child-component></child-component>
</div>
Vue.component('child-component', {
template: '<div v-show="showChild">子元件</div>',
data() {
return {
showChild: true
}
}
});
var app = new Vue({
el: '#app'
});
是以,
slot
分發的内容,作用域是在父元件上的。
slot
用法
slot
單個 slot
slot
在子元件内使用特殊的
<slot>
元素就可以為這個子元件開啟一個
slot
(插槽),在父元件模闆裡,插入在子元件标簽内的所有内容将替代子元件的
<slot>
标簽及它的内容。
<div id="app">
<child-component>
<p>分發的内容</p>
<p>更多分發的内容</p>
</child-component>
</div>
Vue.component('child-component', {
template: `
<div>
<slot>
<p>如果父元件沒有插入内容,我将作為預設出現</p>
</slot>
</div>
`
});
var app = new Vue({
el: '#app'
});
上例渲染後的結果是:
<div id="app">
<div>
<p>分發的内容</p>
<p>更多分發的内容</p>
</div>
</div>
注意:子元件
<slot>
内的備用内容,它的作用域是子元件本身。
具名 slot
slot
給
<slot>
元素指定一個
name
後可以分發多個内容,具名
slot
可以與單個
slot
共存。
<div id="app">
<child-component>
<h2 slot="header">标題</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部資訊</div>
</child-component>
</div>
Vue.component('child-component', {
template: `
<div class="container">
<div class="header">
<slot name="header"></slot>
</div>
<div class="main">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`
});
var app = new Vue({
el: '#app'
});
上例渲染後的結果是:
<div id="app">
<div class="container">
<div class="header">
<h2>标題</h2>
</div>
<div class="main">
<p>正文内容</p>
<p>更多的正文内容</p>
</div>
<div class="footer">
<div>底部資訊</div>
</div>
</div>
</div>
注意:如果沒有指定預設的匿名
slot
,父元件内多餘的内容片段都将被抛棄。
作用域插槽
作用域插槽是一種特殊的
slot
,使用一個可以複用的模闆替換已渲染元素。
<div id="app">
<child-component>
<template scope="props">
<p>來自父元件的内容</p>
<p>{{ props.msg }}</p>
</template>
</child-component>
</div>
Vue.component('child-component', {
template: `
<div class="container">
<slot msg="來自子元件的内容"><slot>
</div>
`
});
var app = new Vue({
el: '#app'
});
觀察子元件的模闆,在
<slot>
元素上有一個類似
props
傳遞資料給元件的寫法
msg="xxx"
,将資料傳到了插槽。父元件中使用了
<template>
元素,而且擁有一個
scope="props"
的特性,這裡的
props
隻是一個臨時變量,就像
v-for="item in items"
裡面的
item
一樣。
template
内可以通過臨時變量
props
通路來自子元件插槽的資料
msg
。
上例渲染後的結果是:
<div id="app">
<child-component>
<p>來自父元件的内容</p>
<p>來自子元件的内容</p>
</child-component>
</div>
作用域插槽更具代表性的用例是清單元件,允許元件自定義應該如何渲染清單每一項。
<div id="app">
<my-list :books="books">
<!-- 作用域插槽也可以是具名的 slot -->
<template slot="book" scope="props">
<li>{{ props.bookName }}</li>
</template>
</my-list>
</div>
Vue.component('my-list', {
props: {
books: {
type: Array,
default() {
return [];
}
}
},
template: `
<ul>
<slot name="book"
v-for="book in books"
:book-name="book.name"
></slot>
</ul>
`
});
var app = new Vue({
el: '#app',
data: {
books: [
{ name: '《book1》' },
{ name: '《book2》' },
{ name: '《book3》' }
]
}
});
子元件
my-list
接收一個來自父級的
prop
數組
books
,并且将它在
name
為
book
的
slot
上使用
v-for
指令循環,同時暴露一個變量
bookName
。
此例的用意主要是介紹作用域插槽的用法,并沒有加入使用場景,而作用域插槽的使用場景既可以複用子元件的
slot
,又可以是
slot
内容不一緻。如果此例還在其他元件内使用,
<li>
的内容渲染權是由使用者掌握的,而資料卻可以通過臨時變量(比如
props
)從子元件内擷取。
通路 slot
slot
Vue 提供了用來通路被
slot
分發的内容的方法
$slots
。
<div id="app">
<child-component>
<h2 slot="header">标題</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部資訊</div>
</child-component>
</div>
Vue.component('child-component', {
template: `
<div class="container">
<div class="header">
<slot name="header"></slot>
</div>
<div class="main">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`,
mounted() {
var header = this.$slots.header,
main = this.$slots.default,
footer = this.$slots.footer;
console.log(footer);
console.log(footer[0].elm.innerHTML);
}
});
var app = new Vue({
el: '#app'
});
通過
$slots
可以通路某個具名
slot
,
this.$slots.default
包括了所有沒有被包含在具名
slot
中的節點。
$slots
在業務中幾乎用不到,在用
render
函數建立元件時會比較有用,但主要還是用于獨立元件開發中。