天天看點

Vue 元件詳解元件與複用使用 props 傳遞資料元件通信使用 slot 分發内容

參考書籍:《Vue.js 實戰》

元件與複用

為什麼使用元件

Vue 的元件就是提高複用性,讓代碼可複用。

元件用法

<div id="app">
    <my-component></my-component>
</div>
           

元件需要注冊後才可以使用,注冊有全局注冊和局部注冊兩種方式。

  1. 全局注冊後,任何 Vue 執行個體都可以使用。
    // 要在父執行個體中使用這個元件,必須要在執行個體建立前注冊。
    Vue.component('my-component', {
        template: '<div>my component</div>'
    });
    
    var app = new Vue({
        el: '#app'
    });
               
  2. 在 Vue 執行個體中,使用

    components

    選項可以局部注冊元件,注冊後的元件隻有在該執行個體作用域下有效。
    var Child = {
        template: '<div>my component</div>'
    };
    
    var app = new Vue({
        el: '#app',
        components: {
            'my-component': Child
        }
    });
               

渲染後的結果是:

<div id="app">
    <div>my component</div>
</div>
           

Vue 元件的模闆在某些情況下會收到

HTML

的限制,比如

<table>

内規定隻允許是

<tr>

<td>

等這些表格元素,是以在

<table>

内直接使用元件是無效的,這種情況下可以使用特殊的

is

屬性來挂載元件。

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>
           
Vue.component('my-component', {
    template: '<div>my component</div>'
});

var app = new Vue({
    el: '#app'
});
           

渲染後的結果是:

<div id="app">
    <table>
        <div>my component</div>
    </table>
</div>
           

在元件中使用

data

時,必須是函數,然後将資料 return 出去。

Vue.component('my-component', {
    template: '<div>my component</div>',
    data() {
        return {
            message: 'message'
        }
    }
});
           

使用

props

傳遞資料

基本用法

元件不僅僅是要把模闆的内容進行複用,更重要的是元件間要進行通信。通常父元件的模闆中包含子元件,父元件要正向地向子元件傳遞資料或參數,子元件接收到後根據參數的不同來渲染不同的内容或執行操作。這個正向傳遞資料的過程就是通過

props

來實作的。

props

中聲明的資料與元件

data

函數内的資料主要差別就是

props

的資料來自父級,而

data

的資料是元件自己的資料,這兩種資料都可以在模闆

template

、計算屬性

computed

和方法

methods

中使用。

通常,傳遞的資料并不是直接寫死的,而是來自父級的動态資料,這時可以使用指令

v-bind

來動态綁定

props

的值,當父元件的資料變化時,也會傳遞給子元件。

由于

HTML

特性不區分大小寫,當時用

DOM

模闆時,駝峰命名(camelCase)的

props

名稱要轉為短橫分隔命名(kebab-case)。

<div id="app">
    <input type="text" v-model="parentMessage" />
    <my-component :my-message="parentMessage"></my-component>
</div>
           
Vue.component('my-component', {
    props: ['myMessage'],
    template: '<div>{{ myMessage}}</div>'
});

var app = new Vue({
    el: '#app',
    data: {
        parentMessage: ''
    }
});
           

渲染後的結果是:

<div id="app">
    <div>dataMes</div>
</div>
           

這裡用

v-model

綁定了父級的資料

parentMessage

,當通過輸入框任意輸入時,子元件接收到的

props

也會實時響應,并更新元件模闆。

單向資料流

Vue 通過

props

傳遞資料是單向的,也就是父元件資料變化時會傳遞給子元件,但是反過來不行。之是以這樣設計,是盡可能将父子元件解耦,避免子元件無意中修改了父元件的狀态。

業務中會經常遇到兩種需要改變

prop

的情況。

  1. 一種是父元件傳遞初始值進來,子元件将它作為初始值儲存起來,在自己的作用域下可以随意使用和修改。
    <div id="app">
        <my-component :init-count="1"></my-component>
    </div>
               
    Vue.component('my-component', {
        props: ['initCount'],
        template: '<div>{{ count }}</div>',
        data() {
            return {
                count: this.initCount
            }
        }
    });
    
    var app = new Vue({
        el: '#app'
    });
               
    元件中聲明了資料

    count

    ,它在元件初始化時會擷取來自父元件的

    initCount

    ,之後就與之無關了,隻用維護

    count

    ,這樣就可以避免直接操作

    initCount

  2. 另一種情況就是

    prop

    作為需要被轉變的原始值傳入。
    <div id="app">
        <my-component :width="100"></my-component>
    </div>
               
    Vue.component('my-component', {
        props: ['width'],
        template: '<div :style="style">元件内容</div>',
        computed: {
            style() {
                return {
                    width: this.width + 'px'
                }
            }
        }
    });
    
    var app = new Vue({
        el: '#app'
    });
               
    因為用

    CSS

    傳遞寬度要帶機關(px),但是每次都寫太麻煩了,而且數值計算一般是不帶機關的,是以統一在元件内使用計算屬性就可以了。

