Vue.js 學習筆記 第7章 組件詳解

 

本篇目錄:

7.1 組件與複用css

7.2 使用props傳遞數據html

7.3 組件通信vue

7.4 使用slot分發內容android

7.5 組件高級用法webpack

7.6 其餘web

7.7 實戰:兩個經常使用組件的開發vuex

 

組件(Component)是Vue.js最核心的功能,也是整個框架設計最精彩的地方,固然也是最難掌握的。
本章將帶領你由淺入深地學習組件的所有內容,並經過幾個實戰項目熟練使用Vue組件。gulp

7.1 組件與複用

7.1.1 爲何使用組件

在正式介紹組件前,咱們先來看一個簡單的場景,如圖7-1所示:
設計模式

圖7-1中是一個很常見的聊天界面,有一些標準的控件,好比右上角的關閉按鈕、輸入框、發送按鈕等。數組

你可能要問了,這有什麼難的,不就是幾個<div><input>嗎?
好,那如今需求升級了,這幾個控件還有別的地方要用到。
沒問題,複製粘貼唄。

那若是輸入框要帶數據驗證,按鈕的圖標支持自定義呢?
這樣用JavaScript封裝後一塊兒複製吧。

那等到項目快完結時,產品經理說,全部使用輸入框的地方,都要改爲支持回車鍵提交。
好吧,給我一天的事件,我一個一個加上去。

上面的需求雖然有點變態,但倒是業務中很常見的,那就是一些控件、JavaScript能力的複用。
沒錯,Vue.js的組件就是提升重用性的,讓代碼可重用。
當學習完組件後,上面的問題就能夠分分鐘搞定了,不再用懼怕剷平經理的奇葩需求。

咱們先看一下圖7-1中的示例用組件來編寫是怎麼的,示例代碼以下:

 1 <Card style="width:350px;">
 2     <p slot="title">與 xxx 聊天中</p>
 3     <a href="#" slot="extra">
 4         <Icon type="android-close" size="18"></Icon>
 5     </a>
 6     <div style="height:100px;"></div>
 7     <div>
 8         <Row :gutter="16">
 9             <i-col span="17">
10                 <i-input v-model="value" placeholder="請輸入..."></i-input>
11             </i-col>
12             <i-col span="4">
13                 <i-button v-model="primary" icon="paper-airplane">發送</i-button>
14             </i-col>
15         </Row>
16     </div>
17 </Card>

 

是否是很奇怪,有不少咱們歷來都沒有見過的標籤,好比<Card><Row><i-col><input><i-button>等。
並且整段代碼除了內聯的幾個樣式外,一句CSS代碼也沒有,但最終實現的UI就是圖7-1的效果。

這些沒見過的自定義標籤就是組件,每一個標籤表明一個組件,在任何使用Vue的地方均可以直接使用。
接下來,咱們就看看組件的具體用法。

7.1.2 組件用法

回顧一下咱們建立Vue實例的方法:

1 var app = new Vue({
2     el: "#app"
3 });

 

組件與之相似,須要註冊以後纔可使用。註冊有全局註冊和局部註冊兩種方式。
全局註冊後,任何Vue實例均可以使用。全局註冊示例代碼以下:

1 Vue.component("my-component", {
2     // 選項
3 });

 

my-component就是註冊的組件自定義標籤名稱,推薦使用小寫加減號分割的形式命名。

要在父實例中使用這個組件,必需要在實例建立前註冊。
以後就能夠用<my-component></my-component>的形式來使用組件了。
實例代碼以下:

 1 <div id="app">
 2     <my-component></my-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         // 選項
 8     });
 9     
10     var app = new Vue({
11         el: "#app"
12     });
13 </script>

 

此時打開頁面仍是空白的,由於咱們註冊的組件沒有任何內容。
在組件選項中添加template就能夠顯示組件內容了。
實例代碼以下:

1 Vue.component("my-component", {
2     template: "<div>這裏是組件的內容</div>"
3 });

 

渲染後的結果是:

1 <div id="app">
2     <div>這裏是組件的內容</div>
3 </div>

 

template的DOM結構必須被一個元素包含,若是直接寫成「這裏是組件的內容」,不帶<div></div>是沒法渲染的。

在Vue實例中,使用components選項能夠局部註冊組件,註冊後的組件只有在該實例做用域下有效。
組件也可使用components選項來註冊組件,該組件能夠嵌套。
示例代碼以下:

 1 <div id="app">
 2     <my-component></my-component>
 3 </div>
 4 
 5 <script>
 6     var Child = {
 7         template: "<div>局部註冊組件的內容</div>"
 8     };
 9     
10     var app = new Vue({
11         el: "#app",
12         components: {
13             "my-component": Child
14         }
15     });
16 </script>

 

Vue組件的模板在某些狀況下回收到HTML的限制,好比<table>內規定只容許是<tr><td><th>等這些表格元素,因此在<table>內直接使用組件是無效的。
這種狀況下,可使用特殊的is屬性來掛載組件,示例代碼以下:

 1 <div id="app">
 2     <table>
 3         <tbody is="my-component"></tbody>
 4     </table>
 5 </div>
 6 
 7 <script>
 8     Vue.component("my-component", {
 9         template: "<div>這裏是組件的內容</div>"
10     });
11     
12     var app = new Vue({
13         el: "#app"
14     });
15 </script>

 

<tbody>在渲染時,會被替換爲組件的內容。
常見的限制元素還有<ul><ol><select>

提示:
若是使用的是字符串模板,是不受限制的,好比後面章節介紹的.vue單文件用法等。

除了template選項外,組件還能夠像Vue實例那樣使用其餘的選項,好比datacomputedmethod等。
可是在使用data時,和實例稍有區別,data必須是函數,而後將數據return出去。
例如:

 1 <div id="app">
 2     <my-component></my-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         template: "<div>{{message}}</div>",
 8         data: function() {
 9             return {
10                 message: "組件內容"
11             };
12         }
13     });
14     
15     var app = new Vue({
16         el: "#app"
17     });
18 </script>

 

JavaScript對象是引用關係,因此若是return出的對象引用了外部的一個對象,那這個對象就是共享的,任何一方修改都會同步。
好比下面的示例:

 1 <div id="app">
 2     <my-component></my-component>
 3     <my-component></my-component>
 4     <my-component></my-component>
 5 </div>
 6 
 7 <script>
 8     var data = {
 9         counter: 0
10     };
11     
12     Vue.component("my-component", {
13         template: "<button @click='counter++'>{{counter}}</button>",
14         data: function() {
15             return data;
16         }
17     });
18     
19     var app = new Vue({
20         el: "#app"
21     });
22 </script>

 

組件使用了3次,可是點擊任意一個<button>,3個按鈕的數字都會加1。
那是由於組件的data引用的是外部的對象,這確定不是咱們指望的效果。
因此給組件返回一個新的data對象來獨立,示例代碼以下:

 1 <div id="app">
 2     <my-component></my-component>
 3     <my-component></my-component>
 4     <my-component></my-component>
 5 </div>
 6 
 7 <script>
 8     Vue.component("my-component", {
 9         template: "<button @click='counter++'>{{counter}}</button>",
10         data: function() {
11             return {
12                 counter: 0
13             };
14         }
15     });
16     
17     var app = new Vue({
18         el: "#app"
19     });
20 </script>

 

這樣,點擊3個按鈕就互不影響了,徹底達到複用的目的。

7.2 使用props傳遞數據

7.2.1 基本用法

組件不只僅是要把模板的內容進行復用,更重要的是組件間要進行通訊。
一般父組件的模板中包含子組件,父組件要正向地向子組件傳遞數據或參數,子組件接收到後根據參數的不一樣來渲染不一樣的內容或執行操做。
這個正向傳遞數據的過程就是經過props來實現的。

在組件中,使用選項props來聲明須要從父級接收的數據。
props的值能夠是兩種,一種是字符串數組,一種是對象,本小節先介紹數組的用法。
好比咱們構造一個數組,接收一個來自父級的數據message,並把它在組件模板中渲染,示例代碼以下:

 1 <div id="app">
 2     <my-component message="來自父組件的數據"></my-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         props: ["message"],
 8         template: "<div>{{message}}</div>"
 9     });
10     
11     var app = new Vue({
12         el: "#app"
13     });
14 </script>

 

渲染後的結果爲:

1 <div id="app">
2     <div>來自父組件的數據</div>
3 </div>

 

props中聲明的數據與組件data函數return的數據主要區別就是props的來自父級,而data中的是組件本身的數據,做用域是組件自己,這兩種數據均可以在模板template及計算屬性computed和方法methods中使用。
上例的數據message就是經過props從父級傳遞過來的,在組件的自定義標籤上直接寫該props的名稱,若是要傳遞多個數據,在props數組中添加項便可。

因爲HTML特性不區分大小寫,當使用DOM模板時,駝峯命名(camelCase)的props名稱要轉爲短橫分隔命名(kebab-case)。例如:

 1 <div id="app">
 2     <my-component warning-text="提示信息"></my-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         props: ["warningText"],
 8         template: "<div>{{warningText}}</div>"
 9     });
10     
11     var app = new Vue({
12         el: "#app"
13     });
14 </script>

 

提示:
若是使用的是字符串模板,仍然能夠忽略這些限制。

