业务组件如何优雅的包装(yang)第三方(Vue)组件--《前端那些事》

简介: 让全天下的组件为我所用,快点在你的项目中试一试吧。(๑•̀ㅂ•́)و✧

场景

回想一下,我们写过的那些业务代码,有遇到过以下场景吗?

  • 业务代码中重复出现类似的使用第三方组件的代码块
  • 业务代码中很多组件其实只是简单的包装或者组合了第三方组件
  • 期望封装第三方组件作为业务组件方便重用。
  • 期望封装了第三方组件的业务组件能够完全兼备第三方组件的特性,而非需要一个一个地搬运。

例如查询列表中的筛选项,相信业务中很多地方都存在着类似的代码片段。

<el-form-item label="手机号码" prop="mobile">
    <el-input v-model="form.mobile" size="mini" clearable maxlength="20" />
</el-form-item>

那么我们应该如何将这些重复的代码片段封装起来,成为一个好用的业务组件呢?

关键

在经历了很多项目实践以后,我们发现,要想将第三方组件封装为好用的业务组件,关键是需要通过以下方式来进行封装:

  • 通过渲染函数(render)来创建第三方组件。
  • 透传数据对象来搬运第三方组件的特性。

    • 属性(props)
    • 事件(on)
    • 插槽(slot)
  • 使用作用域样式(scoped)来覆盖第三方组件的样式。

示例

下面以 Element UI 作为第三方组件,封装一个表单筛选项的业务组件作为示例,方便大家理解。建议后续业务组件我们都可以按照类似的思路来进行封装。

  • 封装业务组件: lib/components/form-item/mobile.vue

    <script>
    import Vue from 'vue';
    
    /**
     * 获取前缀为 `prefix` 的 slot 值并创建 slot 元素
     * 
     * @param {string} prefix 
     * @param {*} slots 
     * @param {*} h createElement
     * @returns 
     */
    function createSlotElements(prefix, slots, h) {
        return getValues(prefix, slots).map(function(item) {
            return h('template', {
                slot: item.name
            }, [item.value]);
        });
    }
    
    /**
     * 获取前缀为 `item-` 的 slot 值并创建 slot 元素
     * 
     * @param {*} slots 
     * @param {*} h createElement
     * @returns 
     */
    function createItemSlotElements(slots, h) {
        return createSlotElements('item-', slots, h);
    }
    
    /**
     * 获取前缀为 `field-` 的 slot 值并创建 slot 元素
     * 
     * @param {*} slots 
     * @param {*} h createElement
     * @returns 
     */
    function createFieldSlotElements(slots, h) {
        return createSlotElements('field-', slots, h);
    }
    
    /**
     * 获取前缀为 `prefix` 的属性值
     * 
     * @param {string} prefix 
     * @param {*} object 
     * @returns [{name, value, originName, prefix}]
     */
    function getValues(prefix, object) {
        return Object.entries(object).map(function([name, value]) {
            return {
                name: name.replace(prefix, ''),
                value,
                originName: name,
                prefix
            };
        });
    }
    
    /**
     * 表单项: 手机号码
     */
    export default Vue.extend({
        props: {
            /**
             * 表单元素 v-model 绑定的对象
             */
            model: {
                type: Object,
                required: true
            },
            /**
             * el-form-item 的 prop
             */
            prop: {
                type: String,
                default: 'mobile'
            },
    
            /**
             * 透传 el-form-item 的属性
             */
            itemProps: {
                type: Object
            },
            /**
             * 透传 el-form-item 的事件
             */
            itemOn: {
                type: Object
            },
            /**
             * 透传 el-form-item 的 native 事件
             */
            itemNativeOn: {
                type: Object
            },
    
            /**
             * 透传表单元素的属性
             */
            fieldProps: {
                type: Object
            },
            /**
             * 透传表单元素的事件
             */
            fieldOn: {
                type: Object
            },
            /**
             * 透传表单元素的 native 事件
             */
            fieldNativeOn: {
                type: Object
            },
        },
        methods: {
            /**
             * 创建 el-form-item 元素
             * 
             * @param {*} fieldElement 表单元素
             * @param {*} h createElement
             * @returns 
             */
            createItemElement(fieldElement, h) {
                return h('el-form-item', {
                    props: {
                        label: '手机号码',
                        prop: this.prop,
                        ...this.itemProps
                    },
                    on: {
                        ...this.itemOn
                    },
                    nativeOn: {
                        ...this.itemNativeOn
                    }
                }, [
                    ...createItemSlotElements(this.$slots, h),
                    fieldElement
                ]);
            },
            /**
             * 创建表单元素
             * 
             * @param {*} h createElement
             * @returns 
             */
            createFieldElement(h) {
                const self = this;
    
                return h('el-input', {
                    attrs: {
                        maxlength: 20,
                        placeholder: '请输入'
                    },
                    props: {
                        size: 'mini',
                        clearable: true,
                        value: self.model[self.prop],
                        ...self.fieldProps
                    },
                    on: {
                        input(value) {
                            self.model[self.prop] = value;
                        },
                        ...self.fieldOn
                    },
                    nativeOn: {
                        ...self.fieldNativeOn
                    }
                }, [
                    ...createFieldSlotElements(self.$slots, h)
                ]);
            }
        },
        render(h) {
            const fieldElement = this.createFieldElement(h);
            return this.createItemElement(fieldElement, h);
        }
    });
    </script>
    <style scoped>
    /deep/.el-form-item__label {
        color: red;
    }
    </style>
  • 使用业务组件

    <template>
    <div>
        <el-form :model="form">
            <Mobile
                :model="form"
                :itemNativeOn="{
                    click: testClick
                }"
                :fieldOn="{
                    focus: testFocus
                }"
            >
                <!-- 通过 item- 前缀来区分插槽, 例如 item-label, 即设置 el-form-item 的 label 插槽 -->
                <template #item-label>
                    <span>el-form-item label slot</span>
                </template>
                <!-- 通过 field- 前缀来区分插槽, 例如 field-prepend, 即设置 el-input 的 prepend 插槽 -->
                <template #field-prepend>
                    <span>el-input prepend slot</span>
                </template>
            </Mobile>
        </el-form>
    </div>
    </template>
    <script>
    import Mobile from '@/lib/components/form-item/mobile.vue';
    
    export default {
        components: {
            Mobile,
        },
        data() {
            return {
                form: {
                    mobile: ''
                }
            };
        },
        methods: {
            testClick(event) {
                console.log('click', event);
            },
            testFocus(event) {
                console.log('focus', event);
            }
        }
    };
    </script>
    <style scoped>
    </style>