資料驗證

Vue.component('my-component', {
    props: {
        propA: Number,
        propB: [String, Number],
        propC: {
            type: Boolean,
            default: true
        },
        propD: {
            type: Number,
            required: true
        },
        // 如果是數組或對象,預設值必須是一個函數來傳回
        propE: {
            type: Array,
            default() {
                return [];
            }
        },
        propF: {
            validator(value) {
                return value > 10;
            }
        }
    }
});
           

元件通信

元件關系可分為父子元件通信、兄弟元件通信和跨級元件通信。

自定義事件

當子元件需要向父元件傳遞資料時,就要用到自定義事件。

子元件用

$emit()

來觸發事件,父元件用

$on

來監聽子元件的事件。

父元件也可以直接在子元件的自定義标簽上使用

v-on

來監聽子元件觸發的自定義事件。

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component
        @increase="handleGetTotal"
        @reduce="handleGetTotal">
    </my-component>
</div>
           
Vue.component('my-component', {
    template: `
        <div>
            <button @click="handleIncrease">+1</button>
            <button @click="handleReduce">-1</button>
        </div>
    `,
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        handleIncrease() {
            this.counter++;
            this.$emit('increase', this.counter);
        },
        handleReduce() {
            this.counter--;
            this.$emit('reduce', this.counter);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleGetTotal(total) {
            this.total = total;
        }
    }
});
           

上面示例中,在改變元件的

counter

後,通過

$emit()

再把它傳遞給父元件。

$emit()

方法的第一個參數是自定義事件的名稱,後面的參數是要傳遞的資料,可以不填或填寫多個。

除了用

v-on

在元件上監聽自定義事件外,也可以監聽

DOM

事件,這時可以用

.native

修飾符表示監聽的是一個原生事件,監聽的是該元件的根元素。

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

使用

v-model

Vue 可以在自定義元件上使用

v-model

指令。

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component v-model="total"></my-component>
</div>
           
Vue.component('my-component', {
    template: '<button @click="handleIncrease">+1</button>',
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        handleClick() {
            this.counter++;
            this.$emit('input', this.counter);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    }
});
           

在使用元件的父級,并沒有在

<my-component>

使用

@input="handler"

,而是直接用了

v-model

綁定的一個資料

total

。這也可以稱作是一個文法糖,因為上面的示例可以間接地用自定義事件來實作:

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component @input="handleGetTotal"></my-component>
</div>
           
// 省略元件代碼

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleGetTotal() {
            this.total = total;
        }
    }
});
           

v-model

還可以用來建立自定義的表單輸入元件,進行資料雙向綁定。

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component v-model="total"></my-component>
    <button @click="handleReduce">-1</button>
</div>
           