有時候,傳遞的數據並非直接寫死的,而是來自父級的動態數據,這是可使用指令v-bind來動態綁定props的值,當父組件的數據變化時,也會傳遞給子組件。示例代碼以下:

 1 <div id="app">
 2     <input type="text" v-model="parentMessage">
 3     <my-component :message="parentMessage"></my-component>
 4 </div>
 5 
 6 <script>
 7     Vue.component("my-component", {
 8         props: ["message"],
 9         template: "<div>{{message}}</div>"
10     });
11     
12     var app = new Vue({
13         el: "#app",
14         data: {
15             parentMessage: ""
16         }
17     });
18 </script>

 

這裏用v-model綁定了父級的數據parentMessage
當經過輸入框任意輸入時,子組件接收的props(message)也會實時響應,並更新組件模板。

提示:
注意,若是你要直接傳遞數字、布爾值、數組、對象,並且不使用v-bind,傳遞的僅僅是字符串,嘗試下面的示例來對比。

 1 <div id="app">
 2     <my-component message="[1,2,3]"></my-component>
 3     <my-component :message="[1,2,3]"></my-component>
 4 </div>
 5 <script>
 6     Vue.component("my-component", {
 7         props: ["message"],
 8         template: "<div>{{message.length}}</div>"
 9     });
10     var app = new Vue({
11         el: "#app"
12     });
13 </script>

同一個組件使用了兩次,區別僅僅是第二個使用的是v-bind

渲染後的結果:第一個是7,第二個纔是數組的長度3。

7.2.2 單向數據流

Vue 2.x與Vue 1.x比較大的一個改變就是,Vue 2.x經過props傳遞數據是單向的了,也就是父組件數據變化時會傳遞給子組件,可是反過來不行。
而在Vue 1.x裏提供了.sync修飾符來支持雙向綁定。
之因此這樣設計,是儘量將父子組件解耦,避免子組件無心中修改了父組件的狀態。

業務中常常用到兩種須要改變prop的狀況,一種是父組件傳遞初始值進來,子組件將它做爲初始值保存起來,在本身的做用域下能夠隨意使用和修改。
何種狀況能夠在組件data內再聲明一個數據,引用父組件的prop,實例代碼以下:

 1 <div id="app">
 2     <my-component :init-count="1"></my-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         props: ["initCount"],
 8         template: "<div>{{count}}</div>",
 9         data: function() {
10             return {
11                 count: this.initCount
12             };
13         }
14     });
15     
16     var app = new Vue({
17         el: "#app"
18     });
19 </script>

 

組件中聲明瞭數據count,它在組件初始化時會獲取來自父組件的initCount,以後就與之無關了,只用維護count,這樣就能夠避免直接操做initCount

另外一種狀況就是prop做爲須要轉變的原始值傳入。
這種狀況用計算屬性就能夠了,示例代碼以下:

 1 <div id="app">
 2     <my-component :width="100"></my-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         props: ["width"],
 8         template: "<div :style='style'>組件內容</div>",
 9         computed: {
10             style: function() {
11                 return {
12                     width: this.width + "px"
13                 };
14             }
15         }
16     });
17     
18     var app = new Vue({
19         el: "#app"
20     });
21 </script>

 

由於用CSS傳遞寬度要帶單位(px),可是每次都寫太麻煩,並且數值計算通常是不帶單位的,因此統一在組件內使用計算屬性就能夠了。

提示:
注意,在JavaScript中對象和數組是引用類型,指向同一個內存空間,因此props是對象和數組時,在子組件內改變時會影響父組件的。

7.2.3 數據驗證

咱們上面所介紹的props選項的值都是一個數組,一開始也介紹過,除了數組外,還能夠是對象,當prop須要驗證時,就須要對象寫法。

通常當你的組件須要提供給別人使用時,推薦都進行數據驗證。
好比某個數據必須是數字類型,若是傳入字符串,就會在控制檯彈出警告。

一下是幾個prop的示例:

 1 Vue.component("my-component", {
 2     props: {
 3         // 必須是數字類型
 4         propA: Number,
 5         // 必須是字符串或數字類型
 6         propB: [String, Number],
 7         // 布爾值,若是沒有定義,默認值就是true
 8         propC: {
 9             type: Boolean,
10             default: true
11         },
12         // 數字,並且是必傳
13         propD: {
14             type: Number,
15             required: true
16         },
17         // 若是是數組或對象,默認值必須是一個函數來返回
18         propE: {
19             type: Array,
20             default: function() {
21                 rturn[];
22             }
23         },
24         // 自定義一個驗證函數
25         propF: {
26             validator: function(value) {
27                 return value > 10;
28             }
29         }
30     }
31 });

 

驗證的type類型能夠是:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function

type也能夠是一個自定義構造器,使用instanceof檢測。
prop驗證失敗時,在開發版本下會在控制檯拋出一條警告。

7.3 組件通訊

咱們已經知道,從父組件向子組件通訊,經過props傳遞數據就能夠了。
但Vue組件通訊的場景不止有這一種,概括起來,組件之間通訊能夠用圖7-2表示。

組件關係可分爲父子組件通訊、兄弟組件通訊、跨級組件通訊。
本節將介紹組中組件之間通訊的方法。

7.3.1 自定義事件

當子組件須要向父組件傳遞數據時,就要用到自定義事件。
咱們在介紹指令 v-on 時有提到,v-on除了監昕DOM事件外,還能夠用於組件之間的自定義事件。

若是你瞭解過JavaScript的設計模式一一觀察者模式,必定知道dispatchEventaddEventListener這兩個方法。
Vue組件也有與之相似的一套模式,子組件用$emit()來觸發事件,父組件用$on()來監昕子組件的事件。

父組件也能夠直接在子組件的自定義標籤上使用v-on來監昕子組件觸發的自定義事件,示例代碼以下:

 1 <div id="app">
 2     <p>總數:{{total}}</p>
 3     <my-component @increase="handleGetTotal" @reduce="handleGetTotal"></my-component>
 4 </div>
 5 
 6 <script>
 7     Vue.component("my-component", {
 8         template: "<div><button @click='handleIncrease'>+1</button><button @click='handleReduce'>-1</button></div>",
 9         data: function() {
10             return {
11                 counter: 0
12             }
13         },
14         methods: {
15             handleIncrease: function() {
16                 this.counter++;
17                 this.$emit("increase", this.counter);
18             },
19             handleReduce: function() {
20                 this.counter--;
21                 this.$emit("reduce", this.counter);
22             }
23         }
24     });
25     
26     var app = new Vue({
27         el: "#app",
28         data: {
29             total: 0
30         },
31         methods: {
32             handleGetTotal: function(total) {
33                 this.total = total;
34             }
35         }
36     });
37 </script>

 

上面示例中,子組件有兩個按鈕,分別實現加1和減1的效果,在改變組件的data(counter)後,經過$emit()再把它傳遞給父組件,父組件用v-on:increasev-on:reduce(示例使用的是語法糖)。
$emit()方法的第一個參數是自定義事件的名稱,例如示例的increasereduce後面的參數都是要傳遞的數據,能夠不填或填寫多個。

除了用v-on在組件上監聽自定義事件外,也能夠監聽DOM事件,這時可使用.native修飾符表示監聽的是一個原生事件,監聽的是該組件的根元素,實例代碼以下:

1 <my-component v-on:click.native="handleClick"></my-component>

 

7.3.2 使用v-model

Vue 2.x能夠在自定義組件上使用v-model指令,咱們先來看一個示例:

 1 <div id="app">
 2     <p>總數:{{total}}</p>
 3     <my-component v-model="total"></my-component>
 4 </div>
 5 
 6 <script>
 7     Vue.component("my-component", {
 8         template: "<button @click='handleClick'>+1</button>",
 9         data: function() {
10             return {
11                 counter: 0
12             }
13         },
14         methods: {
15             handleClick: function() {
16                 this.counter++;
17                 this.$emit("input", this.counter);
18             }
19         }
20     });
21     var app = new Vue({
22         el: "#app",
23         data: {
24             total: 0
25         }
26     });
27 </script>

 

仍然是點擊按鈕加1的效果,不過此次組件$emit()的事件名是特殊的input,在使用組件的父級,並無在<my-component>上使用@input='handler',而是直接用了v-model綁定的一個數據total
這也能夠稱做是一個語法糖,由於上面的示例能夠間接地用自定義事件來實現:

 1 <div id="app">
 2     <p>總數:{{total}}</p>
 3     <my-component @input="handleGetTotal"></my-component>
 4 </div>
 5 
 6 <script>
 7     Vue.component("my-component", {
 8         template: "<button @click='handleClick'>+1</button>",
 9         data: function() {
10             return {
11                 counter: 0
12             }
13         },
14         methods: {
15             handleClick: function() {
16                 this.counter++;
17                 this.$emit("input", this.counter);
18             }
19         }
20     });
21     
22     var app = new Vue({
23         el: "#app",
24         data: {
25             total: 0
26         },
27         methods: {
28             handleGetTotal: function(total) {
29                 this.total = total;
30             }
31         }
32     });
33 </script>

 