细节

针对上述示例,再结合我们实际的项目经验,建议在封装的过程中注意以下细节:

  • 建议将第三方组件的属性放置在一个对外的 props 上,例如:itemProps,不要做过多的封装,减少学习成本。
  • 事件也以类似的方式来处理,例如:itemOnitemNativeOn
  • 当组合了多个第三方组件时,建议通过前缀来区分各组件的插槽,例如: item-field-,便于兼备第三方组件的所有插槽。
  • 通过深度作用选择器(/deep/)来直接覆盖第三方组件的样式。
  • 渲染函数中没有与 v-model 的直接对应——你必须自己实现相应的逻辑。
  • 对比 templaterender模版机制的局限性是 v-on 无法透传原生事件,只能一个一个地搬运

    <template>
    <el-form-item
        v-bind="{
            label: '手机号码',
            prop: prop,
            ...itemProps
        }"
        v-on="itemOn"
    >
        <!-- 遍历 el-form-item 的 slot -->
        <template v-for="item in itemSlots" v-slot:[item.name]>
            <slot :name="item.originName"></slot>
        </template>
        <!-- 表单元素 -->
        <el-input
            v-model="model[prop]"
            v-bind="{
                maxlength: '20',
                placeholder: '请输入',
                size: 'mini',
                clearable: true,
                ...fieldProps
            }"
            v-on="fieldOn"
        >
            <!-- 遍历表单元素的 slot -->
            <template v-for="field in fieldSlots" v-slot:[field.name]>
                <slot :name="field.originName"></slot>
            </template>
        </el-input>
    </el-form-item>
    </template>
    <script>
    import Vue from 'vue';
    
    /**
     * 获取前缀为 `prefix` 的属性值
     * 
     * @param {string} prefix 
     * @param {*} object 
     * @returns [{name, value, originName, prefix}]
     */
    function getValues(prefix, object) {
        return Object.entries(object).map(function([name, value]) {
            return {
                name: name.replace(prefix, ''),
                value,
                originName: name,
                prefix
            };
        });
    }
    
    /**
     * 表单项: 手机号码
     */
    export default Vue.extend({
        props: {
            /**
             * 表单元素 v-model 绑定的对象
             */
            model: {
                type: Object,
                required: true
            },
            /**
             * el-form-item 的 prop
             */
            prop: {
                type: String,
                default: 'mobile'
            },
    
            /**
             * 透传 el-form-item 的属性
             */
            itemProps: {
                type: Object
            },
            /**
             * 透传 el-form-item 的事件
             */
            itemOn: {
                type: Object
            },
    
            /**
             * 透传表单元素的属性
             */
            fieldProps: {
                type: Object
            },
            /**
             * 透传表单元素的事件
             */
            fieldOn: {
                type: Object
            },
        },
        computed: {
            itemSlots() {
                return getValues('item-', this.$slots);
            },
            fieldSlots() {
                return getValues('field-', this.$slots);
            }
        }
    });
    </script>
    <style scoped>
    /deep/.el-form-item__label {
        color: red;
    }
    </style>

