# 组件

组件是可复用的Vue实例,它有一个名字,通过这个名字可以在其他地方进行复用。下面是一个组件实例,注意组件的data必须是一个函数,每个实例维护一份被返回对象的独立拷贝:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>vuejs测试</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>
    <body>
        <div id="app">
            <div>
                分离开
                <button-counter></button-counter>
                <button-counter></button-counter>
                <button-counter></button-counter>
            </div>
        </div>
        <script type="text/javascript">
            // 定义一个名为 button-counter 的新组件
            Vue.component('button-counter', {
                data: function() {
                    return {
                        count: 0
                    }
                },
                template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
            })

            var app = new Vue({
                el: '#app',
                data: {
                    message: "",
                },
            });
        </script>
    </body>
</html>
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

# 组件名

我们对组件进行命名时有几种命名方法:

  • camelCase:驼峰命名法,当变量名是由多个单词组成,除了首个单词首字母小写,其他每个单词首字母大写。
  • kebab-case:短横线分隔命名,所有单词首字母全小写且必须包含一个连字符。
  • PascalCase:首字母大写命名,当变量名是由多个单词组成时,每个单词首字母大写。

HTML中的属性名是大小写不敏感,浏览器会把所有的大写字符解释成小写字符,意味着当你使用DOM中的模板时,camelCase和PascalCase需使用其等价的kebab-case命名,比如my-component-name和MyComponentName是等价的。

# 组件注册

上面是全局注册组件,还可以使用局部注册:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>vuejs测试</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>
    <body>
        <div id="app">
            <!--使用组件-->
            <counter></counter>
            <counter></counter>
            <counter></counter>
        </div>
        <script type="text/javascript">
            //定义组件
            const counter = {
                template: "<button @click='num++'>你点击了{{num}}次</button>",
                data() {
                    return {
                        num: 0
                    }
                }
            };

            //全局注册组件:在所有的vue实例中都可以使用组件
            //参数1:组件名称,参数2:具体的组件
            //Vue.component("counter", counter);

            var app = new Vue({
                el: "#app",
                components: {
                    counter: counter
                }
            });
        </script>
    </body>
</html>
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

如果使用了import/require,则需要在模块系统中注册:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}
1
2
3
4
5
6
7
8
9
10

上面如果是在一个单独模块中则为局部注册,如果是在App.vue中则为全局注册。

# 传递数据

组件之间肯定要有相互通信,先来看下父组件向子组件传递数据,实例1:

<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>

    <body>
        <div id="vueApp">
            <todo-list></todo-list>
        </div>
        <script>
            Vue.component('todo-item', {
                props: {
                    content: String,
                    condition: {
                        type: Boolean,
                        default: false
                    }
                },
                template: `
                    <li>
                    <span v-if="!condition">{{content}}</span>
                    <span v-else style="text-decoration: line-through;">{{content}}</span>
                    </li>
                `,
                data: function() {
                    return {}
                },
                methods: {

                },
            })

            Vue.component('todo-list', {
                template: `
                    <ul>
                        <todo-item v-for="item in list" :content="item.content" :condition="item.condition"></todo-item>
                    </ul>
                `,
                data: function() {
                    return {
                        list: [{
                                content: "course 1",
                                condition: false
                            },
                            {
                                content: "course 2",
                                condition: true
                            }
                        ]
                    }
                }
            })

            var vm = new Vue({
                el: '#vueApp',
            });
        </script>
    </body>

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

实例2:

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

    <head>
        <meta charset="UTF-8">
        <title>vuejs测试</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>

    <body>
        <div id="app">
            <!--使用组件-->
            <my-list :items="lessons"></my-list>
        </div>
        <script type="text/javascript">
            //定义组件
            const myList = {
                template: `
            <ul>
            <li v-for="item in items" :key="item.id">{{item.id}}--{{item.name}}</li>
            </ul>`,
                //定义接收父组件的属性
                props: {
                    items: {
                        //数据类型,如果是数组则是Array,如果是对象则是Object
                        type: Array,
                        //默认值
                        default: []
                    }
                }
            };

            var app = new Vue({
                el: "#app",
                data: {
                    msg: "父组件的msg属性数据内容",
                    lessons: [{
                            "id": 1,
                            "name": "Java"
                        },
                        {
                            "id": 2,
                            "name": "Php"
                        },
                        {
                            "id": 3,
                            "name": "前端"
                        }
                    ]
                },
                components: {
                    myList
                }
            });
        </script>
    </body>

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