v-model還能夠用來建立自定義的表單輸入組件,進行數據雙向綁定,例如:

 1 <div id="app">
 2     <p>總數:{{total}}</p>
 3     <my-component v-model="total"></my-component>
 4     <button @click="handleReduce">-1</button>
 5 </div>
 6 
 7 <script>
 8     Vue.component("my-component", {
 9         props: ["value"],
10         template: "<input :value='value' @input='updateValue'>",
11         methods: {
12             updateValue: function(event) {
13                 this.$emit("input", event.target.value);
14             }
15         }
16     });
17     
18     var app = new Vue({
19         el: "#app",
20         data: {
21             total: 0
22         },
23         methods: {
24             handleReduce: function() {
25                 this.total--;
26             }
27         }
28     });
29 </script>

 

實現這樣一個具備雙向綁定的v-model組件要知足下面兩個要求:

  • 接收一個value屬性。
  • 在有新的value時觸發input事件。

7.3.3 非父子組件通訊

在實際業務中,除了父子組件通訊外,還有不少非父子組件通訊的場景,非父子組件通常有兩種:兄弟組件和跨多級組件。
爲了更加完全地瞭解Vue.js 2.x中的通訊方法,咱們先來看一下在Vue.js 1.x中是如何實現的,這樣便於咱們瞭解Vue.js的設計思想。

在Vue.js 1.x中,除了$emit()方法外,還提供了$dispatch()$broadcast()這兩個方法。
$dispatch()用於向上級派發事件,只要是它的父級(通常或多級以上),均可以在Vue實例的events選項內接收,示例代碼以下:

 1 <!-- 注意:該示例須要使用Vue.js 1.x的版本 -->
 2 <div id="app">
 3     {{message}}
 4     <my-component></my-component>
 5 </div>
 6 
 7 <script>
 8     Vue.component("my-component", {
 9         template: "<button @click='handleDispatch'>源發事件</button>",
10         methods: {
11             handleDispatch: function() {
12                 this.$dispatch("on-message", "來自內部組件的數據");
13             }
14         }
15     });
16     
17     var app = new Vue({
18         el: "#app",
19         data: {
20             message: ""
21         },
22         methods: {
23             "on-message": function(msg) {
24                 this.message = msg;
25             }
26         }
27     });
28 </script>

 

同理,$broadcast()是由上級向下級廣播事件的,用法徹底一致,只是方向相反。

這兩種方法一旦發出事件後,任何組件都是能夠接收到的,就近原則,並且會在第一次接收到後中止冒泡,除非返回true

這兩個方法雖然看起來很好用,可是在Vue.js 2.x中都廢棄了,由於基於組件樹結構的事件流方式讓人難以理解,而且在組件結構擴展的過程當中會變得愈來愈脆弱,而且不能解決兄弟組件通訊的問題。

在Vue.js 2.x中,推薦使用一個空的Vue實例做爲中央事件總線(bus),也就是一箇中介。
爲了更形象地瞭解它,咱們舉一個生活中的例子。

好比你須要租房子,你可能會找房產中介來等級你的需求,而後中介把你的信息發給知足要求的出租者,出租者再把報價和看房時間告訴中介,由中介再轉達給你。
整個過程當中,買家和賣家並無任何交流,都是經過中間人來傳話的。

或者你最近可能要換房了,你會找房產中介登記你的信息,訂閱與你找房需求相關的資訊。
一旦有符合你的房子出現時,中介會通知你,並傳達你房子的具體信息。

這兩個例子中,你和出租者擔任的就是兩個跨級的組件,而房產中介就是這個中央事件總線(bus)。
好比下面的示例代碼:

 1 <div id="app">
 2     {{message}}
 3     <my-component-a></my-component-a>
 4 </div>
 5 
 6 <script>
 7     var bus = new Vue();
 8     
 9     Vue.component("my-component-a", {
10         template: "<button @click='handleEvent'>傳遞事件</button>",
11         methods: {
12             handleEvent: function() {
13                 bus.$emit("on-message", "來自組件 component-a 的內容");
14             }
15         }
16     });
17     
18     var app = new Vue({
19         el: "#app",
20         data: {
21             message: ""
22         },
23         mounted: function() {
24             var _this = this;
25             // 在實例初始化時,監聽來自bus示例的事件
26             bus.$on("on-message", function(msg) {
27                 _this.message = msg;
28             });
29         }
30     });
31 </script>

 

首先建立了一個名爲bus的空Vue實例,裏面沒有任何內容;而後全局定義了組件component-a;最後建立Vue實例app
app初始化時,也就是在生命週期mounted鉤子函數裏監聽了來自bus的事件on-message
而在組件component-a中,點擊按鈕會經過bus把事件on-message發出去,
此時app就會接受到來自bus的事件,進而在回調裏完成本身的業務邏輯。

這種方法巧妙而清涼地實現了任何組件間的通訊,包括父子、兄弟、跨級,並且Vue 1.x和Vue 2.x都適用。
若是深刻使用,能夠擴展bus實例,給它添加datamethodscomputed等選項,這些都是能夠公用的。
在業務中,尤爲是協同開發時很是有用,由於常常須要共享一些通用的信息。
好比用戶登陸的暱稱、性別、郵箱等,還有用戶的受權token等,只需在初始化時讓bus獲取一次,任什麼時候間、任何組件就能夠從中直接使用了,在單頁面富應用(SPA)中會很實用,咱們會在進階篇中逐步介紹這些內容。

當你的項目比較大,有更多的小夥伴參與開發時,也能夠你選擇更好的狀態管理解決方案vuex,在進階篇裏會詳細介紹關於它的用法。

除了中央事件總線bus外,還有兩種方法能夠實現組件間通訊:父鏈和子組件索引。

父鏈

在子組件中,使用this.$parent能夠直接訪問該組件的父實例或組件,父組件也能夠經過this.$children訪問它全部的子組件,並且能夠遞歸向上或向下無限訪問,直到根實例或最內層的組件。
示例代碼以下:

 1 <div id="app">
 2     {{message}}
 3     <my-component-a></my-component-a>
 4 </div>
 5 
 6 <script>
 7     Vue.component("my-component-a", {
 8         template: "<button @click='handleEvent'>經過父鏈直接修改數據</button>",
 9         methods: {
10             handleEvent: function() {
11                 // 訪問到父鏈後,能夠作任何操做,好比直接修改數據
12                 this.$parent.message = "來自組件 component-a 的內容";
13             }
14         }
15     });
16     
17     var app = new Vue({
18         el: "#app",
19         data: {
20             message: ""
21         }
22     });
23 </script>

 

儘管Vue容許這樣操做,但在業務中,子組件應該儘量地避免依賴父組件的數據,更不該該去主動修改它的數據,由於這樣使得父子組件緊耦合,只看父組件,很難理解父組件的狀態,由於它可能被任意組件修改,理想狀況下,只有組件本身能修改它的狀態。
父子組件最好仍是經過props$emit()來通訊。

子組件索引

當子組件較多時,經過this.$children來一一遍歷咱們須要的一個組件實例是比較困難的,尤爲是組件動態渲染時,它們的序列是不固定的。Vue提供了子組件索引的方法,用特殊的屬性ref來爲子組件指定一個索引名稱。
示例代碼以下:

 1 <div id="app">
 2     <button @click="handleRef">經過ref獲取子組件實例</button>
 3     <component-a ref="comA"></component-a>
 4 </div>
 5 
 6 <script>
 7     Vue.component("component-a", {
 8         template: "<div>子組件</div>",
 9         data: function() {
10             return {
11                 message: "子組件內容"
12             };
13         }
14     });
15     
16     var app = new Vue({
17         el: "#app",
18         methods: {
19             handleRef: function() {
20                 // 經過$refs來訪問指定的實例
21                 var msg = this.$refs.comA.message;
22                 console.log(msg);
23             }
24         }
25     });
26 </script>

 

在父組件模板中,子組件標籤上使用ref指定一個名稱,並在父組件內經過this.$refs來訪問指定名稱的子組件。

提示:
\(refs只在組件渲染完成後才填充,而且它是非響應式的。
它僅僅做爲一個直接訪問子組件的應急方案,應當避免在模板或計算屬性中使用\)refs。

與Vue 1.x不一樣的是,Vue 2.x將v-elv-ref合併爲了ref,Vue會自動去判斷是普通標籤仍是組件。
能夠嘗試補全下面的代碼,分別打印出兩個ref看看都是什麼:

1 <div id="app">
2     <p ref="p">內容</p>
3     <child-component ref="child"></child-component>
4 </div>

 

7.4 使用slot分發內容

7.4.1 什麼是slot

咱們先看一個比較常規的網站佈局,如圖7-3所示。

這個網站由一級導航、二級導航、左側列表、正文以及底部版權信息5個模塊組成。
若是要將它們都組件化,這個結構可能會是:

1 <app>
2     <menu-main></menu-main>
3     <menu-sub></menu-sub>
4     <div class="container">
5         <menu-left></menu-left>
6         <container></container>
7     </div>
8     <app-footer></app-footer>
9 </app>

 

當須要讓組件組合使用,混合父組件的內容與子組件的模板時,就會用到slot,這個過程叫作內容分發(transclusion)。
<app>爲例,它有兩個特色:

  • <app>組件不知道它的掛載點會有什麼內容。掛載點的內容是由<app>的父組件決定的。
  • <app>組件極可能有它本身的模板。

props傳遞數據、events觸發事件和slot內容分發就構成了Vue組件的3個API來源,再複雜的組件也是由這3部分構成的。

