Vue 学习笔记(七):组件基础

Components Basics 组件基础

基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 把这个组件作为自定义元素来使用 -->
<div id="components-demo" class="demo">
<button-counter></button-counter>
</div>
<script>
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
// 因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count += 1">You clicked me {{ count }} times.</button>'
})
new Vue({ el: '#components-demo' })
</script>

渲染效果:

组件的复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="components-demo2" class="demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
<script>
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count += 1">You clicked me {{ count }} times.</button>'
})
new Vue({ el: '#components-demo2' })
</script>

渲染结果:

当点击按钮时,每个组件都会各自独立维护它的 count。每用一次组件,就会有一个它的新实例被创建。

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。

通过 Prop 向子组件传递数据

Passing Data to Child Components with Props
Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个 property。
为了给博文组件传递一个标题,我们可以用一个 props 选项将其包含在该组件可接受的 prop 列表中,一个 prop 被注册之后,就可以像这样把数据作为一个自定义 attribute 传递进来:

1
2
3
4
5
6
7
8
9
10
11
12
<div id="blog-post-demo" class="demo">
<blog-post1 title="My journey with Vue"></blog-post1>
<blog-post1 title="Blogging with Vue"></blog-post1>
<blog-post1 title="Why Vue is so fun"></blog-post1>
</div>
<script>
Vue.component('blog-post1', {
props: ['title'],
template: '<h6>{{ title }}</h3>'
})
new Vue({ el: '#blog-post-demo' })
</script>

渲染结果:

可以改写成,使用 v-bind 来动态传递 prop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="blog-post-demo" class="demo">
<blog-post v-for="post in posts" v-bind:key="post.id" v-bind:title="post.title"></blog-post>
</div>
<script>
Vue.component('blog-post', {
props: ['title'],
template: '<h6>{{ title }}</h3>'
})
new Vue({
el: '#blog-post-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
})
</script>

渲染结果和上面一样。

HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名

Prop 类型

以对象形式列出 prop 可以指定 prop 值类型:

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

JavaScript 模板字符串

1
2
3
4
5
6
7
8
`string text`

`string text line 1
string text line 2`

`string text ${expression} string text`

tag `string text ${expression} string text`

监听子组件事件

Listening to Child Components Events
例如,引入一个辅助功能来放大博文的字号,同时让页面的其它部分保持默认的字号。父组件中,添加一个 postFontSize 数据 property 来支持这个功能,在模板中用来控制所有博文的字号。当点击按钮 Enlarge text 时,需要告诉父级组件放大所有博文的文本。 Vue 实例提供了一个自定义事件的系统来解决这个问题。父级组件可以像处理 native DOM 事件一样通过 v-on 监听子组件实例的任意事件,子组件通过调用内建的 $emit 方法并传入事件名称来触发一个事件。

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
<div id="blog-posts-events-demo" class="demo">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post"
v-on:enlarge-text="postFontSize += 0.1"
></blog-post>
</div>
</div>
<script>
Vue.component('blog-post', {
props: ['post'],
template: '\
<div class="blog-post">\
<h3>{{ post.title }}</h3>\
<button v-on:click="$emit(\'enlarge-text\')">\
Enlarge text\
</button>\
<div v-html="post.content"></div>\
</div>\
'
})
new Vue({
el: '#blog-posts-events-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue', content: '...content...' },
{ id: 2, title: 'Blogging with Vue', content: '...content...' },
{ id: 3, title: 'Why Vue is so fun', content: '...content...' }
],
postFontSize: 1
}
})
</script>

渲染结果:

使用事件抛出一个值

有的时候用一个事件来抛出一个特定的值是非常有用的。例如让 <blog-post> 组件决定它的文本要放大多少。可以使用 $emit 的第二个参数:

1
2
3
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>

父级组件监听这个事件时,通过 $event 访问到被抛出的这个值:

1
2
3
4
<blog-post
...
v-on:enlarge-text="postFontSize += $event"
></blog-post>

或者,如果这个事件处理函数是一个方法:

1
2
3
4
<blog-post
...
v-on:enlarge-text="onEnlargeText"
></blog-post>

那么这个值将会作为第一个参数传入这个方法:

1
2
3
4
5
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}

在组件上使用 v-model

1
<input v-model="searchText">

等价于:

1
2
3
4
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>

当用在组件上时,v-model 则会这样:

1
2
3
4
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>

为了让它正常工作,这个组件内的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 value 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出

写成代码之后是这样的:

1
2
3
4
5
6
7
8
9
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})

现在 v-model 就应该可以在这个组件上完美地工作起来了:

1
<custom-input v-model="searchText"></custom-input>

动态组件 Dynamic Components

通过 <component> 元素加一个特殊的 is attribute 来实现。这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value 这样的 property,若想让其如预期般工作,需要使用 .prop 修饰器。

源码:

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
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
class="dynamic-component-demo-tab-button"
v-bind:class="{ 'dynamic-component-demo-tab-button-active': tab === currentTab }"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<component
v-bind:is="currentTabComponent"
class="dynamic-component-demo-tab"
></component>
</div>
<script>
Vue.component('tab-home', { template: '<div>Home component</div>' })
Vue.component('tab-posts', { template: '<div>Posts component</div>' })
Vue.component('tab-archive', { template: '<div>Archive component</div>' })
new Vue({
el: '#dynamic-component-demo',
data: {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
},
computed: {
currentTabComponent: function () {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
</script>
<style>
.dynamic-component-demo-tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: -1px;
}
.dynamic-component-demo-tab-button:hover {
background: #e0e0e0;
}
.dynamic-component-demo-tab-button-active {
background: #e0e0e0;
}
.dynamic-component-demo-tab {
border: 1px solid #ccc;
padding: 10px;
}
</style>

解析 DOM 模板时的注意事项

DOM Template Parsing Caveats

有些 HTML 元素,诸如 <ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部。

例如:

1
2
3
<table>
<blog-post-row></blog-post-row>
</table>

自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。需要使用 is attribute :

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

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

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