Vue Components

Vue:2.5

Vue 的實作是由一個一個的 Components 建立起來的。因此,開發者可以透過 Components 的方式自訂元件以供使用。

Vue Components
(圖片來源:Composing with Components

Basic Components

Vue 的元件在包裝好之後,會以類似 HTML tag 的方式使用。

下面就以 計算按鍵次數 為例來當 demo。

範例

<div id="app">
    <!-- 使用元件 -->
    <button-counter></button-counter>
</div>
// 建立元件(Global)
Vue.component('button-counter', {
    data: function () {
        return {
            count: 0
        };
    },
    // 元件的 HTML
    template: '<button @click="count++">You clicked me {{ count }} times.</button>'
});

const vm = new Vue({
    el: '#app',
});

Notice:Vue Instance 中的 data 可以使用 ObjectFunction,但是在 Components 中只能使用 Function
原因是每個 Components 的資料都要是獨立運作的。細節可參考官網說明。

除了以 Global 的方式註冊元件之外,也可以透過參數的方式代入元件。

範例

const button = {
    data: function () {
        return {
            count: 0
        };
    },
    template: '<button @click="count++">You clicked me {{ count }} times.</button>'
};

// 以參數的方式帶入元件
const vm = new Vue({
    el: '#app',
    components: {
        'button-counter': button
    }
});

Templates

引入元件的 template 有兩種做法:String TemplateDOM Template

其中 String Template 方法就是前面提到的做法,直接利用字串塞到 Component 的 template 參數裡面。但是這個做法的缺點很明顯,當你的介面複雜就會讓 HTML 變得特別冗長及難閱讀。

DOM Template 可以解決這個問題,DOM Template 主要是透過 <script type="text/x-template"> 建立,這樣一來就可以保持原本 HTML 的縮排,可讀性就跟著增高了。

範例

<div id="app">
    <button-counter></button-counter>
</div>

<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
    <div>
        <button @click="count++">You clicked me {{ count }} times.</button>
    </div>
</script>
Vue.component('button-counter', {
    data: function () {
        return {
            count: 0
        };
    },
    // 指定 template
    template: '#button-counter-component'
});

const vm = new Vue({
    el: '#app',
});

Template 部份還有一個隱藏的地雷,在 HTML 中的 <ul><ol><table><select> 這幾個 tag 底下是不能隨便塞其他的 tag 的,例如 <ul> 底下就是要 放 <li>。因此,當我們的 Component 遇到這幾個例外的 tag 如果沒有處理好會導致瀏覽器 parse 出來的結構有問題。

例如:

<div id="app">
    <table>
        <table-render></table-render>
    </table>
</div>

跑出來的結果會變成:

<div id="app">
    <tr>
        <td>TEST</td>
    </tr>
    <table></table>
</div>

為了因應這個案例,Vue 提供了 is 的語法引入 Template。如果是上述特例的 tag ,就可以利用 is 語法繞過原本的錯誤。

<div id="app">
    <table>
        <tr is="table-render"></tr>
    </table>
</div>

另外還有一個小地方要注意,很多人會把 Template 寫成這樣:

<template>
    <button>Open Modal</button>
    <modal>XXXXX</modal>
</template>

這樣寫的話會出現 Component template should contain exactly one root element 的錯誤。

基本上在 Component Template 中要有一個最外層的 Root element,不然沒辦法知道這個元件的邊界在哪。(來源 Vue#7088

元件的資料傳遞

既然 Vue 是透過多個元件組合而成,那一定會遇到資料傳遞的問題。
Vue 裡,由 父元件 傳遞到 子元件 的動作叫 Pass Props,由 子元件 傳遞到 父元件 的動作叫 Emit Events

Vue Components communication
(圖片來源:Kuro's Blog

Emit Events

子元件 需要透過 Event 才能與 父元件溝通。實作上我們必須先自訂一個 Event,再將 Event 拋給 父元件

以剛剛 計算按鍵次數 的例子來說,當我有兩顆一模一樣的按鍵時,其實它們都是各自擁有自己的 count data,如果我想要加總,就必須拋給 父元件 整合。

範例

<div id="app">
    Total: {{ total }}
    <!-- 自訂一個 to-father 的 Event,內容是呼叫父元件的 parentGetOne 方法 -->
    <button-counter @to-father="parentGetOne()"></button-counter>
    <button-counter @to-father="parentGetOne()"></button-counter>
</div>

<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
    <button @click="getOne()">You clicked me {{ count }} times.</button>
</script>
// 子元件
Vue.component('button-counter', {
    data: function () {
        return {
            count: 0
        };
    },
    methods: {
        getOne: function () {
            this.count++;
            // 透過 $emit 將自訂的 to-father Event 丟給父元件
            this.$emit('to-father');
        }
    },
    template: '#button-counter-component'
});

// 父元件
const vm = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        parentGetOne: function () {
            this.total++;
        }
    }
});

除了可以呼叫 父元件 的方法之外,也可以透過 $emit子元件 的資料拋給父類別。

範例

<button-counter @to-father="parentGetOne"></button-counter>
<!-- 也可以寫成代入 ...arguments -->
<button-counter @to-father="parentGetOne(...arguments)"></button-counter>
// 子元件
// $emit 第一個參數為事件名稱,後面傳入的皆是參數
this.$emit('to-father', this.count, 'JohnsonLu');
// 父元件
const vm = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        parentGetOne: function (count, name) {
            console.log(count);
            console.log(name);
        }
    }
});
Pass Props

