Vue-组件

— 点点开发技能树 —
— Vue组件 及 通讯 —

组件

  组件可以把大的框架按照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
<!--全局注册组件-->
<script>
//my-compenent 组件名
Vue.component('my-component',{
//选项
template: '<div>局部注册组件的内容</div>'

})
</script>

<!--局部注册组件-->
<script>
var Child = {
template : '<div>局部注册组件的内容</div>'
}

var app = new Vue({
el: 'app',
components: {
//my-compenent 组件名
'my-component': Child
}
})
</script>

注意局部注册的组件在其子组件中不可用,如果需要使用,需要在子组件中声明

 

组件命名推荐:字母全小写且必须包含一个连字符。避免和当前以及未来的 HTML 元素相冲突。

使用-挂载组件

  一般直接标签使用即可。但在某些情况下会受到HTML限制,比如<table>内规定只允许是<tr>,<td>,<th>等元素,所以在<table>内使用组件是无效的。这时候可以使用is属性来挂载组件。

1
2
3
4
5
6
7
8
9
10
<!--普通使用-->
<div id="app">
<my-component></my-component>
</div>
<!--is属性挂载组件-->
<div id="app">
<table>
<tbody is="my-component"></body>
</table>
</div>

动态挂载组件

  Vue 提供了一个特殊的元素<component>用来动态地挂载不同的组件,使用is特性来选择要挂载的组件。

1
2
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>

手动挂载实例

  Vue提供Vue.extend$mount()两个方法来手动挂载一个实例。

  • Vue.extend是基础Vue构造器,创建一个“子类”,参数时一个包含组件选项的对象
  • 如果Vue实例在实例化时没有收到el选项,他也处于“未挂载”状态
  • $mount手动挂载一个未挂载的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id = "mount-div"></div>

<script>
//创建未挂载实例
var MyComponent = Vue.extend({
template: '<div>Hello: {{ name }}</div>',
data: function(){
return {
name: 'Aresn'
}
}
})
//挂载实例
newMyComponent().$mount('#mount-div');
</script>

模块化中使用组件

  在模块化中使用其他组件,需要在局部注册之前导入每个你想使用的组件

1
2
3
4
5
6
7
8
9
10
11
//ComponentB.vue
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
components: {
ComponentA,
ComponentC
},
// ...
}

  现在ComponentAComponentC都可以在ComponentB的模板中使用了。

  另外可以实现一些基础模块的全局化使用:戳这里

组件选项

  因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 datacomputedwatchmethods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。

常用的有:

  • template 模板
  • data 跟实例稍有区别,必须是函数,将数据return出去
  • computed 计算属性
  • methods 动作
  • components 组建中嵌套组件
1
2
3
4
5
6
7
8
9
Vue.component('my-component',{
//选项
template: '<div>{{message}}</div>',
data: function(){
return{
message: '组件内容';
}
},
})

  组件中return的data如果引用了一个外部的变量,那么这个变量就不是自己独有的,而是所有相同组件共享的,牵一发而动全身。
  如果想要各自独立,就需要组件通讯这个变量,再重新赋值。

组件通讯

父->子 props

  通过props正向传输数据(父组件向子组件传递数据及参数)。父组件中的数据变化会传递给子组件,反过来不行。
  在组件中,使用选项props来声明需要从父级接收的数据,值分为两种

  • 数组:传递值
  • 对象:传递的值需要数据类型验证

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

  props中的数据类似于data,只是前者props来源于父级,后者是组件自己的数据,作用于是组件本身。
  两者都可以在模板template、计算属性computedmethods中使用

数组使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<my-component message="来自父组件的数据"></my-component>
<!-- <my-component message2="来自父组件的数据2" message1="来自父组件的数据1"></my-component> -->
</div>
<script>
Vue.component('my-component',{
props: ['message'], //['message1','message2'] 多个数据
template: '<div>{{ message }}</div>'
})

var app = new Vue({
el: '#app'
})
</script>

由于HTML特性不区分大小写,在props中的warningText在DOM中赋值时需要用warning-text="提示信息",即<my-component warning-text="提示信息"></my-component>

对象使用——数据验证

自带验证的数据类型有:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function
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
Vue.component('my-component',{
props: {
//必须是数字类型
propA: Number,
//必须是字符串或数字类型
propB: [String,Number],
//布尔型,如果没有定义,默认为true
propC: {
type: Boolean,
default: true
},
//数字,而且必须传入
propD: {
type: Number,
required: true
},
//如果是数组或对象,默认值必须是一个函数来返回(没看懂)
propE: {
type: Array,
default: function () {
return [];
}
},
//自定义了一个验证函数
propF: {
validator: function (value){
return value > 10;
}
}

},
template: '<div>{{ count }}</div>'
data:function () {
reutrn {
count: this.initCount
}
}
})