7.4.2 做用域

正式介紹slot前,須要先知道一個概念:編譯的做用域。
好比父組件有以下模板:

1 <child-component>
2     {{message}}
3 </child-component>

 

這裏的message就是一個slot
可是它綁定的是父組件的數據,而不是組件<child-component>的數據。

父組件模板的內容是在父組件做用域內編譯,子組件模板的內容是在子組件做用域內編譯。
例以下面的代碼示例:

 1 <div id="app">
 2     <child-component v-show="showChild"></child-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("child-component", {
 7         template: "<div>子組件</div>"
 8     });
 9     var app = new Vue({
10         el: "#app",
11         data: {
12             showChild: true
13         }
14     })
15 </script>

 

這裏的狀態showChild綁定的是父組件的數據,若是想在子組件上綁定,那應該是:

 1 <div id="app">
 2     <child-component></child-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("child-component", {
 7         template: "<div v-show='showChild'>子組件</div>",
 8         data: function() {
 9             return {
10                 showChild: true
11             };
12         }
13     });
14     
15     var app = new Vue({
16         el: "#app"
17     })
18 </script>

 

所以,slot分發的內容,做用域是在父組件上的。

7.4.3 solt用法

單個Slot

在子組件內使用特殊的<slot>元素就能夠爲這個子組件開啓一個slot(插槽),在父組件模板裏,插入子組件標籤內的全部內容將替代子組件的<slot>標籤及它的內容。
示例代碼以下:

 1 <div id="app">
 2     <child-component>
 3         <p>分發的內容</p>
 4         <p>更多分發的內容</p>
 5     </child-component>
 6 </div>
 7 
 8 <template id="template">
 9     <div>
10         <slot>
11             <p>若是父組件沒有插入內容,我將做爲默認出現</p>
12         </slot>
13     </div>
14 </template>
15 
16 <script>
17     Vue.component("child-component", {
18         template: "#template"
19     });
20     var app = new Vue({
21         el: "#app"
22     })
23 </script>

 

子組件child-component的模板內定義了一個<slot>元素,而且用一個<p>做爲默認的內容,在父組件沒有使用slot時,會渲染這段默認的文本;若是寫入了slot,那就回替換整個<slot>
因此上例渲染後的結果爲:

1 <div id="app">
2     <div>
3         <p>分發的內容</p>
4         <p>更多分發的內容</p>
5     </div>
6 </div>

 

提示:
注意,子組件<slot>內的備用內容,它的做用域是子組件自己。

具名Slot

<slot>元素指定一個name後能夠分發多個內容,具名Slot能夠與單個Slot共存。
例以下面的示例:

 1 <div id="app">
 2     <child-component>
 3         <h2 slot="header">標題</h2>
 4         <p>正文內容</p>
 5         <p>更多的正文內容</p>
 6         <div slot="footer">底部信息</div>
 7     </child-component>
 8 </div>
 9 
10 <template id="template">
11     <div class="container">
12         <div class="header">
13             <slot name="header"></slot>
14         </div>
15         <div class="main">
16             <slot></slot>
17         </div>
18         <div class="footer">
19             <slot name="footer"></slot>
20         </div>
21     </div>
22 </template>
23 
24 <script>
25     Vue.component("child-component", {
26         template: "#template"
27     });
28     var app = new Vue({
29         el: "#app"
30     })
31 </script>

 

子組件內聲明瞭3個<slot>元素,其中在<div name="main">內的<slot>沒有使用name特性,它將做爲默認slot出現,父組件沒有使用slot特性的元素與內容都將會出如今這裏。

若是沒有指定默認的匿名slot,父組件內多餘的內容片段都將被拋棄。

上例最終渲染後的結果爲:

 1 <div id="app">
 2     <div class="container">
 3         <div class="header">
 4             <h2>標題</h2>
 5         </div>
 6         <div class="main">
 7             <p>正文內容</p>
 8             <p>更多的正文內容</p>
 9         </div>
10         <div class="footer">
11             <div>底部信息</div>
12         </div>
13     </div>
14 </div>

 

在組合使用組件時,內容分發API相當重要。

7.4.4 做用域插槽

做用域插槽是一種特殊的slot,使用一個能夠複用的模板替換已渲染元素。
概念比較難理解,咱們先看一個簡單的示例來了解它的基本用法。
示例代碼以下:

 1 <div id="app">
 2     <child-component>
 3         <template scope="props">
 4             <p>來自父組件的內容</p>
 5             <p>{{props.msg}}</p>
 6         </template>
 7     </child-component>
 8 </div>
 9 
10 <template id="template">
11     <div class="container">
12         <slot msg="來自子組件的內容"></slot>
13     </div>
14 </template>
15 
16 <script>
17     Vue.component("child-component", {
18         template: "#template"
19     });
20     var app = new Vue({
21         el: "#app"
22     })
23 </script>

 

觀察子組件的模板,在<slot>元素上有一個相似props傳遞數據給組件的寫法msg="xxx",數據傳到了插槽。
父組件中使用了<template>元素,並且擁有一個scope="props"的特性,這裏的props只是一個臨時變量,就像v-for="item in items"裏面的item同樣。
template內能夠經過臨時變量props訪問來自子組件插槽的數據msg

將上面的示例渲染後的最終結果爲:

1 <div id="app">
2     <div class="container">
3         <p>來自父組件的內容</p>
4         <p>來自子組件的內容</p>
5     </div>
6 </div>

 

做用域插槽根據表明性的用力是列表組件,容許組件自定義應該如何渲染列表每一項。
示例代碼以下:

 1 <div id="app">
 2     <my-list v-bind:books="books">
 3         <template slot="book" scope="props">
 4             <li>{{props.bookName}}</li>
 5         </template>
 6     </my-list>
 7 </div>
 8 
 9 <template id="template">
10     <ul>
11         <slot name="book" v-for="book in books" v-bind:book-name="book.name">
12             <!-- 這裏也能夠寫默認slot內容 -->
13         </slot>
14     </ul>
15 </template>
16 
17 <script>
18     Vue.component("my-list", {
19         props: {
20             books: {
21                 type: Array,
22                 default: function() {
23                     return {};
24                 }
25             }
26         },
27         template: "#template"
28     });
29     
30     var app = new Vue({
31         el: "#app",
32         data: {
33             books: [
34                 {name:"《Vue.js實戰》"},
35                 {name:"《JavaScript語言精粹》"},
36                 {name:"《JavaScript高級程序設計》"}
37             ]
38         }
39     });
40 </script>

 

子組件my-list接收一個來自父級的prop數組books,而且將它在namebookslot上使用v-for指令循環,同時暴露一個變量bookName

若是你仔細揣摩上面的用法,你可能會產生這樣的疑問:我直接在父組件用v-for不就行了嗎?爲何還要繞一步,在子組件裏面循環呢?
的確,若是隻是針對上面的示例,這樣寫是畫蛇添足的。此例的用意主要是介紹做用域插槽的用法,並無加入使用場景,而做用域插槽的適用場景就是既能夠複用子組件的slot,又可使slot內容不一致。
若是上例還在其餘組件內使用,<li>的內容渲染權是由使用者掌握的,而數據卻能夠經過臨時變量(好比props)子組件內獲取。

7.4.5 訪問slot

在Vue.js 1.x中,想要獲取某個slot是比較麻煩的,須要用v-el間接獲取。
而Vue.js 2.x提供了用來訪問被slot分發的內容的方法$slots,請看下面的示例:

 1 <div id="app">
 2     <child-component>
 3         <h2 slot="header">標題</h2>
 4         <p>正文內容</p>
 5         <p>更多的正文內容</p>
 6         <div slot="footer">底部信息</div>
 7     </child-component>
 8 </div>
 9 
10 <template id="template">
11     <div class="container">
12         <div class="header">
13             <slot name="header"></slot>
14         </div>
15         <div class="main">
16             <slot></slot>
17         </div>
18         <div class="footer">
19             <slot name="footer"></slot>
20         </div>
21     </div>
22 </template>
23 
24 <script>
25     Vue.component("child-component", {
26         template: "#template",
27         mounted: function() {
28             var header = this.$slots.header;
29             var main = this.$slots.main;
30             var footer = this.$slots.footer;
31             console.log(footer);
32             console.log(footer[0].elm.innerHTML);
33         }
34     });
35     
36     var app = new Vue({
37         el: "#app"
38     });
39 </script>

 

經過$slots能夠訪問某個具名slot,this.$slots.default包括了全部沒有被包含在具名slot中的節點。
嘗試編寫代碼,查看兩個console打印的內容。

$slots在業務中幾乎用不到,在用render函數(進階篇中將介紹)建立組件時會比較有用,但主要仍是用於獨立組件開發中。

7.5 組件高級用法

本節會介紹組件的一些高級用法,這些用法在實際業務中不是很經常使用,但會獨立組件開發時可能會用到。
若是你感受以上內容已經足骨完成你的業務開發了,能夠跳過本節;若是你想繼續探索Vue組件的奧祕,讀完本節會對你有很大的啓發。

7.5.1 遞歸組件

組件在它的模板內能夠遞歸地調用本身,只要給組件設置name的選項就能夠了。
示例代碼以下:

 1 <div id="app">
 2     <child-component v-bind:count="1"></child-component>
 3 </div>
 4 
 5 <template id="template">
 6     <div class="child">
 7         <child-component v-bind:count="count+1" v-if="count<3"></child-component>
 8     </div>
 9 </template>