父元件 要將資料傳給 子元件 時,就要透過 v-bind 搭配 props 參數來控制。
Emit Events 的範例為例,利用 props父元件 上多一個計數器預設值的功能,讓其他 子元件 可以使用。

範例

<div id="app">
    Total: {{ total }}
    <!-- 透過 v-bind 綁定 initial-counter 屬性,並代入父元件的資料 -->
    <button-counter :initial-counter="button1" @to-father="parentGetOne"></button-counter>
    <!-- 也可以直接傳入 static value -->
    <button-counter :initial-counter="button2" title="Static Value" @to-father="parentGetOne"></button-counter>
</div>

<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
    <button @click="getOne()">You clicked me {{ count }} times.</button>
</script>
// 子元件
Vue.component('button-counter', {
    // 接受父元件的資料(注意 camelCase 和 kebab-case 的問題)
    props: ['initialCounter', 'title'],
    data: function () {
        return {
            count: this.initialCounter
        };
    },
    methods: {
        getOne: function () {
            this.count++;
            // 透過 $emit 將 Event 丟給父元件
            this.$emit('to-father');
        }
    },
    template: '#button-counter-component'
});

// 父元件
const vm = new Vue({
    el: '#app',
    data: {
        // 初始化計數器的值
        button1: 10,
        button2: 20,
        total: 30,
    },
    methods: {
        parentGetOne: function (count, name) {
            this.total++;
        }
    }
});

Notice: HTML 是不區分大小寫的,因此如果遇到像 initialCounter 的問題,在 HTML 會以 - 隔開(camelCase 和 kebab-case)。

Props 也可以進行基本的 Validation,細節可以參考 官方文件
範例

props: {
    initialCounter: Number,
    propA: {
        type: String,
        // 設定預設值
        default: 'No data'
    },
    propB: {
        // 可以同時 allow 多種資料型態
        type: [String, Number],
        // 設定該參數為必要參數
        required: true
    }
}
Event Bus

根據網站的複雜度,元件不會只有單純的父子關係,也會有一些平行的元件需要溝通,再以 計算按鍵次數 為例的話,如果要清除兩個 button 和 total 的數字就會不太好處理,所以我們必須透過 Event Bus$on 來解決各個元件的溝通問題。

Event Bus 可以巡迴各個元件之間,幫助這些元件溝通。

Event Bus
(圖片來源:Kuro's Blog

實作的步驟如下:

  1. 建立 Event Bus 元件
  2. 各元件新增 reset method
  3. 建立 button-reset 元件,並新增 reset 方法,當觸發時會透過 $emit 呼叫對方元件的 reset event
  4. 各元件在 created 階段註冊綁定 Event Bus 拋過來的事件

範例

<div id="app">
    Total: {{ total }}
    <button-counter :initial-counter="button1" @to-father="parentGetOne"></button-counter>
    <button-counter :initial-counter="button2" @to-father="parentGetOne"></button-counter>

    <!-- 新增 Reset 元件 -->
    <button-reset></button-reset>
</div>

<!-- DOM Template -->
<script type="text/x-template" id="button-counter-component">
    <button @click="getOne()">You clicked me {{ count }} times.</button>
</script>
// Event Bus
const bus = new Vue();

// Reset 元件
Vue.component('button-reset', {
    template: '<button @click="reset">reset</button>',
    methods: {
        reset: function () {
            bus.$emit('reset');
        },
    }
});

// 子元件
Vue.component('button-counter', {
    // 接受父元件的資料(注意 camelCase 和 kebab-case 的問題)
    props: ['initialCounter'],
    data: function () {
        return {
            count: this.initialCounter
        };
    },
    methods: {
        getOne: function () {
            this.count++;
            // 透過 $emit 將 Event 丟給父元件
            this.$emit('to-father');
        },
        // 子元件的 reset method
        reset: function () {
            this.count = 0;
        }
    },
    // 當元件建立時綁定
    created: function () {
        // 用 $on 接收 $emit 的 trigger
        bus.$on('reset', this.reset);
    },
    template: '#button-counter-component'
});

// 父元件
const vm = new Vue({
    el: '#app',
    data: {
        // 初始化計數器的值
        button1: 10,
        button2: 20,
        total: 30,
    },
    methods: {
        parentGetOne: function (count, name) {
            this.total++;
        },
        // 父元件的 reset method
        reset: function () {
            this.total = 0;
        },
    },
    // 當元件建立時綁定
    created: function () {
        // 用 $on 接收 $emit 的 trigger
        bus.$on('reset', this.reset);
    }
});

Single File Components

隨著元件數一多,在架構上就必須拆出來統一管理,因此可以透過 Single File Components 的特性將元件封裝成多個 .vue 檔。

範例

<!-- Hello.vue -->

<!-- Component template-->
<template>
    <p>{{ greeting }} World!</p>
</template>

<!-- Component object -->
<script>
module.exports = {
    data: function () {
        return {
            greeting: 'Hello'
        }
    }
}
</script>

<!-- Component CSS -->
<style scoped>
p {
    font-size: 2em;
    text-align: center;
}
</style>
// 引入 Hello Component
import Hello from './Hello.vue';

const vm = new Vue({
    el: '#app',
    data: {
        total: 30,
    },
    components: {
        'hello-button': Hello
    }
});

Notice:

Single File Components 功能要使用有掛載 vue-loaderWebpack 編譯才可以執行。

這邊要注意一下 Hello.vue 中的 <style scoped>scoped 參數代表這個 CSS 設定只限於這個元件,不會影響到 Globalscoped 也不會套用到動態新增的 HTML 中。

Categories: Vue