Vue 可复用性的组件详解

使用组件的原因

作用:提高代码的复用性

组件的使用方法

  1. 全局注册

    Vue.component("my-component", {
      template: "<div>我是组件的内容</div>",
    });
    //优点:所有的nue实例都可以用
    //缺点:权限太大,容错率降低
  2. 局部注册

    var app = new Vue({
      el: "#app",
      components: {
        "my-component": {
          template: "<div>我是组件的内容</div>",
        },
      },
    });
  3. vue 组件的模板在某些情况下会受到 html 标签的限制,比如<table>中只能含有<tr> , <td>这些元素,所以直接在 table 中使用组件是无效的,此时可以使用is属性来挂载组件

    <table>
      <tbody is="my-component"></tbody>
    </table>

组件使用的奇淫技巧

  1. 命名方式必须使用小写字母加 - 进行命名,驼峰命名myComponent没用
  2. template 中的内容必须被一个 DOM 元素包裹,也可以嵌套多个 DOM 元素
  3. 在组件的定义中,除了 template 之外也可以使用其他选项,比如:data,computed,methods
  4. 组件定义中的data必须是一个方法

使用 props 传递数据 父亲向儿子传递数据

  1. 在组件中使用props来从父亲组件接收参数,注意,在props中定义的属性,都可以在组件中直接使用
  2. props来自父级,而组件中data return 的数据就是组件自己的数据,两种情况作用域就是组件本身,可以在 template,computed,methods 中直接使用

单向数据流

  • 解释: 通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。
  • 目的: 是尽可能将父子组件解构,避免子组件无意中修改了父组件的状态。
  • 应用场景: 业务中会经常遇到两种需要改变props的情况
第一种应用场景:父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的 props
步骤一:注册组件
步骤二:将父组件的数据传递进来,并在子组件中用 props 接收
步骤三:将传递进来的数据通过初始值保存起来
<body>
  <div id="app" style="border: 1px solid red;height:160px;">
    <my-comp initcount="我是来自父组件的内容">33</my-comp>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  <script>
    var app = new Vue({
      el: "#app",
      components: {
        "my-comp": {
          props: ["initcount"],
          template: "<div>{{count}}</div>",
          data: function () {
            return {
              count: this.initcount + "hahaha",
            };
          },
        },
      },
    });
  </script>
</body>
另一种应用场景: props 作为需要被转变的原始值传入。这种情况用计算属性就可以了
步骤一:注册组件
步骤二:将父组件的数据传递进来,并在子组件中用 props 接收
步骤三:将传递进来的数据通过计算属性进行重新计算
<body>
  <div id="app" style="border: 1px solid red;height:160px;">
    <!-- 需求:通过input中输入的数据直接改变div的宽度 -->
    <input type="text" v-model="width" />
    <my-comp :width="width"></my-comp>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  <script>
    var app = new Vue({
      el: "#app",
      data: {
        width: "0",
      },
      components: {
        "my-comp": {
          props: ["initcount", "width"],
          template: '<div :style="style">{{initcount}}</div> ',
          computed: {
            style: function () {
              //return出来的直接就是style
              return {
                width: this.width + "px",
                background: "red",
                height: "30px",
              };
            },
          },
        },
      },
    });
  </script>
</body>

数据验证

vue 组件中 camelCased (驼峰式)命名与 kebabcase (短横线)命名

html 中, myMessagemymessage 是一致的,因此在组件中的 html 中使用必须使用 kebabcase(短横线)命名方式。在 html 中不允许使用驼峰!!!!!!
在组件中, 父组件给子组件传递数据必须用短横线。在 template 中,必须使用驼峰命名方式,若为短横线的命名方式。则会直接保错。
在组件的 data 中,用 this.XXX 引用时,只能是驼峰命名方式。若为短横线的命名方式,则会报错。

验证传入的 props 参数的数据规格,如果不符合数据规格,Vue 会发出警告。
能判断的所有种类(也就是 type 值)有:String, Number, Boolean, Function, Object, Array, Symbol

Vue.component("example", {
  props: {
    // 必须是数字类型
    propA: Number,
    // 必须是字符串或数字类型
    propB: [String, Number],
    // 必传且是String
    propC: {
      type: String,
      required: true,
    },
    // 数字有默认值
    propD: {
      type: Number,
      default: 101,
    },
    // 数组、默认值是一个工厂函数返回对象
    propE: {
      type: Object,
      default: function () {
        console.log("propE default invoked.");
        return { message: "I am from propE." };
      },
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10;
      },
    },
  },
});

组件通信

组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信

自定义事件-子组件给父组件传递数据

  1. 自定义一个事件
  2. 在子组件中用$emit 触发这个事件,第一个参数是事件名,后边的参数是要传递的数据
  3. 在自定义事件中用一个参数来接受所要传递的数据

    <body>
    <div id="app">
        <p>您好,您现在的银行余额是{{total}}元</p>
        <!-- @change="handleTotal"是自定义的事件,作用域是父组件,所以methods要定义在父组件中 -->
        <btn-compnent @change="handleTotal"></btn-compnent>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <script>
        //需求:通过加号和减号来给父组件传递数据(银行卡余额)
        var app = new Vue({
        el: "#app",
        data: {
            total: 0,
        },
        components: {
            "btn-compnent": {
            //template中定义的方法(handleincrease和handlereduce)的作用域是子组件
            template: '<div>\
                            <button @click="handleincrease">+1</button>\
                            <button @click="handlereduce">-1</button>\
                            </div>',
            data: function () {
                return {
                count: 0,
                };
            },
            methods: {
                //把子组件的data进行修改
                handleincrease: function () {
                this.count++;
                //触发在父组件中定义的@change事件
                this.$emit("change", this.count);
                },
                handlereduce: function () {
                this.count--;
                //触发在父组件中定义的@change事件
                this.$emit("change", this.count);
                },
            },
            },
        },
        methods: {
            handleTotal: function (total) {
            this.total = total;
            },
        },
        });
    </script>
    </body>

