1. 程式人生 > >Vuejs - 組件式開發

Vuejs - 組件式開發

ML ams directly 應用 時也 mut ted 生命 default

初識組件

組件(Component)絕對是 Vue 最強大的功能之一。它可以擴展HTML元素,封裝可復用代碼。從較高層面講,可以理解組件為自定義的HTML元素,Vue 的編譯器為它添加了特殊強大的功能。所有的 Vue 組件同時也都是 Vue 的實例,因此可以接受相同的選項對象(除了一些特有的選項)並提供相同的生命周期函數。

再來回顧下 你也許不知道的Vuejs - 花式渲染目標元素 中的代碼:

1
2
3
<div id="app1">
<helloworld/>
</div>
1
2
3
4
5
6
7
8
9
10
11
Vue.component("helloworld", {
template: "<h1>{{ msg }}</h1>",
data () {
return {
msg: "Hello Vue.js!"
}
}
})
var app1 = new Vue({
el: "#app1"
})

上面通過 Vue.component 註冊了一個全局組件,然後在 div#app 元素內通過 <helloworld/> 標簽直接使用。可以看出,這裏就是相當於自定義了一個 HTML 元素 helloworld,它的功能就是輸出一個內容為 msgh1 標簽。這就是一個基本全局組件的定義方式,當然你也可以註冊為局部組件:

1
2
3
4
5
6
7
8
9
10
11
12
13
var app2 = new Vue({
el: "#app2",
components: {
‘helloworld‘: {
template: "<h1>{{ msg }}</h1>",
data () {
return {
msg: "Hello Vue.js!"
}
}
}
}
})

無論是全局或者局部註冊組件,它跟上一篇中的指令註冊是非常相似的。局部註冊組件就是在創建 Vue 實例的時候添加一個 components 對象屬性,它的鍵值對就是一個自定義組件,鍵是組件名,值是創建組建的配置對象參數。當然也可以將組件定義放到單獨的文件,然後通過引入的方式,然後添加到components屬性中,這個在單文件組件中會具體講到。

組件間通信

既然說到組件,就不得不說組件間通信了,實際開發中,我們經常需要在不同組件間傳遞/共享數據,所以實現組件間通信是非常重要的。

組件間關系可以總結為 父子組件非父子組件,自然通信方式也就是這兩種了。

父子組件間通信

技術分享圖片

如上圖,在 Vue 中,父子組件的關系可以總結為 props向下傳遞,事件向上傳遞

。也就是父組件通過 prop 給子組件下發數據,子組件通過 $emit 事件, 給父組件發送數據。先來看個例子:

1
2
3
4
<div id="app3">
當前輸入內容:{{ text }}<br>
<com-input :text="text" v-on:change="handleChange"/>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Vue.component(‘com-input‘, {
props: {
text: {
type: String,
default: "請輸入"
}
},
template: ‘<input v-on:change="handleChange" v-model="msg"/>‘,
data () {
return {
// 這裏定義為 input 的 v-model綁定值
msg: this.text
}
},
methods: {
// 當input值變化時,執行函數,通過 $emit change 事件,
// 父級組件通過 v-on:change 來監聽此事件,執行相關操作
handleChange (e) {
this.$emit(‘change‘, this.msg)
}
}
})
var app3 = new Vue({
el: "#app3",
data () {
return {
text: ‘Hello Vue.js‘
}
},
methods: {
handleChange (val) {
this.text = val
}
}
})

原理解析:上面的代碼中,先通過 Vue.component 定義了 com-input 組件,給它添加了 props 屬性,用來接收父級通過屬性傳遞的屬性數據 text,這裏 text 是個對象,含有 type - 屬性值類型default - 默認值 兩個屬性。當然 props 也可以為所有從父級接受的屬性數組,有關 props 基礎知識請直接閱讀 官方文檔。然後將初始值賦值給了 data 中的 msg,該子組件的模板是個 input,通過 v-model 實現 input的值msg 的雙向綁定,當input值變化時,通過 this.$emit(‘change‘, this.msg),發出 change 事件,同時將當前值作為監聽器回調參數,這樣父級組件就可以通過 v-on:change 來監聽此事件,獲取修改後的值,執行相關操作了。

雖然這段代碼同時實現了上述圖片中的 父 -> 子子 -> 父 通信流程,但是代碼還是比較繁瑣的,單純實現單個數據的循環傳遞,就需要父子組件同時監聽改變事件,執行監聽回調函數,是不是太麻煩了。要是能直接修改 props 中的 text 值就好了,實踐證明,這是不行的,因為直接修改,會報下面錯誤(註意只有引入 vue.js 文件才會出現,因為 vue.min.js 文件移除了 [Vue warn] 錯誤提示功能):

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value. Prop being mutated: “text”

這個問題,Vue 作者早就想到了,那就是使用 .sync 修飾符。早在 1.x 版本中此功能是一直存在的,但是作者認為它破壞了 單向數據流 的原則,所以 2.0 發布後,就移除了該修飾符,但是後來發現在實際開發中,有很多相關需求, 於是在 2.3.0+ 版本後,又重新引入了 .sync 修飾符,不過內部實現是跟 1.x 版本有區別的,它並沒有破壞 單向數據流 原則,實際上內部就是幫我們實現了父級組件監聽和修改相關屬性值的操作。

