Vue核心2

89 阅读19分钟

这里我们接着上一篇继续 juejin.cn/post/711112…

12、组件化应用

12.1 组件概述

在这一小节中,重点要理解的就是组件的编程思想。

组件表示页面中的部分功能(包含自己的逻辑与样式),可以组合多个组件实现完整的页面功能。

如下图所示:

组件.png

问题是,如何确定页面中哪些内容划分到一个组件中呢?

但你如何确定应该将哪些部分划分到一个组件中呢?你可以将组件当作一种函数或者是对象来考虑(函数的功能是单一的),根据[单一功能原则]来判定组件的范围。也就是说,一个组件原则上只能负责一个功能。如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。

当然,在上图中,我们发现’Name‘和'Price' 表头 并没有单独的划分到一个组件中,主要考虑的是功能简单,就是展示的作用,所以没有划分到单独一个组件中。如果,该表头具有了一些比较复杂的功能,例如排序。那么这里可以单独的将表头内容划分到一个组件中。

组件有什么特点呢?

可复用、维护、可组合

可复用:每个组件都是具有独立功能的,它可以被使用在多个场景中。

可组合:一个组件可以和其它的组件一起使用或者可以直接嵌套在另一个组件内部。

可维护:每个组件仅仅包含自身的逻辑,更容易被理解和维护。

下面,看一下怎样创建组件?

12.2 组件的基本使用

组件具体的创建过程如下:

 Vue.component('index', {
            template: '<div>我是首页的组件</div>'
        })

第一个参数指定了所创建的组件的名字,第二个参数指定了模板。

组件创建好以后,具体的使用方式如下:

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

注意:1. 模板template中只能有一个根节点;2. 组件的名字,如果采用驼峰命令的话,在使用的时候,就要加上 “-”,比如组件名字叫indexA,那么在使用的时候就叫index-a。

例如:

  Vue.component('componentA', {
            template: "<div>创建一个新的组件</div>"
        })

组件的使用

   <component-a></component-a>

在Vue实例中所使用的选项,在组件中都可以使用* ,但是要注意data,在组件中使用时必须是一个函数。*

下面创建一个about组件。

  Vue.component('about', {
            template: '<div>{{msg}}<button @click="showMsg">单击</button></div>',
            data() {
                return {
                    msg: '大家好'
                }
            },
            methods: {
                showMsg() {
                    this.msg = "关于组件"
                }
            }
        })

组件的使用如下:

  <about></about>

在组件中关于data不是一个对象,而是一个函数的原因,官方文档有明确的说明

cn.vuejs.org/v2/guide/co…

组件创建完整的代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>组件创建</title>
    <script src="./vue.js"></script>
</head>

