Vue躬行記(4)——元件
元件是可複用的Vue例項,擁有屬於自己的資料、模板、指令碼和樣式,可避免繁重的重複性開發。由於元件都是獨立的,因此其內部程式碼不會影響其它元件,但可以包含其它元件,並且相互之間還能通訊。
一、註冊
在使用元件之前,需要先將其註冊,Vue提供了兩種註冊方式:全域性註冊和區域性註冊。
1)全域性註冊
通過Vue.component()方法可註冊全域性的元件,它能接收兩個引數,第一個是元件名稱,第二個既可以是擴充套件過的構造器(即Vue.extend()的返回值),也可以是選項物件(會自動呼叫Vue.extend()),如下所示。
Vue.component("btn-custom", Vue.extend({ })); //擴充套件過的構造器 Vue.component("btn-custom", { }); //選項物件
元件的選項物件也會包含data、methods、計算屬性和生命週期鉤子等成員,但不包含掛載目標el選項,並且data選項也不再是一個物件而是一個函式,因為只有這樣才能讓每個例項維護各自的資料物件,互不影響。注意,只有在元件註冊之後才能將其應用於其它Vue根例項的模板中,如下所示。
<div id="container"> <btn-custom></btn-custom> </div> <script> Vue.component("btn-custom", { data: function() { return { txt: "提交" }; }, template: '<button>{{txt}}</button>' }); var vm = new Vue({ el: "#container" }); </script>
渲染出的DOM結構如下所示。
<div id="container"> <button>提交</button> </div>
元件的命名方式除了上面的連字元分隔式之外,還有另一種大駝峰式(例如BtnCustom)。當把元件引用至字串模板中時,兩種命名方式都是有效的;而當把元件直接應用到DOM模板中時(如下所示),就不能用大駝峰命名,因為標籤會被自動轉換成小寫(即<btncustom>),於是就找不到這個元件的定義,進而丟擲錯誤。
<div id="container"> <BtnCustom></BtnCustom> </div>
2)區域性註冊
Vue的區域性註冊需要分兩步,首先通過建立選項物件的方式來定義元件,如下所示。
var BtnCustom = { data: function() { return { txt: "提交" }; }, template: "<button>{{txt}}</button>" };
然後在Vue根例項的components選項中註冊要使用的元件(如下所示),其中屬性名就是模板中要使用的自定義元素名,屬性值就是元件。
var vm = new Vue({ el: "#container", components: { "btn-custom": BtnCustom } });
注意,當構建一個元件時,其模板中必須包含一個根元素,之前的示例都只有一個元素。如果有多個元素,那麼就得像下面這樣用一個元素(<div>)包裹其它元素(<span>和<button>)。
var BtnCustom = { template: `<div> <span>按鈕</span> <button>提交</button> </div>` };
二、資料傳遞
元件的props選項能接收從外部(可以是父元件)傳遞進來的資料,其值是一個由HTML特性組成的陣列或物件,如下所示。
<btn-custom in-html="提交"></btn-custom> <script> Vue.component("btn-custom", { props: ["inHtml"], template: '<button>{{inHtml}}</button>' }); </script>
由於HTML特性的名稱大小寫不敏感,因此瀏覽器會將所有大寫字母自動轉換成小寫。這意味著如果在元件內為props選項新增駝峰式的特性(例如inHtml),那麼在DOM模板中需要宣告成等價的連字元分隔式的特性(例如in-html),否則在元件內將讀取不到該特性。有一點要注意,在字串模板中使用特性,兩種命名方式都是有效的。
1)動態傳值
特性值的型別除了上文的字串之外,還可以通過v-bind指令動態的將任意型別傳遞給元件的props選項,例如傳入一個數字,如下所示。
<btn-custom :digit="1"></btn-custom> <script> Vue.component("btn-custom", { props: ["digit"], created: function() { typeof this.digit; //"number" } }); </script>
在created鉤子中呼叫typeof運算子計算this.digit,得到的值為“number”,說明數字傳遞成功。
如果要傳遞物件的所有屬性,那麼不必一個一個宣告,只需要不定義v-bind的引數即可,如下所示,兩個btn-custom元件是等價的。
<div id="container"> <btn-custom v-bind="obj"></btn-custom> <!-- 相當於 --> <btn-custom :id="obj.id" :name="obj.name"></btn-custom> </div> <script> Vue.component("btn-custom", { props: ["id"], template: '<button>{{id}}</button>' }); var vm = new Vue({ el: "#container", data: { obj: { id: 1, name: "strick" } } }); </script>
注意,在props選項中宣告的是id或name,而不是obj。
2)資料流
在Vue中,元件之間的資料是自頂向下單向流動的(即單向資料流),父元件通過props將資料傳遞給子元件。一旦父元件的資料有所更新,那麼子元件也會自動更新,如果在子元件中修改接收的props(例如下面的digit特性),那麼Vue會丟擲錯誤警告,避免改變父元件的狀態。
Vue.component("btn-custom", { props: ["digit"], created: function() { this.digit = 2; } });
很多需要改變props的情況,其實都能以另一種更合理的方式解決,例如將其儲存到元件的data屬性中或定義成一個計算屬性等。
3)校驗特性
元件的props能以物件的形式指定值型別,其鍵是接收的特性名稱,值是型別建構函式。這樣既有助於閱讀,也可以避免傳遞無效的值。在下面的示例中,指定了digit必須是數字,而number既可以是數字也可以是字串。
Vue.component("btn-custom", { props: { digit: Number, number: [Number, String] } });
除了Number和String之外,內建的建構函式還有Boolean、Array、Object、Date、Function和Symbol。不僅如此,還可以自定義建構函式,通過instanceof運算子來檢查。在下面的示例中,驗證man特性是否是通過new Person()建立的。
Vue.component("btn-custom", { props: { man: People } }); function People(name) { this.name = name; }
除了基礎的型別檢查之外,元件還允許自定義驗證函式、新增必填標記和附帶預設值,如下所示。
Vue.component("btn-custom", { props: { digit: { type: Number, required: true //必填 }, number: { type: Number, default: 100 //數字預設值 }, people: { type: Object, default: function() { //物件預設值 return { name: "strick" }; } }, name: { validator: function(value) { //驗證函式 return value.length > 5; } } } });
在使用這些校驗規則時,有兩點需要注意:
(1)當預設值是物件或陣列時,需要從函式中獲取。
(2)由於props會在元件例項建立之前進行驗證,因此在default()和validator()函式中不能使用元件的屬性,例如data、computed、methods等。
4)未在props中的特性
元件可以宣告任意多個特性,而那些沒有在props中定義的特性不但會被儲存到例項屬性$attrs中,還會被新增到根元素上。注意,class和style兩個特性未包含在$attrs屬性中,並且它們會與原特性進行合併,而不是替換。以下面的btn-custom元件為例,根元素<button>會接收type和class兩個特性。
<btn-custom type="submit" class="size"></btn-custom> <script> Vue.component("btn-custom", { props: ["digit"], created: function() { console.log(this.$attrs); //{type: "submit"} }, template: '<button type="button" class="warning">{{digit}}</button>' }); </script>
渲染出的<button>元素如下所示,其中type的值被替換成了“submit”,而class的值變成了“warning size”。
<button type="submit" class="warning size"></button>
如果不想讓根元素繼承特性,那麼可以將元件的inheritAttrs選項設為false,但要注意,inheritAttrs不會影響class和style的傳遞。還是以btn-custom元件為例,props和template兩個選項與之前相同。
<btn-custom type="submit" class="size"></btn-custom> <script> Vue.component("btn-custom", { inheritAttrs: false }); </script>
渲染出的<button>元素如下所示,其中type的值未被替換,而class的值仍然是“warning size”。
<button type="button" class="warning size"></button>
三、混入
混入(mixin)是一種程式碼複用技術,一個混入物件可包含任意元件選項,並能將其與普通元件混合在一起。
1)選項合併策略
當元件和混入物件包含同名選項時,這些選項將會通過2種策略進行合併。
(1)當資料物件或值為物件的選項(例如methods、components等)發生衝突時,同名的屬性將以元件的為準。如下程式碼所示,雖然混入物件Mixin的資料物件也包含name屬性,但是依然會被btn-custom元件中的name屬性所覆蓋,並且它的getName()也會被替換。
var Mixin = { data: function() { return { name: "strick" }; }, methods: { getName: function() { console.log("mixin"); } } }; Vue.component("btn-custom", { mixins: [Mixin], data: function() { return { name: "freedom" }; }, methods: { getName: function() { console.log("component"); } } });
(2)當生命週期鉤子發生衝突時,同名的鉤子將合併成一個數組,混入物件的鉤子在前,元件的鉤子在後,如下所示,先輸出“mixin”,再輸出“component”。
var Mixin = { created: function() { console.log("mixin"); } }; Vue.component("btn-custom", { mixins: [Mixin], created: function() { console.log("component"); } });
2)全域性混入
通過Vue.mixin()方法可註冊全域性的混入物件,如下所示。
Vue.mixin({ created: function () { console.log("global"); } });
全域性混入會影響所有的Vue例項,包括自定義的元件或第三方元件,因此要謹慎使用。大部分情況下它只適合自定義的選項,在官方的程式碼風格指南中,為混入中的這些選項制訂了專門的命名規範,即以“$_”和自定義的名稱空間為字首(例如$_namespace_),從而避免與其它例項中的選項相沖突,下面是一個簡單的示例。
Vue.mixin({ $_namespace_getAge: function () { return 28; } });
3)自定義選項合併策略
除了預定義的合併策略之外,Vue還允許自定義合併策略,只需在Vue.config.optionMergeStrategies中新增一個包含合併邏輯的函式即可。
下面是一個示例,首先在混入物件和元件中都聲明瞭一個自定義的age選項;然後在Vue.config.optionMergeStrategies中新增一個同名的age()函式,並且需要在元件之前宣告合併函式;最後在created鉤子中呼叫例項屬性$options,讀取到的age值為28,符合age()函式中的合併規則。
var Mixin = { age: 28 }; Vue.config.optionMergeStrategies.age = function(toVal, fromVal) { return fromVal > toVal ? toVal : fromVal; }; Vue.component("btn-custom", { mixins: [Mixin], created: function() { this.$options.age; //28 }, age: 30 });
四、動態元件
Vue內建的<component>元素可渲染一個元元件為動態元件,通過它的is特性來決定使用哪個元件。下面用一個例子來演示<component>元素的用法,首先全域性註冊兩個元件tab1和tab2;然後將它們合併成陣列賦給vm例項的tabs屬性,而另一個current屬性記錄了當前要渲染的元件,預設值為tab1;最後將該屬性值傳遞給is特性,並在DOM模板中建立兩個按鈕,每個按鈕都註冊了點選事件,可更改要渲染的元件。
<div id="container"> <button v-for="tab in tabs" @click="current = tab">{{ tab }}</button> <component :is="current"></component> </div> <script> Vue.component("tab1", { template: '<input type="text"/>' }); Vue.component("tab2", { template: '<input type="text"/>' }); var vm = new Vue({ el: "#container", data: { current: "tab1", tabs: ["tab1", "tab2"] } }); </script>
1)<keep-alive>
雖然可以動態切換元件,但是元件的狀態無法保持,例如在tab1元件的文字框中輸入字元,來回切換後,這些字元就消失了。如果要快取元件的狀態,那麼可以用Vue提供的另一個內建的<keep-alive>元素,如下所示,用它來包裹<component>元素,就不會銷燬失活的元件,從而提升渲染效能。
<keep-alive> <component :is="current"></component> </keep-alive>
注意,<keep-alive>元素自身不會渲染成一個DOM元素,並且其可與任意元素配合,但子元素只能渲染一個。由此可知,<keep-alive>元素內可包含條件指令(如下所示),但不能包含v-for指令。
<keep-alive> <tab1 v-if="current == 'tab1'"></tab1> <tab2 v-else></tab2> <keep-alive>
有兩個與<keep-alive>元素相關的生命週期鉤子:activated和deactivated。以之前的tab1元件為例,為其新增這兩個鉤子(如下程式碼所示),它被包裹在<keep-alive>元素中。當啟用tab1元件時,會觸發activated鉤子;而當停用tab1元件時,會觸發deactivated鉤子。
Vue.component("tab1", { template: '<input type="text"/>', activated: function() { console.log("activated"); }, deactivated: function() { console.log("deactivated"); } });
&n