组件渲染机制

Vue中有一个内置组件<component>,我们使用它就可以达到动态渲染组件的目的。假设已经有了这样一个组件,它可以接收一个名为msg的属性,一个名为btnCilck的事件,一个名为btnText的插槽

// Foo.vue
<template>
    <div>msg:{{ msg }}</div>
    <div>
        <button @click="emits('btnCilck')">
            <slot name="btnText"></slot>
        </button>
    </div>
</template>
<script setup lang="ts">
defineProps({
    msg: String
})
const emits = defineEmits(['btnCilck'])
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

正常的使用方式如下

<template>
    <Foo :msg="msg" @btn-cilck="handleClick">
        <template #btnText>点我</template>
    </Foo>
</template>
<script setup lang="ts">
import Foo from "./Foo.vue"
const msg = "你好"
const handleClick = () => {
    alert(msg)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

效果如下

msg:你好

使用<component>来选渲染它

<template>
    <component :is="Foo" :msg="msg" @btn-cilck="handleClick">
        <template #btnText>点我</template>
    </component>
</template>
<script setup lang="ts">
import Foo from "./Foo.vue"
const msg = "你好"
const handleClick = () => {
    alert(msg)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

效果如下

msg:你好

虽然上述效果和正常使用组件得效果一致了,但是这样还不行,因为上述方式在传入属性、事件以及插槽的时候,直接指定了属性名称、事件名称和插槽名称。我们需要找到一个可以动态绑定这些名称的方式,恰巧的是,Vue天然支持了这种方式,我们改造一下之前代码,如下

<template>
    <component :is="Foo" v-bind="dynamicProps" v-on="dynamicHanlders">
        <template v-for="(slot,name) in slots" v-slot:[name]>{{ slot }}</template>
    </component>
</template>
<script setup lang="ts">
import Foo from "./Foo.vue"
const dynamicProps = {
    msg: "你好"
}
const dynamicHanlders = {
    btnCilck: () => {
        alert(dynamicProps.msg)
    }
}
const slots = {
    btnText: '点我'
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

效果如下

msg:你好

现在我们已经可以动态绑定组件的属性、事件以及插槽了,这时候我们可以很容易的写出一个ComponentRender,如下

<template>
    <component :is="config.component" v-bind="config?.props" v-on="config?.events || {}">
        <template v-for="(slot,name) in config?.slots" v-slot:[name]>{{ slot }}</template>
    </component>
</template>
<script setup lang="ts">
import type { Component } from 'vue';
export interface IRenderConfig {
    component: Component
    props?: Record<string, string>,
    slots?: Record<string, string>,
    events?: Record<string, Function>,
}
defineProps(
    {
        config: {
            type: Object as () => IRenderConfig,
            required: true
        }
    }
)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

用它渲染一个使用Foo的示例

<template>
    <ComponentRender :config="renderConfig"></ComponentRender>
</template>
<script setup lang="ts">
import ComponentRender, { IRenderConfig } from "./ComponentRender.vue"
import Foo from "./Foo.vue"

const renderConfig: IRenderConfig = {
    component: Foo,
    props: {
        msg: "你好"
    },
    slots: {
        btnText: '点我'
    },
    events: {
        btnCilck: () => {
            alert(renderConfig.props?.msg)
        }
    }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

效果如下

msg:你好

此时还有一个问题,就是在绑定对应插槽的时候,传入的子组件怎么动态渲染的问题。这就需要ComponentRender可以递归的渲染自己,我们再次修改下代码

// ComponentRendeWithRecursion.vue
<template>
    <component :is="config.component" v-bind="config?.props" v-on="config?.events || {}">
        <template v-for="(slot,name) in config?.slots" v-slot:[name]>
            <ComponentRendeWithRecursion :config="_config" v-for="_config in slot"></ComponentRendeWithRecursion>
        </template>
    </component>
</template>
<script setup lang="ts">
import type { Component } from 'vue';
export interface IRenderConfig {
    component: Component
    props?: Record<string, string>,
    slots?: Record<string, IRenderConfig[]>,
    events?: Record<string, Function>,
}
defineProps(
    {
        config: {
            type: Object as () => IRenderConfig,
            required: true
        }
    }
)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

用它渲染一个使用Foo的示例

<template>
    <ComponentRender :config="renderConfig"></ComponentRender>
</template>
<script setup lang="ts">
import ComponentRender, { IRenderConfig } from "./ComponentRendeWithRecursion.vue"
import Foo from "./Foo.vue"

const renderConfig: IRenderConfig = {
    component: Foo,
    props: {
        msg: "你好"
    },
    slots: {
        btnText: [
            {
                component: () => '点我'
            }
        ]
    },
    events: {
        btnCilck: () => {
            alert(renderConfig.props?.msg)
        }
    }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

效果如下

msg:你好

组件的渲染机制就是这样,后面再通过一定的方式将IRenderConfig拆分为IComponentMakerIComponentConfig,将无法转成字符串存储的部分放入IComponentMaker,如component这样类型的字段,将其他可以持久化存储的字段放入IComponentConfig,如props等。这样,就可以通过动态的配置,然后动态的渲染组件了。