Vue组件

作者 新城 日期 2017-10-09
Vue
Vue组件

组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。

注册组件

1.创建一个 Vue 实例:

1
2
3
4
new Vue({
el: '#some-element',
// 选项
})

2.可以使用 Vue.component(tagName, options)

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

1
2
3
4
5
6
7
8
9
10
11
12
//使用
<div id="example">
<my-component></my-component>
</div>
// 注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
el: '#example'
})

渲染

1
2
3
<div id="example">
<div>A custom component!</div>
</div>

局部注册

不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用:

1
2
3
4
5
6
7
8
9
10
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
// ...
components: {
// <my-component> 将只在父模板可用
'my-component': Child
}
})

DOM模板解析

data必须是函数

1
2
3
4
5
6
7
8
9
10
11
12
var data = { counter: 0 }
Vue.component('simple-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
// 技术上 data 的确是一个函数了,因此 Vue 不会警告,
// 但是我们返回给每个组件的实例却引用了同一个 data 对象
data: function () {
return data
}
})
new Vue({
el: '#example-2'
})

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

1
return {counter:0}  //每个都独立返回一个counter

组合组件

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

Props

使用props传递数据

组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的 props 选项。
子组件要显式地用 props 选项声明它期待获得的数据:

1
2
3
4
5
6
7
8
9
Vue.component('child', {
// 声明 props
props: ['message'],
// 就像 data 一样,prop 可以用在模板内
// 同样也可以在 vm 实例中像“this.message”这样使用
template: '<span>{{ message }}</span>'
})
//子组件使用父组件传递过来的参数
<child message="hello!"></child>

如果使用的是驼峰命名 需要转换成为短横线

动态props

在模板中,要动态地绑定父组件的数据到子模板的 props,与绑定到任何普通的 HTML 特性相类似,就是用 v-bind。每当父组件的数据变化时,该变化也会传导给子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="d1">
<input v-model="parentMsg">
<br>
<child v-bind:mymessage="parentMsg"></child>
</div>
<script>
Vue.component('child', {
// 声明 props
props: ['mymessage'],
// 就像 data 一样,prop 可以用在模板内
// 同样也可以在 vm 实例中像“this.message”这样使用
template: '<span>{{ mymessage }}</span>'
});
new Vue({
el:'#d1',
data:{
parentMsg:'123'
}
})
</script>

单项数据流

为什么我们会有修改 prop 中数据的冲动呢?通常是这两种原因:
prop 作为初始值传入后,子组件想把它当作局部数据来用;
prop 作为初始值传入,由子组件处理成其它数据输出。

对这两种原因,正确的应对方式是:
定义一个局部变量,并用 prop 的值初始化它:

1
2
3
4
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}

定义一个计算属性,处理 prop 的值并返回。

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

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 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
<div id="app">
<p>{{ message }}</p>
<my-component :num="testNum"></my-component>
</div>

var child = {
template:'<div>A custom component{{num}}</div>',
props:{
num:{
type:'number', //是数字的话返回100 否则返回数字
default:function(){
return 100
}
}
}
};
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
testNum : 123
},
components:{
'my-component':child
}
})

非Prop特性

自定义事件 子组件去触发父组件的事件

使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件

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
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
incrementCounter: function () {
this.counter += 1
this.$emit('increment')
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})

在本例中,子组件已经和它外部完全解耦了。它所做的只是报告自己的内部事件,至于父组件是否关心则与它无关。留意到这一点很重要。

.sync修饰符(2.3.0+)

忽略 2.2.3之后新增

非父子组件之间的通信

1
2
3
4
5
6
7
var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})

使用插槽分布内容

1
2
3
4
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发

单个插槽

solt除非子组件模板包含至少一个 插口,否则父组件的内容将会被丢弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//my-component 组件有下面模板:
<div>
<h2>我是子组件的标题</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
</div>
//父组件
<div>
<h1>我是父组件的标题</h1>
<my-component>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
</div>
//渲染结果
<div>
<h1>我是父组件的标题</h1>
<div>
<h2>我是子组件的标题</h2>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</div>
</div>

具名插槽

元素可以用一个特殊的属性 name 来配置如何分发内容。多个插槽可以有不同的名字。具名插槽将匹配内容片段中有对应 slot 特性的元素。
仍然可以有一个匿名插槽,它是默认插槽,作为找不到匹配的内容片段的备用插槽。如果没有默认插槽,这些找不到匹配的内容片段将被抛弃。

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
//app-layout组件
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
//父组件模板
<app-layout>
<h1 slot="header">这里可能是一个页面标题</h1>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
<p slot="footer">这里有一些联系信息</p>
</app-layout>
//渲染结果
<div class="container">
<header>
<h1>这里可能是一个页面标题</h1>
</header>
<main>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
</main>
<footer>
<p>这里有一些联系信息</p>
</footer>
</div>

作用域插槽

作用域插槽是一种特殊类型的插槽,用作一个替换已渲染元素的 (能被传递数据的) 可重用模板。

在父级中,具有特殊属性 scope 的

动态组件

通过使用保留的 元素,动态地绑定到它的 is 特性,我们让多个组件可以使用同一个挂载点,并动态切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="examplea">
<component v-bind:is="currentView">
<!-- 组件在 vm.currentview 变化时改变! -->
</component>
</div>
var vm1 = new Vue({
el: '#examplea',
data: {
currentView: 'home'
},
components: {
home: { template: '<p>Welcome home!</p>'},
posts: { template: '<p>Welcome posts!</p>'},
archive: { template: '<p>Welcome archive!</p>' }
}
})

可复用组件

Vue组件的api来之三部分

  • props 父组件传递的数据
  • event js原生的属性事件
  • solt 允许额外的组件组合到组件当中
异步组件