在组件中使用v-model

$emit 这行代码实际上会触发一个input事件,input 后的参数就是传递给v-model 绑定的属性的值。
v-model 其实本质上是一个语法糖,这背后其实做了两个操作

  • v-bind 绑定一个 value 属性
  • v-on 指令给当前元素绑定 input 事件

要使用 v-model, 要做到:

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

上面的例子可以将这几行代码改写,并将handleTotal方法删除

  <btn-compnent v-model="total"></btn-compnent>
  this.$emit('input', this.count);

非父组件之间的通信

官网描述:有时候两个组件也需要通信(非父子关系)。在简单的场景下,可以使用一个空的Vue实例作为中央事件总线:

  var bus = new Vue()
  //触发组件A中的事件
  bus.$emit('id-selected',1)
  //在组件B创建的钩子中监听事件
  bus.$on('id-selected', function(id){
    //...
  })
<body>
    <div id="app">
        <my-acomponent></my-acomponent>
        <my-bcomponent></my-bcomponent>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js">
    </script>
    <script>
        Vue.component('my-acomponent', {
            template: '<div><button @click="handle">点击我向B组件传递数据</button></div>',
            data: function () {
                return {
                    aaa: '我是来自A组件的内容'
                }
            },
            methods: {
                handle: function () {
                    //触发事件
                    this.$root.bus.$emit('lala', this.aaa);
                }
            }
        })
        Vue.component('my-bcomponent', {
            template: '<div></div>',
            created: function () {
                //钩子函数创建的时候就监听事件---lala事件
                this.$root.bus.$on('lala', function (value) {
                    alert(value)
                });
            }
        })

        var app = new Vue({
            el: "#app",
            data: {
                bus:new Vue()
            }
        })
    </script>
</body>

使用slot分发内容

什么是slot(插槽)

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板,这个过程被称为内容分发。Vue.js 实现了一个内容分发 API,使用特殊的 slot 元素作为原始内容的插槽。

编译的作用域

在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为:

  <child-component>
  {{ message }}
  </child-component>

message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。子组件的数据是在Vue.component中进行绑定的。

插槽的用法

  • 单个插槽
<body>
    <div id="app">
        <my-acomponent>
            <p>我是父组件的内容</p>
        </my-acomponent>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js">
    </script>
    <script>
        Vue.component('my-acomponent', {
            template: '<div>\
            <slot>如果父组件没有插入内容,我就作为默认出现</slot>\
            </div>',
        })

        var app = new Vue({
            el: "#app",
            data: {
            }
        })
    </script>
  • 具名插槽
<body>
    <div id="app">
        <name-component>
            <h3 slot="header">我是标题</h3>
            <p>我是正文内容</p>
            <p>正文内容两段</p>
            <p slot="footer">我是底部信息</p>
        </name-component>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js">
    </script>
    <script>
        Vue.component('name-component', {
            template: 
            '<div>\
                <div class="header">\n' +
                    '<slot name="header"></slot>\n' +
                '</div>\n' +

                '<div class="contatiner">\n' +
                    '<slot></slot>\n' +
                '</div>\n' +

                '<div class="footer">\n' +
                    '<slot name="footer"></slot>\n' +
                '</div>' +
            '</div>'
        })

        var app = new Vue({
            el: "#app",
            data: {
            }
        })
    </script>
</body>

作用域插槽

作用域插槽是一种特殊的slot,使用一个可以复用的模板来替换已经渲染的元素 —— 从子组件获取数据

<body>
    <div id="app">
        <name-component>
            <!-- template模板是不会被渲染的 -->
            <template slot="abc" slot-scope="prop">
            <!-- text属性可以被访问 -->
                {{prop.text}}
            <!-- name属性无法被访问 -->
                {{prop.name}} 
            </template>
            <!-- 2.5.0之后任意标签上都可以使用作用域插槽,不用再使用template标签了 -->
        </name-component>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js">
    </script>
    <script>
        Vue.component('name-component', {
            template: '<div>\
            <slot text="我是来自子组件的数据" name="abc"></slot>\
            </div>',
        })

        var app = new Vue({
            el: "#app",
            data: {
            }
        })
    </script>
</body>

访问slot

通过this.$slots.(NAME)

  mounted:function(){
      //访问插槽
      var header = this.$slots.header;
      var text = header[0].elm.innerText;
      var html = header[0].elm.innerHTML;
      console.log(header)
      console.log(text)
      console.log(html)
    }

组件高级用法-动态组件

Vue 给我们提供了一个元素叫component
作用是:用来动态地挂载不同的组件
实现:使用is特性来进行实现的

<body>
    <div id="app">
        <component :is="thisView"></component>   
        <button @click="switchView('A')">第一句</button>
        <button @click="switchView('B')">第二句</button>
        <button @click="switchView('C')">第三句</button>
        <button @click="switchView('D')">第四句</button>

    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js">
    </script>
    <script>
        //需求:通过点击不同的按钮切换不同的视图
        Vue.component('compA', {
            template: '<div>离离原上草</div>',
        })
        Vue.component('compB', {
            template: '<div>一岁一枯荣</div>',
        })
        Vue.component('compC', {
            template: '<div>野火烧不尽</div>',
        })
        Vue.component('compD', {
            template: '<div>春风吹又生</div>',
        })

        var app = new Vue({
            el: "#app",
            data: {
                thisView:'compA'
            },
            methods:{
                switchView:function(tag){
                    this.thisView = 'comp' + tag
                }
            }
        })
    </script>
</body>