什么是組件
組件(Component)是 Vue.js 最強大的功能之一。(好比電腦中的每一個元件(鍵盤,鼠標,CPU),它是一個具有獨立的邏輯和功能或界面,同時又能根據規定的接口規則進行互相融合,變成一個完整的應用)
頁面就是由一個個類似這樣的部分組成的,比如:導航、列表、彈窗、下拉菜單等,頁面只不過是這些組件的容器,組件自由結合形成功能完整的界面,當不需要某個組件,或者想要替換某個組件時,可以隨時進行替換和刪除,而不影響整個應用的運行
前端組件化的核心思想就是將巨大復雜的東西拆分成粒度合理的小東西

使用組件的好處
提高開發效率,方便重復使用,簡化調試步驟,提升整個項目的可維護性,便於協同開發
前端組件化的必要性
多年以前前端開發者們就一直嘗試組件化的道路上不斷探索,從一開始YUI、ExtJs到現在的Angular 、React、Vue、Bootstrap等,前端的組件化道路從來沒有停止過
組件的特性
高內聚性,組件功能必須是完整的,如我要實現下拉菜單功能,那在下拉菜單這個組件中,就把下拉菜單所需要的所有功能全部實現
低耦合性,通俗點說,代碼獨立不會和項目中的其他代碼發生沖突,實際工程中,我們經常會涉及到團隊協作,傳統按照業務線去編寫代碼的方式,就很容易相互沖突,所以運用組件化方式就可以大大避免這種沖突的存在
每個組件都有自己清晰的職責,完整的功能,較低的耦合便於單元測試和重復利用
vue中的組件
Vue中組件是一個自定義標簽,vue.js的編譯器為它添加特殊功能,也可以擴展原生的html元素,封裝成可重復使用的代碼
vue組件的基本組成(單文件組件):Js,css,html 存在一個文件中,是一個單文件組件,下面vue模板文件里的Hello.vue就是一個單文件組件
<template> <div class="hello"> <h2>{{ msg }}</h2> </div> </template> <script> export default { name: 'hello', data () { return { msg: 'Hello Vue' } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h1, h2 { font-weight: normal; } </style>
組件的引入方式一
首先定義兩個組件:Hello.vue和HelloWorld.vue
<template> <div class="hello"> <h2>{{ msg }}</h2> </div> </template> <script> export default { name: 'hello', data () { return { msg: 'Hello Vue' } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h1, h2 { font-weight: normal; } </style>
<template> <div class="helloWorld"> {{ helloWorld }} </div> </template> <script type="text/ecmascript-6"> export default { name: 'HelloWorld', data () { return { helloWorld: 'Hello World' } } } </script> <style scoped></style>
然后再父組件中引入這兩個子組件,引入的時候需要注意標簽名的寫法
<template> <div id="app"> {{ message }} <hello></hello> <!-- 如果組件名是這樣的形式:AaaBbb,那么在標簽這里應該這樣寫:aaa-bbb的格式 --> <hello-world></hello-world> </div> </template> <script> import Hello from './components/Hello' import HelloWorld from './components/HelloWorld' export default { components: { HelloWorld, Hello }, name: 'app', data () { return { message: 'Welcome' } } } </script> <style> </style>

組件引入方式二(動態組件)
這種引入方式的好處就是可以動態的引入組件,就是將組件作為一個變量,下面示例中如果comToRender這個變量的值發生改變那么引入的組件就發生改變
<keep-alive></keep-alive>標簽,這個標簽的作用會將子組件緩存起來,比如兩個組件之間相互切換,當a組件,切換到b組件的時候,a組件會被緩存起來,切換回a組件的時候,b組件緩存起來
下面來做一個示例,點擊切換按鈕后,切換引入的組件
<template> <div id="app"> <button v-on:click="change">切換</button> <!-- 引入組件還有下面這種方式:這表示div標簽就是組件Hello --> <keep-alive><div v-bind:is="comToRender"></div></keep-alive> </div> </template> <script> import Hello from './components/Hello' import HelloWorld from './components/HelloWorld' export default { components: { HelloWorld, Hello }, name: 'app', data () { return { message: 'Welcome', comToRender: 'hello' } }, methods: { change () { if (this.comToRender === 'hello') { this.comToRender = 'hello-world' } else { this.comToRender = 'hello' } } } } </script> <style> </style>
測試,點擊切換的時候即可實現兩個組件之間切換

組件之間進行通信
組件間通信基本原則——不要在子組件中直接修改父組件的狀態數據,數據在哪, 更新數據的行為(函數)就應該定義在哪
父子組件之間是需要進行通信的,比如select列表,父組件需要告訴子組件有哪些選項,子組件也需要向父組件傳遞,比如子組件選擇了哪個選項,需要告訴父組件
prop 是父組件用來傳遞數據的一個自定義屬性。父組件的數據需要通過 props 把數據傳給子組件,子組件需要顯式地用 props 選項聲明 "prop":
父子組件通信示意圖

組件之間的通信示例一父組件向子組件傳遞數據:prop 是單向綁定的:當父組件的屬性變化時,將傳導給子組件,但是不會反過來
父組件中傳遞數據的方法有幾種
直接將一個固定的值傳遞給子組件,下面是將一個字符串為5的一個值傳給了hello-world這個子組件
<hello-world number-size="5"></hello-world>
如果是動態引入的組件,寫法也一樣,他會將這個值傳遞給每個子組件(引入那個子組件就傳給那個子組件)
<keep-alive><div v-bind:is="comToRender" number-size="5"></div></keep-alive>
使用動態綁定的方式將父組件的某個變量值傳遞給子組件,比如下面將父組件中的number這個變量傳遞給子組件使用v-bind動態綁定的方式
<hello-world v-bind:number-size="number"></hello-world>
<keep-alive><div v-bind:is="comToRender" v-bind:number-size="number"></div></keep-alive>
在傳遞數據的時候也能規定傳遞的數據類型,下面表示傳遞的是一個Number類型的數據,子組件接收的時候也要規定有Number類型,否則報錯
<keep-alive><div v-bind:is="comToRender" v-bind:number-size.number="number"></div></keep-alive>
子組件接收父組件傳遞過來的數據
需要知道的是,子組件接收到數據之后就相當於data里面有了這個變量,可以在任意地方進行使用這個變量
首先需要將父組件傳遞過來的數據進行注冊,使用props進行注冊,props接收兩種類型的值,一種是數組,一種是對象,對象可以做的事情比較多
首先來看接收數組的形式
props: ['number-size']
<template> <div class="helloWorld"> {{ helloWorld }} <p>{{ numberSize }}</p> </div> </template> <script type="text/ecmascript-6"> export default { props: ['number-size'], name: 'HelloWorld', data () { return { helloWorld: 'Hello World' } } } </script> <style scoped></style>
測試結果是可以將numberSize打印出來的
如果使用對象的方式去接收數據,可以規定接收的數據類型,比如下面是Number,如果子組件規定了只接收Number,那么父組件只能傳遞Number類型的數據給子組件,否則會報錯
props: {
'number-size': Number
}
當然可以接收多種數據類型,下面表示可以接受Number和String類型的數據,父組件就要傳遞這兩種數據類型的數據,否則就報錯了
props: {
'number-size': [Number, String]
}
組件之間的通信—子組件向外傳遞事件(自定義事件)
父組件是使用 props 傳遞數據給子組件,但如果子組件要把數據傳遞回去,就需要使用自定義事件!
我們可以使用 v-on 綁定自定義事件, 每個 Vue 實例都實現了事件接口(Events interface),即:
使用 $on(eventName) 監聽事件
使用 $emit(eventName) 觸發事件
另外,父組件可以在使用子組件的地方直接用 v-on 來監聽子組件觸發的事件。
首先子組件定義一個事件,這個事件向父組件提交子組件的數據
<template> <div class="helloWorld"> <p>{{ helloWorld }}</p> <button v-on:click="emitMyEvent">提交</button> </div> </template> <script type="text/ecmascript-6"> export default { name: 'HelloWorld', data () { return { helloWorld: 'Hello World' } }, methods: { emitMyEvent () { this.$emit('my-event', this.helloWorld) } } } </script> <style scoped></style>
父組件中接收子組件傳遞過來的數據
<template> <div id="app"> <button v-on:click="change">切換</button> <keep-alive><div v-bind:is="comToRender" v-on:my-event="getMyEvent"></div></keep-alive> </div> </template> <script> import Hello from './components/Hello' import HelloWorld from './components/HelloWorld' export default { components: { HelloWorld, Hello }, name: 'app', data () { return { message: 'Welcome', comToRender: 'hello', number: 3 } }, methods: { change () { if (this.comToRender === 'hello') { this.comToRender = 'hello-world' } else { this.comToRender = 'hello' } }, getMyEvent (param) { console.log(param) } } } </script> <style> </style>
使用 Slot 分發內容
使用 Slot 分發內容也是一種組件之間通信的方式,此方式用於父組件向子組件傳遞`標簽數據`
在父組件中的的模板標簽內可以插入元素,子組件中使用slot接收父組件插入的元素,這個功能的使用場景,比如這個子組件是一個footer或者header,這樣就可以很方便的在父組件中向子組件插入元素
<template> <div> <slot name="xxx">不確定的標簽結構 1</slot> <div>組件確定的標簽結構</div> <slot name="yyy">不確定的標簽結構 2</slot> </div> </template> <script type="text/ecmascript-6"> export default { name: 'child' } </script> <style scoped> </style>
<template> <child> <div slot="xxx">xxx 對應的標簽結構</div> <div slot="yyy"><h1>yyyy 對應的標簽結構</h1></div> </child> </template> <script> import Child from './child' export default { name: 'prent', components: { Child } } </script>

通過slot的屬性name來標識父組件的元素插入到哪個slot,當父組件中沒有對應的slot元素,那么就是顯示子組件中的slot中的內容
當父組件中沒有插入元素的時候,就會顯示子組件中的slot里面的內容, 如下父組件中solt為yyy的沒有寫上,那么就顯示子組件中的內容
<template> <child> <div style="background-color: red" @click="sss" slot="xxx">xxx 對應的標簽結構</div> </child> </template> <script> import Child from './child' export default { name: 'prent', components: { Child }, methods: { sss () { alert('sss') } } } </script> <style scoped> </style>

可以看到父組件對應的solt中的事件和樣式都是有效的
組件之間的通信:消息訂閱與發布(PubSubJS 庫)
PubSub是一種設計模式,中文叫發布訂閱模式,簡單來說就是消息發布者不直接向訂閱者發布消息,而是發布到中介,而中介根據不同主題對消息進行過濾,並通知對該主題感興趣的訂閱者。該模式在前端現在很火的組件化開發十分常用,因為該模式松耦合,易於擴展的優點正式組件化開發所需要的。
此方式可實現任意關系組件間通信(數據),在發送數據的組件中定義發布消息方法,在接收數據的組件中定義訂閱方法
兄弟組件之間的通信
<template> <div id="header"> <div class="container"> <div class="row center"> <h1>Search Github Users</h1> <input type="text" v-model="searchName"> <input type="button" value="Search" class="btn btn-primary" placeholder="請輸入github用戶名" @click="search"> </div> </div> </div> </template> <script> import PubSub from 'pubsub-js' export default { name: 'Header', data () { return { searchName: '' } }, methods: { search () { // 發布消息 const searchName = this.searchName.trim() if (searchName) { PubSub.publish('search', searchName) } } } } </script> <style> #header{ height: 200px; } .container .center{ text-align: center; } </style>
<template> <div id="main"> {{searchName}} </div> </template> <script> import PubSub from 'pubsub-js' export default { name: 'Main', data () { return { searchName: '' } }, mounted () { // 訂閱消息(一般定義在mounted周期函數中,當訂閱了該消息的組件發布了消息,這邊就能接收到) PubSub.subscribe('search', (msg, searchName) => { // 這里使用箭頭函數,那么函數體內的this就是外部的this(vue實例對象),如果不是箭頭函數那么this就不是vue實例了 this.searchName = searchName }) } } </script>
<template> <div id="app"> <search-header></search-header> <serach></serach> </div> </template> <script> import Header from './view/header' import Serach from './view/serach' export default { name: 'app', components: { 'search-header': Header, // 需要注意的是當自定義的組件名跟原生的h5標簽有沖突時,需要自定義一個別的標簽名 Serach }, data () { return {} } } </script>
父子組件之間的通信
<template> <div id="app"> <search-header></search-header> <p>{{searchName}}</p> </div> </template> <script> import Header from './view/header' import PubSub from 'pubsub-js' export default { name: 'app', components: { 'search-header': Header // 需要注意的是當自定義的組件名跟原生的h5標簽有沖突時,需要自定義一個別的標簽名 }, data () { return { searchName: '' } }, mounted () { // 訂閱消息(一般定義在mounted周期函數中,當訂閱了該消息的組件發布了消息,這邊就能接收到) PubSub.subscribe('search', (msg, searchName) => { // 這里使用箭頭函數,那么函數體內的this就是外部的this(vue實例對象),如果不是箭頭函數那么this就不是vue實例了 this.searchName = searchName }) } } </script>
<template> <div id="header"> <div class="container"> <div class="row center"> <h1>Search Github Users</h1> <input type="text" v-model="searchName"> <input type="button" value="Search" class="btn btn-primary" placeholder="請輸入github用戶名" @click="search"> </div> </div> </div> </template> <script> import PubSub from 'pubsub-js' export default { name: 'Header', data () { return { searchName: '' } }, methods: { search () { // 發布消息 const searchName = this.searchName.trim() if (searchName) { PubSub.publish('search', searchName) } } } } </script> <style> #header{ height: 200px; } .container .center{ text-align: center; } </style>
其他關系的組件之間通信也是一樣的寫法,發布消息的地方發送消息,接收消息的地方訂閱消息(寫在mounted周期函數內)