行动

现在大家知道应该如何让全天下的组件为我所用了吧,快点在你的项目中试一试吧。(๑•̀ㅂ•́)و✧

参考

目录
相关文章
|
6月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
640 1
|
8月前
|
前端开发 API 开发者
harmonyOS基础- 快速弄懂HarmonyOS ArkTs基础组件、布局容器(前端视角篇)
本文由黑臂麒麟(6年前端经验)撰写,介绍ArkTS开发中的常用基础组件与布局组件。基础组件包括Text、Image、Button等,支持样式设置如字体颜色、大小和加粗等,并可通过Resource资源引用统一管理样式。布局组件涵盖Column、Row、List、Grid和Tabs等,支持灵活的主轴与交叉轴对齐方式、分割线设置及滚动事件监听。同时,Tabs组件可实现自定义样式与页签切换功能。内容结合代码示例,适合初学者快速上手ArkTS开发。参考华为开发者联盟官网基础课程。
731 75
harmonyOS基础- 快速弄懂HarmonyOS ArkTs基础组件、布局容器(前端视角篇)
|
7月前
|
移动开发 前端开发 JavaScript
Vue与React两大前端框架的主要差异点
以上就是Vue和React的主要差异点,希望对你有所帮助。在选择使用哪一个框架时,需要根据项目的具体需求和团队的技术栈来决定。
464 83
|
9月前
|
前端开发 安全 开发工具
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
607 90
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
8月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
466 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
6月前
|
JavaScript 前端开发 编译器
Vue与TypeScript:如何实现更强大的前端开发
Vue.js 以其简洁的语法和灵活的架构在前端开发中广受欢迎,而 TypeScript 作为一种静态类型语言,为 JavaScript 提供了强大的类型系统和编译时检查。将 Vue.js 与 TypeScript 结合使用,不仅可以提升代码的可维护性和可扩展性,还能减少运行时错误,提高开发效率。本文将介绍如何在 Vue.js 项目中使用 TypeScript,并通过一些代码示例展示其强大功能。
285 22
|
10月前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
372 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
10月前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
348 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
8月前
|
人工智能 JavaScript 前端开发
Vue 性能革命:揭秘前端优化的终极技巧;Vue优化技巧,解决Vue项目卡顿问题
Vue在处理少量数据和有限dom的情况下技术已经非常成熟了,但现在随着AI时代的到来,海量数据场景会越来越多,Vue优化技巧也是必备技能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
JavaScript 前端开发 搜索推荐
Vue的数据驱动视图与其他前端框架的数据驱动方式有何不同?
总的来说,Vue 的数据驱动视图在诸多方面展现出独特的优势,其与其他前端框架的数据驱动方式的不同之处主要体现在绑定方式、性能表现、触发机制、组件化结合、灵活性、语法表达以及与后端数据交互等方面。这些差异使得 Vue 在前端开发领域具有独特的地位和价值。
261 58

热门文章

最新文章