Vue.js 自定义组件 & 父子组件间的通信

  1. 云栖社区>
  2. 博客>
  3. 正文

Vue.js 自定义组件 & 父子组件间的通信

技术小牛人 2017-11-14 19:39:00 浏览623
展开阅读全文

什么是组件?

组件(component)是Vue.js最强大的功能之一,核心目标是扩展HTML元素,封装可重用的代码。我们可以把组件代码按照template、style、script的拆分方式,放置到对应的 .vue  文件中。


Vue.js的组件可以理解为预先定义好行为的ViewModel类,一个组件可以预定义很多选项,但最核心的是以下几个:


模板(template) --  模板声明了数据和最终展现给用户的DOM之间的映射关系


初始数据(data) --  一个组件的初始数据状态。对于可复用的组件来说,通常是私有的状态


接受的外部参数(props)  --  组件之间通过参数来进行数据的传递和共享。参数默认是单向绑定(由上而下),但也可以显示声明为双向绑定


方法(methods)  --  对数据的改动操作一般都在组件的方法内进行,可以通过v-on指令将用户输入事件和组件方法进行绑定


生命周期钩子函数(lifecycle hooks)  --  一个组件会触发多个生命周期钩子函数,比如created、attached、destroyed等。在这些钩子函数中,我们可以封装一些自定义的逻辑。和传统的MVC相比,这可以理解为Controller的逻辑分散到了这些钩子函数中



组件注册

1、全局注册     Vue.component(tagName, options)

1
2
3
Vue.component('my-component', {
    // 选项
})


如上所示,第一个参数是注册组件的名称(即在HTML中我们可以这样使用组件:<didi-component></didi-component>);第二个参数是组件的构造函数,它可以是Function,也可以是Object


Function  --  第二个参数可以是用Vue.extend()创建的一个组件构造器

1
2
3
var myComponent = Vue.extend({
    //选项...
})


Object  --  第二个参数传入选项对象,Vue.js在背后自动调用Vue.extend()

1
2
3
4
//在一个步骤中扩展与注册
Vue.component('didi-component',{
    template:'<div>A custom component!</div>'
})


组件在注册之后,便可以在父实例的模块中以自定义元素 <my-component></my-component> 的形式使用。要确保在初始化根实例 之前 注册了组件:


全局注册案例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="example">
    <my-component></my-component>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.7/vue.js"></script>
<script>
    //注册
    Vue.component('my-component',{
        template:'<div>A custom component!</div>'
    })
    //创建根实例
    new Vue({
        el:'#example'
    })
</script>

wKioL1gwAY2B1tSpAABSZICSvVg744.png


全局注册案例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="app">
    <aaa></aaa>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    //注册全局组件
    Vue.component('aaa',{
        template:'<table><thead><tr><th>姓名</th><th>性别</th><th>年龄</th></tr></thead><tbody>' +
        '<tr><td>李雷</td><td>男</td><td>12</td></tr>' +
        '<tr><td>吉姆</td><td>男</td><td>13</td></tr>' +
        '<tr><td>露西</td><td>女</td><td>11</td></tr>' +
        '</tbody></table>'
    });
    //创建根实例
    var vm = new Vue({
        el:'#app'
    })
 
</script>
</body>
</html>


wKiom1hGJV3T5csjAAAyS4OczJE467.png

全局注册案例三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <link href="//cdn.bootcss.com/bootstrap/3.0.1/css/bootstrap.css" rel="stylesheet">
 
</head>
<body>
<div id="app">
    <template id="my-component">
        <form action="myForm.php">
            <div class="form-group">
                <label for="uName">姓名:</label>
                <input type="text" id="uName" name="uName">
            </div>
            <div class="form-group">
                <label for="gender">性别:</label>
                <label class="radio-inline">
                    <input type="radio" name="gender" id="male" value="male"> 男
                </label>
                <label class="radio-inline">
                    <input type="radio" name="gender" id="female" value="female"> 女
                </label>
            </div>
            <div class="form-group">
                <label for="age">年龄:</label>
                <input type="text" id="age" name="age">
            </div>
            <div class="form-group">
                <label for="interests">兴趣爱好:</label>
                <label class="checkbox-inline">
                    <input type="checkbox" name="interests" id="reading" value="reading"> 阅读
                </label>
                <label class="checkbox-inline">
                    <input type="checkbox" name="swimming" id="swimming" value="swimming"> 游泳
                </label>
                <label class="checkbox-inline">
                    <input type="checkbox" name="interests" id="basketball" value="basketball"> 篮球
                </label>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
 
 
    </template>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    //注册全局组件
    Vue.component('#my-component');
 
    //创建根实例
    var vm = new Vue({
        el:'#app'
    })