下面是子组件向父组件传递消息,触发事件,实例1:

<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>

    <body>
        <div id="vueApp">
            <todo-list></todo-list>
        </div>
        <script>
            Vue.component('todo-item', {
                props: {
                    content: String,
                    condition: {
                        type: Boolean,
                        default: false
                    }
                },
                template: `
                    <li>
                    <span v-if="!condition">{{content}}</span>
                    <span v-else style="text-decoration: line-through;">{{content}}</span>
                    <button v-show="!condition" @click="handleClick">删除操作</button>
                    </li>
                `,
                data: function() {
                    return {}
                },
                methods: {
                    handleClick() {
                        console.log("点击删除按钮")
                        this.$emit('delete', this.content);
                    }
                },
            })

            Vue.component('todo-list', {
                template: `
                    <ul>
                        <todo-item v-for="item in list" @delete="handleDelete" :content="item.content" :condition="item.condition"></todo-item>
                    </ul>
                `,
                data: function() {
                    return {
                        list: [{
                                content: "course 1",
                                condition: false
                            },
                            {
                                content: "course 2",
                                condition: true
                            }
                        ]
                    }
                },
                methods: {
                    handleDelete() {
                        console.log("handle delete!");
                    }
                }
            })

            var vm = new Vue({
                el: '#vueApp',
            });
        </script>
    </body>

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

也可以向事件抛出一个值:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" type="text/javascript" charset="utf-8"></script>
    </head>
    <body>
        <div id="app">
            <button-counter title="title1 : " @clicknow="clicknow">
                <h2>嵌套slot</h2>
            </button-counter>
            <button-counter title="title2 : " v-on:clicknow="count += $event">
                {{count}}
            </button-counter>
        </div>
        <script type="text/javascript">
            Vue.component('button-counter', {
                props: ['title'],
                data: function() {
                    return {
                        count: 0
                    }
                },
                template: '<div><h1>子组件h1标题</h1><button v-on:click="clickfun">{{title}} You clicked me {{ count }} times.</button><slot></slot></div>',
                methods: {
                    clickfun: function() {
                        this.count++;
                        this.$emit('clicknow', this.count);
                    }
                }
            })
            var vm = new Vue({
                el: "#app",
                data: {
                    count: 0,
                },
                methods: {
                    clicknow: function(e) {
                        console.log(e);
                    }
                }
            });
        </script>
        <style type="text/css">

        </style>
    </body>
</html>
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

# 属性

组件中很重要的一个东西是属性,它的命名同组件名一样,建议尽量使用kebab-case命名法。属性可以只指定名字,也可以指定名称和类型:

props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}
1
2
3
4
5
6
7
8
9
10
11

# 传递属性

下面是传递属性的样例:

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注意传入对象时,即便对象是静态的,我们仍然需要v-bind来告诉vue。

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>
1
2
3
4
5
6
7
8
9
10
11

传递对象时,如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)。比如:

post: {
  id: 1,
  title: 'My Journey with Vue'
}

<blog-post v-bind="post"></blog-post>

等价于下面:

<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 数据流

所有的属性都使得父子prop之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

有两种常见变更一个属性的情况:

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值:

    props: ['initialCounter'],
    data: function () {
      return {
        counter: this.initialCounter
      }
    }
    
    1
    2
    3
    4
    5
    6
  2. 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }
    
    1
    2
    3
    4
    5
    6

# 属性验证

可以对属性的类型进行验证:

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})
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

上面的类型可以是下面的几种:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

除上面外还可以是自己定义的构造函数:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

Vue.component('blog-post', {
  props: {
    author: Person
  }
})
1
2
3
4
5
6
7
8
9
10

# 其他属性相关

不在prop中的属性指的是一个组件没有在prop相应的定义,因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上。

对于大部分属性来说,从外部提供给组件的值会替换掉组件内部设置好的值,但class 和 style两个属性会智能一些,将两边的值合并起来并得到最终值。

如果不希望组件的根元素继承属性,在组件项中设置inheritAttrs: false即可。

