Vue Components
Vue:2.5
Vue
的實作是由一個一個的 Components
建立起來的。因此,開發者可以透過 Components
的方式自訂元件以供使用。
(圖片來源:Composing with Components)
Basic Components
Vue
的元件在包裝好之後,會以類似 HTML tag 的方式使用。
下面就以 計算按鍵次數
為例來當 demo。
範例
<div id="app">
<!-- 使用元件 -->
<button-counter></button-counter>
</div>
// 建立元件(Global)
Vue.component('button-counter', {
data: function () {
return {
count: 0
};
},
// 元件的 HTML
template: '<button @click="count++">You clicked me {{ count }} times.</button>'
});
const vm = new Vue({
el: '#app',
});
Notice: 在
Vue Instance
中的data
可以使用Object
或Function
,但是在Components
中只能使用Function
。
原因是每個Components
的資料都要是獨立運作的。細節可參考官網說明。
除了以 Global
的方式註冊元件之外,也可以透過參數的方式代入元件。
範例
const button = {
data: function () {
return {
count: 0
};
},
template: '<button @click="count++">You clicked me {{ count }} times.</button>'
};
// 以參數的方式帶入元件
const vm = new Vue({
el: '#app',
components: {
'button-counter': button
}
});
Templates
引入元件的 template
有兩種做法:String Template
、DOM Template
。
其中 String Template
方法就是前面提到的做法,直接利用字串塞到 Component 的 template
參數裡面。但是這個做法的缺點很明顯,當你的介面複雜就會讓 HTML 變得特別冗長及難閱讀。
而 DOM Template
可以解決這個問題,DOM Template
主要是透過 <script type="text/x-template">
建立,這樣一來就可以保持原本 HTML 的縮排,可讀性就跟著增高了。
範例
<div id="app">
<button-counter></button-counter>
</div>
<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
<div>
<button @click="count++">You clicked me {{ count }} times.</button>
</div>
</script>
Vue.component('button-counter', {
data: function () {
return {
count: 0
};
},
// 指定 template
template: '#button-counter-component'
});
const vm = new Vue({
el: '#app',
});
Template
部份還有一個隱藏的地雷,在 HTML 中的 <ul>
、<ol>
、<table>
、<select>
這幾個 tag 底下是不能隨便塞其他的 tag 的,例如 <ul>
底下就是要 放 <li>
。因此,當我們的 Component 遇到這幾個例外的 tag 如果沒有處理好會導致瀏覽器 parse 出來的結構有問題。
例如:
<div id="app">
<table>
<table-render></table-render>
</table>
</div>
跑出來的結果會變成:
<div id="app">
<tr>
<td>TEST</td>
</tr>
<table></table>
</div>
為了因應這個案例,Vue
提供了 is
的語法引入 Template
。如果是上述特例的 tag ,就可以利用 is
語法繞過原本的錯誤。
<div id="app">
<table>
<tr is="table-render"></tr>
</table>
</div>
另外還有一個小地方要注意,很多人會把 Template
寫成這樣:
<template>
<button>Open Modal</button>
<modal>XXXXX</modal>
</template>
這樣寫的話會出現 Component template should contain exactly one root element
的錯誤。
基本上在 Component Template
中要有一個最外層的 Root element
,不然沒辦法知道這個元件的邊界在哪。(來源 Vue#7088)
元件的資料傳遞
既然 Vue
是透過多個元件組合而成,那一定會遇到資料傳遞的問題。
在 Vue
裡,由 父元件
傳遞到 子元件
的動作叫 Pass Props
,由 子元件
傳遞到 父元件
的動作叫 Emit Events
。
(圖片來源:Kuro's Blog)
Emit Events
子元件
需要透過 Event
才能與 父元件溝通
。實作上我們必須先自訂一個 Event
,再將 Event
拋給 父元件
。
以剛剛 計算按鍵次數
的例子來說,當我有兩顆一模一樣的按鍵時,其實它們都是各自擁有自己的 count
data,如果我想要加總,就必須拋給 父元件
整合。
範例
<div id="app">
Total: {{ total }}
<!-- 自訂一個 to-father 的 Event,內容是呼叫父元件的 parentGetOne 方法 -->
<button-counter @to-father="parentGetOne()"></button-counter>
<button-counter @to-father="parentGetOne()"></button-counter>
</div>
<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
<button @click="getOne()">You clicked me {{ count }} times.</button>
</script>
// 子元件
Vue.component('button-counter', {
data: function () {
return {
count: 0
};
},
methods: {
getOne: function () {
this.count++;
// 透過 $emit 將自訂的 to-father Event 丟給父元件
this.$emit('to-father');
}
},
template: '#button-counter-component'
});
// 父元件
const vm = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
parentGetOne: function () {
this.total++;
}
}
});
除了可以呼叫 父元件
的方法之外,也可以透過 $emit
將 子元件
的資料拋給父類別。
範例
<button-counter @to-father="parentGetOne"></button-counter>
<!-- 也可以寫成代入 ...arguments -->
<button-counter @to-father="parentGetOne(...arguments)"></button-counter>
// 子元件
// $emit 第一個參數為事件名稱,後面傳入的皆是參數
this.$emit('to-father', this.count, 'JohnsonLu');
// 父元件
const vm = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
parentGetOne: function (count, name) {
console.log(count);
console.log(name);
}
}
});
Pass Props
當 父元件
要將資料傳給 子元件
時,就要透過 v-bind
搭配 props
參數來控制。
以 Emit Events
的範例為例,利用 props
在 父元件
上多一個計數器預設值的功能,讓其他 子元件
可以使用。
範例
<div id="app">
Total: {{ total }}
<!-- 透過 v-bind 綁定 initial-counter 屬性,並代入父元件的資料 -->
<button-counter :initial-counter="button1" @to-father="parentGetOne"></button-counter>
<!-- 也可以直接傳入 static value -->
<button-counter :initial-counter="button2" title="Static Value" @to-father="parentGetOne"></button-counter>
</div>
<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
<button @click="getOne()">You clicked me {{ count }} times.</button>
</script>
// 子元件
Vue.component('button-counter', {
// 接受父元件的資料(注意 camelCase 和 kebab-case 的問題)
props: ['initialCounter', 'title'],
data: function () {
return {
count: this.initialCounter
};
},
methods: {
getOne: function () {
this.count++;
// 透過 $emit 將 Event 丟給父元件
this.$emit('to-father');
}
},
template: '#button-counter-component'
});
// 父元件
const vm = new Vue({
el: '#app',
data: {
// 初始化計數器的值
button1: 10,
button2: 20,
total: 30,
},
methods: {
parentGetOne: function (count, name) {
this.total++;
}
}
});
Notice: HTML 是不區分大小寫的,因此如果遇到像
initialCounter
的問題,在 HTML 會以-
隔開(camelCase 和 kebab-case)。
Props
也可以進行基本的 Validation
,細節可以參考 官方文件。
範例
props: {
initialCounter: Number,
propA: {
type: String,
// 設定預設值
default: 'No data'
},
propB: {
// 可以同時 allow 多種資料型態
type: [String, Number],
// 設定該參數為必要參數
required: true
}
}
Event Bus
根據網站的複雜度,元件不會只有單純的父子關係,也會有一些平行的元件需要溝通,再以 計算按鍵次數
為例的話,如果要清除兩個 button 和 total 的數字就會不太好處理,所以我們必須透過 Event Bus
和 $on
來解決各個元件的溝通問題。
Event Bus
可以巡迴各個元件之間,幫助這些元件溝通。
(圖片來源:Kuro's Blog)
實作的步驟如下:
- 建立
Event Bus
元件 - 各元件新增
reset
method - 建立
button-reset
元件,並新增reset
方法,當觸發時會透過$emit
呼叫對方元件的reset event
- 各元件在
created
階段註冊綁定Event Bus
拋過來的事件
範例
<div id="app">
Total: {{ total }}
<button-counter :initial-counter="button1" @to-father="parentGetOne"></button-counter>
<button-counter :initial-counter="button2" @to-father="parentGetOne"></button-counter>
<!-- 新增 Reset 元件 -->
<button-reset></button-reset>
</div>
<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
<button @click="getOne()">You clicked me {{ count }} times.</button>
</script>
// Event Bus
const bus = new Vue();
// Reset 元件
Vue.component('button-reset', {
template: '<button @click="reset">reset</button>',
methods: {
reset: function () {
bus.$emit('reset');
},
}
});
// 子元件
Vue.component('button-counter', {
// 接受父元件的資料(注意 camelCase 和 kebab-case 的問題)
props: ['initialCounter'],
data: function () {
return {
count: this.initialCounter
};
},
methods: {
getOne: function () {
this.count++;
// 透過 $emit 將 Event 丟給父元件
this.$emit('to-father');
},
// 子元件的 reset method
reset: function () {
this.count = 0;
}
},
// 當元件建立時綁定
created: function () {
// 用 $on 接收 $emit 的 trigger
bus.$on('reset', this.reset);
},
template: '#button-counter-component'
});
// 父元件
const vm = new Vue({
el: '#app',
data: {
// 初始化計數器的值
button1: 10,
button2: 20,
total: 30,
},
methods: {
parentGetOne: function (count, name) {
this.total++;
},
// 父元件的 reset method
reset: function () {
this.total = 0;
},
},
// 當元件建立時綁定
created: function () {
// 用 $on 接收 $emit 的 trigger
bus.$on('reset', this.reset);
}
});
Single File Components
隨著元件數一多,在架構上就必須拆出來統一管理,因此可以透過 Single File Components
的特性將元件封裝成多個 .vue
檔。
範例
<!-- Hello.vue -->
<!-- Component template-->
<template>
<p>{{ greeting }} World!</p>
</template>
<!-- Component object -->
<script>
module.exports = {
data: function () {
return {
greeting: 'Hello'
}
}
}
</script>
<!-- Component CSS -->
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
// 引入 Hello Component
import Hello from './Hello.vue';
const vm = new Vue({
el: '#app',
data: {
total: 30,
},
components: {
'hello-button': Hello
}
});
Notice:
Single File Components
功能要使用有掛載vue-loader
的Webpack
編譯才可以執行。這邊要注意一下
Hello.vue
中的<style scoped>
,scoped
參數代表這個 CSS 設定只限於這個元件,不會影響到Global
,scoped
也不會套用到動態新增的 HTML 中。