10 
11 <script>
12     Vue.component("child-component", {
13         name: "child-component",
14         props: {
15             count: {
16                 type: Number,
17                 default: 1
18             }
19         },
20         template: "#template"
21     });
22     
23     var app = new Vue({
24         el: "#app"
25     });
26 </script>

 

設置name後,在組件模板內就能夠遞歸使用了。
不過須要注意的是,必須給一個條件來限制遞歸數量,不然會拋出錯誤:max stack size exceeded

組件遞歸使用能夠用來開發一些具備未知層級關係的阻力組件,好比級聯選擇器和樹形控件等。
如圖7-4和圖7-5所示:

在實戰篇裏,咱們會詳細介紹級聯選擇器的實現。
 

7.5.2 內聯模板

組件的模板通常都是在template選項內自定義的,Vue提供了一個內聯模板的功能,在使用組件時,給組件標籤使用inline-template特性,組件就會把它的內容當作模板,而不是把它當內容分發,這讓模板更靈活。
示例代碼以下:

 1 <div id="app">
 2     <child-component inline-template :data="message">
 3         <div>
 4             <h2>在父組件中定義了組件的模板</h2>
 5             <p>{{data}}</p>
 6             <p>{{msg}}</p>
 7         </div>
 8     </child-component>
 9 </div>
10 
11 <script>
12     Vue.component("child-component", {
13         props: ["data"],
14         data: function() {
15             return {
16                 msg: "在子組件聲明的數據"
17             };
18         }
19     });
20     
21     var app = new Vue({
22         el: "#app",
23         data: {
24             message: "在父組件聲明的數據"
25         }
26     });
27 </script>

 

渲染後的結果爲:

1 <div id="app">
2     <div>
3         <h2>在父組件中定義了組件的模板</h2> 
4         <p>在父組件聲明的數據</p> 
5         <p>在子組件聲明的數據</p>
6     </div>
7 </div>

 

7.5.3 動態組件

Vue.js提供了一個特殊的元素<component>用來動態地掛載不一樣的組件,使用is特性來選擇要掛載的組件。
示例代碼以下:

 1 <div id="app">
 2     <input type="text" placeholder="請輸入組件名稱: A/B/C">
 3     <button @click="handleChangeView">肯定切換</button>
 4     <component v-bind:is="currentView"></component>
 5 </div>
 6 
 7 <script>
 8     var app = new Vue({
 9         el: "#app",
10         components: {
11             comA: {
12                 template: "<div>組件A</div>"
13             },
14             comB: {
15                 template: "<div>組件B</div>"
16             },
17             comC: {
18                 template: "<div>組件C</div>"
19             }
20         },
21         data: {
22             currentView: "comA"
23         },
24         methods: {
25             handleChangeView: function() {
26                 var value = document.querySelector("input").value;
27                 this.currentView = "com" + value;
28             }
29         }
30     });
31 </script>

 

動態地改變currentView的值就能夠動態掛載組件了。
也能夠直接綁定在組件對象上:

 1 <div id="app">
 2     <component v-bind:is="currentView"></component>
 3 </div>
 4 
 5 <script>
 6     var Home = {
 7         template: "<p>Welcome home!</p>"
 8     };
 9     var app = new Vue({
10         el: "#app",
11         data: {
12             currentView: Home
13         }
14     });
15 </script>

 

7.5.4 異步組件

當你的工程足夠大,使用的組件足夠多時,是時候考慮下性能的問題了,由於一開始把全部的組件都加載是不必的一筆開銷。
好在Vue.js容許將組件定義爲一個工廠函數,動態地解析組件。
Vue.js值在組件須要渲染時觸發工廠函數,而且把結果緩存起來,用於後面的再次渲染。
例以下面的示例:

 1 <div id="app">
 2     <child-component></child-component>
 3 </div>
 4 
 5 <script>
 6     Vue.component("child-component", function(resolve, reject) {
 7         window.setTimeout(function() {
 8             resolve({
 9                 template: "<div>我是異步渲染的</div>"
10             });
11         }, 2000);
12     });
13     var app = new Vue({
14         el: "#app"
15     });
16 </script>

 

工廠函數接收一個resolve回調,在收到從服務器下載的組件定義時調用。
也能夠調用reject(reason)指示加載失敗。
這裏setTimeout只是爲了演示異步,具體的下載邏輯能夠本身決定,好比把組件配置寫成一個對象配置,經過Ajax來請求,而後調用resolve傳入配置選項。

在進階篇裏,咱們還會介紹主流的打包編譯工具webpack和.vue單文件的用法,更優雅地實現異步組件(路由)。

7.6 其餘

7.6.1 $nextTick

咱們先來看這樣一個場景:
有一個div,默認用v-if將它隱藏,點擊一個按鈕後,改變v-if的值,讓它顯示出來,同時拿到這個div的文本內容。
若是v-if的值是false,直接去獲取div的內容是獲取不到的,由於此時div尚未被建立出來。
那麼應該在點擊按鈕後,改變v-if的值爲true, div纔會被建立,此時再去獲取。
示例代碼以下:

 1 <div id="app">
 2     <div id="div" v-if="showDiv">這是一段文本</div>
 3     <button v-on:click="getText">獲取div內容</button>
 4 </div>
 5 
 6 <script>
 7     var app = new Vue({
 8         el: "#app",
 9         data: {
10             showDiv: false
11         },
12         methods: {
13             getText: function() {
14                 this.showDiv = true;
15                 var text = document.getElementById("div").innerHTML;
16                 console.log(text);
17             }
18         }
19     });
20 </script>

 

這段代碼並不難理解,可是運行後在控制檯會拋出一個錯誤:Cannot read property 'innerHTML' of null。意思就是獲取不到div元素。這裏就涉及Vue一個重要的概念:異步更新隊列。

Vue在觀察到數據變化時並非直接更新DOM,從而避免沒必要要的計算和DOM操做。
而後,在下一個事件循環tick中,Vue刷新隊列並執行實際(已去重的)工做。
因此若是你用一個for循環來動態改變數據100次,其實它只會應用最後一次改變,若是沒有這種機制,DOM就要重繪100次,這當然是一個很大的開銷。

Vue會根據當前瀏覽器環境優先使用原生的Promise.thenMutationObserver,若是都不支持,就會採用setTimeout代替。

知道了Vue異步更新DOM的原理,上面示例的報錯也就不難理解了。
事實上,在執行this.showDiv=true時,div仍然仍是沒有被建立出來,直到下一個Vue事件循環時,纔開始建立。
$nextTick就是用來知道何時DOM更新完成的,因此上面的示例代碼須要修改成:

 1 <div id="app">
 2     <div id="div" v-if="showDiv">這是一段文本</div>
 3     <button v-on:click="getText">獲取div內容</button>
 4 </div>
 5 
 6 <script>
 7     var app = new Vue({
 8         el: "#app",
 9         data: {
10             showDiv: false
11         },
12         methods: {
13             getText: function() {
14                 this.showDiv = true;
15                 this.$nextTick(function() {
16                     var text = document.getElementById("div").innerHTML;
17                     console.log(text);
18                 });
19             }
20         }
21     });
22 </script>

 

這時再點擊按鈕,控制檯就打印出div的內容「這時一段文本了」。

理論上,咱們應該不用去主動操做DOM,由於Vue的核心思想就是數據驅動DOM,但在不少業務裏,咱們避免不了會使用一些第三方庫,好比popper.jsswiper等,這些基於原生JavaScript的庫都有建立和更新及銷燬的完整生命週期,與Vue配合使用時,就要利用好$nextTick

7.6.2 X-Templates

若是你沒有使用webpack、gulp等工具,試想一下你的組件template的內容很冗長、複雜,若是都在JavaScript裏拼接字符串,效率是很低的,由於不能像寫HTML那樣舒服。
Vue提供了另外一種定義模板的方式,在<script>標籤裏使用text/x-template類型,而且指定一個id,將這個id賦給template。示例代碼以下:

 1 <div id="app">
 2     <my-component></my-component>
 3     <script type="text/x-template" id="my-component">
 4         <div>這是組件的內容</div>
 5     </script>
 6 </div>
 7 
 8 <script>
 9     Vue.component("my-component", {
10         template: "#my-component"
11     });
12     var app = new Vue({
13         el: "#app"
14     });
15 </script>

 

<script>標籤裏,你能夠愉快地編寫HTML代碼,不用考慮執行等問題。

不少剛接觸Vue開發的新手會很是喜歡這個功能,由於用它,再加上組件知識,就能夠很輕鬆地完成交互相對複雜的頁面和應用了。若是再配合一些構建工具(gulp)組織好代碼結構,開發一些中小型產品是沒有問題的。
不過,Vue的初衷並非濫用它,由於它將模板和組件的其餘定義隔離了。
在進階篇裏,咱們會介紹如何使用webpack來編譯.vue的單文件,從而優雅地解決HTML書寫的問題。

7.6.3 手動掛載實例

咱們如今所建立的實例都是經過new Vue()的形式建立出來的。
在一些很是特殊的狀況下,咱們須要動態地去建立Vue實例,Vue提供了Vue.extend$mount兩個方法來手動掛載一個實例。