使用 .sync 修改後的代碼如下:

1
2
3
4
<div id="app4">
當前輸入內容:{{ text }}<br>
<com-input2 v-bind:text.sync="text"/>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Vue.component(‘com-input2‘, {
props: {
text: {
type: String,
default: "請輸入"
}
},
template: ‘<input v-on:change="handleChange" v-model="msg"/>‘,
data () {
return {
msg: this.text
}
},
methods: {
handleChange (e) {
this.$emit(‘update:text‘, this.msg)
}
}
})
var app4 = new Vue({
el: "#app4",
data () {
return {
text: ‘Hello Vue.js‘
}
}
})

這次我們只是將子組件的 $emit 事件名修改為 update:text,並刪除了父級組件 v-on:change 監聽和相關監聽回調,並在模板中 v-bind:text 後面添加了 .sync 修飾符,這樣就是實現了相同的功能,代碼確實精簡了很多。實際上 Vue 在編譯含有 .sync 修飾符的 v-bind 指令時,會自動實現監聽 update 事件的相關代碼,也就是:

1
<com-input2 v-bind:text.sync="text"/>

會被擴展為:

1
<com-input2 v-bind:text="text" v-on:update="val => text = val"/>

註意:val => text = val 是箭頭函數,關於箭頭函數的介紹可以看這裏:箭頭函數

這樣一解析就很好理解了,全部是我們上一節講到的內容。

非父子組件間通信

如果是兩個非父子組件,並且有共同的父級組件,那麽它拆解為 子 -> 父 -> 子 的過程,這個就完全可以使用 父子組件間通信 方法實現。如果是多個組件或者不同父組件的組件間通信,這時我們可以借助創建空的 Vue 實例作為事件總線,通過 發布訂閱模式 進行數據傳遞。 代碼如下:

1
2
3
4
<div id="app5">
組件A: <com-a></com-a><br>
組件B: <com-b></com-b>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var bus = new Vue()
Vue.component(‘com-a‘, {
template: ‘<input v-on:change="handleChange" v-model="msg"/>‘,
data () {
return {
msg: ‘Hello Vue.js‘
}
},
methods: {
handleChange() {
bus.$emit(‘a-change‘, this.msg)
}
},
created () {
var me = this
bus.$on(‘b-change‘, function (msg) {
me.msg = msg
})
}
})
Vue.component(‘com-b‘, {
template: ‘<input v-on:change="handleChange" v-model="msg"/>‘,
data () {
return {
msg: ‘Hello Vue.js‘
}
},
methods: {
handleChange() {
bus.$emit(‘b-change‘, this.msg)
}
},
created () {
var me = this
bus.$on(‘a-change‘, function (msg) {
me.msg = msg
})
}
})
var app5 = new Vue({
el: ‘#app5‘
})

熟悉 發布訂閱模式 的同學,應該很容易理解上面這段代碼,創建的全局空 Vue 實例 bus 就是用來充當中央事件總線,所有的事件都經過它來觸發和傳播。

思路解析:在組件 com-a 中,當 input 值發生改變時,通過 bus.$emit(‘a-change‘, this.msg) 來觸發修改事件,並將其更新後的值做為參數傳遞,組件 com-b 通過 bus.$on(‘a-change‘, xxx) 來監聽,進行值更新操作,組件 com-b 也是相同原理。

當然在復雜情況下,我們應該考慮使用專門的 狀態管理模式,比如 vuex,這個將在後續的文章中講到。

動態組件

Vue 中還提供了 component 元素,允許我們在實際開發中,通過修改其 is 屬性值,來動態切換組件。這個在某些應用場景非常實用,筆者曾經有個需求就是,需要根據參數 type來繪制不同類型的圖表,而我的所有圖表類型都已經裝成了一個獨立的組件,所以我只需要依據此特性,通過參數 type 來動態修改元素 component 的屬性 is 為對應的組件名稱即可。

下面來看示例代碼:

1
2
3
4
<div id="app6">
<button v-on:click="changeType">改變組件</button><br>
<component v-bind:is="currentComp"></component>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var app6 = new Vue({
el: ‘#app6‘,
data () {
return {
type: ‘a‘
}
},
computed: {
currentComp () {
return this.type === ‘a‘ ? ‘com-a‘ : ‘com-b‘;
}
},
components: {
‘com-a‘: {
template: ‘<h1>我是組件a</h1>‘
},
‘com-b‘: {
template: ‘<h1>我是組件b</h1>‘
}
},
methods: {
changeType () {
this.type = this.type === ‘a‘ ? ‘b‘ : ‘a‘;
}
}
})

運行上面代碼,點擊改變組件按鈕,就可以輕松的實現組件 com-acom-b 的動態切換了,是不是很酷?趕緊動手嘗試下吧。

Vuejs - 組件式開發