【Vue學習(二)元件和插槽】

Moluuu發表於2020-12-05

元件

元件是 Vue 強大的功能之一

Vue元件具有封裝可複用的特點,能夠讓你在複雜的應用中拆分成獨立模組使用

在開發中,我們可以將重複使用的功能封裝為元件,方便開發提高效率

因為元件是可複用的 Vue 例項,所以它們與 new Vue 接收相同的選項

例如 datacomputedwatchmethods 以及生命週期鉤子等。僅有的例外是像 el 這樣根例項特有的選項。

為什麼需要元件

  1. 提高複用性
  2. 解耦
  3. 提升開發效率

建立元件

Vue.js 使用component();函式來建立元件,該函式中可以傳入兩個引數 分別為元件名、以物件的形式定義(描述)元件。

我們來看一下如何定義一個簡單的元件

<body>
<div id="app">
    <!--
    定義一個 sc標籤,該標籤沒有任何功能
    甚至可以說該標籤在 html中不合法,
    我們可以 以定義元件的方式使其擁有功能且合法
    -->
    <sc></sc>
</div>
    
    <!--引入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    //定義元件: 第一個引數為元件名,第二個引數為定義元件
    Vue.component("sc",{
        data: function () {
            return {
                x: 0
            }
        },
        //template屬性的值就是元件的模板,這裡我們定義了一個點選會 +1的小按鈕
        template: '<div><button v-on:click="x++">點我+1s</button><span>--->{{x}}s</span></div>'
    });
    //元件是可複用的 Vue 例項,且帶有一個名字,在這個例子中是 <sc>。
    // 我們可以在一個通過 new Vue 建立的 Vue 根例項中,把這個元件作為自定義元素來使用
    var vm = new Vue({
        el: "#app"
    });
</script>
</body>

這裡就不測試了,感興趣可以自己拷過去玩一下。

元件的複用

元件是可複用的,且每個元件中資料是封裝在元件內部的,相同元件之間資料互不影響。

我們可以將上述元件多複製幾個來測試一下

<div id="app">
    <!--將 sc元件複製四份在網頁中觀察元件中的資料是否獨立-->
    <sc></sc>
    <sc></sc>
    <sc></sc>
    <sc></sc>
</div>

沒有問題,x的值是獨立的

當我們定義這個 <sc> 元件時,你可能會發現它的 data 並不是像這樣直接提供一個物件:

data: {
  x: 0
}

取而代之的是,一個元件的 data 選項必須是一個函式,因此每個例項可以維護一份被返回物件的獨立的拷貝:

data: function () {
  return {
    count: 0
  }
}

如果 Vue 沒有這條規則,那我們就沒辦法做到元件之間的資料相互獨立。

元件的組織

通常一個應用會以一棵巢狀的元件樹的形式來組織:

例如,你可能會有頁頭、側邊欄、內容區等元件,每個元件又包含了其它的像導航連結、博文之類的元件。

​ 為了能在模板中使用,這些元件必須先註冊以便 Vue 能夠識別。這裡有兩種元件的註冊型別:全域性註冊區域性註冊

以 Vue.component();函式定義的元件為全域性註冊的元件

​ 全域性註冊的元件可以用在其被註冊之後的任何 (通過 new Vue) 新建立的 Vue 根例項,也包括其元件樹中的所有子元件的模板中。

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

在所有子元件中也是如此,也就是說這三個元件在各自內部也都可以相互使用

區域性註冊

全域性註冊往往是不夠理想的。比如,如果你使用一個像 webpack 這樣的構建系統,全域性註冊所有的元件意味著即便你已經不再使用一個元件了,它仍然會被包含在你最終的構建結果中。這造成了使用者下載的 JavaScript 的無謂的增加。

在這些情況下,你可以通過一個普通的 JavaScript物件來定義元件,並使用 Vue例項中的 components屬性來新增一個元件作為該例項中的區域性元件

<body>
<div id="app">
    <sc></sc>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    // 使用普通 JavaScript物件的形式定義一個元件例項
    var sc = {
        data(){
            return{
                x: 0
            }
        },
        template:'<div><button v-on:click="x++">點我+1s</button><span>--->{{x}}s</span></div>'
    }

    var vm = new Vue({
        el: "#app",
        // 使用 components屬性區域性註冊到該 Vue例項中
        components: {
            // key為 元件名,value為元件例項
            'sc':sc
        }
    });
</script>
</body>

注意:區域性註冊的元件在其子元件中不可用

例如,如果你希望 ComponentAComponentB 中可用,則你需要這樣寫:

var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

Prop

每個Vue元件例項都有獨立範圍的作用域的,這意味著不能並且不應該在子元件的模板內直接引用父元件的資料。