Vue.extend是基礎Vue構造器,建立一個「子類」,參數是一個包含組件選項的對象。

若是Vue實例在實例化時沒有收到el選項,它就處於「未掛載」狀態,沒有關聯的DOM元素。
可使用$mount()手動地掛載一個未掛載的實例。
這個方法返回實例自身,於是能夠鏈式調用其餘實例方法。
示例代碼以下:

 1 <div id="mount-div"></div>
 2 
 3 <script>
 4     var MyComponent = Vue.extend({
 5         template: "<div>Hello: {{name}}</div>",
 6         data: function() {
 7             return {
 8                 name: "Jack"
 9             };
10         }
11     });
12     
13     new MyComponent().$mount("#mount-div");
14 </script>

 

運行後,idmount-div的div元素會被替換爲組件MyComponenttemplate的內容:

1 <div>Hello: Jack</div>

 

除了這種寫法外,如下兩種寫法也是能夠的:

1 new MyComponent().$mount("#mount-div");
2 // 同上
3 new MyComponent({
4     el: "#mount-div"
5 });
6 // 或者,在文檔以外渲染而且隨後掛載
7 var component = new MyComponent().$mount();
8 document.getElementById("mount-div").appendChild(component.$el);

 

手動掛載實例(組件)是一種比較極端的高級用法,在業務中幾乎用不到,只是開發一些複雜的獨立組件時可能會使用,因此只作瞭解就好。

7.7 實戰:兩個經常使用組件的開發

本節以組件知識爲基礎,整合指令、事件等前面兩章的內容,開發兩個業務中經常使用的組件,即數字輸入框和標籤頁。

7.7.1 開發一個數字輸入框組件

數字輸入框時對普通輸入框的擴展,用來快捷輸入一個標準的數字。
如圖7-6所示:

數字輸入框只能輸入數字,並且有兩個快捷按鈕,能夠直接減1或加1.
除此以外,還能夠設置初始值、最大值、最小值,在數值改變時,觸發一個自定義事件來通知父組件。

瞭解了基本需求後,咱們先定義目錄文件:

  • index.html 入口頁
  • input-number.js 數字輸入框組件
  • index.js 根實例

由於該示例是以交互功能爲主,因此就不寫CSS美化樣式了。

首先寫入基本的結構代碼,初始化項目。

index.html:

 1 <!DOCTYPE html>
 2 <html lang="zh">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6     <meta http-equiv="X-UA-Compatible" content="ie=edge">
 7     <title>數字輸入框組件</title>
 8 </head>
 9 <body>
10     <div id="app"></div>
11     
12     <script src="vue.js"></script>
13     <script src="input-number.js"></script>
14     <script src="index.js"></script>
15 </body>
16 </html>

 

index.js:

1 var app = new Vue({
2     el: "#app"
3 });

 

input-number.js:

 1 Vue.component("input-number", {
 2     template: "\
 3         <div class='input-number'> \
 4             \
 5         </div>",
 6     props: {
 7         max: {
 8             type: Number,
 9             default: Infinity
10         },
11         min: {
12             type: Number,
13             default: -Infinity
14         },
15         value: {
16             type: Number,
17             default: 0
18         }
19     }
20 });

 

該示例的主角是input-number.js,全部的組件配置都在這裏面定義。
如今template裏面定義了組件的根節點,由於是獨立組件,因此應該對每一個prop進行校驗。
這裏面根據需求有最大值、最小值、默認值(也就是綁定值)3個prop,maxmin都是數字類型,默認值是正無限大和負無限大;value也是數字類型,默認值是0。

接下來,咱們先在父組件引入input-number組件,並給它一個默認值5,最大值10,最小值0。

index.js:

1 var app = new Vue({
2     el: "#app",
3     data: {
4         value: 5
5     }
6 });

 

index.html:

1 <div id="app">
2     <input-number v-model="value" :max="10" :min="0"></input-number>
3 </div>

 

value是一個關鍵的綁定至,因此用了v-model,這樣既優雅地實現了雙向綁定,也讓API看起來很合理。
大多數的表單類組件都應該有一個v-model,好比輸入框、單選框、多選框、下拉選擇器等。

剩餘的代碼量就都聚焦到了input-number.js上。

咱們以前介紹過,Vue組件是單向數據流,因此沒法從組件內部直接修改prop:value的值。
解決辦法也介紹過,就是給組件聲明一個data,默認引用value的值,而後在組件內部維護這個data

1 Vue.component("input-number", {
2     // ...
3     data: function() {
4         return {
5             currentValue: this.value
6         };
7     }
8 });

 

這樣只解決了初始化時引用父組件value的問題。
可是若是從父組件修改了value,input-number組件的currentValue也要一塊兒更新。
爲了實現這個功能,咱們須要用到一個新的概念,監聽(watch)。

watch選項用來監聽某個prop或data的改變,當他們發生變化時,就會觸發watch配置的函數,從而完成咱們的業務邏輯。
在本例中,咱們要監聽兩個量:valuecurrentValue
監聽value是要知曉從父組件修改了value,監聽currentValue是爲了當currentValue改變時,更新value
相關代碼以下:

 1 Vue.component("input-number", {
 2     // ...
 3     data: function() {
 4         return {
 5             currentValue: this.value
 6         };
 7     },
 8     watch: {
 9         currentValue: function(val) {
10             this.$emit("input", val);
11             this.$emit("on-change", val);
12         },
13         value: function(val) {
14             this.updateValue(val);
15         }
16     },
17     methods: {
18         updateValue: function(val) {
19             if (val > this.max) {
20                 val = this.max;
21             }
22 
23             if (val < this.min) {
24                 val = this.min;
25             }
26 
27             this.currentValue = val;
28         }
29     },
30     mounted: function() {
31         this.updateValue(this.value);
32     }
33 });

 

從父組件傳遞過來的value有多是不符合當前條件的(大於max或小於min),因此在選項methods裏寫了一個方法updateValue,用來過濾出一個正確的currentValue

watch監聽的數據的回調函數有2個參數可用,第一個是新的智,第二個是舊的值,這裏沒有太複雜的邏輯,就只用了第一個參數。
再回調函數裏,this是指向當前組件實例的,因此能夠直接調用this.updateValue(),由於Vue代理了propsdatacomputedmethods

監聽currentValue的回調裏:

  • this.$emit("input", val)是在使用v-model時改變value的;
  • this.$emit("on-change", val)是觸發自定義事件on-change,用於告知父組件數字輸入框的值有所改變(示例中沒有使用該事件〉。

在生命週期mounted鉤子裏也調用了updateValue()方法,是由於第一次初始化時,也對value進行了過濾。
這裏也有另外一種寫法,在data選項返回對象前進行過濾:

 1 Vue.component("input-number", {
 2     // ...
 3     data: function () {
 4         var val = this.value;
 5         
 6         if (val > this.max) {
 7             val = this.max;
 8         }
 9 
10         if (val < this.min) {
11             val = this.min;
12         }
13         
14         return {
15           currentValue: val
16         };
17     }
18 });

 

實現的效果是同樣的。

最後剩餘的就是補全模板template,內容是一個輸入框和兩個按鈕。
相關代碼以下:

 1 function isValueNumber(value) {
 2     return (/(^-?[0-9]+\.(1)\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1})/.test(value + ""));
 3 }
 4 
 5 Vue.component("input-number", {
 6     // ...
 7     template: "\
 8         <div class='input-number'> \
 9             <input type='text' :value='currentValue' @change='handleChange'/> \
10             <button @click='handleDown' :disabled='currentValue<=min'>-</button> \
11             <button @click='handleUp' :disabled='currentValue>=max'>+</button> \
12         </div>",
13     methods: {
14         handleDown: function() {
15             if (this.currentValue <= this.min) {
16                 return;
17             }
18             this.currentValue -= 1;
19         },
20         handleUp: function() {
21             if (this.currentValue >= this.max) {
22                 return;
23             }
24             this.currentValue += 1;
25         },
26         handleChange: function(event) {
27             var val = event.target.value.trim();
28             var max = this.max;
29             var min = this.min;
30 
31             if (isValueNumber(val)) {
32                 val = Number(val);
33                 this.currentValue = val;
34                 if (val > max) {
35                     this.currentValue = max;
36                 } else if (val < min) {
37                     this.currentValue = min;
38                 }
39             } else {
40                 event.target.value = this.currentValue;
41             }
42         }
43     }
44 });

 