Vue.component('my-component', {
    props: ['value'],
    template: '<input :value="value" @input="updateValue" />',
    methods: {
        updateValue(event) {
            this.$emit('input', event.target.value);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleReduce() {
            this.total--;
        }
    }
});
           

實作這樣一個具有雙向綁定的

v-model

元件要滿足下面兩個條件:

  1. 接收一個

    value

    屬性。
  2. 在有新的

    value

    時觸發

    input

    事件。

非父子元件通信

非父子元件一般有兩種,兄弟元件和跨多級元件。

在 Vue 中,推薦使用一個空的 Vue 執行個體作為中央事件總線(bus),也就是一個中介。

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
           
var bus = new Vue();

Vue.component('component-a', {
    template: '<button @click="handleEvent">傳遞事件</button>',
    methods: {
        handleEvent() {
            bus.$emit('on-message', '來自元件 component-a 的内容');
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        message: ''
    },
    mounted() {
        var _this = this;
        
        // 在執行個體初始化時,監聽來自 bus 執行個體的事件
        bus.$on('on-message', function(msg) {
            _this.message = msg;
        });
    }
});
           

首先建立一個名為

bus

的空 Vue 執行個體,然後定義全局元件

component-a

,最後建立 Vue 執行個體

app

。在

app

初始化時,監聽了來自

bus

的事件

on-message

,而在元件

component-a

中,點選按鈕會通過

bus

把事件

on-message

發出去,此時

app

就會接收到來自

bus

的事件,進而在回調裡完成自己的業務邏輯。

這種方法巧妙而輕量地實作了任何元件間的通信,包括父子、兄弟和跨級。如果深入使用,可以擴充

bus

執行個體,給它添加

data

methods

computed

等選項,這些都是可以共用的,在業務中,尤其是協同開發時非常有用,因為經常需要共享一些通用的資訊,比如使用者登入的昵稱、性别、郵箱和授權等。隻需子安初始化時讓

bus

擷取一次,任何時間、任何元件就可以從中直接使用,在單頁面富應用(SPA)中會很實用。

除了中央事件總線

bus

外,還有兩種方法可以實作元件間通信:父鍊和子元件索引。

父鍊

在子元件中,使用

this.$parent

可以直接通路該元件的父執行個體或元件,父元件也可以通過

this.$children

通路它所有的子元件,而且可以遞歸向上或向下無限通路,直到根執行個體或最内層的元件。

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
           
Vue.component('component-a', {
    template: '<button @click="handleEvent">通過父鍊直接修改資料</button>',
    methods: {
        handleEvent() {
            // 通路到父鍊後,可以做任何操作,比如直接修改資料
            this.$parent.message = '來自元件 component-a 的内容'
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        message: ''
    }
});
           

盡管 Vue 允許這樣操作,但在業務中,子元件應該盡可能避免依賴父元件的資料,更不應該去主動修改它的資料,因為這樣使得父子元件耦合,隻看父元件,很難了解父元件的狀态,因為它可能被任意元件修改,理想情況下,隻有元件自己能修改它的狀态。父子元件最好還是通過

props

$emit()

來通信。

子元件索引

當子元件較多時,通過

this.$children

來一一周遊出我們需要的一個元件執行個體時比較困難的,尤其是元件動态渲染時,它們的序列是不固定的。Vue 提供了子元件索引的方法,用特殊的屬性

ref

來為子元件指定一個索引名稱。

<div id="app">
    <button @click="handleRef">通過 ref 擷取子元件執行個體</button>
    <component-a ref="comA"></component-a>
</div>
           
Vue.component('component-a', {
    template: '<div>子元件</div>',
    data() {
        return {
            message: '子元件内容'
        }
    }
});

var app = new Vue({
    el: '#app',
    methods: {
        handleRef() {
            // 通過 $refs 來通路指定的執行個體
            var msg = this.$refs.comA.message;
            console.log(msg);
        }
    }
});
           

提示:

$refs

隻在元件渲染完成後才填充,并且它是非響應式的。它僅僅作為一個直接通路子元件的應急方案,應當避免在模闆或計算屬性中使用

$refs

使用

slot

分發内容

什麼是

slot

下面是一個正常的網站布局元件化後的機構:

<app>
    <menu-main></menu-main>
    <menu-sub></menu-sub>
    <div class="container">
        <menu-left></menu-left>
        <container></container>
    </div>
    <app-footer><app-footer>
</app>
           

當需要讓元件組合使用,混合父元件的内容與子元件的模闆時,就會用到

slot

,這個過程叫做内容分發(transclusion)。以

<app>

為例,它有兩個特點:

  1. <app>

    元件不知道它的挂載點會有什麼内容。挂載點的内容由

    <app>

    的父元件決定。
  2. <app>

    元件很可能有它自己的模闆。

props

傳遞資料、

events

觸發事件和

slot

内容分發就構成了 Vue 元件的 3 個 API來源,再複雜的元件也是由這 3 部分構成的。

作用域

父元件模闆的内容是在父元件作用域内編譯,子元件模闆的内容是在子元件作用域内編譯。

<div id="app">
    <child-component v-show="showChild"></child-component>
</div>
           
Vue.component('child-component', {
    template: '<div>子元件</div>'
});

var app = new Vue({
    el: '#app',
    data: {
        showChild: true
    }
});
           

這裡的狀态

showChild

綁定的是父元件的資料,如果想在子元件上綁定,應該是:

<div id="app">
    <child-component></child-component>
</div>
           
Vue.component('child-component', {
    template: '<div v-show="showChild">子元件</div>',
    data() {
        return {
            showChild: true
        }
    }
});

var app = new Vue({
    el: '#app'
});
           

是以,

slot

分發的内容,作用域是在父元件上的。

slot

用法

單個

slot

在子元件内使用特殊的

<slot>

元素就可以為這個子元件開啟一個

slot

(插槽),在父元件模闆裡,插入在子元件标簽内的所有内容将替代子元件的

<slot>

标簽及它的内容。

<div id="app">
    <child-component>
        <p>分發的内容</p>
        <p>更多分發的内容</p>
    </child-component>
</div>
           
Vue.component('child-component', {
    template: `
        <div>
            <slot>
                <p>如果父元件沒有插入内容,我将作為預設出現</p>
            </slot>
        </div>
    `
});

var app = new Vue({
    el: '#app'
});
           

上例渲染後的結果是:

<div id="app">
    <div>
        <p>分發的内容</p>
        <p>更多分發的内容</p>
    </div>
</div>
           

注意:子元件

<slot>

内的備用内容,它的作用域是子元件本身。

具名

slot

<slot>

元素指定一個

name

後可以分發多個内容,具名

slot

可以與單個

slot

共存。

<div id="app">
    <child-component>
        <h2 slot="header">标題</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部資訊</div>
    </child-component>
</div>
           
Vue.component('child-component', {
    template: `
        <div class="container">
            <div class="header">
                <slot name="header"></slot>
            </div>
            <div class="main">
                <slot></slot>
            </div>
            <div class="footer">
                <slot name="footer"></slot>
            </div>
        </div>
    `
});

var app = new Vue({
    el: '#app'
});
           

上例渲染後的結果是:

<div id="app">
    <div class="container">
        <div class="header">
            <h2>标題</h2>
        </div>
        <div class="main">
            <p>正文内容</p>
            <p>更多的正文内容</p>
        </div>
        <div class="footer">
            <div>底部資訊</div>
        </div>
    </div>
</div>
           

注意:如果沒有指定預設的匿名

slot

,父元件内多餘的内容片段都将被抛棄。

作用域插槽

作用域插槽是一種特殊的

slot

,使用一個可以複用的模闆替換已渲染元素。

<div id="app">
    <child-component>
        <template scope="props">
            <p>來自父元件的内容</p>
            <p>{{ props.msg }}</p>
        </template>
    </child-component>
</div>
           
Vue.component('child-component', {
    template: `
        <div class="container">
            <slot msg="來自子元件的内容"><slot>
        </div>
    `
});

var app = new Vue({
    el: '#app'
});
           

觀察子元件的模闆,在

<slot>

元素上有一個類似

props

傳遞資料給元件的寫法

msg="xxx"

,将資料傳到了插槽。父元件中使用了

<template>

元素,而且擁有一個

scope="props"

的特性,這裡的

props

隻是一個臨時變量,就像

v-for="item in items"

裡面的

item

一樣。

template

内可以通過臨時變量

props

通路來自子元件插槽的資料

msg

上例渲染後的結果是:

<div id="app">
    <child-component>
        <p>來自父元件的内容</p>
        <p>來自子元件的内容</p>
    </child-component>
</div>
           

作用域插槽更具代表性的用例是清單元件,允許元件自定義應該如何渲染清單每一項。

<div id="app">
    <my-list :books="books">
        <!-- 作用域插槽也可以是具名的 slot -->
        <template slot="book" scope="props">
            <li>{{ props.bookName }}</li>
        </template>
    </my-list>
</div>
           
Vue.component('my-list', {
    props: {
        books: {
            type: Array,
            default() {
                return [];
            }
        }
    },
    template: `
        <ul>
            <slot name="book"
                v-for="book in books"
                :book-name="book.name"
            ></slot>
        </ul>
    `
});

var app = new Vue({
    el: '#app',
    data: {
        books: [
            { name: '《book1》' },
            { name: '《book2》' },
            { name: '《book3》' }
        ]
    }
});
           

子元件

my-list

接收一個來自父級的

prop

數組

books

,并且将它在

name

book

slot

上使用

v-for

指令循環,同時暴露一個變量

bookName

此例的用意主要是介紹作用域插槽的用法,并沒有加入使用場景,而作用域插槽的使用場景既可以複用子元件的

slot

,又可以是

slot

内容不一緻。如果此例還在其他元件内使用,

<li>

的内容渲染權是由使用者掌握的,而資料卻可以通過臨時變量(比如

props

)從子元件内擷取。

通路

slot

Vue 提供了用來通路被

slot

分發的内容的方法

$slots

<div id="app">
    <child-component>
        <h2 slot="header">标題</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部資訊</div>
    </child-component>
</div>
           
Vue.component('child-component', {
    template: `
        <div class="container">
            <div class="header">
                <slot name="header"></slot>
            </div>
            <div class="main">
                <slot></slot>
            </div>
            <div class="footer">
                <slot name="footer"></slot>
            </div>
        </div>
    `,
    mounted() {
        var header = this.$slots.header,
            main = this.$slots.default,
            footer = this.$slots.footer;
            
        console.log(footer);
        console.log(footer[0].elm.innerHTML);
    }
});

var app = new Vue({
    el: '#app'
});
           

通過

$slots

可以通路某個具名

slot

this.$slots.default

包括了所有沒有被包含在具名

slot

中的節點。

$slots

在業務中幾乎用不到,在用

render

函數建立元件時會比較有用,但主要還是用于獨立元件開發中。

繼續閱讀