08-Vue基础-组件详解

组件基础

1. 组件复用

组件可以全局注册,也可以局部注册。全局注册后,任何Vue实例都可以使用,如下所示:

Vue.component("my-component", {
  template: `
        <div>hello</div>
    `
});

my-component是组件名,下一个参数是组件的一些选项参数,包含模板template,还可以有datamethods等选项。然后在父组件中调用子组件就可以了。完整示例如下:

<body>
  <div id="app">
    <!-- 组件调用 -->
    <my-component></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      template: `
                <select>
                    <option>html</option>
                    <option>js</option>
                    <option>css</option>
                </select>
            `
    });
    // 根组件
    var app = new Vue({
      el: "#app"
    });
  </script>
</body>

Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在改实例作用域下有效。组件中也可以使用 components 选项来注册组件,是组件嵌套的基础,示例如下:

<body>
  <div id="app">
    <!-- 组件调用 -->
    <my-child></my-child>
  </div>
  <script>
    // 局部组件
    var Child = {
      template: "<div>局部注册组件的内容</div>"
    };
    // 根组件
    var app = new Vue({
      el: "#app",
      // 注册组件
      components: {
        "my-child": Child
      }
    });
  </script>
</body>

除了template选项外,组件中还可以项Vue实例那样使用其他选项,比如datacomputedmethods等,但是,在使用data时,和实例稍有区别,然后将数据return出去,例如:

<body>
  <div id="app">
    <!-- 组件调用 -->
    <my-component></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      template: `
                <div> {{ message }} </div>
            `,
      data: function() {
        return {
          message: "组件内容"
        };
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app"
    });
  </script>
</body>

2. 父组件传值

组件不仅仅是要把模板的内容进行复用,有时候还需要组件间进行通讯。通常父组件的模板中包含子组件,父组件向子组件传递参数或数据,子组件接收到参数或数据进行不同的渲染。父组件向子组件传递参数是通过props来实现的。

在组件中,使用选项props来声明需要从父级接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。如下示例:

<body>
  <div id="app">
    <!-- 组件调用 -->
    <my-component message="来自父组件的数据"></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      props: ["message"],
      template: `
                <div> {{ message }} </div>
            `
    });
    // 根组件
    var app = new Vue({
      el: "#app"
    });
  </script>
</body>

props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件自己本身,这两种数据都可以在模板和计算属性以及方法选项上使用。

注意:DOM模板中接收的props名称为驼峰命名法时,需要转为短横线分隔命名。例如:

<div id="app">
  <my-component warning-text="提示消息"></my-component>
</div>

父组件传递的数据也可动态绑定,可以使用v-bind属性。例如:

<body>
  <div id="app">
    <input type="text" v-model="parentMessage" />
    <my-component :message="parentMessage"></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      props: ["message"],
      template: `
                <div> {{ message }} </div>
            `
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        parentMessage: ""
      }
    });
  </script>
</body>

一般传递给组件的数据都是需要验证的,谁也不清楚传递给子组件的数据是否符合预期效果,所以对传递的数据进行校验就显得有必要了。示例如下:

<script>
  // 全局组件
  Vue.component("my-component", {
    props: {
      // 必须是数字类型
      propA: Number,
      // 必须是字符串或数字类型
      propB: [String, Number],
      // 布尔值类型,没有定义,默认为true
      propC: {
        type: Boolean,
        default: true
      },
      // 数字,而且是必传项
      prodD: {
        type: Number,
        required: true
      },
      // 如果是数组或对象,默认值必须是一个函数来返回
      propE: {
        type: Array,
        default: function() {
          return [];
        }
      },
      // 自定义一个函数验证
      propF: {
        validator: function(value) {
          return value > 10;
        }
      }
    }
  });
</script>

验证的type类型可以是:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function

type也可以是一个自定义构造器,使用instanceof检测。
prop验证失败时,开发版本Vue会在浏览器控制台会抛出一条警告。