# 插槽分发

有的时候想向组件传递内容,这个时候我们在组件模板中定义slot插槽即可,比如把上面的改成如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title></title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
    <div id="vueApp">
        <todo-list>
            <todo-item v-for="item in list" @delete="handleDelete" :content="item.content" :condition="item.condition">
            </todo-item>
        </todo-list>
    </div>
    <script>
        Vue.component('todo-item', {
            props: {
                content: String,
                condition: {
                    type: Boolean,
                    default: false
                }
            },
            template: `
                    <li>
                    <span v-if="!condition">{{content}}</span>
                    <span v-else style="text-decoration: line-through;">{{content}}</span>
                    <button v-show="!condition" @click="handleClick">删除操作</button>
                    </li>
                `,
            data: function () {
                return {}
            },
            methods: {
                handleClick() {
                    console.log("点击删除按钮")
                    this.$emit('delete', this.content);
                }
            },
        })

        Vue.component('todo-list', {
            template: `
                    <ul>
                         <slot></slot>
                    </ul>
                `,
            data: function () {
                return {

                }
            },

        })

        var vm = new Vue({
            el: '#vueApp',
            data: {
                list: [{
                    content: "course 1",
                    condition: false
                },
                {
                    content: "course 2",
                    condition: true
                }],
            },
            methods: {
                handleDelete() {
                    console.log("handle delete!");
                }
            }
        });
    </script>
</body>

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

关于插槽vue 2.6与vue 2.5也不太一样:

<!DOCTYPE html>
<html>

    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>

    <body>
        <div id="vueApp">
            <todo-list>
                <template v-slot>
                    <todo-item v-for="item in list" @delete="handleDelete" :content="item.content" :condition="item.condition">
                        <!-- vue 2.6语法 -->
                        <template v-slot:pre-icon="{value}">
                            <span>前置图标 {{value}}</span>
                        </template>
                        <template v-slot:suf-icon>
                            <span>后置图标</span>
                        </template>
                        <!-- vue 2.5语法 -->
                        <!-- <span slot="pre-icon">前置图标</span>
                <span slot="suf-icon">后置图标</span> -->
                    </todo-item>
                </template>
            </todo-list>
        </div>
        <script>
            Vue.component('todo-item', {
                props: {
                    content: String,
                    condition: {
                        type: Boolean,
                        default: false
                    },
                },
                template: `
                    <li>
                    <slot name="pre-icon" :value="value"></slot>
                    <span v-if="!condition">{{content}}</span>
                    <span v-else style="text-decoration: line-through;">{{content}}</span>
                    <slot name="suf-icon"></slot>
                    <button v-show="!condition" @click="handleClick">删除操作</button>
                    <slot name="default-icon">默认图标</slot>
                    </li>
                `,
                data: function() {
                    return {
                        value: Math.random()
                    }
                },
                methods: {
                    handleClick() {
                        console.log("点击删除按钮")
                        this.$emit('delete', this.content);
                    }
                },
            })

            Vue.component('todo-list', {
                template: `
                    <ul>
                         <slot></slot>
                    </ul>
                `,
                data: function() {
                    return {

                    }
                },

            })

            var vm = new Vue({
                el: '#vueApp',
                data: {
                    list: [{
                            content: "course 1",
                            condition: false
                        },
                        {
                            content: "course 2",
                            condition: true
                        }
                    ],
                },
                methods: {
                    handleDelete() {
                        console.log("handle delete!");
                    }
                }
            });
        </script>
    </body>

</html>
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
86
87
88
89
90
91
92
93
94
95
96
97

# 注意事项

注意,有些HTML元素,比如<ul>,<ol>,<table>,<select>对于哪些元素可以出现在其内部是有严格限制的,有些元素比如<li>,<tr>和<option>只能出现在其他某些特定元素内部。这个时候在这些元素内部使用组件就需要注意:

下面是有问题的:
<table>
  <blog-post-row></blog-post-row>
</table>

可以变通如下:
<table>
  <tr is="blog-post-row"></tr>
</table>
1
2
3
4
5
6
7
8
9

注意,如果是从下列来源使用模板的话就不存在这条限制:

  • 字符串 (例如:template: '...')
  • 单文件组件 (.vue)
  • <script type="text/x-template">