《Vue》组件的设计和复用
前言
只要是项目,绕不开的就是组件的封装和复用,虽然现在有很多前端的UI库,比如Element等等,但是这些UI库很多是基于最小颗粒度做的组件,实际项目中往往需要将多个基础组件封装在一个大组件里,举个例子,标题栏,在后台管理系统中往往一个标题栏包含了:标题,返回按钮,有时还有会新增,或者用于切换的按钮组;
组件设计
思路
组件的封装不是一开始就可以随便封装的,因为组件的迭代会影响到之后的所有使用的页面的正常与否,因此,对于组件的封装需要仔细斟酌,不必要的组件封装有时候会成为后期维护的包袱;
因此,第一个页面开始的时候,可以不封装任何组件;当其中某一部分被重复使用了2次的时候,那么就可以开始考虑是否需要封装,当被重复了3次的时候,那么这个时候就必须考虑组件的封装了,因为一旦次数到达三次,那么往往就会认为4,5次也是肯定会被需要的;
插槽
插槽,也就是slot这个功能,在组件的封装中,插槽这个功能是非常之常用的,比如,现在有一个页面,其中顶部导航和尾部导航是固定的,只有中间的正文部分每个页面不一样,那么,就可以考虑封装成一个基础模版;
基础用法
模版:
<template>
<div>
<header>
<slot name="header">默认导航</slot>
</header>
<main>
<slot name="default">默认正文</slot>
</main>
<footer>
<slot name="footer">默认尾部</slot>
</footer>
</div>
</template>
<script>
export default {};
</script>
<style scoped lang='scss'>
</style>
模版使用
<template>
<div>
<s-index-layout>
//通过v-slot:header指定放入模版中名为header插槽
<template v-slot:header>头部</template>
<template #:default>hello world</template>
<template v-slot:footer>尾部</template>
</s-index-layout>
</div>
</template>
<script>
import SIndexLayout from "./SIndexLayout.vue";
export default {
components: {
SIndexLayout
}
};
</script>
<style scoped lang='scss'>
</style>
v-slot:header可以简写成#:header;
插槽作用域
在vue中插槽的内容使用的是当前的组件内的内容,比如
<template>
<div>
<s-index-layout>
<template v-slot:header>头部</template>
<template v-slot:default>{{name}}</template>
<template v-slot:footer>尾部</template>
</s-index-layout>
</div>
</template>
<script>
import SIndexLayout from "./SIndexLayout.vue";
export default {
data(){
return {
name:"oliver"
}
},
components: {
SIndexLayout
}
};
</script>
<style scoped lang='scss'>
</style>
假设插槽内有一个变量name,基础模版中也有一个变量name,那么插槽中的name对应的是当前组件中,而基础插槽中的name对应的则是基础插槽中的变量name,这两者不会混淆;
那么,问题来了,比如下例
<template>
<div>
<header>
<slot name="header">默认导航</slot>
</header>
<main>
<slot name="default" v-bind:user="user">{{user.name}}</slot>
</main>
<footer>
<slot name="footer">默认尾部</slot>
</footer>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: "oliver",
phone: "13382211234"
}
};
}
};
</script>
<style scoped lang='scss'>
</style>
默认显示的是名字这个变量,在某一个页面,要求显示的电话,但是这个电话需要使用的是基础组件内的数据,而不是外界定义的,因此在组件的设计之初就需要将数据暴露出去,比如上例中的v-bind:user=“user”,这个就是绑定了一个user属性,值是组件内部的变量user,之后在调用的地方可以获取到
<template v-slot:default="{user}">{{user.phone}}</template>
之后可以通过解构,将user这个属性获取到,再然后使用其值
小案例
题目:基础模版带异步请求,具体地址有父组件传入,请求完成后将值上传到父组件,由父组件决定显示什么内容
基础模版
<template>
<div>
<div v-if="loading">你好,加载中</div>
<main v-else>
<!-- data的值暴露给父组件 -->
<slot name="header" :data="data">默认</slot>
</main>
</div>
</template>
<script>
export default {
props: {
url: String
},
data() {
return {
loading: true,
data: {}
};
},
created() {
//异步的地址由props传入
console.log(this.url);
//发起异步请求
setTimeout(() => {
this.data = { name: "lilei" };
this.loading = false;
}, 1000);
}
};
</script>
<style scoped lang='scss'>
</style>
父组件使用
<template>
<div>
<baseLayout :url="url">
<template v-slot:header="{data}">{{data.name}}</template>
</baseLayout>
</div>
</template>
<script>
import baseLayout from "./baseLayout.vue";
export default {
data() {
return {
url: "www.xxx.com/api/xxx"
};
},
components: {
baseLayout
}
};
</script>
<style scoped lang='scss'>
</style>
小结
页面模块的划分已重复使用为标准,只有当重复度高的组件模块需要封装成组件,在组件的封装中,往往使用到了slot插槽的功能,将不变的,基础的功能写在插槽内部,将可变的部分通过插槽的方式暴露给父组件;
另外插槽内部的数据可以通过v-bind暴露给父组件,在父组件中通过v-slot加解构接收;
组件通信
当组件嵌套组件的时候,不可避免的就遇到了组件的通信,尤其是跨层级通信;
组件的跨层级访问
向父级元素传递消息,是通过emit进行的,另外,子组件也可以通过root访问根组件上的信息
$parent
<template>
<div>
<div v-if="loading">你好,加载中</div>
<main v-else>
<!-- data的值暴露给父组件 -->
<slot name="header" :data="data">默认</slot>
</main>
</div>
</template>
<script>
export default {
props: {
url: String
},
data() {
return {
loading: true,
data: {}
};
},
created() {
//异步的地址由props传入
console.log(`这是props获取的值:${this.url}`);
console.log("------------");
console.log(`这是$parent获取的:${this.$parent.url}`);
}
};
</script>
<style scoped lang='scss'>
</style>
另外$parent不仅仅可以访问父级上的属性,也可以调用父级上的方法,这样大大减少了属性传递的速度
$root
root用法和parent几乎一致,区别在与parent访问的对象是父级元素,而root访问的对象是根元素
依赖注入
provide和inject,这是一对属性,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效;
使用
祖先组件
在祖先组件里定义了一个provide,里面定义了许许多多的值
export default {
data() {
return {
url: "www.xxx.com/api/xxx"
};
},
//注入一个namename属性
provide(){
return {
namename:this.url
//...
}
}
};
子孙组件
子孙组件里定义inject,值是一个数组,数组中的每一项就是祖先组件上provide里的某个值,一旦定以后,就可以直接通过this获取到对于的值了
export default {
data() {
return {
user: {
name: "oliver",
phone: "13382211234"
}
};
},
inject:["namename"],
methods:{
say(){
console.log(this.namename)
}
}
};
这里的this.namename的值就是www.xxx.com/api/xxx
原理
归根结底,在vue中的provide/indject是对$parent的优化和封装,在源码中(路径:vue/src/core/instance/inject.js),有这么一段代码,它会变量父节点上是否有provided这个属性,如果没有继续往父节点上遍历已达到寻找属性的结果
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
listeners
vue在2.4.0版本新增的两个属性,通常父组件与子组件如果要传递消息,那么就是props和emit,那么真的是非常的繁琐;
因此,在2.4版本之后,vue官方新增了这两个属性,简单的可以将其是作为属性和方法的集合,:
- $attrs:是一个对象,简单点讲就是包含了所有父组件在子组件上设置的属性(除了prop传递的属性、class 和 style );
- $listners:是一个对象,简单点讲就是包含了父组件在子组件上设置的所有方法;
这两个属性最大的用处就是封装第三方插件,比如element-ui,假设项目中的UI库是基于element-ui封装的,封装的过程中加入了大量的自定义属性,此时可以通过listeners这两个属性进行将值都传递给element-ui;
示例:
//改写前
<el-input v-model="value" @input="$emit('input',value)" @blur="blur"></el-input>
//改写后
<el-input v-bind="$attrs" v-on="$listeners"></el-input>
父组件
<ts-input :inputType="type" v-model="value" @blur="onBlur"></ts-input>
比如封装的ts-input,在父组件调用这个封装后的输入框的后,在其父组件上绑定的inputType属性,value属性,blur事件等等,通通都会直接传递给el-input,完全不需要写props,$emit的过程;
组件的复用
mixin
简介
mixin,用于函数的复用,和vue的组件,指令等类似,vue中的mixin也分全局注册和局部注册,全局注册的mixin会影响所有注册的实例(也就是所有单文件组件内都具有混入的函数),局部注册的mixin则是只会在当前组件内混入功能函数;
需要注意的是,在vue中,钩子函数假如混入了,则会依次执行(先调用mixin中的钩子函数,再调用组件内部的钩子函数),普通的methods中的函数如果重名了,则会生效组件内部的methods,mixin中的重名函数会被覆盖掉;
使用
新建一个js,或者.vue文件,在其内部定义一个methods对象,其内定义了需要被混入的函数
export default {
methods: {
validate() {
console.log("mixinV")
return false;
}
}
}
需要混入的vue文件内,引入文件后,使用mixins属性,混入
import validateMixin from "./mixinV.js";
export default {
name: "tsInput",
mixins: [validateMixin],
data() {
return {
value: ""
};
},
methods: {
blur() {
//这样就可以直接调用mixinV内的函数了
if (this.validate()) {
this.$emit("blur");
}
}
}
};
</script>
小结
打破了组件的封装性,增加了组件的复杂度,现阶段使用mixin大部分是对js的逻辑复用
插件-Vue.use()
Vue提供了一个use()函数,用于对插件的安装,比如:
Vue.use(VueX);
Vue.use(VueRouter);
Vue.use(ElementUI);
如果要使用自定义的插件,那么该插件必须提供一个install函数,这个install函数是给Vue识别安装的,下面看个具体的例子:
假如现在对el-input进行了2次封装,名为ts-input,那么在该文件夹下,需要提供了一个js文件,里面有install函数
//引入自定义二次封装的输入框组件
import TsInput from "./ts-input.vue";
//添加一个install函数
TsInput.install = function(Vue){
//注册全局组件
Vue.component(TsInput.name,TsInput)
}
//导出
export default TsInput;
之后,在main.js文件里引入,并使用Vue.use()安装
import Vue from 'vue'
import App from './App.vue'
import 'element-ui/lib/theme-chalk/index.css';
//引入自定义组件
import TsInput from "./views/input";
//安装插件
Vue.use(TsInput);
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
HOC高阶组件
简介
在react社区使用的比较多,通俗的讲,就是函数接收一个组件作为参数,并返回一个新的组件,可复用的逻辑包含在函数中实现;
使用
高阶组件的函数,接收了一个组件component作为参数,之后
const ValidateHoc = Component => ({
name: `hoc-${Component.name}`,
props: ["rules"],
data() {
return {
errMsg: "",
value: ""
};
},
methods: {
validate(value) {
this.value = value;
let validate = this.rules.reduce((pre, cur) => {
let check = cur && cur.test && cur.test(this.value);
this.errMsg = check ? "" : cur.message;
return pre && check;
}, true);
return validate;
}
},
render() {
console.log(this.value);
return (
<div>
<Component on-blur={this.validate} initValue={this.value} />
{this.errMsg || ""}
</div>
);
}
});
export default ValidateHoc;
基础组件
<template>
<input type="text" @blur="$emit('blur', value)" v-model="value">
</template>
<script>
export default {
name: "TsInput",
props: ["initValue"],
data() {
return {
value: this.initValue
};
}
};
</script>
组合
import CustomInput from "./views/hocInput/input.vue";
import ValidateHoc from "./views/hocInput/hoc.js";
const ValidateInput = ValidateHoc(CustomInput);
组合后的ValidateInput就是最终的输入框了,可以直接在components中注册ValidateInput;
小结
高阶组件做到了可以复用模版,不仅仅是js函数,但是缺点也很明显,就是复杂度高,尤其是高阶嵌套高阶的时候,复杂度直接上升,因此官方不推荐使用HOC的方式做组件
Renderless(推荐)
简介
官方推荐的是该模式,rednerless,这种模式是将可复用的逻辑沉淀在包含slot插槽的组件中的,接口由插槽prop暴露
使用
校验模版组件
<template>
<div>
<!--通过插槽将validate暴露出去-->
<slot :validate="validate"></slot>
{{errMsg}}
</div>
</template>
<script>
export default {
data() {
return {
errMsg: ""
};
},
props: {
value: String,
rules: Array
},
methods: {
validate() {
//对校验数组中的所有项进行执行
let validate = this.rules.reduce((prev, cur) => {
//总的是否是true,当前项是否存在,当前项是否存在test这个属性,执行当前项的test函数
let check = prev && cur && cur.test && cur.test(this.value);
//假如是真那么就是空字符串,假如是假那么就将message赋值给errMsg
this.errMsg = check ? "" : cur.message;
return prev && check;
}, true);
return validate;
}
}
};
</script>
<style scoped lang='scss'>
</style>
使用组件
<template>
<div>
<ts-input v-slot:default="{validate}" :value="value" :rules="rules">
<input type="text" @blur="validate" v-model="value">
</ts-input>
</div>
</template>
<script>
import TsInput from "./input.vue";
export default {
components: {
TsInput
},
data() {
return {
rules: [
{
test(value) {
console.log(value);
return /^\d+$/g.test(value);
},
message: "请输入一个数字"
}
],
value: ""
};
}
};
</script>
<style scoped lang='scss'>
</style>
这样就达成了对模版和校验函数的复用