欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Vue》组件的设计和复用

程序员文章站 2022-05-26 11:12:20
...

前言

只要是项目,绕不开的就是组件的封装和复用,虽然现在有很多前端的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进行的,另外,子组件也可以通过parent访parent访问父级上的信息,也可以通过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
}

attrsattrs和listeners

vue在2.4.0版本新增的两个属性,通常父组件与子组件如果要传递消息,那么就是props和emit56propsemit,但是如果是自定义组件,那么往往一个组件上5,6个甚至更多的属性或方法,那么如果每一个都要写props和emit,那么真的是非常的繁琐;
因此,在2.4版本之后,vue官方新增了这两个属性,简单的可以将其是作为属性和方法的集合,:

  • $attrs:是一个对象,简单点讲就是包含了所有父组件在子组件上设置的属性(除了prop传递的属性、class 和 style );
  • $listners:是一个对象,简单点讲就是包含了父组件在子组件上设置的所有方法;

这两个属性最大的用处就是封装第三方插件,比如element-ui,假设项目中的UI库是基于element-ui封装的,封装的过程中加入了大量的自定义属性,此时可以通过attrsattrs和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>

这样就达成了对模版和校验函数的复用