组件(Component) 是Vue最核心的功能,也是整个框架最精彩的地方,当然也是最难掌握的。(所有实例代码基于vue.js^2.*)

一、为什么要使用组件

vue组件的作用是提高重用性,让代码可复用

二、组件用法

 组件需要注册才能使用。组件有全局注册和局部注册两种方式。
全局注册

Vue.component("test", {
        data() {
            return {
                message: "message"
            }
        },
        template: "<div>我是测试组件{{message}}</div>"
    })

局部注册

new Vue({
        el: "#app",
        components: {
            test: {
                data() {
                    return {
                        message: "message"
                    }
                },
                template: "<div>我是测试组件{{message}}</div>"
            }
        }
    })

        注册局部组件,该组件只能再该实例作用域下有效;全局注册组件,在任何Vue实例都可以使用。

注意:若局部注册的组件名和全局注册的组件名重复,局部注册的组件会覆盖全局注册的组件

        Vue组件的模板在某些情况下会受到HTML的限制,比如<table>内规定只允许<tr>、<td>、<th>等这些表格元素,所以在<table>内直接使用组件是无效的。这种情况下,可以使用特殊的is属性来挂载组件。示例代码:

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>
<script>
    Vue.component('my-component', {
        template: '<div>这里是组件的内容</div>'
    });
    var app = new Vue({
        el: '#app'
    })
</script>

tbody在渲染时,会被替换为组件的内容,常见的限制元素还有<ul>、<ol>、<select>。

提示:如果使用的是字符串模板,是不受限制的。
除了template选项外,组件中还可以像Vue实例那样使用其他选项,比如data、computed、methods等。但是在使用data时,和实例稍有区别,data必须时函数,然后将数据return出去

<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>

 三、使用props传递数据

        组件不仅仅是要把模板的内容进行复用,最重要的是组件要进行通信。通常父组件的模板中包含子组件,父组件要正向的向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过props来实现的。

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

<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>

         props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板template及计算属性computed和方法methods中使用。上例的数据message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称,如果要传递多个数据,在props数组中添加项即可。
        由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名(camelCase)的props名称要转为短横分隔命名(kebab-case)。
        有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。代码示例:

<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>

        这里用v-model绑定了父级的数据parentMessage,当通过输入框任意输入时,子组件接收到的props “message”也会实时响应,并更新组件模板。

提示: 如果你要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅仅是字符串,下面的示例来对比:

<div id="app">
    <my-component message="[1,2,3]"></my-component>
    <my-component :message="[1,2,3]"></my-component>
</div>
<script>
    Vue.component('my-component', {
        props: ['message'],
        template: '<div>{{ message.length }}</div>'
    });

    var app = new Vue({
        el: '#app'
    })
</script>

同一个组件使用了两次,区别仅仅是第二个使用的是v-bind。渲染后的结果,第一个是字符串的长度7,第二个才是数组的长度3。

单向数据流

        业务中会经常遇到两种需要改变prop的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件data内再声明一个数据,引用父组件的prop。

<div id="app">
    <my-component :init-count="1"></my-component>
</div>
<script>
    Vue.component('my-component', {
        props: ['initCount'],
        template: '<div>{{ count }}</div>',
        data: function () {
            return {
                count: this.initCount
            }
        }
    });

    var app = new Vue({
        el: '#app'
    })
</script>

组件中声明了数据count,它在组件初始化时会获取来自父组件的initCount,之后就与之无关了,只用维护count,这样就可以避免直接操作initCount。
另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了。

<div id="app">
    <my-component :width="100"></my-component>
</div>
<script>
    Vue.component('my-component', {
        props: ['width'],
        template: '<div :style="style">组件内容</div>',
        computed: {
            style: function () {
                return {
                    width: this.width+'px'
                }
            }
        }
    });

    var app = new Vue({
        el: '#app'
    })
</script>

提示:注意,在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变是会影响父组件的。

数据验证 

        props选项除了数组外,还可以是对象,当prop需要验证时,就需要对象写法。

        一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。
以下是几个prop的示例:

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

验证的type类型可以是:

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

组件通信

        从父组件向子组件通信,通过props传递数据就可以了,但Vue组件通信的场景不止有这一种,归纳起来,组件之间通信可以用下图表示:

 

vue项目一开始为什么要先yarn一下 vue为什么要用组件_html

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

自定义事件

        当子组件需要向父组件传递数据时,就要用到自定义事件。我们在介绍指令v-on时有提到,v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。
        子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件。父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件。

<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, 3);
                },
                handleReduce: function() {
                    if (this.counter > 0) {
                        this.counter--;
                    }
                    this.$emit('reduce', this.counter, 2);
                }
            }
        });

        var app = new Vue({
            el: '#app',
            data: {
                total: 0
            },
            methods: {
                handleGetTotal: function(total, my) {
                    this.total = total;
                    console.log(my)
                }
            }
        })
    </script>

上面示例中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data“counter”后,通过$emit()再把它传递给父组件,父组件用v-on:increase和v-on:reduce(示例使用的是语法糖)。$emit()方法的第一个参数是自定义事件的名称,例如示例的increase和reduce后面的参数都是要传递的数据,可以不填或填写多个(多个的话在后面用逗号隔开)。

非父子组件通信

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

中央事件总线(bus)