3. 组件通信

组件关系可以分为父子组件通信、兄弟组件通信、非父子组件通讯。

3.1. 自定义事件

当子组件需要向父组件传递数据时,就要用到自定义事件。v-on指令除了监听DOM事件外,还可以用于组件之间的自定义事件。

如果你了解JavaScript的设计模式--观察者模式,一定知道dispatchEventaddEventListener这两个方法。Vue组件也有类似的模式,子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件。

父组件也可以直接在子组件上自定义标签上使用v-on来监听子组件触发的自定义事件,示例如下:

<body>
  <div id="app">
    <p>总数: {{ total }}</p>
    <my-component
      @increase="handleGetTotal"
      @reduce="handleGetTotal"
    ></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      template: `
          <div>
            <button @click="handleIncrease">+1</button>
            <button @click="handleReduce">-1</button>
          </div>
          `,
      data: function() {
        return {
          counter: 0
        };
      },
      methods: {
        handleIncrease: function() {
          this.counter++;
          this.$emit("increase", this.counter);
        },
        handleReduce: function() {
          this.counter--;
          this.$emit("reduce", this.counter);
        }
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        total: 0
      },
      methods: {
        handleGetTotal: function(total) {
          this.total = total;
        }
      }
    });
  </script>
</body>

上面的示例中,子组件有两个按钮,分别是实现加一和减一的效果,在改变组件datacounter变量的值后,通过$emit()方法再把它传递给父组件,父组件再使用v-on指令接收参数。$emit()方法的第一个参数是自定义事件的名称,如示例的increasereduce,后边的参数是传递的数据,可以填写多个或者不填写。

3.2. 使用 v-model

在自定义组件上可以使用v-model指令:

<body>
  <div id="app">
    <p>总数: {{ total }}</p>
    <my-component v-model="total"></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      template: `
          <div>
            <button @click="handClick">+1</button>
          </div>
          `,
      data: function() {
        return {
          counter: 0
        };
      },
      methods: {
        handClick: function() {
          this.counter++;
          this.$emit("input", this.counter);
        }
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        total: 0
      }
    });
  </script>
</body>

v-model还可以用来创建自定义表单输入组件,进行双向数据绑定,例如:

<body>
  <div id="app">
    <p>总数: {{ total }}</p>
    <my-component v-model="total"></my-component>
    <button @click="handleReduce">-1</button>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      props: ["value"],
      template: `
          <input :value="value" @input="updateValue">
          `,
      methods: {
        updateValue: function() {
          this.$emit("input", event.target.value);
        }
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        total: 0
      },
      methods: {
        handleReduce: function() {
          this.total--;
        }
      }
    });
  </script>
</body>

实现这样一个具有双向数据绑定的v-model组件要满足下面连个要求:

  • 接收一个value属性
  • 在有新的value时触发input事件

3.3. 非父子组件通信

在实际业务中,除了父子组件通讯,还可能存在非父子组件通讯的场景,非父子组件一般有两种,兄弟组件和跨多级组件。

Vue1.x中,除了$emit()方法外,还有$dispatch()$broadcast()这两个方法。$dispatch()用于向上级派发事件,只要是它的父级,都可以在Vue实例的events选项中接收,示例代码如下:

<!-- 注意:该示例需使用Vue.js 1.x的版本 -->
<body>
  <div id="app">
    {{ message }}
    <my-component></my-component>
  </div>
  <script>
    // 全局组件
    Vue.component("my-component", {
      template: '<button @click="handleDispatch">派发事件</button>',
      methods: {
        handleDispatch: function() {
          this.$dispatch("on-message", "来自内部组件的数据");
        }
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        message: ""
      },
      events: {
        "on-message": function(msg) {
          this.message = msg;
        }
      }
    });
  </script>
</body>

这两种方法看起来很好用,但是在Vue 2.x中都弃用了。Vue 2.x中推荐使用一个空的事件总线bus,即设置一个中介。他是消息传递的中间件。
示例代码如下:

<body>
  <div id="app">
    {{ message }}
    <component-a></component-a>
  </div>
  <script>
    // bus 事件总线
    var bus = new Vue();
    // 全局组件
    Vue.component("component-a", {
      template: `
            <button @click="handleEvent">传递事件</button>
          `,
      methods: {
        handleEvent: function() {
          bus.$emit("on-message", "来自组件component-a的内容");
        }
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        message: ""
      },
      mounted: function() {
        var _this = this;
        // 在实例初始化时,监听来自 bus 实例的事件
        bus.$on("on-message", function(msg) {
          _this.message = msg;
        });
      }
    });
  </script>
</body>

分析案例:

首先创建了一个Vue空实例,并赋值为bus变量;然后定义了全局组件component-a;最后创了Vue根实例app,在app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus事件中间件的on-message事件,而在组件component-a中,点击按钮会通过on-message事件,将消息发出去,此时,app会接收来自bus的事件,进而在回调里完成自己的业务逻辑。

这种方法巧妙而轻量地实现了任何组件间的通信,包括父子组件、兄弟组件、非父子组件、跨级组件等。如果深入使用,可以扩展bus实例,给他添加datamethodscomputed等选项,这些都是可以共用的,在业务中,尤其是协同开发非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,甚至是用户的授权token信息。只需要在初始化时让bus获取一次,任何时间、任何组件都可以监听获取其数据,在单页面富应用(SPA)中会很实用。

当然,如果你的项目更大的话,推荐使用状态管理解决方案vuex,在进阶教程,我们会继续讨论。

除了中央事件总线bus,还有两种方法可以实现组件间通信:父链和子组件索引。

  • 父链

在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层组件。示例代码如下:

<body>
  <div id="app">
    {{ message }} <br />
    <component-a></component-a>
  </div>
  <script>
    // 全局组件
    Vue.component("component-a", {
      template: `
            <button @click="handleEvent">通过父链直接修改数据</button>
          `,
      methods: {
        handleEvent: function() {
          // 访问父链后,可以直接操作数据,比如修改
          this.$parent.message = "来自子组件component-a的内容";
        }
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {
        message: "我是父组件的内容"
      }
    });
  </script>
</body>

注意:尽管Vue允许这样操作,但是在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该主动修改它的数据,因为这会使得父子组件紧密耦合,只看父组件,很难理解父组件的状态,因为它可能随时随地就被其他子组件修改,理想情况下是,只有组件自己修改自己的数据。推荐父子组件通信通过props$emit

  • 子组件索引

当子组件较多时,通过this.$children来一一遍历出我们需要的一个组件是比较困难的,尤其在组件进行动态渲染时,他们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称。示例如下:

<body>
  <div id="app">
    <button @click="handleRef">通过<strong>ref</strong>获取子组件实例</button>
    <component-a ref="comA"></component-a>
  </div>
  <script>
    // 全局组件
    Vue.component("component-a", {
      template: `
            <div>我是子组件</div>
          `,
      // 注意哦,这里data是一个方法
      data: function() {
        return {
          message: "来自子组件的内容"
        };
      }
    });
    // 根组件
    var app = new Vue({
      el: "#app",
      data: {},
      methods: {
        handleRef: function() {
          // 通过 $refs 来访问指定的实例
          var msg = this.$refs.comA.message;
          document.write(msg);
        }
      }
    });
  </script>
</body>

注意,$ref只在组件渲染完成后才填充,并且它是响应式的,它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$ref

4. 使用 solt 分发内容

4.1. 什么是 solt

  • 开发背景

我们先设计一个组件化网站布局案例,它的结构可能是这样的:

<app-content>
  <!-- 一级导航菜单 -->
  <menu-main></menu-main>
  <!-- 二级导航菜单 -->
  <menu-sub></menu-sub>
  <div class="contanier">
    <!-- 侧边栏 -->
    <menu-left></menu-left>
    <!-- 主体内容 -->
    <menu-content></menu-content>
  </div>
  <app-footer></app-footer>
</app-content>

当需要让组件组合使用,混合组件的内容和子组件的模板时,就会用到solt,这个过程叫做内容分发(transclusion),有时也叫插槽。

  • 编译的作用域

父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。注意slot分发的内容,作用域是父组件上的。

4.2. solt 用法

  • 单个solt

在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot插槽,在父组件模板里,插入子组件标签内的所有内容将替代子组件的<slot>标签以及它的内容。示例代码如下:

<body>
  <div id="app">
    <children-component>
      <p>分发的内容</p>
      <p>更多分发的内容</p>
    </children-component>
  </div>
  <script>
    // 全局组件
    Vue.component("children-component", {
      template: `
        <div>
            <slot>
                <p>若父组件没有插入内容,我将作为默认出现</p>
            </slot>
        </div>
      `
    });
    // 根组件
    var app = new Vue({
      el: "#app"
    });
  </script>
</body>

分析案例:

子组件children-component的模板内定义了一个<slot>元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本;如果写入了slot,那就会替换整个<slot>。以上案例渲染结果为:

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

注意,子组件<slot>内的备用内容,它的作用域是子组件本身。

  • 具名 slot

<slot>元素指定一个name后可以分发多个内容,具名slot可以与多个slot共存。如下示例:

<body>
  <div id="app">
    <children-component>
      <h2 slot="header">标题</h2>
      <p>分发的内容</p>
      <p>更多分发的内容</p>
      <div slot="footer">底部信息</div>
    </children-component>
  </div>
  <script>
    // 全局组件
    Vue.component("children-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"
    });
  </script>