但 Vue.js可以通過使用 props引數實現父元件向子元件傳遞資料這一操作。

示例:

為了便於理解,你可以將這個Vue例項看作<lc>的父元件。
如果我們想使父元件的資料,則必須先在子元件中定義props屬性,否則無法拿到父元件中的資料。

<body>
<div id="app">
    <!--但實際上並拿不到-->
    <lc v-for="item in la"></lc>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    Vue.component('lc',{
        // 元件定義沒有任何問題,按理應該可以拿到陣列中遍歷的每一項
        template: '<li>{{item}}</li>'
    });
    new Vue({
        el: "#app",
        data: {
            la: ["js","java","c#","go"]
        }
    });
</script>
</body>

那麼父元件如何動態的傳遞資料給子元件呢?

還記得v-bind指令的作用嗎,其作用是用於動態繫結 html屬性或者是元件的 props值,所以應該使用v-bind指令來動態傳遞資料。

<body>
<div id="app">
    <!--新增 v-bind:將遍歷出來的每一項繫結到 test中-->
    <lc v-for="item in la" v-bind:test="item"></lc>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    Vue.component('lc',{
        // 使用 props屬性拿到上面 v-bind指令中繫結的test
        props:['test'],
        // 在模板中取出props拿到的 test
        template: '<li>{{test}}</li>'
    });
    new Vue({
        el: "#app",
        data: {
            la: ["js","java","c#","go"]
        }
    });
</script>
</body>

關於Prop如果存在疑問請移步 Vue官網

template標籤

在上文中我們使用 component();函式建立元件,該方式屬於一種註冊語法糖。

儘管語法糖簡化了元件註冊,但在 template屬性中拼接 HTML元素是比較麻煩的。

這也導致了 HTML和 JavaScript的高耦合性,慶幸的是,Vue.js提供了兩種方式將定義在 JavaScript中的 HTML模板分離出來。

一種是使用 script標籤,但這種方式仍不是最優解 就不提了。

我們直接快進到 <template></template>

<body>
<div id="app">
    <!-- 3、使用該元件-->
<mc></mc>
</div>
<!-- 1、使用 template標籤,在標籤體中定義元件的內容-->
<template id="myComponent">
    <div>
        <strong>在這裡定義元件模板</strong>
        <p>的話就不需要</p>
        <span>做一些拼接 Html標籤的操作了</span>
    </div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    // 2、註冊元件
    Vue.component("mc",{
        // 選中 template標籤中的 id
        template: "#myComponent"
    });
    new Vue({
        el: "#app"
    });
</script>
</body>

如上

我建議使用<script><template>標籤來定義元件的 HTML模板。
這使得 HTML程式碼和 JavaScript程式碼是分離的,便於閱讀和維護。
另外,在Vue.js中,可建立.vue字尾的檔案,在.vue檔案中定義元件,這個內容我會在後面的文章介紹。

插槽

為了讓元件可以組合,我們需要一種方式來混合父元件的內容與子元件自己的模板。

這個處理稱為內容分發,Vue.js實現了一個內容分發 API,使用特殊的 <slot> 元素作為原始內容的插槽。

插槽簡單示例

如下,定義了一個父元件和一個子元件。

在父元件中使用了子元件,並嘗試為子元件分發文字內容。

<body>
<div id="app">
    <!--使用父元件-->
    <parent></parent>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
// 註冊父元件
Vue.component("parent",{
    // 在父元件的模板中使用子元件
    template: '<div><p>我是父元件</p><child><p>使用子元件,看看能不能在渲染時顯示這行字</p></child></div>'
});
// 註冊子元件
Vue.component("child",{
    template: '<div><p>我是子元件</p></div>'
});
new Vue({
    el: "#app"
});
</script>
</body>

測試:

結果是父元件分發的文字內容 並沒有顯示出來。

怎麼讓父元件分發的內容顯示出來呢?

很簡單,在子元件中為父元件留一個插槽即可 也就是使用<solt></solt>標籤

使用<solt></solt>標籤後,如果父元件在使用子元件時 為子元件分發了內容,則顯示父元件分發的內容。

反之未分發內容 則會顯示子元件<solt></solt>標籤中的內容

// 註冊子元件
Vue.component("child",{
    // 在子元件模板中留一個插槽
    template: '<div><slot>我是子元件中的 slot</slot></div>'
});

使用該標籤後,再進行測試

再將父元件中分發的內容注掉

// 註冊父元件
Vue.component("parent",{
    // 在父元件的模板中使用子元件,註釋掉父標籤分發的內容
    template: '<div><p>我是父元件</p><child><!--<p>使用子元件,看看能不能在渲染時顯示這行字</p>--></child></div>'
});