</script>
</body>
</html>


wKioL1hGJYig04VEAAA9XRWf1XI382.png


2、局部注册

不需要每个组件都全局注册,可以让组件只能用在其它组件内。我们可以用实例选项components注册


局部注册案例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="app">
    <aaa></aaa>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
 
    //局部注册
    var vm = new Vue({
        el:'#app',
        components:{
            'aaa':{
                template:'<h2>局部注册组件数据</h2>'
            }
        }
    })
</script>
</body>
</html>

wKioL1hGMr2BftLjAABmD-sDKkE159.png


局部注册案例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="app">
    <aaa></aaa>
</div>
 
<!--<script src="//cdn.bootcss.com/vue/1.0.26/vue.js"></script>-->
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
 
<script>
    //注册一个子组件
    var Child = {
        template:'<h3>子组件数据</h3>'
    };
    //注册一个父组件
    var Parent = {
        //这里必须用一个容器把注册组件包起来,否则会报错
        template:'<div><h1>父组件数据</h1><child></child></div>',
        components:{
            'child':Child
        }
    };
    //创建一个根实例
    new Vue({
        el:'#app',
        components:{
            //<aaa>只能用在父组件模板内
            'aaa':Parent
        }
    })
</script>
</body>
</html>

wKiom1hGMzix2nbPAAB42lShMN4683.png

局部注册案例三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="app">
    <aaa></aaa>
</div>
 
<template id="bbb">
    <div>
        <h2>`msg`</h2>
        <ul>
            <li v-for="data in myData">`data`</li>
        </ul>
    </div>
</template>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    var vm = new Vue({
        el:'#app',
        data:{
 
        },
        components:{
            'aaa':{
                template:'#bbb',
                data:function(){
                    return{
                        msg:'fruits',
                        myData:['apple','banana','cherry','duria']
                    }
                }
            }
 
        }
    })
</script>
</body>
</html>

wKiom1hGSKzw7KAoAAB_F1j1vAc421.png

DOM模板解析说明


当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。尤其像这些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出现在其它元素内部。


在自定义组件中使用这些受限制的元素时会导致一些问题,例如:

1
2
3
<table>
<my-row>...</my-row>
</table>


自定义组件 <my-row> 被认为是无效的内容,因此在渲染的时候会导致错误。变通的方案是使用特殊的 is 属性:

1
2
3
<table>
    <tr is="my-row"></tr>
</table>


应当注意,如果您使用来自以下来源之一的字符串模板,这些限制将不适用:


<script type="text/x-template">

JavaScript内联模版字符串

.vue 组件


因此,有必要的话请使用字符串模版。


针对is属性的小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<div id="app">
    <table>
        <tr>
            <td><label for="uName">用户名:</label></td>
            <td><input type="text" id="uName"></td>
        </tr>
        <tr>
            <td>性别:</td>
            <td>
                <label for="male">男</label>
                <input type="radio" id="male" value="male" v-model="gender">
                <label for="female">女</label>
                <input type="radio" id="female" value="female" v-model="gender">
                <span>Gender: `gender`</span>
            </td>
        </tr>
        <tr>
            <td>年龄:</td>
            <td>
                <input type="number" id="uAge" v-model="age">
                <span>Age: `age`</span>
            </td>
        </tr>
        <tr>
            <td>兴趣:</td>
            <td>
                <input type="checkbox" id="reading" value="reading" v-model="interests">
                <label for="reading">阅读</label>
                <input type="checkbox" id="swimming" value="swimming" v-model="interests">
                <label for="swimming">游泳</label>
                <input type="checkbox" id="running" value="running" v-model="interests">
                <label for="running">游泳</label>
                <span>Interest: `interests`</span>
            </td>
        </tr>
        <tr is="my-row"></tr>
        <!--<tr>-->    <!-- 如果是这种写法的话,页面什么内容都不显示,审查元素也不存在 -->
            <!--<my-row></my-row>-->
        <!--</tr>-->
        <tr>
            <td>所在城市:</td>
            <td>
                <select  v-model="cities">
                    <option value="GZ" selected>广州</option>
                    <option value="BJ">北京</option>
                    <option value="SZ">深圳</option>
                    <option value="SH">上海</option>
                </select>
                <span>City: `cities`</span>
            </td>
        </tr>
        <tr>
            <td>自我介绍:</td>
            <td>
                <textarea></textarea>
            </td>
        </tr>
 
    </table>
