---
title: 表单配置化生成方案
createTime: 2022/03/17 10:10:04
author: pengzhanbo
permalink: /article/y979pp8x/
---

一个简单的基于 vue3 的表单配置化生成方案，可以作为思路参考。

<!-- more -->

## 前言

本方案是个人在做某个项目时，需要实现一个简单的表单配置化生成，而做的一个方案。
最终结果是通过一份表单配置，渲染为一个表单，并支持：

- 支持多种表单字段类型 （单行文本/多行文本/数字/下拉/多选/单选 等），并支持嵌套字段（对象/数组）
- 支持字段校验
- 支持条件判断显示/隐藏 表单字段
- 支持字段分组展示


::: tip
完整代码仓库地址： [formative](https://github.com/pengzhanbo/formative)
:::

## 效果展示

**通过配置生成表单：**
![](/images/formative-demo-1.png)

**配置的字段为对象或者数组时：**
![](/images/formative-demo-2.png)

**对配置的字段进行分组展示：**
![](/images/formative-demo-3.png)

## 方案说明

通过配置生成表单，需要明确如何实现 **表单字段** 与对应的表单组件进行关联，并实现**数据绑定**。

### 协议配置

约定一个协议，通过一个对象来表示 表单中某一个字段：

**FieldSchema:**
```json
{
  "type": "表示字段的类型，比如单选、多选、单行文本、多行文本等",
  "field": "表示字段的名",
  "label": "表示字段显示的标签",
  "default": "字段默认值",
  "description": "字段描述信息，作为提示信息使用"
}
```

对于完整的表单，则使用 `FieldSchema[]` 的数组形式进行配置。

### 表单字段与组件关联

在 vue 中， 组件可以通过 `h(component, attrs[, children])` 渲染函数 进行渲染，
通过 `h()` 函数，可以实现动态渲染不同的组件。

我们可以利用这一特性，首先 字段类型与对应的组件进行映射，通过映射关系，用 字段类型获取对应的组件，
然后调用 `h()` 进行渲染。

``` ts
// 字段类型 与 组件 映射
const componentMap = {
  radio: RadioField,
  checkbox: CheckboxField,
  text: TextField,
  select: SelectField,
}
```
``` tsx
{
  name: 'Field',
  props: ['fieldType'],
  setup(props) {
    return () => h(componentMap[props.fieldType])
  }
}
```

### 表单数据绑定

对于表单数据，需要实现 数据初始化，以及 对应的 Field组件对数据的可读写。

在vue中，我们可以使用 `provide/inject` API， 在 表单组件的根组件，通过 `provide()` 进行注入，在内部的
`FieldComponent` 中通过 `inject()` 获取对应的数据，进行读写。

``` ts
/**
 * 通过 provide 输入数据
 */
const useFormDataProvide = (formData: FormData): FormInjectKey => {
  const key: FormInjectKey = Symbol('formData')
  provide(key, formData)

  return key
}

/**
 * 通过 inject 获取数据
 */
const useFormData = (key: FormInjectKey): FormData => inject<FormData>(key)!
```

由于需要支持 字段为对象/数组时产生的 多层级数据结构，还需要考虑 `FieldComponent` 对 `FormData.a.b` 的
深层数据的读写。

可以通过实现 `dotPath`， 通过 `FormData['a.b']` 的形式来读写 `FormData.a.b`，这样 `FieldComponent`
只需要知道 `a.b` 的 `dotPath` 字段路径即可配合 `inject()` 获取的表单数据进行读写。

``` ts
const useDotPath = <T = any>(model: FormData, dotKey: ComputedRef<string>) => {
  const binding = computed<T>({
    set(data) {
      setDotPath(model.value, dotKey.value, data)
    },
    get() {
      return getDotPath(model.value, dotKey.value)
    },
  })

  return binding
}
``` 

## 技术实现

在 [formative](https://github.com/pengzhanbo/formative) 仓库中， 通过本方案实现了一个 配置化生成表单的库。

- `src/components/Formative.tsx` 文件作为表单根组件。
- `src/components/Field.tsx` 文件作为根据表单字段动态选择对应组件进行表单字段渲染。
- `src/components/Group.tsx` 文件实现了对表单字段进行分组。
- `src/components/[other].tsx` 其他文件实现了 各种表单字段组件。

有兴趣了解细节的，可以阅读 [formative 源码](https://github.com/pengzhanbo/formative)，
我在关键位置进行了相关的代码注释说明。

同时，你可以直接拉取 源码，在本地进行运行。并授权代码使用。