验证失败时,在开发版本下会在控制台抛出一条警告

动态参数传入

利用 v-bind 动态更新组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<!-- :message 等于 v-bind:message -->
<my-component :message="parentMessage"></my-component>
</div>
<script>
Vue.component('my-component',{
props: ['message'],
template: '<div>{{ message }}</div>'
})

var app = new Vue({
el: '#app',
data: {
parentMessage: ''
}
})
</script>

直接传递数字、布尔值、数组、对象 需要使用v-bind,即:message="[1,2,3]"
不然message="[1,2,3]"传递的是字符串

  如果你想要将一个对象的所有属性都作为prop传入,你可以使用不带参数的 v-bind(取代v-bind:prop-name)。例如,对于一个给定的对象post

1
2
3
4
5
6
7
8
9
10
11
12
//数据
post: {
id: 1,
title: 'My Journey with Vue'
}
//v-bind="对象"
<blog-post v-bind="post"></blog-post>
//等价于
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>

应用-单向数据流

由于在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以prop中的变量是对象和数组时,在子组件内改变是会影响父组件的

使用场景:从父组件传递数据进来,保存原值,还可以可以随意操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<my-component :init-count="1"></my-component>
<!-- 变为短横分割命名 / v-bind传入数字-->
</div>
<script>
Vue.component('my-component',{
props: ['initCount'],
template: '<div>{{ count }}</div>'
data:function () {
reutrn {
count: this.initCount
}
}
})

var app = new Vue({
el: '#app'
})
</script>

还可以类似的使用computed转变传入值

子->父 $emit $on

子组件需要向父组件传递数据时可以使用自定义事件——$emit()$on()

  • 子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件
  • 父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件

跟组件和 prop 不同,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。所以建议始终使用 kebab-case 的事件名。

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
<!-- 此处父组件接受,执行handleGetTotal函数 -->
<div id = "app">
<mycomponent
@increase="handleGetTotal"></my-component>
</div>

<script>
Vue.component('my-component',{

template: '\
<div>\
<button @click="handleIncrease">+1</button>\
</div>',

data:function () {
reutrn {
counter: 0
}
},

methods: {
handleIncrease: function () {
this.counter++;
// 此处子组件向父组件发送发送increase信号,和参数
this.$emit('increase',this.counter);
}
}
});
var app =new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal: function (total) {
this total = total;
}
}
})
</script>

  以上的 父函数 处理方式是一个方法。如果是在语句中处理,子函数抛出的参数被放在$event变量中

1
2
3
4
5
<!-- 增加字体大小 -->
<blog-post
...
v-on:enlarge-text="postFontSize += $event"
></blog-post>

$emit使用方法:

1
$emit(自定义事件名称,抛出参数)

组件使用 v-model

  v-model:totalthis.$emit('input',this.counter) 实现子组件动态更新父组件数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<my-component v-model="total"></my-component>
<script>
//组件内
template:'<button @click="handleClick">+1</button>'
data:function(){
return {counter:0}
}
methods: {
handleClick: function() {
this.counter++;
this.$emit('input',this.counter);
}
}
//new Vue父组件内
data: {
total: 0
}
</script>

  此处$emit使用input作为特殊事件名,而父组件并没有使用@input="hander"监听,也没有对应的method中有hander这个函数来更新参数total。
  v-model:total 可以说就是一种语法糖。他就是相当于完成了以上的操作,自定义事件input接受子组件的请求,并且完成对应total的数据更新。一个组件上的v-model默认会利用名为value的prop和名为input的事件

v-model 双向数据绑定

其实现的功能就跟 在父组件内部中使用v-model 一样,会同步数据。需要满足两个要求:

  • 子函数 props 接受一个value属性
  • 有新的value时触发input事件
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

<p>总数:{{total}}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
<!-- 父组件更新,同步到子组件 -->
<script>
Vue.component('my-component',{
//父组件的变化会同步到子组件
props: ['value'],
template: '<input :value:"value" @input="updateValue">',
methods: {
//子组件内函数动态更新,每一次更新都触发emit,到父组件
updateValue: function (event) {
this.$emit('input',event.target.value)
}
}
})

var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleReduce: function () {
this.total--;
}
}
})
</script>

非父子组件通讯