官方文件對於插槽的應用場景是這樣描述的:

我們經常需要向一個元件傳遞內容

Vue 自定義的 <slot> 元素讓這變得非常簡單

只要在需要的地方加入插槽就行了——就這麼簡單!

結合上面的例子來理解就是這樣的:

1.父元件在引用子元件時希望向子元件傳遞模板內容<p>使用子元件,看看能不能在渲染時顯示這行字</p>

2.子元件讓父元件傳過來的模板內容在所在的位置顯示

3.子元件中的<slot>就是一個槽,可以接收父元件傳過來的模板內容,<slot> 元素自身將被替換

4.<child></child>元件沒有包含一個 <slot> 元素,則該元件起始標籤和結束標籤之間的任何內容都會被拋棄

插槽也有分類,像上面使用的這種就是預設插槽,它是一個匿名slot,它只能表示一個插槽。

除了預設插槽還有具名插槽以及作用域插槽。

具名插槽

顧名思義,具名插槽 就是存在名字的插槽,也就是擁有 name屬性的插槽。

為插槽定義 name自然是為了能夠更準確的將資料插入到指定的插槽中。

我們來看一下示例:

下列程式碼和預設插槽示的例程式碼沒有太大區別,無非是多了個插槽 且都加上了 name屬性。

可以預見的 網頁中會渲染出子元件中預設的標題和文字內容。

<body>
<div id="app">
<!-- 3、使用父元件,不進行任何操作-->
<page></page>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('page',{
    // 2、在父元件的模板中呼叫子元件,不進行任何額外操作。
    template: "<div><ct></ct></div>"
});
// 1、註冊子元件
Vue.component('ct',{
    // 1.1、在子元件的模板中定義兩個插槽,併為這兩個個插槽分別 新增 name屬性
    template: "<div><h1><slot name='pageTitle'>預設標題</slot></h1><p><slot name='pageText'>預設文字內容</slot></p></div>"
});
new Vue({
    el: "#app",
});
</script>
</body>

網頁中的顯示:

我們在父元件的模板中做一些操作

Vue.component('page',{
    // 對模板進行一些改動,在子元件中新增 span標籤,為該 span標籤新增 slot屬性,值為'pageText'
    template: "<div><ct><span slot='pageText'>新文字內容</span></ct></div>"
});

在模板中 往呼叫的子元件裡 寫一個 span標籤,併為其新增 slot屬性。

該屬性的值為:子元件中定義插槽 name屬性的值

<slot name='pageText'>預設文字內容</slot>

我們再來看看網頁會如何渲染

子元件中 name為 ‘pageText’ 插槽的文字內容,被替換為了 span標籤中的文字內容。

那我們再將該 span標籤的 slot屬性值改為 ‘pageTitle’,是不是就意味著能夠替換掉 name為 ‘pageTitle’ 插槽的文字內容呢?

Vue.component('page',{
    // 對模板進行一些改動,在子元件中新增 span標籤,為該 p標籤新增 slot屬性,值為'pageTitle'
    template: "<div><ct><span slot='pageTitle'>新文字內容</span></ct></div>"
});

確實是如此

也就是說為插槽設定 name後,父元件想要使用哪個插槽 指定該插槽的 name即可。

當然了,該用法早已廢棄。❌(雖然被廢棄,但仍可繼續使用)

新用法

我們再看看2.6.0之後的具名插槽如何使用

在2.6.0後,v-slot 指令替代了 slot屬性。

需要注意的是,v-slot指令只能使用在 template標籤中,這一點和已經廢棄的 slot屬性有所不同。

我們在一個 <template> 元素上使用 v-slot 指令,並傳入引數的形式提供其名稱:

<body>
<div id="app">
<!--3、使用父元件-->
<page>
</page>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

    // 1、註冊子元件
Vue.component("ct",{
    // 1.1、在模板中使用 slot標籤並新增對應的 name屬性
    template: `<div> 
                  <slot name="pageTitle">預設標籤</slot>
                  <slot>預設插槽的內容</slot>
                  <slot name="pageText">預設內容</slot>
              /div>`
});
// 2、註冊父元件
Vue.component("page",{
    template: `<div>
                    // 2.1、在父元件模板中呼叫子元件標籤
                    <ct>
                        // 2.2、在子元件標籤中書寫 template標籤,並使用 v-slot指令,指令的引數為插槽 name的值
                        <template v-slot:pageTitle>
                            <h1>
                                <span>新標籤</span>
                            </h1></template>
                        // 2.3、該 template標籤不使用 v-slot指令
                        <template>
                        <p>我會替換掉預設插槽的內容</p>
                        </template>
                        <template v-slot:pageText>
                        </template>
                    </ct>
                </div>`
});
</script>
</body>