input綁定了數據currentValue和原生的change事件,在句柄handleChange函數中,判斷了當前輸入的是不是數字。
注意,這裏綁定的currentValue也是單向數據流,並無用v-model,因此在輸入時,currentValue的值並無實時改變。
若是輸入的不是數字(好比英文和漢字等〉,就將輸入的內容重置爲以前的currentValue。
若是輸入的是符合要求的數字,就把輸入的值賦給
currentValue`。

數字輸入框組件的核心邏輯就是這些。
回顧一下咱們設計一個通用組件的思路,首先,在寫代碼前必定要明確需求,而後規劃好API。
一個Vue組件的API只來自propseventsslots,肯定好這3部分的命名、規則,剩下的邏輯即便初版沒有作好,後續也能夠迭代完善。可是API若是沒有設計好,後續再改對使用者成本就很大了。

完整的示例代碼以下:

index.html:

 1 <!DOCTYPE html>
 2 <html lang="zh">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6     <meta http-equiv="X-UA-Compatible" content="ie=edge">
 7     <title>數字輸入框組件</title>
 8 </head>
 9 <body>
10     <div id="app">
11         <input-number v-model="value" :max="10" :min="0"></input-number>
12     </div>
13     
14     <script src="vue.js"></script>
15     <script src="input-number.js"></script>
16     <script src="index.js"></script>
17 </body>
18 </html>

 

index.js:

1 var app = new Vue({
2     el: "#app",
3     data: {
4         value: 5
5     }
6 });

 

input-number.js:

 1 function isValueNumber(value) {
 2     return (/(^-?[0-9]+\.(1)\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1})/.test(value + ""));
 3 }
 4 
 5 Vue.component("input-number", {
 6     template: "\
 7         <div class='input-number'> \
 8             <input type='text' :value='currentValue' @change='handleChange'/> \
 9             <button @click='handleDown' :disabled='currentValue<=min'>-</button> \
10             <button @click='handleUp' :disabled='currentValue>=max'>+</button> \
11         </div>",
12     props: {
13         max: {
14             type: Number,
15             default: Infinity
16         },
17         min: {
18             type: Number,
19             default: -Infinity
20         },
21         value: {
22             type: Number,
23             default: 0
24         }
25     },
26     data: function() {
27         return {
28             currentValue: this.value
29         };
30     },
31     watch: {
32         currentValue: function(val) {
33             this.$emit("input", val);
34             this.$emit("on-change", val);
35         },
36         value: function(val) {
37             this.updateValue(val);
38         }
39     },
40     methods: {
41         handleDown: function() {
42             if (this.currentValue <= this.min) {
43                 return;
44             }
45             this.currentValue -= 1;
46         },
47         handleUp: function() {
48             if (this.currentValue >= this.max) {
49                 return;
50             }
51             this.currentValue += 1;
52         },
53         handleChange: function(event) {
54             var val = event.target.value.trim();
55             var max = this.max;
56             var min = this.min;
57 
58             if (isValueNumber(val)) {
59                 val = Number(val);
60                 this.currentValue = val;
61                 if (val > max) {
62                     this.currentValue = max;
63                 } else if (val < min) {
64                     this.currentValue = min;
65                 }
66             } else {
67                 event.target.value = this.currentValue;
68             }
69         },
70         updateValue: function(val) {
71             if (val > this.max) {
72                 val = this.max;
73             }
74 
75             if (val < this.min) {
76                 val = this.min;
77             }
78 
79             this.currentValue = val;
80         }
81     },
82     mounted: function() {
83         this.updateValue(this.value);
84     }
85 });

 

  • 練習1: 在輸入框聚焦時,增長對鍵盤上下鍵的支持,至關於加1和減1;
  • 練習2: 增長一個控制步伐的prop:step,好比設置爲10,點擊加號按鈕,一次增長10。

7.7.2 開發一個標籤組件

本小節將開發一個比較有挑戰的組件:標籤頁組件。
標籤頁(即選項卡切換組件)是網頁和佈局中常常用到的元素,經常使用於平級區域大塊內容的收納和展示。
如圖7-7所示:

根據上個示例的經驗,咱們先分析業務需求,制定出API,這樣不至於一上來就無從下手。

每一個標籤頁的主體內容確定是由使用組件的父級控制的,因此這部分是一個slot,並且slot的數量決定了標籤切換按鈕的數量。
假設咱們有3個標籤頁,點擊每一個標籤按鈕時,另外兩個標籤對應的slot應該被隱藏。
通常這個時候,比較容易想到的解決辦法是,在slot裏寫3個div,在接收到切換通知時,顯示和隱藏相關div。
這樣設計沒有問題,只不過提現不出組件的價值來,由於咱們仍是一些了一些與業務無關的業務邏輯,而這部分邏輯最好組件自己幫忙處理了,咱們只用聚焦在slot內容自己,這纔是咱們業務最相關的。
這種狀況下,咱們在定義一個子組件panel,嵌套在標籤頁組件tabs裏,咱們的業務代碼都放在panel的slot內,而3個panel組件做爲總體成爲tabs的slot。

因爲tabs和panel兩個組件是分離的,可是tabs組件上的標題應該由panel組件來定義,由於slot是卸載panel裏,所以在組件初始化(及標籤標題動態改變)時,tabs要從panel裏獲取標題,並保存起來,本身使用。

肯定好告終構,咱們先建立所需的文件:

  • index.html 入口頁
  • style.css 樣式表
  • tabs.js 標籤頁外層的組件 tabs
  • panel.js 標籤頁嵌套的組件 panel

先初始化各個文件:

index.html:

 1 <!DOCTYPE html>
 2 <html lang="zh">
 3     <head>
 4         <meta charset="UTF-8">
 5         <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6         <meta http-equiv="X-UA-Compatible" content="ie=edge">
 7         <title>標籤頁組件</title>
 8         <link rel="stylesheet" type="text/css" href="style.css">
 9     </head>
10     <body>
11         <div id="app"></div>
12 
13         <script src="vue.js"></script>
14         <script src="panel.js"></script>
15         <script src="tabs.js"></script>
16         <script>
17             var app = new Vue({
18                 el: "#app"
19             });
20         </script>
21     </body>
22 </html>

 

tabs.js:

 1 Vue.component("tabs", {
 2     template: "\
 3         <div class='tabs'> \
 4             <div class='tabs-bar'> \
 5                 <!-- 標籤頁標題,這裏要用v-for --> \
 6             </div> \
 7             <div class='tabs-content'> \
 8                 <!-- 這裏的slot就是嵌套的panel --> \
 9                 <slot></slot> \
10             </div> \
11         </div>"
12 });

 

panel.js

1 Vue.component("panel", {
2     name: "panel",
3     template: "\
4         <div class='panel'> \
5             <slot></slot> \
6         </div>"
7 });

 

panel須要控制標籤頁內容的顯示與隱藏。
設置一個data:show,而且用v-show指令來控制元素:

 1 Vue.component("panel", {
 2     name: "panel",
 3     template: "\
 4         <div class='panel' v-show='show'> \
 5             <slot></slot> \
 6         </div>",
 7     data: function() {
 8         return {
 9             show: true
10         };
11     }
12 });

 

當點擊到這個panel對應的標籤頁標題按鈕時,此panel的show值設置爲true,不然應該是false
這步操做是在tabs組件完成的,咱們稍後再介紹。

既然要單擊對應的標籤頁標題按鈕,那應該有一個惟一的值來標識這個panel,咱們能夠設置一個prop:name讓用戶來設置,但它不是必須的,若是使用者不設置,能夠默認從0開始自動設置,這不操做仍然是tabs執行的,由於panel自己並不知道本身是第幾個。
除了name,還須要標籤頁標題的prop:label,tabs組件須要將它顯示在標籤頁標題裏。
這部分代碼以下:

1 props: {
2     name: {
3         type: String
4     },
5     label: {
6         type: String,
7         default: ""
8     }
9 }

 

上面的prop:label用戶是能夠動態調整的,因此在panel初始化以及label更新時,都要通知父組件也更新,由於是獨立組件,因此不能依賴像bus.js或vuex這樣的狀態管理辦法,咱們能夠直接經過this.$parent訪問tabs組件的實例來調用它的方法更新標題,該方法暫定爲updateNav
注意,在業務中儘量不要使用$parent來操做父鏈,這種方法適合於標籤頁這樣的獨立組件。
這部分代碼以下:

 1 methods: {
 2     updateNav() {
 3         this.$parent.updateNav();
 4     }
 5 },
 6 watch: {
 7     label() {
 8         this.updateNav();
 9     }
10 },
11 mounted() {
12     this.updateNav();
13 }

 

在生命週期mounted,也就是panel初始化時,調用一遍tabs的updateNav方法。
同時監聽了prop:label,在label更新時,一樣調用。

剩餘任務就是完成tabs.js組件。

首先須要把panel組件設置的標題動態渲染出來,也就是當panel觸發tabs的updateNav方法時,更新標題內容。
咱們先看一下這部分的代碼:

 1 Vue.component("tabs", {
 2     // ...
 3     data: function() {
 4         return {
 5             // 用於渲染tabs的標題
 6             navList: []
 7         };
 8     },
 9     methods: {
10         getTabs() {
11             // 經過遍歷子組件,獲得全部的panel組件
12             return this.$children.filter(function(item) {
13                 return item.$options.name === "panel";
14             });
15         },
16         updateNav() {
17             this.navList = [];
18             // 設置對this的引用,在function回調裏,this指向的並非Vue實例
19             var _this = this;
20             this.getTabs().forEach(function(panel, index) {
21                 _this.navList.push({
22                     label: panel.label,
23                     name: panel.name || index
24                 });
25                 // 若是沒有給panel設置name,默認設置它的索引
26                 if (!panel.name) {
27                     panel.name = index;
28                 }
29                 // 設置當前選中的tab的索引,在後面介紹
30                 if (index === 0) {
31                     if (!_this.currentValue) {
32                         _this.currentValue = panel.name || index;
33                     }
34                 }
35             });
36         },
37         updateStatus() {
38             var tabs = this.getTabs();
39             var _this = this;
40             // 顯示當前選中的tab對應的panel組件,隱藏沒有選中的
41             tabs.forEach(function(tab) {
42                 return tab.show = tab.name === _this.currentValue;
43             });
44         }
45     }
46 });

 

getTabs是一個公用的方法,使用this.$children來拿到全部的panel組件實例。

須要注意的是,在methods裏使用了有function回調的方法時(例如遍歷數組的方法forEach),在回調內的this再也不執行當前的Vue實例,也就是tabs組件自己,因此要在外層設置一個_this=this的局部變量來間接使用this
若是你熟悉ES2015,也能夠直接使用箭頭函數=>,咱們會在實戰篇裏介紹相關的用法。

遍歷了每個panel組件後,把它的labelname提取出來,構成一個Object並添加到數據navList數組裏,後面咱們會在template裏用到它。

設置完navList數組後,咱們調用了updateStatus方法,又將panel組件遍歷了以便,不過這時是爲了將當前選中的tab對應的panel組件內容顯示出來,把沒有選中的隱藏掉。
由於在上一步操做裏,咱們有可能須要設置currentValue來標識當前選中項的name(在用戶沒有設置value時,纔會自動設置),因此必需要遍歷2次才能夠。

拿到navList後,就須要對它用v-for指令把tab的標題渲染出來,而且判斷每一個tab當前的狀態。
這部分代碼以下:

 1 Vue.component("tabs", {
 2     template: "\
 3         <div class='tabs'> \
 4             <div class='tabs-bar'> \
 5                 <div :class='tabCls(item)' v-for='(item,index) in navList' @click='handleChange(index)'>{{item.label}}</div> \
 6             </div> \
 7             <div class='tabs-content'> \
 8                 <slot></slot> \
 9             </div> \
10         </div>",
11     props: {
12         // 這裏的value是爲了可使用v-model
13         value: {
14             type: [String, Number]
15         }
16     },
17     data: function() {
18         return {
19             // 由於不能修改value,因此複製一份本身維護
20             currentValue: this.value,
21             navList: []
22         };
23     },
24     methods: {
25         tabCls: function(item) {
26             return [
27                 "tabs-tab",
28                 {
29                     // 給當前選中的tab加一個class
30                     "tabs-tab-active": item.name === this.currentValue
31                 }
32             ];
33         },
34         // 點擊tab標題時觸發
35         handleChange: function(index) {
36             var nav = this.navList[index];
37             var name = nav.name;
38             // 改變當前選中的tab,並觸發下面的watch
39             this.currentValue = name;
40             // 更新value
41             this.$emit("input", name);
42             // 觸發一個自定義事件,供父級使用
43             this.$emit("on-click", name);
44         }
45     },
46     watch: {
47         value: function(val) {
48             this.currentValue = val;
49         },
50         currentValue: function() {
51             // 在當前選中的tab發生變化時,更新panel的顯示狀態
52             this.updateStatus();
53         }
54     }
55 });

 

在使用v-for指令循環顯示tab標題時,使用v-bind:class指向了一個名爲tabClsmethods來動態設置class名稱。
由於計算屬性不能接收參數,沒法知道當前tab是不是選中的,因此這裏咱們纔用到methods。
不過要知道,methods是不緩存的,能夠回顧關於計算屬性的章節。

點擊每一個tab標題時,會觸發handleChange方法來改變當前選中tab的索引,也就是panel組件的name。
在watch選項裏,咱們監聽了currentValue,當其發生變化時,觸發updateStatus方法來更新panel組件的顯示狀態。

以上就是標籤頁組件的核心代碼分解。
總結一下該示例的技術難點:

  • 使用了組件嵌套的方式,將一系列panel組件做爲tabs組件的slot;
  • tabs組件和panel組件通訊上,使用了$parent$children的方法訪問父鏈和子鏈;
  • 定義了prop:value和data:currentValue,使用$emit("input")來實現v-model的用法。

如下是標籤頁組件的完整代碼:

index.html:

 1 <!DOCTYPE html>
 2 <html lang="zh">
 3     <head>
 4         <meta charset="UTF-8">
 5         <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6         <meta http-equiv="X-UA-Compatible" content="ie=edge">
 7         <title>標籤頁組件</title>
 8         <link rel="stylesheet" type="text/css" href="style.css">
 9     </head>
10     <body>
11         <div id="app">
12             <tabs v-model="activeKey">
13                 <panel label="標籤一" name="1">標籤一的內容</panel>
14                 <panel label="標籤二" name="2">標籤二的內容</panel>
15                 <panel label="標籤三" name="3">標籤三的內容</panel>
16             </tabs>
17         </div>
18 
19         <script src="vue.js"></script>
20         <script src="panel.js"></script>
21         <script src="tabs.js"></script>
22         <script>
23             var app = new Vue({
24                 el: "#app",
25                 data: {
26                     activeKey: "1"
27                 }
28             });
29         </script>
30     </body>
31 </html>

 

panel.js:

 1 Vue.component("panel", {
 2     name: "panel",
 3     template: "\
 4         <div class='panel' v-show='show'> \
 5             <slot></slot> \
 6         </div>",
 7     props: {
 8         name: {
 9             type: String
10         },
11         label: {
12             type: String,
13             default: ""
14         }
15     },
16     data: function() {
17         return {
18             show: true
19         };
20     },
21     methods: {
22         updateNav() {
23             this.$parent.updateNav();
24         }
25     },
26     watch: {
27         label() {
28             this.updateNav();
29         }
30     },
31     mounted() {
32         this.updateNav();
33     }
34 });

 

tabs.js:

 1 Vue.component("tabs", {
 2     template: "\
 3         <div class='tabs'> \
 4             <div class='tabs-bar'> \
 5                 <div :class='tabCls(item)' v-for='(item,index) in navList' @click='handleChange(index)'>{{item.label}}</div> \
 6             </div> \
 7             <div class='tabs-content'> \
 8                 <slot></slot> \
 9             </div> \
10         </div>",
11     props: {
12         value: {
13             type: [String, Number]
14         }
15     },
16     data: function() {
17         return {
18             currentValue: this.value,
19             navList: []
20         };
21     },
22     methods: {
23         tabCls: function(item) {
24             return [
25                 "tabs-tab",
26                 {
27                     "tabs-tab-active": item.name === this.currentValue
28                 }
29             ];
30         },
31         // 點擊tab標題時觸發
32         handleChange: function(index) {
33             var nav = this.navList[index];
34             var name = nav.name;
35             // 改變當前選中的tab,並觸發下面的watch
36             this.currentValue = name;
37             // 更新value
38             this.$emit("input", name);
39             // 觸發一個自定義事件,供父級使用
40             this.$emit("on-click", name);
41         },
42         getTabs() {
43             // 經過遍歷子組件,獲得全部的panel組件
44             return this.$children.filter(function(item) {
45                 return item.$options.name === "panel";
46             });
47         },
48         updateNav() {
49             this.navList = [];
50             // 設置對this的引用,在function回調裏,this指向的並非Vue實例
51             var _this = this;
52             this.getTabs().forEach(function(panel, index) {
53                 _this.navList.push({
54                     label: panel.label,
55                     name: panel.name || index
56                 });
57                 // 若是沒有給panel設置name,默認設置它的索引
58                 if (!panel.name) {
59                     panel.name = index;
60                 }
61                 // 設置當前選中的tab的索引,在後面介紹
62                 if (index === 0) {
63                     if (!_this.currentValue) {
64                         _this.currentValue = panel.name || index;
65                     }
66                 }
67             });
68         },
69         updateStatus() {
70             var tabs = this.getTabs();
71             var _this = this;
72             // 顯示當前選中的tab對應的panel組件,隱藏沒有選中的
73             tabs.forEach(function(tab) {
74                 return tab.show = tab.name === _this.currentValue;
75             });
76         }
77     },
78     watch: {
79         value: function(val) {
80             this.currentValue = val;
81         },
82         currentValue: function() {
83             // 在當前選中的tab發生變化時,更新panel的顯示狀態
84             this.updateStatus();
85         }
86     }
87 });

 

style.css:

 1 [v-cloak]{display:none;}
 2 
 3 .tabs{font-size:14px; color:#657180;}
 4 .tabs-bar:after{
 5     content:""; display:block; 
 6     width:100%; height:1px; 
 7     background:#D7DDE4; margin-top:-1px;
 8 }
 9 .tabs-tab{
10     display:inline-block; cursor:pointer; position:relative;
11     padding:4px 16px; margin-right:6px; 
12     background:#FFF; border:1px solid #D7DDE4;
13 }
14 .tabs-tab-active{
15     cursor:#3399FF; 
16     border-top:1px solid #3399FF; border-bottom:1px solid #FFF;
17 }
18 .tabs-tab-active:before{
19     content:""; display:block; height:1px; background:#3399FF; 
20     position:absolute; top:0; left:0; right:0;
21 }
22 .tabs-content{padding:8px 0;}

 

練習1: 給panel組件新增一個prop:closable的布爾值,來支持是否能夠關閉這個panel,若是開啓,在tabs的標籤標題上會有一個關閉的按鈕;

提示:
在初始化panel時,咱們是在mounted裏通知的。
關閉時,你會用到beforeDestroy。

練習2: 嘗試在切換panel的顯示與隱藏時,使用滑動的動畫。提示:可使用CSS3的transform:translateX

相關文章
相關標籤/搜索