BUS数据总线

利用中间vue,形成bus数据总线,一者往里扔数据,一直从里面取数据,使用如下。

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
<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
var bus = new Vue();

Vue.component('component-a', {
template: '<button @click="handleEvent">传递事件</button>',
methods: {
handleEvent: function () {
//通过bus把事件on-message
bus.$emit('on-message','来自组件component-a的内容')
}
}
});

vat app = new Vue({
el: '#app',
data: {
message: ''
},
mounted: function(){
var _this = this;
//在实例初始化时,监听来自bus实例的事件
bus.$on('on-message', function (msg){
_this.message = msg;
})
}
})
</script>

  BUS可以实现任何组件间的通讯,包括父子,兄弟,跨级。还可以添加data、methods、computed等选项,这都都是公用的。

父链 子链 根

在子组件中,使用this.$parent可以直接访问该组件的父实例和组件
在父组件中,使用this.$children可以访问他的所有的子组件
在子组件中,使用this.$root可以用访问他的根实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//this.$parent
methods:{
handleEvent: function (){
//访问到父链后,可以做任何操作,比如直接修改数据,但是不推荐这样操作,耦合性太高
this.$parent.message = '来自组件component-a的内容';
}
}
//this.$root
// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

子组件索引

在使用$children会返回全部子组件,另外Vue提供了子组件索引的方法来定位组件 ———— ref

1
2
3
4
5
6
7
8
9
10
//为子组件设定索引名称
<component-a ref="comA"></component-a>
...
methods: {
handleRef: function() {
//定位子组件
var msg = this.$refs.comA.message;
concole.log(msg);
}
}

依赖注入

  可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里

  provide选项允许我们指定我们想要提供给后代组件的数据/方法。
  在任何后代组件里,我们都可以使用inject`选项来接收指定的我们想要添加在这个实例上的属性

1
2
3
4
5
6
7
8
//父组件
provide: function () {
return {
getMap: this.getMap
}
}
//后代组件
inject: ['getMap']

slot(插槽)

  slot————内容分发,父组件向子组件分发HTML内容

  • 父组件需要分发的内容卸载子组件标签内
  • 子组件模板(template)中使用<slot>标签
  • 当父组件没有插入内容时,<slot>标签中的内容默认出现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="app">
<component-child>
<p>分发的内容</p>
<p>再来一个</p>
</component-child>
</div>
<script>
var bus = new Vue();

Vue.component('component-child', {
template: '\
<div>\
<slot>\
<p>如果父组件没有插入内容,我将作为默认出现<p>\
</slot>\
</div>\
'
});

vat app = new Vue({
el: '#app'
})
</script>

具名 slot

  具有name属性的slot,可以为slot识别分类插入至子组件模板的不同地方

  • 父组件中用slot="Name"作为标签的属性
  • 子组件中<slot name="Name">作为对应插槽位置
  • 具有name属性的slot,可以与普通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
<div id="app">
<component-child>
<h2 slot="header">标题</h2>
<p>分发的内容</p>
<p>再来一个</p>
</component-child>
</div>
<script>
var bus = new Vue();

Vue.component('component-child', {
template: '\
<div>\
<slot name="header"></slot>\
<slot>\
<p>如果父组件没有插入内容,我将作为默认出现<p>\
</slot>\
</div>\
'
});

vat app = new Vue({
el: '#app'
})
</script>

其他

$nextTick

  解决异步更新队列产生的不同步问题

  Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一时间循环中发生的所有数据改变。在缓冲时会取出重复数据,从而避免不必要的计算和DOM操作。然后在下一个时间循环tick中,Vue刷新队列并执行实际(已去重的)工作。

1
2
3
4
5
6
//更改DOM
this.$nextTick(function (){
//读取新更新的DOM,需要使用$nextTick
var text = document.getElementById('div').innerHTML;
console.log(text);
})

在动态组件上使用 keep-alive

  动态组件是用is特性来切换不同组件:

1
<component v-bind:is="currentTabComponent"></component>

  这种情况下每次切换组件都会创建一个新的currentTabComponent实例。不会记录实例状态,解决这个问题,比如保存页面状态,可以用<keep-alive>元素将动态组件包裹起来


单文件组件

  通常使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素。这种方式有很多缺点:

  • 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
  • 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的
  • 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
  • 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel

  文件扩展名为.vuesingle-file components(单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具。

.vue文件格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<p>{{greeting}}</p>
</template>
<script>
module.exports = {
data: function(){
return{
greeting: 'hello'
}
}
}
</script>

<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>