业务组件如何优雅的包装(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>

行动

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

参考

目录
相关文章
|
2月前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
217 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
3月前
|
JavaScript 前端开发 API
Vue.js:现代前端开发的强大框架
【10月更文挑战第11天】Vue.js:现代前端开发的强大框架
100 41
|
2月前
|
前端开发 JavaScript 开发者
React与Vue:前端框架的巅峰对决与选择策略
【10月更文挑战第23天】React与Vue:前端框架的巅峰对决与选择策略
|
2月前
|
前端开发 JavaScript 数据管理
React与Vue:两大前端框架的较量与选择策略
【10月更文挑战第23天】React与Vue:两大前端框架的较量与选择策略
|
2月前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
50 1
|
3月前
|
JavaScript 前端开发 算法
前端优化之超大数组更新:深入分析Vue/React/Svelte的更新渲染策略
本文对比了 Vue、React 和 Svelte 在数组渲染方面的实现方式和优缺点,探讨了它们与直接操作 DOM 的差异及 Web Components 的实现方式。Vue 通过响应式系统自动管理数据变化,React 利用虚拟 DOM 和 `diffing` 算法优化更新,Svelte 通过编译时优化提升性能。文章还介绍了数组更新的优化策略,如使用 `key`、分片渲染、虚拟滚动等,帮助开发者在处理大型数组时提升性能。总结指出,选择合适的框架应根据项目复杂度和性能需求来决定。
|
2月前
|
JavaScript 前端开发 搜索推荐
Vue的数据驱动视图与其他前端框架的数据驱动方式有何不同?
总的来说,Vue 的数据驱动视图在诸多方面展现出独特的优势,其与其他前端框架的数据驱动方式的不同之处主要体现在绑定方式、性能表现、触发机制、组件化结合、灵活性、语法表达以及与后端数据交互等方面。这些差异使得 Vue 在前端开发领域具有独特的地位和价值。
|
3月前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
3月前
|
前端开发 JavaScript 安全
在vue前端开发中基于refreshToken和axios拦截器实现token的无感刷新
在vue前端开发中基于refreshToken和axios拦截器实现token的无感刷新
200 4
|
3月前
|
前端开发 JavaScript
CSS样式穿透技巧:利用scoped与deep实现前端组件样式隔离与穿透
CSS样式穿透技巧:利用scoped与deep实现前端组件样式隔离与穿透
363 1