Vue.js 學習筆記之六:構建更復雜的元件

凌傑發表於2020-10-28

在掌握瞭如何構建與編譯 Vue 元件的基礎知識之後,接下來就可以試著來構建一些更具有實際用處的複雜元件了。為了賦予元件更具實用性的後面,首先要做的就是讓這些元件具備監聽使用者自定義事件的能力,並且允許使用者為這些自定義事件註冊相應的處理函式,而這一切都要從v-on指令在 Vue 元件中的使用說起。

元件中的v-on指令

接下來,我將會通過記錄為之前的say-hello元件增加一個名為"show-message"的自定義事件,併為該事件註冊處理函式的過程來學習如何賦予 Vue 元件事件處理能力。首先,我會暫且假設<say-hello>標籤所對應的元件已經在監聽一個名為"show-message"的自定義事件,並在index.htm中使用v-on指令為其指定了事件處理函式,具體程式碼如下:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>學習 vue 元件實驗(3):以專用檔案格式註冊元件</title>
</head>
<body>
    <div id="app">
        <say-hello @show-message="showMessage"
                   :who="who">
        </say-hello>
    </div>
</body>
</html>

然後,我只需要和之前使用 HTML 標準標籤元素一樣,在main.js中與上述頁面對應的 Vue 例項中新增"show-message"事件處理函式的實現,具體程式碼如下:

import Vue from 'vue';
import sayHello from './sayHello.vue';

new Vue({
    el: '#app',
    components: {
        'say-hello': sayHello
    },
    data: {
        who:'vue'
    },
    methods: {
        showMessage : function() {
            window.alert('Hello, ' + this.who);
        }
    }
});

到目前為止,大家所看到的步驟與普通的事件處理過程並沒有什麼區別,接下來我只需要讓之前做的假設成真就可以了。換而言之,現在的任務是要真正實現讓<say-hello>元件監聽"show-message"這個自定義事件。對於此類問題,Vue.js 框架為我們提供的解決方案是:利用元件內部的某個 HTML 元素標籤的標準事件來觸發該元件的自定義事件。具體到當前專案中,我要做的就是對sayHello.vue檔案做出如下修改:

<template>
    <div class="box">
        <h1>你好, {{ you }}!</h1>
        <input type="text" v-model="you" />
        <input type="button" value="彈出對話方塊" @click="showMessage">
    </div>
</template>

<script>
    const sayHello = {
        name: 'sayHello',
        props : ['who'],
        data : function() {
            return {
                you: this.who
            }
        },
        methods: {
            showMessage: function() {
                this.$emit('show-message', this.who);
            }
        }
    };
    export default sayHello;
</script>

<style scoped>
    .box {
        height: 130px;
        width: 250px;
        background-color: #ccc;
    }
</style>

在上述程式碼中,我首先在元件template模版部分中新增了一個顯示文字為"彈出對話方塊"的按鈕元素,併為其註冊了單擊事件的處理函式。然後在實現該單擊事件的處理函式時,我通過呼叫this.$emit()方法通知了當前元件的呼叫方(即index.htm頁面):"show-message"事件被觸發了。在這裡,this.$emit()方法的第一個實參應該是一個用於指定該元件被觸發的事件名稱。爾後,如果還有要傳遞給該元件事件處理函式的實參,還可以在後面依次加上這些實參(例如這裡的this.who)。這樣一來,<say-hello>標籤的使用者只需要單擊該元件中的"彈出對話方塊"按鈕,就可以觸發"show-message"事件了。

元件中的v-model指令