<template> 元素中的所有內容都將會被傳入相應的插槽。

任何沒有被包裹在帶有 v-slot<template> 中的內容都會被視為預設插槽的內容。

測試:

一個未新增 name屬性 的 <slot>標籤 ,會預設攜帶隱藏的 name屬性值 “default” 。

也就是說我們可以在 template標籤中使用 v-slot指令,傳入 ‘default’ 引數使其包裹的內容改變為預設插槽的內容。

template: `<div>
                // 2.1、在父元件模板中呼叫子元件標籤
                <ct>
                    // 2.2、在子元件標籤中使用 template標籤,並使用 v-slot指令,指令的引數為插槽 name的值
                    <template v-slot:pageTitle>
                        <h1>
                            <span>新標籤</span>
                        </h1></template>
                    // 2.3、該 template標籤不使用 v-slot指令
                    <template>
                    <p>我會替換掉預設插槽的內容</p>
                    </template>
    				/* 
    				  2.4、為該template標籤的 v-slot指令傳入引數 default,
    				  使其覆蓋掉第二個 template標籤中預設插槽的內容。
    				*/
                    <template v-slot:default>
                    <p>嘗試再次替換預設插槽的內容</p>
                    </template>
                </ct>
            </div>`

測試:

作用域插槽

在上述兩種插槽中,都是子元件接收父元件傳遞的資料。

而作用域插槽則是用來,向父元件中傳入子元件的資料

可能有些人就要問了,我們直接在父元件呼叫子元件時不分發內容,使用子元件插槽中的內容不就實現了該操作嗎?

匿名插槽和具名插槽中,插槽上不繫結資料,所以父元件提供的模板既要包括樣式又要包括資料。

而作用域插槽是子元件提供資料,父元件只需要提供一套樣式即可。

使用作用域插槽比較簡單,無非兩步:

  1. 將子元件資料繫結給 slot 上的屬性
  2. 父元件模板中通過 slot-scope 拿到 slot 物件,並進行屬性訪問。

以下為示例:

<body>
<div id="app">
<!-- 2、為子元件中拿到的資料新增不同的樣式-->
<ct>
    <!--
    2.1、呼叫子元件標籤後書寫 template標籤,並使用 slot-scope拿到子元件插槽中的資料,
    將該資料命名為 sc(這個可以隨意),我們可以通過該名稱訪問到子元件中的資料
    -->
    <template slot-scope="sc">
        <!--ul li的形式輸出-->
        <ul>
            <li v-for="item in sc.test">{{item}}</li>
        </ul>
    </template>
</ct>
<ct>
    <template slot-scope="sc">
            <!--以加粗的形式輸出,每個值之間使用 "--》"字串分隔 -->
            <strong>{{sc.test.join('-->' )}}</strong>
    </template>
</ct>
<ct>
    <template slot-scope="sc">
        <!--正常輸出,每個值之間使用 " / "分隔-->
        <p>{{sc.test.join(' / ')}}</p>
    </template>
</ct>


</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

    // 1、註冊子元件
Vue.component("ct",{
    // 1.1、在模板中 通過:test="name" ,將子元件的 name繫結到 slot的 test屬性上
    template: '<div><slot :test="names"></slot></div>',
    data(){
        return{
            names: ["molu","qiu","lin","zhang"]
        }
    }
});
new Vue({
    el: "#app"
});
</script>
</body>

程式碼塊比較長,但都是一些很基礎的東西,對著註解順序應該很容易看懂

測試:

同樣的,作用域插槽在 2.6.0之後的版本也有新的寫法,上面這種寫法也已被廢棄。

新用法

新寫法沒什麼講究也沒什麼需要注意的,將 slot-scope 修改為 v-slot指令即可,如下:

<ct>
    <!--將 slot-scope 修改為 v-slot-->
    <template v-slot:default="sc">
        <ul>
            <li v-for="item in sc.test">{{item}}</li>
        </ul>
    </template>
</ct>

關於插槽更多的可以移步Vue.js官網

作者本人實際上是寫後端的(從程式碼風格應該也能看出端倪),對這些東西研究尚淺 就不發表什麼高論了。

也查閱了很多,發現作用域插槽好像能做不少文章 不是我一個業餘前端能說明白的,感興趣可以自行搜尋其他文章。

寫的比較匆忙也比較亂,應該有不少錯誤的地方吧。

醒過來再檢查吧…現在的時間是凌晨三點半,寫到昏厥


放鬆一下眼睛

原圖P站地址

畫師主頁


相關文章