# 组件
组件是可复用的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>
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>
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
},
// ...
}
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>
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>
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>
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>
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
}
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>
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>
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>
2
3
4
5
6
7
8
9
10
11
12
13
# 数据流
所有的属性都使得父子prop之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
有两种常见变更一个属性的情况:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
1
2
3
4
5
6这个 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
}
}
}
})
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
}
})
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>
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>
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>
2
3
4
5
6
7
8
9
注意,如果是从下列来源使用模板的话就不存在这条限制:
- 字符串 (例如:template: '...')
- 单文件组件 (.vue)
- <script type="text/x-template">