當然,和普通的 HTML 元素標籤一樣,使用v-on指令繫結事件的方法只能處理一些簡單的操作,對於更為複雜的表單操作,還是需要用到v-model指令來實現。下面,我將通過記錄實現一個"計數器"元件的過程來了解v-model指令在 Vue 元件中的使用方法。為此,我們需要開啟第四個實驗專案,其具體構建步驟如下:

  1. 配置專案並安裝依賴項
    首先,我需要在code/00_test目錄中再建立一個名為component_4的實驗目錄。然後,由於component_4專案所需要的依賴項以及薑母結構都與component_3專案相同,為了免除不必要的工作量,以節省花費專案配置上的時間,我們可以直接將component_3目錄下的package.jsonwebpack.config.js這兩個專案配置檔案拷貝至component_4目錄下(如果需要的話,也可以稍作修改),並在component_4目錄下執行npm install命令來安裝已經配置在package.json檔案中的專案依賴項。

  2. 假設元件已支援v-model指令
    與之前一樣,我可以暫且假設自己手裡已經有了一個支援v-model指令的"計數器"元件,並在專案的src/main.js中實現 Vue 例項時將其註冊成了區域性元件,具體程式碼如下:

     import Vue from 'vue';
     import counter from './counter.vue';
    
     new Vue({
         el: '#app',
         components: {
             'my-counter': counter
         },
         data: {
             num: 0
         },
         methods: {}
     });
    

    然後,我只需在src/index.htm檔案中像使用普通的 HTML 元素標籤一樣,對其使用v-model指令即可,具體程式碼如下:

     <!DOCTYPE html>
     <html lang="zh-cn">
     <head>
         <meta charset="UTF-8">
         <title>學習 vue 元件實驗(4):構建更復雜的元件</title>
     </head>
     <body>
         <div id="app">
             <my-counter v-model="num"></my-counter>
         </div>
     </body>
     </html>
    
  3. 實現元件對v-model指令的支援
    現在的任務就是實現這個"計數器"並讓其以符合之前假設的方式支援v-model指令。在此之前我們首先知道,在 Vue.js 框架的元件語義下,v-model指令本質上只是一個語法糖。換而言之,<my-counter v-model="num"></my-counter>實際上等價於:

    <my-counter :value="num" @input="num = $event.target.value"></my-counter>
    

    所以,我們在src/counter.vue檔案中實現"計數器"元件時只需要按照之前學習到的方法分別處理一下由v-bind指令繫結的value元件屬性和由v-on指令註冊的input自定義事件即可,具體程式碼如下:

     <template>
         <div class="box">
             <input type="button" value="-" @click="changeCounter(-1)">
             <input type="text" :value="value" @input="changeInput" >
             <input type="button" value="+" @click="changeCounter(1)">
         </div>
     </template>
    
     <script>
         const counter = {
             name: 'counter',
             props : ['value'],
             methods: {
                 changeCounter: function(count) {
                     this.$emit('input', this.value + count);
                 },
                 changeInput: function(event) {
                     let num = parseInt(event.target.value);
                     if(isNaN(num)) {
                         num = 0;
                     }
                     this.$emit('input', num);
                 }
             }
         };
         export default counter;
     </script>
    
     <style scoped>
         .box {
             width: 340px;
             padding: 5px;
             background-color: #ccc;
         }
     </style>
    

元件插槽

到目前為止,大家看到的都是一些功能單一的元件,但在實際生產環境中,我們在很多時候可能需要將這些功能單一的元件組合成一個更為複雜的元件,這意味著我們需要像使用普通 HTML 元素標籤一樣巢狀使用元件的自定義標籤,例如在下面這段 HTML 程式碼中:

<div>
    <h1>標題</h1>
</div>

<h1>標籤是被插入到<div>標籤的內部的,要想在 Vue 元件的自定義標籤中實現這樣的效果,就必須要預先在用作外層標籤的元件模版中預留一些插槽標籤(即<slot>),以指定內層標籤可以插入的位置。例如,我可以執行一下步驟來為上面的"計數器"元件增加一個外層盒子,以便使用者可以為自己的計數器增加一個標題:

首先,在component_4/src目錄下建立一個名為box.vue檔案,並在其中編寫如下程式碼:

<template>
    <div>
        <header>
            <slot name="title"></slot>
        </header>
        <main>
            <slot></slot>
        </main>
    </div>
</template>

<script>
    const box = {
        name: 'box'
    };
    export default box;
</script>

<style scoped>
</style>

然後在使用該元件之前,我同樣需要先在component_4/src/main.js檔案中將其註冊為 Vue 例項的區域性元件,具體程式碼如下:

import Vue from 'vue';
import box from './box.vue'
import counter from './counter.vue';

new Vue({
    el: '#app',
    components: {
        'my-box' : box,
        'my-counter': counter
    },
    data: {
        num: 0
    },
    methods: {}
});

現在,我們就可以在component_4/src/index.htm檔案中巢狀使用元件的標籤了。正如大家所見,我在box元件中為使用者預留了兩種最常用的插槽,一種是帶name 屬性的<slot>標籤,通常被稱之為"具名插槽";另一種是不帶屬性的<slot>標籤,通常被稱之為"預設插槽"。其中,具名插槽的作用是指定待插入元素的意義或功能,需用帶v-slot指令的<template>標籤來指定,而預設插槽實際上就是名稱為default的插槽。在這裡,所有被插入到box元件標籤內部,且沒有被<template>標籤指定的元素標籤都會被插入到該插槽中。例如,我們可以像下面這樣為box元件插入內容:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>學習 vue 元件實驗(4):構建更復雜的元件</title>
</head>
<body>
    <div id="app">
        <my-box>
            <template v-slot:title>
                <h1>我的計數器</h1>
            </template>
            <p>這是一個"計數器"元件:</p>
            <my-counter v-model="num"></my-counter>
        </my-box>
    </div>
</body>
</html>

當然,如果想要讓 HTML 文件的結構更清晰一些,我們也可以使用帶v-slot指令的<template>標籤來指定要插入到預設插槽中的內容,具體做法如下:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>學習 vue 元件實驗(4):構建更復雜的元件</title>
</head>
<body>
    <div id="app">
        <my-box>
            <template v-slot:title>
                <h1>我的計數器</h1>
            </template>
            <template v-slot:default>
                <p>這是一個"計數器"元件:</p>
                <my-counter v-model="num"></my-counter>
            </template>
        </my-box>
    </div>
</body>
</html>

相關文章