huangshuwei / blog

🈲黄书伟的个人博客
54 stars 8 forks source link

vue3 组件 props 透传 #62

Open huangshuwei opened 1 year ago

huangshuwei commented 1 year ago

前言

有这样一个场景,页面渲染的信息是配置出来的,也就是我们渲染的内容是动态的。这种场景我们往往需要开发动态组件去支持。而动态组件可能会使用多种组件去实现。下面将介绍在 vue3 中,如何进行组件props 透传。

以下结合 element-plus 组件为例。

动态组件

下面是动态组件里包含的几种 element-plus 组件:

而每一种 element-plus 组件都可能包含不同的 属性、事件、插槽、实例方法。比如 el-input 组件,如何让动态组件支持 el-input 所有的功能呢。一种方法是将 el-input 所有支持的属性、事件、插槽、实例方法在动态中都去实现一遍。但是如果动态组件也需要支持 el-date-picker 组件、 el-select 组件呢。那么工作量将会翻几番,并且当这几种组件有更新,你的动态组件也要进行调整。

如果要满足上面的所有需求,那么要支持: 1、属性透传:props属性、事件、style样式、class 名称 2、slots 插槽透传 3、v-model 指令的支持 4、实例方法的支持

注意:而为了同时满足上面的需求,目前发现只有使用 jsx方式才能支持,所以下面的示例也基本是基于jsx 去实现了。

属性透传

首先定义一个 props 去接收 element-plus 组件的动态属性

props: {
        // element-plus props
        elCompProps: {
            type: Object,
            default: null,
        },
}

属性绑定。使用对象结构的方式

// DynamicComp
import { ElInput } from "element-plus";
export default defineComponent({
    props: {
        // element-plus props
        elCompProps: {
            type: Object,
            default: null,
        },
    },
    setup(props, { attrs, slots, emit, expose }) {
        return () => (
            <div>
                <ElInput
                    /* 
                    支持属性绑定、事件传递
                    */
                    {...props.elCompProps}
                >
                </ElInput>
            </div>
        );
    },
});

而如果仅仅是支持该功能,也可以通过 template模板方式,可以通过 v-bind指令实现。这里不多介绍。

在使用动态组件时,我们就可以轻易的将 props属性、事件、style样式、class 名称 进行透传了:

<script setup lang="tsx">
import DynamicComp from "./dynamic-comp";

const handlerValueChange = (val: string) => {
    alert(val);
};
</script>

<template>
    <div style="margin-top: 100px">
        <DynamicComp
            :el-comp-props="{
                /*
                attrbute props
                可以支持 style、class 
                */
                style: 'width:300px;font-size:20px;',
                class: 'custom-class-name',
                placeholder: '随便录入',
                /* 
                event props
                记得加上前缀 'on'
                */
                onChange: handlerValueChange,
            }"
        >
        </DynamicComp>
    </div>
</template>

slots 插槽透传

slots 插槽透传,目前只有 jsx方式可以支持(或许其他方式我不知道),而通过jsx方式实现起来也非常简单。

// DynamicComp
import { ElInput } from "element-plus";
export default defineComponent({
    setup(props, { attrs, slots, emit, expose }) {
        return () => (
            <div>
                <ElInput >
                 {/* 支持插槽 */}
                 {slots}
                </ElInput>
            </div>
        );
    },
});

使用动态组件的地方就和你直接使用 el-input 方式一样:

<script setup lang="tsx">
import DynamicComp from "./dynamic-comp";

</script>

<template>
    <div style="margin-top: 100px">
        <DynamicComp>
            <!-- el-input 组件支持的2种插槽 -->
            <template #suffix><div style="color: red">111</div></template>
            <template #prefix>222</template>
        </DynamicComp>
    </div>
</template>

v-model 指令的支持

对于动态组件,支持v-model 会提高使用组件的友好度。v-mode 在jsx 中的使用,你也参考官方文档

// DynamicComp
import { ElInput } from "element-plus";
export default defineComponent({
  props: {
        // 可以通过 v-model 使用该组件
        modelValue: [String, Number],
    },
    setup(props, { attrs, slots, emit, expose }) {
        return () => (
            <div>
                <ElInput 
                    /* 
                    支持 v-model
                    */
                    modelValue={props.modelValue}
                    onUpdate:modelValue={(value) => {
                        emit("update:modelValue", value);
                    }}
                >
                </ElInput>
            </div>
        );
    },
});

使用动态组件

<script setup lang="tsx">
import DynamicComp from "./dynamic-comp";

// v-model 值
const modelValue = ref("1123");
</script>

<template>
    <div style="margin-top: 100px">
        <DynamicComp v-model="modelValue">
        </DynamicComp>
    </div>
</template>

实例方法的支持

像 el-input 组件存在一些实例方法或者属性,我们同样可以在jsx中通过expose暴露出去。

暴露el-input 组件的实例,我是通过定义一个变量去操作的:

// element-plus 组件的ref
const elCompRef = ref<ComponentPublicInstance | null>(null);

动态组件实现:

// DynamicComp
import { ElInput } from "element-plus";
import type { ComponentPublicInstance } from "vue";
export default defineComponent({
    setup(props, { attrs, slots, emit, expose }) {
        // element-plus 组件的ref
        const elCompRef = ref<ComponentPublicInstance | null>(null);

        const currentInstanceMethod = () => {
            alert(1);
        };

        expose({
            // el-plus 相关组件的实例
            elCompRef,
            // 动态组件本身提供的实例
            test: currentInstanceMethod,
        });

        return () => (
            <div>
                <ElInput
                    ref={elCompRef}
                >
                </ElInput>
            </div>
        );
    },
});

使用动态组件

<script setup lang="tsx">
import DynamicComp from "./dynamic-comp";

const dynamicCompRef = ref<InstanceType<typeof DynamicComp> | null>(null);

const handlerValueChange = (val: string) => {
    alert(modelValue.value);
};

const clearTest = () => {
    // @ts-ignore
    dynamicCompRef.value?.elCompRef.clear();
};
</script>

<template>
    <div style="margin-top: 100px">
        <DynamicComp
            ref="dynamicCompRef"
        >
        </DynamicComp>
        <button @click="clear">清除</button>
    </div>
</template>

总结

动态组件支持了上述的功能,你将大大减少维护成本。

--完--

SublimeCT commented 1 year ago
<template>
  <a-table ref="component" v-bind="props">
    <template v-for="(slot, k) in $slots" :key="k" v-slot:[k]="slotProps">
      <component :is="slot" v-bind="slotProps"></component>
    </template>
  </a-table>
</template>

<script lang="ts" setup>
import { Table } from 'ant-design-vue' // 引入 antd 组件
import { tableProps } from 'ant-design-vue/es/table/Table' // 引入组件定义的 props
import { onMounted, ref } from 'vue';

const props = defineProps(tableProps()) // 定义 props
const component = ref()
</script>

slots 是可以通过 <component :is="slot"> 传递的

huangshuwei commented 1 year ago

@SublimeCT 谢谢你的方案