</div>
 
 
 
<script src="//cdn.bootcss.com/vue/1.0.26/vue.js"></script>
<script>
    Vue.component('my-row',{
        template:'<td>报名院校:</td><td>' +
        '<select>' +
        '<option value="qinghua" selected>清华大学</option>' +
        '<option value="beida">北京大学</option>' +
        '<option value="hagongda">哈尔冰工业大学</option>' +
        '<option value="fudan">复旦大学</option>' +
        '</select>' +
        '</td>'
    })
    new Vue({
        el:'#app',
        data:{
            gender:'',
            age:'',
            interests:[],
            cities:''
        }
    })
</script>

wKioL1gyZJ7w4QxXAADrXIIm1rU977.png



data必须是函数

使用组件时,大多数选项可以被传入到 Vue 构造器中,有一个例外: data 必须是函数。 实际上,如果你这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
     <my-component></my-component>
</div>
 
<script src="//cdn.bootcss.com/vue/1.0.26/vue.js"></script>
<script>
    Vue.component('my-component',{
        template:'<span>`message`</span>',
        data:{
            message:'hello'
        }
    })
</script>


那么 Vue 会在控制台发出警告,告诉你在组件中 data 必须是一个函数。最好理解这种规则的存在意义。wKiom1gyZtHhkuKlAAA-fNnBlak197.png


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
    <simple-counter></simple-counter>
    <simple-counter></simple-counter>
    <simple-counter></simple-counter>
</div>
 
<script src="//cdn.bootcss.com/vue/1.0.26/vue.js"></script>
<script>
    var data={counter:0};
    Vue.component('simple-counter',{
        template:'<button @click="counter+=1">`counter`</button>',
        data:function(){
            return data
        }
    })
    new Vue({
        el:'#app'
    })
</script>

wKiom1gyaW-StpHyAACHNdJBD5g501.png


由于这三个组件共享了同一个 data , 因此增加一个 counter 会影响所有组件!我们可以通过为每个组件返回新的 data 对象来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
    <simple-counter></simple-counter>
    <simple-counter></simple-counter>
    <simple-counter></simple-counter>
</div>
 
<script src="//cdn.bootcss.com/vue/1.0.26/vue.js"></script>
<script>
    Vue.component('simple-counter',{
        template:'<button @click="counter+=1">`counter`</button>',
        data:function(){
            return {counter:0}
        }
    })
    new Vue({
        el:'#app'
    })
</script>


现在每个 counter 都有它自己内部的状态了:

wKioL1gyadaB6mX1AACHyfE5CGE374.png


data函数必须返回一个对象

还是把上面的例子稍作修改,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
    <counter></counter>
    <counter></counter>
    <counter></counter>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    Vue.component('counter',{
        template:'<button @click="count+=1">`count`</button>',
        data:function(){
            return count=0
        }
    })
    new Vue({
        el:'#app'
    })
</script>


看上去似乎没有什么问题,但是页面并没有渲染出效果来,打开控制台可以看到报错信息,data函数必须返回一个对象

wKioL1g3sXOBj4aaAACk-XQ_T10556.png



组件数据传递

组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。


在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。

wKiom1gyapyjDebRAABntyrS_o8091.png

Props  ---> 父组件把数据传递给子组件

使用Props传递数据


组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。


prop 是父组件用来传递数据的一个自定义属性。子组件需要显式地用 props 选项 声明 “prop”:

1
2
3
4
5
6
7
Vue.component('child', {
    // 声明 props
    props: ['message'],
    // 就像 data 一样,prop 可以用在模板内
    // 同样也可以在 vm 实例中像 “this.message” 这样使用
    template: '<span>{{ message }}</span>'
})