推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。为了更形象地了解它,举一个生活中的例子。

        比如你需要租房子,你可能会找房产中介来登记你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你,整个过程中,买家和卖家并没有任何交流,都是通过中间人来传话的。或者你最近可能要换房了,你会找房产中介登记你的信息,订阅与你找房需求相关的资讯,一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。这两个例子中,你和出租者担任的就是两个跨级的组件,而房产中介就是这个中央事件总线(bus)。代码示例:

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
<script>
    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>

        先创建了一个名为bus的空Vue实例,里面没有任何内容;然后全局定义了组件component-a;最后创建Vue实例app,在app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus的事件on-message,而在组件component-a中,点击按钮会通过bus把事件on-message发出去,此时app就会接收到来自bus的事件,进而在回调里完成自己的业务逻辑。
        这种方法巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。如果深入使用,可以扩展bus实例,给它添加data、methods、computed等选项,这些都是可以公用的,在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,还有用户的授权token等。只需在初始化时让bus获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,我们会在进阶篇里逐步介绍这些内容。
        当你的项目比较大,有更多的小伙伴参与开发时,也可以选择更好的状态管理解决方案vuex,在进阶篇里会详细介绍关于它的用法。
        除了中央事件总线bus外,还有两种方法可以实现组件间通信:父链和子组件索引。

父链

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

<div id="app">
    {{ message }}
    <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>

尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过props和$emit来通信。

子组件索引

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

<div id="app">
    <button @click="handleRef">通过ref获取子组件实例</button>
    <component-a ref="comA"></component-a>
</div>
<script>
    Vue.component('component-a', {
        template: '<div>子组件</div>',
        data: function () {
            return {
                message: '子组件内容'
            }
        }
    });

    var app = new Vue({
        el: '#app',
        methods: {
            handleRef: function () {
                // 通过$refs来访问指定的实例
                var msg = this.$refs.comA.message;
                console.log(msg);
            }
        }
    })
</script>

在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过`this.$refs`来访问指定名称的子组件。

提示:$refs只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs。Vue会自动去判断是普通标签还是组件。

 四、使用slot分发内容

<app>
    <menu-main></menu-main>
    <menu-sub></menu-sub>
    <div class="container">
        <menu-left></menu-left>
        <container></container>
    </div>
    <app-footer></app-footer>
</app>

        当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot,这个过程叫作内容分发(transclusion)。以<app>为例,它有两个特点:
<app>组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
<app>组件很可能有它自己的模板。
props传递数据、events触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。

slot用法

单个slot

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

<div id="app">
    <child-component>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </child-component>
</div>
<script>
    Vue.component('child-component', {
        template: '\
        <div>\
            <slot>\
                <p>如果父组件没有插入内容,我将作为默认出现</p>\
            </slot>\
        </div>'
    });

    var app = new Vue({
        el: '#app'
    })
</script>

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

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

具名slot

 给<slot>元素指定一个name后可以分发多个内容,具名slot可以与单个slot共存。

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
<script>
    Vue.component('child-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>

子组件内声明了3个<slot>元素,其中在<div class="main">内的<slot>没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。如果没有指定默认的匿名slot,父组件内多余的内容片段都将被抛弃。上例最终渲染后的结果为:

<div id="app">
    <div class="container">
        <div class="header">
            <h2>标题</h2>
        </div>
        <div class="main">
            <p>正文内容</p>
            <p>更多的正文内容</p>
        </div>
        <div class="footer">
            <div>底部信息</div>
        </div>
    </div>
</div>

在组合使用组件时,内容分发API至关重要。

作用域插槽

        作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。概念比较难理解,我们先看一个简单的示例来了解它的基本用法。示例代码:

<div id="app">
    <child-component>
        <template scope="props">
            <p>来自父组件的内容</p>
            <p>{{ props.msg }}</p>
        </template>
    </child-component>
</div>
<script>
    Vue.component('child-component', {
        template: '\
        <div class="container">\
            <slot msg="来自子组件的内容"></slot>\
        </div>'
    });

    var app = new Vue({
        el: '#app'
    })
</script>

        观察子组件的模板,在<slot>元素上有一个类似props传递数据给组件的写法msg="xxx",将数据传到了插槽。父组件中使用了<template>元素,而且拥有一个scope="props"的特性,这里的props只是一个临时变量,就像v-for="item in items"里面的item一样。template内可以通过临时变量props访问来自子组件插槽的数据msg。

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

<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内容 -->\
            </slot>\
        </ul>'
    });

    var app = new Vue({
        el: '#app',
        data: {
            books: [
                { name: '《Vue.js实战》' },
                { name: '《JavaScript语言精粹》' },
                { name: '《JavaScript高级程序设计》' }
            ]
        }
    })
</script>

子组件my-list接收一个来自父级的prop数组books,并且将它在name为book的slot上使用v-for指令循环,同时暴露一个变量bookName。
如果你仔细揣摩上面的用法,你可能会产生这样的疑问:我直接在父组件用v-for不就好了吗,为什么还要绕一步,在子组件里面循环呢?的确,如果只是针对上面的示例,这样写是多此一举的。此例的用意主要是介绍作用域插槽的用法,并没有加入使用场景,而作用域插槽的使用场景就是既可以复用子组件的slot,又可以使slot内容不一致。如果上例还在其他组件内使用,<li>的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)从子组件内获取。

访问slot

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
<script>
    Vue.component('child-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>',
        mounted: function () {
            var header = this.$slots.header;
            var main = this.$slots.default;
            var footer = this.$slots.footer;
            console.log(footer);
            console.log(footer[0].elm.innerHTML);
        }
    });

    var app = new Vue({
        el: '#app'
    })
</script>

通过$slots可以访问某个具名slot,this.$slots.default包括了所有没有被包含在具名slot中的节点