前言
为了提高编程效率,程序员们总是会将写过的代码进行抽象去业务化并进行抽离,这种“高内聚,低耦合”增加代码复用性的做法久而久之便形成了组件化的思想。组件化能提高开发效率,方便代码的重复使用,简化调试步骤,提升项目的可维护性,便于多人协同开发。而 Vue 中也同样可以用组件化的思想,Vue 的组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,仍以类型的应用界面都可以抽象为一个组件数。这篇文章便是总结了我在 Vue 中进行组件化时的技巧与想法。
v-model 数据双向绑定的实现
在开发中,我们经常用到 v-model 属性实现数据的双向绑定,那么在封装自定义组件时,我们如何实现 v-model 的数据双向绑定呢?数据的双向绑定顾名思义,就是当数据发生变化时,绑定的组件要跟着发生变化,反之亦然。这就说明如果一个 input 要实现这个功能,需要实现 :value
和 @input
。下面假设我们要封装一个自定义 input 组件 x-input:
// XInput.vue
<input :type="type" :value="value" @input="onInput">
props:{
type:{
type: String,
default: 'text'
},
value:{
type: String,
default: ''
}
},
methods:{
onInput(e){
this.$emit('input', e.target.value)
}
}
// XInput.vue
<input :type="type" :value="value" @input="onInput">
props:{
type:{
type: String,
default: 'text'
},
value:{
type: String,
default: ''
}
},
methods:{
onInput(e){
this.$emit('input', e.target.value)
}
}
这样在使用自定义组件时,使用 v-model 便可使数据双向绑定:
<x-input v-model="inputValue"></x-input>
data(){ return { inputValue: '' } }
<x-input v-model="inputValue"></x-input>
data(){ return { inputValue: '' } }
巧用listeners 二次封装组件
在平常的开发中,有许多贴近业务场景但大量重复出现的业务组件,这些组件又是基于现有组件库(比如 ElementUI)的。这种情况下就需要我们对组件库进行二次封装。组件库中的组件提供了很多属性和方法,在二次封装时如果使用 props 传递将会使代码变的臃肿而不优雅,这就需要用到 $attrs
、$listeners
了。$attrs
、$listeners
包含了父作用域中不作为 prop 被识别(且获取)的特性绑定(class 和 style 除外),当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定(class 和 style 除外),并且可以通过 v-bind="$attrs"
和 v-on="$listeners"
传入内部组件。
假设我们现在要对 ElementUI 中的 Input 组件进行二次封装,封装成新的 x-input:
// XInput.vue
<el-input v-bind="$attrs" v-on="$listeners" v-if="isShow"></el-input>
props:{ isShow:{ type: Boolean, default: true } }
// XInput.vue
<el-input v-bind="$attrs" v-on="$listeners" v-if="isShow"></el-input>
props:{ isShow:{ type: Boolean, default: true } }
我们假设 x-input 要提供一个我们自己的 is-show 属性,而 el-input 其余的属性和方法需要仍然可用。这里使用 $attrs
、$listeners
后,你会发现代码变的很优雅,不需要使用 props 将 el-input 原有的属性和方法,从 x-input 中一个一个的传递下去。我们不但可以使用自己封装的属性,还可以正常的使用 el-input 原有的属性和方法:
<x-input
placeholder="请输入密码"
v-model="input"
show-password
:is-show="false"
>
</x-input>
<x-input
placeholder="请输入密码"
v-model="input"
show-password
:is-show="false"
>
</x-input>
当使用我们二次封装的组件 x-input 时,查看组件的 DOM 会发现组件的根标签上有许多属性导致看起来很乱,这是因为 Vue 默认会将继承下来的特性会在根标签上展开显示。如果我们不想让它们显示在根标签上,可以使用 inheritAttrs
将其关闭:
// XInput.vue
<el-input v-bind="$attrs" v-on="$listeners" v-if="isShow"></el-input>
export default{ inheritAttrs: false, props:{ isShow:{ type: Boolean, default:
true } } }
// XInput.vue
<el-input v-bind="$attrs" v-on="$listeners" v-if="isShow"></el-input>
export default{ inheritAttrs: false, props:{ isShow:{ type: Boolean, default:
true } } }
provide 和 inject 的响应处理
在组件封装过程中,无可避免的会遇到数据的跨层级传递,官方为我们提供了 provide 和 inject 来实现数据的跨层级传递。这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。而官方最后有这样一段提示:
TIP提示
提示:provide
和 inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
那么我们如何让它们绑定变成可响应式呢?通过观察我们发现,官方有提到我们如果想实现绑定可响应,需要传入一个可监听的对象。我们当然可以实现一个响应式的对象来进行传递,但如果你嫌那样麻烦,可以传递当前组件实例,每个组件自身其实就是一个可监听的对象,我们可以通过传递组件实例自身来实现这个功能:
// ancestor.vue
export default {
provide() {
return {
msg: this, // 这里将当前组件实例传递出去
};
},
data() {
return {
value: "",
};
},
};
// ancestor.vue
export default {
provide() {
return {
msg: this, // 这里将当前组件实例传递出去
};
},
data() {
return {
value: "",
};
},
};
如果你传递了组件实例,那么这样可以接收数据和使用数据:
// child.vue
<p>{{msg.value}}</p>
export default{ inject: ["msg"] }
// child.vue
<p>{{msg.value}}</p>
export default{ inject: ["msg"] }
ElementUI 中的广播和派发
当我们在封装自定义组件时,事件的广播和派发一直是令人头疼的问题,因为这里如果处理不到位,将会造成组件之间耦合度很高通用性变差,而且代码的健壮性会大大降低。而 ElementUI 对此给出了一种很好的解决办法——混入广播方法和派发方法。我们来看一下是怎么实现的:
// src/mixins/emitter.js
/**
* 广播方法:从根组件向下广播事件(自上而下)
* componentName:组件名称
* eventName:事件名称
* params:参数数组
*/
function broadcast(componentName, eventName, params) {
//遍历所有子元素
this.$children.forEach((child) => {
var name = child.$options.componentName;
//如果子元素componentName和传入的相同则派发事件
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
//递归树形向下遍历
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
/**
* 冒泡(自下而上)查找componentName相同的组件并派发事件
* componentName:组件名称
* eventName:事件名称
* params:参数数组
*/
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
//向上查找直到找到相同名称的组件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
// 如果找到就派发事件
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
//这里保证广播方法的this指向
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
},
},
};
// src/mixins/emitter.js
/**
* 广播方法:从根组件向下广播事件(自上而下)
* componentName:组件名称
* eventName:事件名称
* params:参数数组
*/
function broadcast(componentName, eventName, params) {
//遍历所有子元素
this.$children.forEach((child) => {
var name = child.$options.componentName;
//如果子元素componentName和传入的相同则派发事件
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
//递归树形向下遍历
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
/**
* 冒泡(自下而上)查找componentName相同的组件并派发事件
* componentName:组件名称
* eventName:事件名称
* params:参数数组
*/
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
//向上查找直到找到相同名称的组件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
// 如果找到就派发事件
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
//这里保证广播方法的this指向
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
},
},
};
在组件内使用 mixins
混入 emitter.js 文件便可直接使用这两个方法进行事件的广播和派发。需要注意的是,使用时我们想要响应的组件必须设置 componentName
:
// Example.vue
export default {
componentName: "Example",
data() {
return {
example: "",
};
},
};
// Example.vue
export default {
componentName: "Example",
data() {
return {
example: "",
};
},
};
由于时间不足,先写到这里,今后会慢慢补充!