然后向它传入一个普通字符串:

1
<child message="hello!"></child>


先看第一种情况,不使用 props 传递数据

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
    <child message="hello"></child>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    Vue.component('child',{
        template:'<span>`message`</span>'
    })
    new Vue({
        el:"#app"
    })
</script>


查看页面,发现报错了

属性或方法“message”在实例中未定义,但是在渲染的时候被引用了,在data选项中需要确保声明该属性


wKioL1g3tN_BTk6FAABMq7yfB8c190.png


我们可以这样修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="app">
    <child message="hello"></child>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    Vue.component('child',{
        template:'<span>`message`</span>',
        data(){
            return {message:'你好,世界!'}
        }
    });
    var vm = new Vue({
        el:'#app'
    })
</script>
</body>
</html>


wKiom1hGZ3TD_Q-3AACIY6bmFjo002.png


第二种情况,使用 props 传递数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
    <child message="hello"></child>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    Vue.component('child',{
        template:'<span>`message`</span>',
        //声明 props
        props:['message']
    });
    new Vue({
        el:"#app",
        data:{
            message:'你好'
        }
    })
</script>


此时,页面可以渲染出自定义的组件了

wKioL1g3tkbwVE94AABRZnorhKM901.png


同时,注意观察上面的代码,Vue实例中有一个同名的message 属性 '你好',但是页面渲染出来的却是 hello。这就回到了props 的作用域问题了


props作用域

组件实例的作用域是孤立的,也就是说,即使组件与组件之间有同名属性,值也不共享


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div id="app">
    <component1></component1>
    <component2></component2>
</div>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    new Vue({
        el:'#app',
        components:{
            'component1':{
                template:'<p>`msg`</p>',
                data:function(){
                    return {msg:'组件一'}
                }
            },
            'component2':{
                template:'<p>`msg`</p>',
                data:function(){
                    return {msg:'组件二'}
                }
            }
        }
    })
</script>


两个组件都使用了同一个属性msg,但是渲染出来的内容却是不一样的

wKiom1g3yH_C5YVaAABTxtqjD5A233.png


再来看个例子,模仿帖子评论的“点赞”和 “反对”功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <link href="//cdn.bootcss.com/bootstrap/3.0.1/css/bootstrap.css" rel="stylesheet">
 
</head>
<body>
<div id="app">
    <counter heading="Like"></counter>
    <counter heading="Dislike"></counter>
</div>
 
<template id="counter-template">
    <div>
        <h1>`heading`</h1>
        <button @click="count+=1">`count`</button>
    </div>
</template>
 
<script src="//cdn.bootcss.com/vue/2.0.8/vue.js"></script>
<script>
    var vm = new Vue({
        el:'#app',
        components:{
            'counter':{
                template:'#counter-template',
                data:function(){
                    return {count:0}
                }
            }
        }
    })
</script>
</body>
</html>


如下图所示,页面提示报错,heading未定义,需要在data属性中声明。前面的例子也提到过,可以在data函数中返回一个 heading的对象,但是这里的heading 的值是动态的,可以是 Like 也可以是 Dislike,所以最好的办法就是遵循规则,把父组件 counter 要传给 子组件 template 的数据放到 props里面

wKioL1hGa8yw5rHcAABXWdJW0Oc928.png

添加一个 props 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
var vm = new Vue({
    el:'#app',
    components:{
        'counter':{
            template:'#counter-template',
            //把父组件 counter 传递给子组件 template 的数据放到 props属性中
            props:['heading'],
            data:function(){
                return {count:0}
            }
        }
    }
})


正常显示:

wKioL1hGbAfRJo8QAAA-qIqup-w901.png


这里,再增加一个颜色提示,点赞的按钮用绿色,反对的按钮用红色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
 
</head>
<body>
<div id="app">
    <!-- 这里如果用color就可以正常显示,换成其他的名字就无法显示背景色-->
    <counter heading="Likes" color="green"></counter>
    <counter heading="Dislikes" color="red"></counter>
</div>
 
<template id="counter-template">
    <div>
        <h1>`heading`</h1>
        <button @click="count+=1" :style="{background: color}">`count`</button>
    </div>
</template>