组件渲染机制
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
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
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
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
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
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
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
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
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
拆分为IComponentMaker
和IComponentConfig
,将无法转成字符串存储的部分放入IComponentMaker
,如component
这样类型的字段,将其他可以持久化存储的字段放入IComponentConfig
,如props
等。这样,就可以通过动态的配置,然后动态的渲染组件了。