《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进行的,另外,子组件也可以通过 p a r e n t 访 问 父 级 上 的 信 息 , 也 可 以 通 过 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
}

a t t r s 和 attrs和 attrslisteners

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

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

这两个属性最大的用处就是封装第三方插件,比如element-ui,假设项目中的UI库是基于element-ui封装的,封装的过程中加入了大量的自定义属性,此时可以通过 a t t r s 和 attrs和 attrslisteners这两个属性进行将值都传递给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>

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

Vue组件封装复用是指将代码逻辑和功能封装在一个独立的组件中,并在需要的地方重复使用该组件的过程。 Vue组件封装可以通过以下步骤实现: 1. 创建组件:使用Vue框架提供的组件选项来创建一个组件。可以使用Vue.extend方法或者直接在Vue实例中定义一个组件。 2. 封装功能:在组件中添加业务逻辑、数据和方法等功能。可以通过计算属性、监听器、方法等实现具体的功能。 3. 编写模板:使用Vue的模板语法编写组件的结构和样式。通过将标签、属性和事件绑定到组件的数据和方法来实现交互效果。 4. 注册组件:将组件注册到Vue实例中,使其可以在其他组件中使用。可以使用Vue.component方法全局注册组件,也可以在局部组件中通过components选项注册组件。 5. 使用组件:在需要的地方使用组件,可以通过标签的方式将组件插入到页面中。 通过封装组件,可以将代码逻辑和UI元素进行有效地拆分和复用。例如,可以将页面中重复出现的按钮、表单、卡片等元素封装组件,通过复用组件来提高代码的可维护性和复用性。同时,组件化的思想也使得团队协作更加高效,不同开发者可以独立开发、测试和维护自己负责的组件,最终组合成完整的应用程序。 总之,Vue组件封装复用是一种有效的开发方式,可以提高代码的可维护性和可复用性,同时也促进了团队协作和开发效率的提升。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Oliver尹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值