<body>
    <div id="app">
        <component-a></component-a>
        <index></index>
        <index></index>
        <about></about>
    </div>

    <script>
        Vue.component('componentA', {
            template: "<div>创建一个新的组件</div>"
        })
        Vue.component('index', {
            template: '<div>我是首页的组件</div>'
        })
        Vue.component('about', {
            template: '<div>{{msg}}<button @click="showMsg">单击</button></div>',
            data() {
                return {
                    msg: '大家好'
                }
            },
            methods: {
                showMsg() {
                    this.msg = "关于组件"
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

</html>

在使用组件的时候,需要注意以下几点内容:

第一点:data必须是一个函数

第二:组件模板中必须有一个跟元素。

第三:组件模板内容可以使用模板字符串。

    Vue.component("about", {
        template: `<div>
                 {{msg}}
                <button @click='showMsg'>单击
                </button>
            </div>`,
        data() {
          return {
            msg: "大家好",
          };
        },
        methods: {
          showMsg() {
            this.msg = "关于VUE组件";
          },
        },
      });

在上面的代码中,我们在组件的模板中使用类模板字符串,这样就可以调整对应的格式,例如换行等。

第四:现在我们创建的组件是全局组件,可以在其它组件中使用。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件基本使用</title>
  </head>
  <body>
    <div id="app">
      <index></index>
      <component-a></component-a>
      <about></about>
      <!-- 使用HelloWorld组件 -->
      <hello-world></hello-world>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("index", {
        template: "<div>我是Index组件</div>",
      });
      //   创建了HelloWorld组件
      Vue.component("HelloWorld", {
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: "<div>{{ msg}}</div>",
      });
      //   使用HelloWorld组件
      Vue.component("componentA", {
        template: "<div>我是一个新的组件:<HelloWorld></HelloWorld></div>",
      });

      Vue.component("about", {
        template: `<div>
                 {{msg}}
                <button @click='showMsg'>单击
                </button>
            </div>`,
        data() {
          return {
            msg: "大家好",
          };
        },
        methods: {
          showMsg() {
            this.msg = "关于VUE组件";
          },
        },
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的代码中,我们又创建了一个HelloWorld组件,并且在componentA组件中去使用了HelloWorld组件,这里还需要注意的一点就是,在componentA这个组件中使用HelloWorld这个组件的时候,可以使用驼峰命名的方式,但是在<div id="app"></div>这个普通的标签模板中,必须使用短横线的方式,才能使用组件。

12.3 局部组件注册

我们可以在一个组件中,再次注册另外一个组件,这样就构成了父子关系。

可以通过components 来创建对应的子组件。

组件的创建过程如下:

<script>
        Vue.component('father', {
            template: '<div><p>我是父组件</p><son></son></div>',
            components: {
                // 创建一个子组件
                son: {
                    template: '<p>我是子组件</p>'
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>

组件的使用

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

完整代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>父子组件创建</title>
    <script src="./vue.js"></script>
</head>

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        Vue.component('father', {
            template: '<div><p>我是父组件</p><son></son></div>',
            components: {
                // 创建一个子组件
                son: {
                    template: '<p>我是子组件</p>'
                }
            }
        })
        var vm =  new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>
</html>

在上面的代码中,我们是在全局的father组件中,又创建了一个子组件son.

那么son这个子组件也就是一个局部的组件。也就是它只能在father组件中使用。

当然,我们在father中定义子组件son的时候,直接在其内部构件模板内容,这样如果代码非常多的时候,就不是很直观。

所以这里,我们可以将son组件,单独的进行定义,然后在father组件中进行注册。

改造后的代码如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>局部组件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子组件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      Vue.component("father", {
        template: "<div><p>我是父组件</p><son></son></div>",
        components: {
          // 创建一个子组件
          //   son: {
          //     template: "<p>我是子组件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的代码中,我们将son组件单独的进行了定义,这时注意写法,是一个对象的格式,在对象中包含了关于组件很重要的内容为data函数与template属性。

同时在father组件中通过components属性完成了对son组件的注册。

我们说过son组件是一个局部的组件,那么只能在其注册的父组件中使用。

现在,我们可以测试一下:

完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>局部组件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
      <!-- 使用ComponentA组件 -->
      <component-a></component-a>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子组件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      //定义ComponentA组件
      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
      });
      Vue.component("father", {
        template: "<div><p>我是父组件</p><son></son></div>",
        components: {
          // 创建一个子组件
          //   son: {
          //     template: "<p>我是子组件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的代码中,我们又创建了一个全局的组件ComponentA,并且在该组件中使用了son组件,注意这里没有在ComponentA中使用components来注册son组件,而是直接使用。同时在<div id='app'></div>中使用了ComponentA组件。这时在浏览器中,打开上面的程序,会出现错误。

如果现在就想在ComponentA组件中使用son组件,就需要使用components来注册。

      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
        components: {
          son: son,
        },
      });

现在在ComponentA组件中已经注册了son组件,这时刷新浏览器就不会出错了。

在上面这些案例中,我们是在一个全局的组件中注册一个局部的组件,其实,我们也可以在Vue实例中,

注册对应的局部组件。因为,我们也可以将vue实例作为一个组件。

详细代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>局部组件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
      <component-a></component-a>
      <hello-msg></hello-msg>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子组件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      // 定义HelloMsg组件
      const HelloMsg = {
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
        components: {
          son: son,
        },
      });
      Vue.component("father", {
        template: "<div><p>我是父组件</p><son></son></div>",
        components: {
          // 创建一个子组件
          //   son: {
          //     template: "<p>我是子组件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {},
        components: {
          "hello-msg": HelloMsg,
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们又创建了一个组件HelloMsg

然后将HelloMsg组件注册到了 Vue实例中,注意:在进行注册的时候的语法格式。

左侧为组件的名称,由于这个组件创建的时候采用的是驼峰命名的方式,所以组件的名称采用短横线的方式。

右侧为组件的内容。

下面就可以在其<div id="app"></div>中使用了。

同理,在其他的组件中是无法使用HelloMsg组件的。

13、组件通信

13.1 父组件向子组件传值

当我们将整个页面都拆分了不同的组件以后,这样就会涉及到组件之间的数据传递问题。

常见的组件的通信可以分为三类:

第一类: 父组件向子组件传递数据

第二类: 子组件向父组件传递数据

第三类:兄弟组件的数据传递。

下面,我们先来看一下父组件向子组件传递数据的情况

第一:子组件内部通过props接收传递过来的值。

Vue.component('menu-item',{
 props:['title'] // props后面跟一个数组,数组中的内容为字符串,这个字符串可以当做属性类使用。
 template:'<div>{{title}}</div>'   
})

第二: 父组件通过属性将值传递给子组件

<menu-item title="向子组件传递数据"> </menu-item>
<menu-item :title="title"></menu-item> <!--可以使用动态绑定的方式来传值-->

下面看一下具体的案例演示:

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        // 创建一个父组件
        Vue.component('father', {
            // 2、在使用子组件的地方,通过v-bind指令来给子组件中的props赋值。
            template: '<div><p>我是父组件</p><son :myName="mySonName"></son></div>',
            data() {
                return {
                    mySonName: '小强'
                }
            },
            components: {
                // 创建一个子组件
                // 1.声明props,它的作用是:用来接收父组件传递过来的数据。
                // props可以跟一个数组,数组里面的内容可以是字符串,这个字符串可以当属性来使用。
                son: {
                    props: ['myName'],
                    template: '<p>我是子组件,我的名字叫{{myName}}</p>'
                }
            }
        })
        var vm = new new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

下面我们在看一个例子,这个例子是前面我们写的关于局部组件的案例,我们在这个案例的基础上实现组件的传值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>局部组件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
      <component-a></component-a>
      <hello-msg title="你好" :pcontent="content"></hello-msg>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子组件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      // 定义HelloMsg组件
      const HelloMsg = {
        props: ["title", "pcontent"],
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: `<div>{{msg+'----------'+title+'-----------'+pcontent}}</div>`,
      };
      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
        components: {
          son: son,
        },
      });
      Vue.component("father", {
        template: "<div><p>我是父组件</p><son></son></div>",
        components: {
          // 创建一个子组件
          //   son: {
          //     template: "<p>我是子组件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {
          content: "来自父组件中的内容",
        },
        components: {
          "hello-msg": HelloMsg,
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们首先给hello-msg 这个组件传递了一个属性title,该属性的值是固定的。在对应的HelloMsg组件内容定义props,来接收传递过来的title属性的值。然后在template模板中展示title的值。

接下来,又在vue实例中指定了一个content的属性,下面要将该属性的值传递给HelloMsg组件。

    <hello-msg title="你好" :pcontent="content"></hello-msg>

这里需要动态绑定的方式将content的值传递到HelloMsg组件。这里动态绑定的属性为pcontent,所以在HelloMsg组件内部,需要在props的数组中添加一个pcontent,最后在template模板中展示出pcontent的内容。

    // 定义HelloMsg组件
      const HelloMsg = {
        props: ["title", "pcontent"],
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: `<div>{{msg+'----------'+title+'-----------'+pcontent}}</div>`,
      };

通过上面的案例,我们可以看到,在子组件中可以使用props来接收父组件中传递过来的数据。

但是,props在进行命名的时候,也是有一定的规则的。

如果在props中使用驼峰形式,模板中需要短横线的形式,如下代码案例所示:

Vue.component('menu-item',{
    //在JavaScript中是驼峰形式
    props:['menuTitle'],
    template:'<div>{{menuTitle}}</div>'
})
<!--在html中是短横线方式--->
    <menu-item menu-title="hello world"></menu-item>

下面看一下具体的代码演示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件传值</title>
  </head>
  <body>
    <div id="app">
      <menu-item :menu-title="ptitle"></menu-item>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("menu-item", {
        props: ["menuTitle"],
        template: `<div>来自{{menuTitle}}</div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          ptitle: "父组件中的数据",
        },
      });
    </script>
  </body>
</html>

下面再来看一下props属性值的类型。

props 可以接收各种类型的值。

如下:

字符串(String

数值(Number)

布尔值(Boolean)

数组(Array)

对象(Object)

下面,将上面的类型都演示一下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>props类型</title>
  </head>
  <body>
    <div id="app">
      <menu-item
        :str="str"
        :num="10"
        b="true"
        :marr="arr"
        :obj="obj"
      ></menu-item>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("menu-item", {
        props: ["str", "num", "b", "marr", "obj"],
        template: `<div>
                <div>{{str}}</div>
                <div>{{typeof num}}</div>
                <div>{{typeof b}}</div>
                <div>
                   <ul>
                    <li :key=item.id v-for='item in marr'>{{item.userName}}</li>
                   </ul>
               </div>
               <div>
                姓名: {{obj.name}}
                年龄:{{obj.age}}
                </div>
                </div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          str: "hello",
          arr: [
            { id: 1, userName: "zhangsan" },
            {
              id: 2,
              userName: "lisi",
            },
          ],
          obj: {
            name: "wangwu",
            age: 18,
          },
        },
      });
    </script>
  </body>
</html>

在上面的代码中,向menu-item组件中传递了各种类型的数据。

注意:

  <menu-item :str="str" :num="10" b="true" :marr="arr"></menu-item>

在上面的代码中,:num="10"表示传递的是数字,如果写成num='10' 表示传递的是字符,

同理b="true"传递的是字符,如果修改成:b=true表示传递的是布尔类型。

最后还传递了数组类型与对象类型的内容。

13.2 子组件向父组件传值

第一:子组件通过自定义事件向父组件传递信息。

<button v-on:click='$emit("countSum")'> 计算</button>

第二:父组件监听子组件的事件

<menu-item v-on:countSum='sum+=1'></menu-item>

具体的实现步骤如下:

1、构建基本的结构

     <div id="app">      
    </div>
var vm = new Vue({
            el: '#app',
            data: {

            }
        })

2、构建相应的父组件。

  Vue.component('father', {
            template: '<div>我的儿子叫{{mySonName}}</div>',
            data() {
                return {
                    mySonName: ''
                }
            }
  }

3、 构建相应的子组件, 并且单击子组件中的按钮给父组件传值。

  Vue.component('father', {
            template: '<div>我的儿子叫{{mySonName}}</div>',
            data() {
                return {
                    mySonName: ''
                }
            },
        components: {
                son: {
                    data() {
                        return {
                            myName: '小强'
                        }
                    },
                    template: '<button @click="emitMyName">我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 子组件传值给父组件需要用到$emit()方法,这个方法可以传递两个参数,一个是事件名称,一个是需要传递的数据
                            this.$emit('tellMyFatherMyName', this.myName)
                        }
                    }
                }
            }
  }

4、父组件接收子组件传递过来的数据。

注意在父组件中引用子组件,同时指定在子组件中定义的事件。

   Vue.component('father', {
            template: '<div>我的儿子叫{{mySonName}}<son @tellMyFatherMyName="getMySonName"></son></div>',
            data() {
                return {
                    mySonName: ''
                }
            },
            methods: {
                getMySonName(data) {
                    this.mySonName = data;
                }
            }
   }

5、组件使用

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

6、完整代码如下:

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        Vue.component('father', {
            template: '<div>我的儿子叫{{mySonName}}<son @tellMyFatherMyName="getMySonName"></son></div>',
            data() {
                return {
                    mySonName: ''
                }
            },
            methods: {
                getMySonName(data) {
                    this.mySonName = data;
                }
            },
            components: {
                son: {
                    data() {
                        return {
                            myName: '小强'
                        }
                    },
                    template: '<button @click="emitMyName">我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 子组件传值给父组件需要用到$emit()方法,这个方法可以传递两个参数,一个是事件名称,一个是需要传递的数据
                            this.$emit('tellMyFatherMyName', this.myName)
                        }
                    }
                }
            }

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

            }
        })
    </script>
</body>

13.3 兄弟组件之间数据传递

兄弟组件传值,通过事件总线完成。

1、定义父组件并且在父组件中,完成两个兄弟组件的创建。

  <script>
        Vue.component('father', {
            template: '<div><son></son><daughter></daughter></div>',
            components: {
                son: {
                    data() {
                        return {
                            mySisterName: ''
                        }
                    },
                    template: '<div>我妹妹叫{{mySisterName}}</div>'
                },
                daughter: {
                    data() {
                        return {
                            myName: '小雪'
                        }
                    },
                    template: '<button @click="emitMyName">告诉哥哥我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {

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

            }
        })
    </script>

2、创建事件总线

通过事件总线发射一个事件名称和需要传递的数据 。

  // 创建一个空的vue实例,作为事件总线
        var eventbus = new Vue()
          daughter: {
                    data() {
                        return {
                            myName: '小雪'
                        }
                    },
                    template: '<button @click="emitMyName">告诉哥哥我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 通过事件总线发射一个事件名称和需要传递的数据
                            eventbus.$emit('tellBroMyName', this.myName)
                        }
                    }
                }

3、通过eventbus的$on()方法去监听兄弟节点发射过来的事件

 son: {
                    data() {
                        return {
                            mySisterName: ''
                        }
                    },
                    template: '<div>我妹妹叫{{mySisterName}}</div>',
                    mounted() {
                        // 通过eventbus的$on()方法去监听兄弟节点发射过来的事件
                        // $on有两个参数,一个是事件名称,一个是函数,该函数的默认值就是传递过来的数据
                        eventbus.$on('tellBroMyName', data => {
                            this.mySisterName = data
                        })
                    }
                },

4、组件的使用

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

5、完整的代码如下:

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        // 创建一个空的vue实例,作为事件总线
        var eventbus = new Vue()
        Vue.component('father', {
            template: '<div><son></son><daughter></daughter></div>',
            components: {
                son: {
                    data() {
                        return {
                            mySisterName: ''
                        }
                    },
                    template: '<div>我妹妹叫{{mySisterName}}</div>',
                    mounted() {
                        // 通过eventbus的$on()方法去监听兄弟节点发射过来的事件
                        // $on有两个参数,一个是事件名称,一个是函数,该函数的默认值就是传递过来的数据
                        eventbus.$on('tellBroMyName', data => {
                            this.mySisterName = data
                        })
                    }
                },
                daughter: {
                    data() {
                        return {
                            myName: '小雪'
                        }
                    },
                    template: '<button @click="emitMyName">告诉哥哥我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 通过事件总线发射一个事件名称和需要传递的数据
                            eventbus.$emit('tellBroMyName', this.myName)
                        }
                    }
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

14、组件插槽应用

14.1 插槽基本使用

生活中的插槽

其实我们生活中有很多很多的插槽。比如电脑的USB插槽、插板中的电源插槽等等。每个插槽都有它们之间的价值。比如电脑的USB插槽,可以用来插U盘,链接鼠标,链接手机、音响等等,通过这些插槽,大大拓展了原有设备的功能。

组件中的插槽

  组件中的插槽,让使用者可以决定组件内部的一些内容到底展示什么,也就是,插槽可以实现父组件向子组件传递模板内容。具有插槽的组件将会有更加强大的拓展性,

下面看一个实际应用的例子来体会一下插槽的引用场景。

插槽.jpg

三个页面中都有导航栏,基本结构都是一样的:左中右分别有一个东西,只是显示的内容不同而已。那我们如何来实现这种结构相似但是内容不同呢?  你一定是想着,直接定义三个组件,然后在模板中分别显示不同的内容,对不对?恭喜你,你就快要被炒了。  首先,如果我们封装成三个组件,显然不合适,比如每个页面都有返回,这部分的内容我们就要重复去封装  其次,如果我们封装成一个,还是不合理,因为有些左侧是菜单栏,有些中间是搜索框,有些是文字。 那我们该怎么办呢?其实很简单,用组件插槽。

上面最佳的解决办法是将共性抽取到组件中,将不同暴露给插槽,一旦我们使用了插槽,就相当于预留了空间空间的内容取决于使用者

如下图所示: 插槽1.png 通过上图,我们可以在父组件中使用子组件,同时由于在子组件中创建插槽slot,也就是相当于预留了空间,这时在父组件中使用子组件时,可以传递不同的内容。

下面看一下插槽的应用

基本使用方式

第一:确定插槽的位置

Vue.component('alert-box',{
 template:`
   <div class="demo-alert-box">
        <strong>子组件</strong>
        <slot></slot>
   </div>
`
})

在子组件中,通过<slot>确定出插槽的位置。

第二:插槽内容

<alert-box>Hello World</alert-box>

向插槽中传递内容。

下面看一下具体的代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>插槽基本使用</title>
  </head>
  <body>
    <div id="app">
      <alert-box>程序出现了bug</alert-box>
      <alert-box>程序出现了警告</alert-box>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("alert-box", {
        template: `
                <div>
                    <strong>ERROR:</strong>
                    <slot></slot>
                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

通过上面的代码我们可以看到,在alert-box这个组件中,定义了一个插槽,也就是预留了一个位置,下面使用该组件的时候,都可以向该插槽中传递数据。而<strong>标签中的内容就相当于是一个公共的内容了。

当然在插槽中也是可以添加默认的内容的。

  <div id="app">
      <alert-box>程序出现了bug</alert-box>
      <alert-box>程序出现了警告</alert-box>
      <alert-box></alert-box>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("alert-box", {
        template: `
                <div>
                    <strong>ERROR:</strong>
                    <slot>默认内容</slot>
                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>

在上面的代码中,我们给插槽添加了默认的内容,如果在使用alert-box组件的时候,没有给插槽传递值,就会展示插槽中的默认内容。

14.2 具名插槽

所谓的具名插槽就是有名字的插槽。

第一:插槽定义

<div class="container">
    <header>
        <slot name="header"></slot>
    </header>
   <main>
      <slot></slot>
    </main>
    <footer>
     <slot name="footer"></slot>
    </footer>
</div>

第二:插槽内容

<base-layout>
  <h1 slot="header"> 标题内容</h1>
  <p>
    主要内容    
  </p>  
 <p>
    主要内容    
  </p> 
    <p slot="footer">
        底部内容
    </p>
</base-layout>

下面我们来看一下具体的代码实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>具名插槽</title>
  </head>
  <body>
    <div id="app">
      <base-layout>
        <p slot="header">头部内容</p>
        <p>主要内容1</p>
        <p>主要内容2</p>
        <p slot="footer">底部信息</p>
      </base-layout>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("base-layout", {
        template: `
                <div>
                    <header>
                        <slot name="header"></slot>
                     </header>
                     <main>
                        <slot></slot>
                     </main> 
                     <footer>
                        <slot name="footer"></slot>
                     </footer>   

                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的代码中, <p slot="header">头部内容</p>会插入到base-layout 组件的header这个插槽中。

<p slot="footer">底部信息</p>会插入到footer这个插槽中。

剩余的内容会插入到默认的(没有名称)的插槽内。

在上面的应用中,有一个问题就是,我们把插槽的名称给了某个html标签,例如p标签,这样就只能将该标签插入到插槽中。

但是,在实际的应用中,有可能需要向插槽中插入大量的内容,这时就需要用到template标签。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>具名插槽</title>
  </head>
  <body>
    <div id="app">
      <!-- <base-layout>
        <p slot="header">头部内容</p>
        <p>主要内容1</p>
        <p>主要内容2</p>
        <p slot="footer">底部信息</p>
      </base-layout> -->
      <base-layout>
        <template slot="header">
          <div>标题名称</div>
          <div>标题区域的布局</div>
        </template>
        <div>
          中间内容区域的布局实现
        </div>
        <template slot="footer">
          <div>底部信息</div>
          <div>对底部内容区域进行布局</div>
        </template>
      </base-layout>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("base-layout", {
        template: `
                <div>
                    <header>
                        <slot name="header"></slot>
                     </header>
                     <main>
                        <slot></slot>
                     </main> 
                     <footer>
                        <slot name="footer"></slot>
                     </footer>   

                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的代码中,我们给template标签添加了插槽的名称,并且在template标签中嵌入了其它的多个标签,从而完成布局。

在这里,可以统一查看浏览器端所生成的代码结构。

14.3 作用域插槽

应用场景:父组件对子组件的内容进行加工处理。这也是作用域插槽的一个很重要特性,

下面我们通过一个例子来体会一下这句话的作用。

首先,我们先创建一个用户列表。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽</title>
  </head>
  <body>
    <div id="app">
      <user-list :list="userList"></user-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("user-list", {
        props: ["list"],
        template: `<div>
                <ul>
                    <li :key="item.id" v-for='item in list'>{{item.userName}}</li>
                 </ul>   
                </div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          userList: [
            {
              id: 1,
              userName: "张三",
            },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们首先创建了一个user-list组件,在这个组件中接收父组件传递过来的用户数据,通过循环的方式展示传递过来的用户数据。

现在,这里有一个新的需求,就是修改某个用户名的颜色,让其高亮显示。这个需求应该怎样来处理呢?

我们是否可以在子组件user-list中实现这个功能呢?

虽然可以,但是一般不建议你这么做,因为一个组件创建好以后,一般不建议修改。你可以想一下,如果这个组件是其它人创建的,而且很多人都在用,如果直接修改这个子组件,就会造成很多的问题。

所以这里,还是从父组件中进行修改。也是通过父组件来决定子组件中的哪个用户名进行高亮显示。

下面对代码进行修改:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽</title>
  </head>
  <body>
    <div id="app">
      <user-list :list="userList">
        <template slot-scope="slotProps">
          <strong v-if="slotProps.info.id===2"
            >{{slotProps.info.userName}}</strong
          >
          <span v-else>{{slotProps.info.userName}}</span>
        </template>
      </user-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("user-list", {
        props: ["list"],
        template: `<div>
                <ul>
                    <li :key="item.id" v-for='item in list'>
                        <slot :info="item">
                            {{item.userName}}
                            </slot>
                        </li>
                 </ul>   
                </div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          userList: [
            {
              id: 1,
              userName: "张三",
            },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

通过上面的代码可以看到,为了能够实现父组件决定子组件中哪个用户名能够高亮显示,需要在设计子组件的时候,为其添加对应的插槽。

template: `<div>
                <ul>
                    <li :key="item.id" v-for='item in list'>
                        <slot :info="item">
                            {{item.userName}}
                            </slot>
                        </li>
                 </ul>   
                </div>`,

在子组件的template模板中,添加了插槽,同时为其动态绑定一个属性info(这个属性的名字是可以随意命名的),该属性的值为用户的信息。

绑定该属性的目的就是为了能够在父组件中获取用户的信息。

下面看一下父组件中的修改

 <div id="app">
      <user-list :list="userList">
        <template slot-scope="slotProps">
          <strong v-if="slotProps.info.id===2"
            >{{slotProps.info.userName}}</strong
          >
          <span v-else>{{slotProps.info.userName}}</span>
        </template>
      </user-list>
    </div>

父组件在使用子组件user-list的时候,这里为其添加了template这个标签,而且这个标签的属性slot-scope是固定的,为其指定了一个值为slotProps,该值中,存储的就是从子组件中获取到的用户数据。

所以接下来通过slotProps获取info(注意这里要与子组件中的slot属性保持一致)中的用户数据。然后进行判断,如果用户编号为2的,为其加错,否者正常展示。

通过以上的案例,我们可以看到父组件通过作用域插槽实现了对子组件中数据的处理。其实这也就是为什么叫做作用域插槽的原因:

是因为模板虽然是在父级作用域(父组件)中渲染的,却能拿到子组件的数据。

14.4. 作用域插槽案例

下面,我们通过一个列表的案例,来体会一下作用域插槽的应用。

首先我们先来做一个基本的列表组件

列表.png

这里,我们首先使用的是具名插槽完成的,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽案例</title>
  </head>
  <body>
    <div id="app">
      <my-list>
        <template slot="title">
          用户列表
        </template>
        <template slot="content">
          <ul>
            <li v-for="item in listData" :key="item.id">{{item.userName}}</li>
          </ul>
        </template>
      </my-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("my-list", {
        template: `
                <div class="list">
                    <div class="list-title">
                        <slot name="title"></slot>
                    </div>
                    <div class="list-content">
                        <slot name="content"></slot>
                    </div>
                </div>
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          listData: [
            { id: 1, userName: "张三" },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们在子组件my-list中使用了具名插槽。然后父组件在使用子组件my-list的时候,可以通过template标签加上slot属性向具名插槽中传递数据。

虽然以上的写法满足了基本的需求,但是作为组件的使用者,这样的一个组件会让我们感觉非常的麻烦,也就是我们在使用my-list这个组件的时候,还需要自己去编写content区域的循环逻辑。这样就比较麻烦了,下面对上面的代码在做一些修改。

为了解决这个问题,我们可以把循环写到子组件中,这样我们在使用的时候,不需要写循环了,只是传递数据就可以了,这样就方便多了。其实这里我们就可以不用具名插槽了。

所以修改后的代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽案例</title>
  </head>
  <body>
    <div id="app">
      <my-list title="用户列表" :content="listData">
      </my-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("my-list", {
        props: ["title", "content"],
        template: `
                <div class="list">
                    <div class="list-title">


                        {{title}}
                    </div>
                    <div class="list-content">

                        <ul class="list-content">
                            <li v-for="item in content" :key="item.id">{{item.userName}}</li>
                        </ul> 
                    </div>
                </div>
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          listData: [
            { id: 1, userName: "张三" },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们没有使用插槽,直接将数据传递到子组件my-list中,然后在该子组件中接收到数据,并进行了循环遍历。

经过这一次的改造,满足了我们前面所提到的易用性问题,但是现在又有了新的问题,组件的拓展性不好。

每次只能生成相同结构的列表,一旦业务需要发生了变化,组件就不再使用了。比如,我现在有了新的需求,在一个列表的每个列表项前面加上一个小的logo,我总不能又写一个新的组件来适应需求的变化吧?

这里就可以使用作用域插槽来解决这个问题。

具体的实现代码如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽案例</title>
  </head>
  <body>
    <div id="app">
      <!-- 如果没有传递模板,那么子组件的插槽中只会展示用户名 -->
      <my-list title="用户列表" :content="listData"></my-list>
        <!-- 传递模板 -->
        <my-list title="用户列表2" :content="listData">
          <template slot-scope="scope">
            <img src="./one.png" width="20"/>
            <span>{{scope.item.userName}}</span>
          </template>
        </my-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("my-list", {
        props: ["title", "content"],
        template: `
                <div class="list">
                    <div class="list-title">
                        {{title}}
                    </div>
                    <div class="list-content">

                   <ul class="list-content">
                            <li v-for="item in content" :key="item.id">
                           <!--这里将content中的每一项数据绑定到slot的itemb变量上,在父组件中就可以获取到item变量-->     
                        <slot :item="item">{{item.userName}}</slot>

                                </li>
                        </ul> 
                    </div>
                </div>
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          listData: [
            { id: 1, userName: "张三" },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的代码中,我们首先在子组件my-list中,添加了作用域的插槽。

 <ul class="list-content">
                            <li v-for="item in content" :key="item.id">
                           <!--这里将content中的每一项数据绑定到slot的itemb变量上,在父组件中就可以获取到item变量-->     
                        <slot :item="item">{{item.userName}}</slot>

                                </li>
                        </ul> 

同时在父组件中,使用对应的插槽

 <div id="app">
      <!-- 如果没有传递模板,那么子组件的插槽中只会展示用户名 -->
      <my-list title="用户列表" :content="listData"></my-list>
        <!-- 传递模板 -->
        <my-list title="用户列表2" :content="listData">
          <template slot-scope="scope">
            <img src="./one.png" width="20"/>
            <span>{{scope.item.userName}}</span>
          </template>
        </my-list>
    </div>

再回到开始的问题,作用域插槽到底是干嘛用的?很显然,它的作用就如官网所说的一样:将组件的数据暴露出去。而这么做,给了组件的使用者根据数据定制模板的机会,组件不再是写死成一种特定的结构。

以上就是作用域插槽的应用,需要你仔细体会。

那么,在这里再次问一个问题,就是在你所使用的Vue插件或者是第三方的库中,有没有遇到使用作用域插槽的情况呢?

其实,比较典型的就是element-uitable组件,它就可以通过添加作用域插槽改变渲染的原始数据。

如下图所示:

作用域插槽.png

14.5 插槽应用总结

为什么要使用插槽

组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力。

  组件的复用性常见情形如在有相似功能的模块中,他们具有类似的UI界面,通过使用组件间的通信机制传递数据,从而达到一套代码渲染不同数据的效果

  然而这种利用组件间通信的机制只能满足在结构上相同,渲染数据不同的情形;假设两个相似的页面,他们只在某一模块(区域)有不同的UI效果(例如,前面所做的列表,发现可以显示不同的ui效果),以上办法就做不到了。可能你会想,使用 v-ifv-else来特殊处理这两个功能模块,不就解决了?很优秀,解决了,但不完美。极端一点,假设我们有一百个这种页面,就需要写一百个v-ifv-else-ifv-else来处理?那组件看起来将不再简小精致,维护起来也不容易。

  而 插槽 “SLOT”就可以完美解决这个问题

什么情况下使用插槽

顾名思义,插槽即往卡槽中插入一段功能块。还是举刚才的例子。如果有一百个基本相似,只有一个模块功能不同的页面,而我们只想写一个组件。可以将不同的那个模块单独处理成一个卡片,在需要使用的时候将对应的卡片插入到组件中即可实现对应的完整的功能页。而不是在组件中把所有的情形用if-else罗列出来(这里还是体会用户列表的案例)

  可能你会想,那我把一个组件分割成一片片的插槽,需要什么拼接什么,岂不是只要一个组件就能完成所有的功能?思路上没错,但是需要明白的是,卡片是在父组件上代替子组件实现的功能,使用插槽无疑是在给父组件页面增加规模,如果全都使用拼装的方式,和不用组件又有什么区别(例如,用户列表案例中需要其他的显示方式,需要在父组件中进行添加)。因此,插槽并不是用的越多越好

插槽是组件最大化利用的一种手段,而不是替代组件的策略,当然也不能替代组件。如果能在组件中实现的模块,或者只需要使用一次v-else, 或一次v-else-ifv-else就能解决的问题,都建议直接在组件中实现。