如何动态的生成 Vue 组件?

1,137 阅读3分钟

需求

要做一个中台的详情页,和其他中台详情页不同的是,这个详情页的组件是希望通过配置 json 文件去动态的生成的。

例子

[{
    key: 'search',
    label: '搜索内容',
    template: {
      type: 'a-input',
      config: {
        placeholder: '请输入',
        style: { width: '100%' }
        allowClear: true
      }
    }
  },
  {
    key: 'status',
    label: '活动状态',
    template: {
      type: 'a-select',
      options: [a, b, c],
      config: {
        allowClear: true,
        placeholder: '请选择'
      }
    }
 }]

例如上面的 json 内容,需要生成下面的 vue 组件。(部分变量做了简写处理)

<a-form-item label="搜索内容">
    <a-input 
      placeholder="请输入"
      v-model:value="search"
      :style="{ width: '100%' }"
      :allowClear="true"
    />
</a-form-item>
<a-form-item label="活动状态">
    <a-select 
      placeholder="请选择"
      v-model:value="state" 
      :allowClear="true"
    >
      <a-select-option v-for="item in options" :key="item" :value="item">{{ item }}</a-select-option>
    </a-select>
</a-form-item>

问题

如何动态的将 config 字段的指令和方法动态的解析到组件的中去?

一开始打算使用全部写一遍然后一个个判断的笨方法,但是因为使用了 antdv 组件库,且每个组件都有大量的 API 和事件,全部写一遍不太现实,所以才想着怎么样去动态的生成他。

image.png

实践过的方案

h 函数

第一个想到的就是使用 h 函数去实现,但是转念一想肯定不行,因为 h 函数生成的是 vNode 而并不是组件。h('a-input', {}, 'test') 这样的代码倒是可以由上述 json 文件动态生成,但它渲染出来的直接就是 <a-input>test</a-input> 这样的 DOM 元素,没有任何组件上的功能和样式。这样并不能达成我的需求,所以不行。

jsx

jsx 可以直接返回 <a-input>test</a-input>,且页面上也能按照组件的样子渲染出来,但是动态的添加 config 却依然是一个问题。

h 函数和 resolveComponent

  • resolvecomponent 在官网找到了 resolveComponent 这个 api,他可以加载已有的组件,经过实践,该方法确实能正确的识别加载对应的组件。接下来就要解决加载指令的问题。

resolveDirective

  • resolveDirective resolveDirective 这个方法可以解析指令,然后通过 withDirectives 就可以把解析出来的指令挂载到前面使用 resolveComponent 解析出来的组件上。

然而理想是丰满的,现实却是骨感的。在挂载指令的时候却又碰到了问题,我完全照搬了官网的教程去引入 resolveDirective 去解析指令,但是反馈的却是一直报错。。。

<template>
  <testTemp />
</template>

<script setup>
import { h, resolveComponent, withDirectives, resolveDirective } from "vue";

const testTemp = () => {
  // 这一步确定没有问题
  const aInput = resolveComponent('a-input');
  // 这里的 foo,打印出来的却是 undefined
  const foo = resolveDirective('bind');
  console.log(`foo 打印内容为:`, foo);
  if (foo) {
    return withDirectives(h(aInput), [[foo, 'test']])
  }  
}
</script>

他却始终报 [Vue warn]: Failed to resolve directive: bind,无法解析 bind 指令。页面上没有显示任何东西,连前面确定可以用的 resolveComponent 都没有了,我起初以为是 a-input 上确实没有 bind 指令,但是后来发现并不是,我无论挂载什么指令他都是报这个 warning,且 resolveDirective 返回的结果一直都是 undefined

image.png

检查了我的 vue 版本,是 3.2.19,没有问题的。也注意到文档上写了上面的三个 API 只能在 setuprender 函数中执行,我也分别尝试了 <script setup> 语法糖和传统的 export default 导出 setuprender 函数也都是一样的结果。

点击直达 codesandbox 调试代码

未解决

目前并没有一个完美的解决方案,且看起来完美的 resolveDirective 方案也没有正常的工作。不知道各位有没有碰到过这个报错?或者能解决这个需求的方法都可以,感谢各位大佬!