# 一、vue中的组件
# 写在前面
vue.js 最精髓的,正是它的组件与组件化,写一个vue工程,也正是写一个个的组件。
业务场景千变万化,而不变的是vue.js组件的核心思想使用技巧,掌握了vue.js组件的各种开发模式,再复杂的场景也可以应对。
# 组件的分类
一般来说,vue.js 组件主要分为三类:
由 vue-router 产生的每个页面,本质上也是一个组件 (.vue),主要承载当前页面的HTML结构,包含数据的获取、数据整理、数据可视化等常规业务。整个文件相对较大,但是一般不会有
props
选项和自定义事件
,因为它作为路由的渲染,不会被复用,因此也不会对外提供接口。在项目开发中,我们写的大部分代码都是这类组件(页面),协同开发时候,每个人维护自己的页面,很少有交集。这类组件相对好些,因为主要是还原设计稿,完成需求,不需要太多模块和架构设计上的考虑。不包含业务,独立、具体功能的基础组件,比如、模态框等。这类组件作为项目的基础控件,会被大量使用,因此组价你的API进行过高强度的抽象,可以通过不同的配置实现不同的功能。独立组件的开发难度要高于第一类组件,因为它的侧重点是API的设计、兼容性、性能,以及复杂的功能。 这类组件对JavaScript的编程能力有一定的要求,也会包含非常多的技巧,比如在不依赖 Vuex 和 Bus(因为独立组件,无法依赖其他库)的情况下,各组件的通信、还会涉及到很多其他问题。
业务组件。它不像第二类独立组件只包含某个功能,而是在业务中被多个页面复用的,它与独立组件的区别是,业务组件只在当前项目中会用到,不具有通用性,而且会包含一些业务,比如数据请求;而独立组件不含业务,在任何项目中都可以使用,功能单一,比如一个具有数据校验功能的输入框。业务组件更像是介于第一类和第二类之间,在开发上也与独立组件类似,但寄托于项目,你可以使用项目中的技术栈,比如 Vuex、axios、echarts 等,所以它的开发难度相对独立组件要容易点,但也有必要考虑组件的可维护性和复用性。
# 二、3个API: prop、event、slot
# 组件的构成
一个再复杂的组件,都是由三个部分组成:prop、event、slot,它们构成了vue.js的组价的API,如果你开发的是一个通用组件,那一定要事先设计好这三部分,因为组件一旦发布,后面再修改 API 就很困难了,使用者都是希望不断新增功能,修复 bug,而不是经常变更接口。如果你阅读别人写的组件,也可以从这三个部分展开,它们可以帮助你快速了解一个组件的所有功能。
# 属性prop
prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来确定的。写通用组件的时候,props最好使用对象的写法,这样可以针对每个属性设置类型、默认值或者自定义校验属性的值,这点在组件开发中很重要,如果我们使用props的数组用法,这样的组件往往不够严谨,比如我们封装一个自己的按钮组件 <i-button>
<template>
<button :class="'i-button-size' + size" :disabled="disabled"></button>
</template>
<script>
// 判断参数是否是其中之一
function oneOf (value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) {
return true;
}
}
return false;
}
export default {
props: {
size: {
validator (value) {
return oneOf(value, ['small', 'large', 'default']);
},
default: 'default'
},
disabled: {
type: Boolean,
default: false
}
}
}
</script>
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
使用组件:
<i-button size="large"></i-button>
<i-button disabled></i-button>
2
让我们分析下上面的组件,组件中我们定义了两个属性:尺寸size和是否禁用disabled。其中size使用 validator
进行了值的自定义验证,也就是说,从父级传入的size,它的值必须是指定的 small、large、default 中的一个,默认值是default,如果传入这三个以外的值,都会抛出一条警告。
要注意的是,组件里定义的props,都是单向数据流,也就是只能通过父级修改,组件自己不能修改props的值,只能修改定义在data里面的数据,非要修改,也是通过后面介绍的自定义事件通知父级,由父级来修改。
在使用组件的时候,也可以传入一些标准的html特性,比如 id class
<i-button id="btn1" class="btn-submit"></i-button>
这样的html特性,在组件内的 <button>
元素上会继承,并不需要在props里面再定义一遍,这个特性是默认支持的,如果不期望开启,在组件选项里面配置inheritAttrs: false
就可以禁用了。
# 属性prop
如果要给上面的按钮组件 <i-button>
添加一些文字内容,就要用到组件的第二个API: 插槽 slot,它可以分发组件的内容,比如在上面的按钮组件中定义一个插槽:
<template>
<button :class="'i-button-size' + size" :disabled="disabled">
<slot></slot>
</button>
</template>
2
3
4
5
这里的 <slot>
节点就是指定的一个插槽的位置,这样在组件内部就可以扩展内容了:
<i-button>按钮 1</i-button>
<i-button>
<strong>按钮 2</strong>
</i-button>
2
3
4
当需要多个插槽时候,会用到具名的slot,比如上面的组件我们再添加一个slot,用于设置另一个图标组件:
<template>
<button :class="'i-button-size' + size" :disabled="disabled">
<slot name="icon"></slot>
<slot></slot>
</button>
</template>
2
3
4
5
6
<i-button>
<i-icon slot="icon" type="checkmark"></i-icon>
按钮 1
</i-button>
2
3
4
这样,父级内定义的内容,就会出现在组件对应的slot里面,没有写名字的,就是默认的slot。
在组件的 <slot>
里也可以写一些默认的内容,这样在父级没有写任何slot时候,它们就会出现,比如:
<slot>提交</slot>
# 自定义事件 event
现在我们给组件<i-button>
添加一个点击事件,目前有两种写法,我们先看自定义事件event:
<template>
<button @click="handleClick">
<slot></slot>
</button>
</template>
<script>
export default {
methods: {
handleClick (event) {
this.$emit('on-click', event);
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
通过 $emit
,就可以出发自定义事件 on-click
, 在父级通过@on-click
来监听。
<i-button @on-click="handleClick"></i-button>
上面的click事件,是在组件内部的 <button>
元素上声明的,这里还有另一种方法,直接在父级声明,但是为了区分原生事件和自定义事件,要用到事件修饰符.native
,所以上面的示例也可以这样写
<i-button @click.native="handleClick"></i-button>
如果不写 .native
修饰符,那上面的 @click
就是自定义事件 click,而非原生事件click, 但是我们在组件中只触发了 on-click
事件,而不是 click
,所以直接写 @click
会监听不到。
# 组件的通信
一般来说,组件可以有以下几种关系
A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)。组件间经常会通信,Vue.js 内置的通信手段一般有两种:
ref
: 给元素或者组件注册引用信息;$parent
/$children
: 访问父 / 子实例。
这两种都是直接得到组件实例,使用后可以直观调用组件的方法或者访问数据,比如下面的示例中,用ref来访问组件:(部分代码省略)
// component-a
export default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<component-a ref="comA"></component-a>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // Vue.js
comA.sayHello(); // 弹窗
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
$parent
和 $children
类似,也是基于当前上下文访问父组件或全部子组件的。
这两种方法的弊端是,无法在跨级或兄弟间通信,比如下面的结构:
// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>
2
3
4
我们想在 component-a 中,访问到引用它的页面中(这里就是 parent.vue)的两个 component-b 组件,那这种情况下,就得配置额外的插件或者工具了,比如 Vuex 和Bus的解决方案,不过他们都是依赖第三方插件的存在,这在开发独立组件的时候是不可取的,后续我们会介绍一些其他的黑科技,用于解决这种问题。
# 三、组件的通信1: provide / inject
我们在上面已经提到 ref 和 $parent / $children 在跨级通信的时候是有弊端的。当组件A和组件B中间隔了数代(甚至不确定具体级别)时候,以往会借助Vuex或者Bus 这样的方案,不得不引入第三方的库来支持。其实 Vue 内置了provide / inject接口。
# 什么是 provide / inject
provide / inject 是vue2.2.0版本后新增的API。
在vue的官方文档中也提到
provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
虽然官方不建议,但是用好了也是非常有用的。
我们先来看一下这个API怎么使用,假设有两个组件: A.vue 和 B.vue B 是 A的子组件。
// A.vue
export default {
provide: {
name: 'Aresn'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // Aresn
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到 在A.vue里面,我们设置了一个 provide:name,值为Aresn, 它的作用就是将name这个变量提供给它的所有子组件。而在B.vue中通过inject注入了A组件中提供的name变量,那么在组件B中,就可以通过 this.name 访问这个变量了,它的值也是Aresn,这就是provide / inject API 最核心的用法。
需要注意的是:
provide 和 inject 绑定并不是可响应的。这也是刻意为之,然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
所以,上面A.vue的name如果改变了,B.vue 的this.name 是不会改变的,仍然是 Aresn.
# 替代Vuex
我们知道,在做vue大型项目的时候,可以使用vuex做状态管理,它是一个专为vue.js开发的状态管理模式,用于集中式存储管理应用的所有组件的状态,并以响应的规则保证状态以一种可以预测的发生变化。
我们在了解了 provide / inject 的用法,下面来看看如何替代vuex,当然,我们的目的并不是为了替代vuex,它还是有相当大的用处,这里只是介绍另一种可能性。
使用vuex,最最主要的目的是跨组件通信、全局数据维护、多人协同开发。需求比如有:用户的登录信息维护、通知信息维护等全局的状态和数据。
一般在webpack中使用vue.js,都会有一个入口文件 main.js 里面通常导入了Vue、VueRouter等库,通常也会导入一个入口组件 app.vue 作为根组件。一个简单的app.vue可能有如下代码:
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
}
</script>
2
3
4
5
6
7
8
9
10
使用 provide / inject 替代 Vuex, 就是在这个app.vue文件上做文章。
我们把app.vue理解为一个最外层的根组件,用来存储所有需要的全部数据和状态,甚至是计算属性,方法。因为你的项目中所有的组件(包含路由)。它的父组件都是根组件,所以我们把整个app.vue实例通过 provide 对外提供。
app.vue
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
provide () {
return {
app: this
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
上面,我们把整个app.vue的实例 this 对外提供。命名为 app(这个名字可以自定义,推荐使用app,使用这个名字以后,子组件不能再使用它作为局部属性)。接下来,任何组件(或路由)只要通过 inject 注入 app.vue的app的话,都可以直接通过 this.app.xxx 来访问app.vue中的data、computed、method等内容。
app.vue 是整个项目第一个被渲染的组件,而且只会被渲染一次(即使切换路由,app.vue也不会被再次渲染),利用这个特性,很适合做一次全局状态数据管理,例如:我们将用户的登录信息保存起来:
app.vue 部分代码省略
<script>
export default {
provide () {
return {
app: this
}
},
data () {
return {
userInfo: null
}
},
methods: {
getUserInfo () {
// 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
$.ajax('/user/info', (data) => {
this.userInfo = data;
});
}
},
mounted () {
this.getUserInfo();
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这样,任何页面或者组件,只要通过 inject 注入app后,就可以直接访问 userInfo的数据了,比如:
<template>
<div>
{{ app.userInfo }}
</div>
</template>
<script>
export default {
inject: ['app']
}
</script>
2
3
4
5
6
7
8
9
10
是不是很简单呢? 除了直接使用数据,还可以调用方法。比如在某个页面里,修改了个人资料,这时一开始在app.vue
里面获取的userInfo
已经不是最新的了,需要重新获取,可以这样使用:
某个页面:
<template>
<div>
{{ app.userInfo }}
</div>
</template>
<script>
export default {
inject: ['app'],
methods: {
changeUserInfo () {
// 这里修改完用户数据后,通知 app.vue 更新,以下为伪代码
$.ajax('/user/update', () => {
// 直接通过 this.app 就可以调用 app.vue 里的方法
this.app.getUserInfo();
})
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
同样非常简单。只要理解了 this.app
是直接获取整个 app.vue
的实例后,使用起来就得心应手了。想一想,配置复杂的 Vuex 的全部功能,现在是不是都可以通过 provide / inject
来实现了呢?
# 进阶技巧
如果你的项目足够复杂,或者需要多个协同开发,在 app.vue
里会写非常多的代码,多到结构复杂难以维护,这个时候使用Vue.js的混合 mixins,将不同的逻辑分开到不同的js文件里面。
比如上面的用户信息,就可以放到混合里面:
user.js
export default {
data() {
return {
userInfo: null
}
},
methods:{
getUserInfo() {
// 这里通过ajax获取用户信息后,赋值给this.userInfo 以下为伪代码
$.ajax('/user/info', (data)=>{
this.userInfo = data;
})
}
},
mounted() {
this.getUserInfo();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后在app.vue中混合:
app.vue
<script>
import mixins_user from '../mixins/user.js';
export default {
mixins: [mixins_user],
data () {
return {
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
这样,和用户信息相关的逻辑,都可以在 user.js 里面维护,或者由个人来维护,app.vue 也就很容易维护了。
# 独立组件中使用
只要一个组件使用了 provide 向下提供了数据,那其它所有的子组件都可以通过 inject 来注入,不管中间隔了多少代,而且可以注入来自多个不同父级提供的数据,需要注意的是,一旦注入某个数据,比如上面示例中的app,那这个组件中就不能再声明 app 这个数据了,因为它已经被父级占有了。
独立组件中使用provide/inject的场景,主要是具有联动关系的组件,比如接下来很快会介绍一个实战:具有数据校验功能的表单组件 Form 它其实是两个组件 一个是 Form 一个是 FormItem, FormItem 是 Form 的子组件,它会依赖 Form 组件上的一些特性 props,所以就需要得到父组件Form,这在vue.js 2.2.0 版本以前 是没有 provide/inject 这对API的,而 Form 和 FormItem 不一定是父子关系,中间还可能隔了其他组件,所以不能单纯使用 $parent 来向上获取实例,在vue.js 2.2.0 之前,一种比较可行的方案是使用计算属性动态获取:
computed: {
form () {
let parent = this.$parent;
while (parent.$options.name !== 'Form') {
parent = parent.$parent;
}
return parent;
}
}
2
3
4
5
6
7
8
9
每个组件都可以设置 name 选项,作为组件名的标识,利用这个特点,通过向上遍历,直到找到需要的组件。这个方法可行,但相比一个 inject 来说,太费劲了,而且不那么优雅和 native。如果用 inject,可能只需要一行代码:
export default {
inject: ['form']
}
2
3
不过,这一切的前提是你使用 Vue.js 2.2.0 以上版本。