</body>

子组件内声明了 3 个<slot>元素,其中在<div class="main"></div>内的<slot>没有使用name特性,它将作为默认的slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。

slot通俗的理解就是占坑,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中<slot>位置),当插槽也就是坑<slot name="mySlot">有命名时,组件标签中使用属性slot="mySlot"的元素就会替换该对应位置内容。

  • 作用域插槽

作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染的元素。我们先来一个简单的示例看下:

<body>
  <div id="app">
    <children-component>
      <template scope="props">
        <p>来自父组件的内容</p>
        <p>{{ props.msg }}</p>
      </template>
    </children-component>
  </div>
  <script>
    // 全局组件
    Vue.component("children-component", {
      template: `
            <div class="container">
                <slot msg="来自子组件的内容"></slot>
            </div>
          `
    });
    // 根组件
    var app = new Vue({
      el: "#app"
    });
  </script>
</body>

上面案例父组件使用了<template>标签,而且标签拥有一个scope="props"的特性,props是一个临时变量,通过临时变量props可以调用子组件插槽的数据msg

作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。示例代码如下:

<body>
  <div id="app">
    <my-list :books="books">
      <!-- 作用域插槽也可以是具名 slot -->
      <template slot="book" scope="props">
        <li>{{ props.bookName }}</li>
      </template>
    </my-list>
  </div>
  <script>
    // 全局组件
    Vue.component("my-list", {
      props: {
        books: {
          type: Array,
          // 设置默认值
          default: function() {
            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: "《Vue.js 实战》" },
          { name: "《JavaScript 实战》" },
          { name: "《CSS 实战》" }
        ]
      }
    });
  </script>
</body>

仔细查看以上案例,会发现,直接使用v-for指令简单多了,此处作用只是为介绍作用域插槽。

  • 访问 slot

如果我们想要访问某个具名slot该怎么办呢?其实,Vue.js也提供了相关方法,通过$slot就可以访问某个具名slotthis.$slot.default包含了所有没有被包含的具名slot中的节点。

未完待续。。。

当你的才华撑不起自己的野心,那就努力学习吧!

posted on 2020-01-22 16:29  cculin  阅读(129)  评论(0)    收藏  举报