五一假期,我开发了一个Form表单库,你觉得这样优雅吗?

发布于:2024-05-08 ⋅ 阅读:(22) ⋅ 点赞:(0)

表单开发是咱们前端的一个重要工作,基本上每天都在进行,然而我觉得目前的表单开发非常的不优雅,一直期望能够找出一个更加优雅的表单开发范式,在这个五一假期的最后1天,我开发了一个简单的库,希望能够给大家提供一些启发。

当前表单开发存在问题

首先看一下我们现在是如何进行开发表单的,假设我们要实现一个非常简单的表单,表单中只有2项内容,名称和标签,按照现在的习惯,我们大致实现如下:


<template>
  <el-form
      :model="formData"
      size="mini"
      label-width="100px"
  >
    <el-form-item
        prop="name"
        label="名称"
    >
      <el-input
          v-model="formData.name"
          placeholder="请填写名称"
          minlength="2"
          maxlength="30"
          clearable
      />
    </el-form-item>
    <el-form-item
        prop="tag"
        label="标签"
    >
      <el-select
          v-model="formData.tag"
          class="w-100"
          multiple
          filterable
          allow-create
          default-first-option
          placeholder="请选择或输入标签"
          @visible-change="$event && getTags()"
      >
        <el-option
            v-for="(item, index) in tagOptions"
            :key="index"
            :label="item"
            :value="item"
        />
      </el-select>
    </el-form-item>
  </el-form>
</template>
<script>
export default {
    data(){
        return {
            formData: {
                name: '',
                tag: []
            },
            rules: {
                name: [{required: true}],
                tag: [{required: true}]
            },
            tagOptions: []
         
        }
    },
    methods: {
        getTags(){
            //请求接口获取tags
            this.tagOptions = [];
        }
    }
}
</script>

繁琐的DOM

仅仅只是2个表单项,Dom就有40来行代码,而随着表单项的增多,代码行数也会越来越多,每次看到这样繁琐的Dom都感觉头大,根本无法一眼看清整个表单的结构,特别是下拉选择类组件,还要通过v-for渲染option,总感觉这样的Dom结构非常的啰嗦。

表单配置分散

在上述示例中,标签tag的的实现分散在多个地方,Dom实现位于template中,校验规则rules位于data中,获取标签的方法getTags位于methods中,如果你想搞清楚tag的完整逻辑,势必要来回翻看查找相关代码,不符合高内聚的编程原则,期望每个表单项的相关内容集中到一起。

配置表单

通过配置驱动表单生成的方式,可以很好地解决上述问题,针对上述示例,我们可以通过配置表单改造为如下形式:

<template>
    <config-form
        :model="formData"
        :fields="fields"
    />
</template>
<script>
export default {
    name: 'App',
    data() {
        return {
            formData: {
                name: '',
                tag: []
            },
            fields: {
                name: {
                    label: '姓名',
                    component: 'input',
                    componentProps: {
                        placeholder: "请填写姓名",
                        minlength: "2",
                        maxlength: "30",
                        clearable: true
                    },
                    rules: [{required: true}]
                },
                tag: {
                    label: '标签',
                    component: 'select',
                    componentProps: {
                         class: "w-100"
                         multiple: true,
                         filterable: true,
                         allow-create: true,
                         placeholder: "请选择或输入标签",
                         options: () =>{
                             return request('/api/tags');
                         }
                    },
                    rules: [{required: true}]
                }
            }
        };
    }
};
</script>

可以看到,Dom部分大大减少,取而代之的是更加结构化的js配置;而且每个表单的信息几乎都集中在配置中,如果要修改一个表单项,就无需来回跳转查看逻辑了,可维护性明显增强。

编写表单的过程变成了写配置,只需要一股脑地编写fields对象的内容即可,不再需要一会去写Dom,一会去写逻辑,开发体验变的更好。

做配置表单,不得不面对3个问题

  • 某个特殊表单项如何实现
  • 表单项之间交互如何实现
  • 如何实现自定义布局

第一个问题很好解决,只要借助插槽,就可以解决特殊表单项的定制。

<template>
    <config-form
        :model="formData"
        :fields="fields"
    >
        <template slot="name-label">
            <span style="color:red;">您的大名</span>
        </template>
        <template slot="name">
            <input v-model="formData.name" /> <i class="el-icon-question"></i>
        </template>
    </config-form>
</template>

表单项交互主要体现在某些表单项的显示/隐藏、启用/禁用依赖其他表单项,针对每个表单项的配置,我增加了disabled和hidden属性配置,可以配置为函数。

const fields = {
    name: {
        label: '姓名',
        component: 'input'
    },
    age: {
        label: '年龄',
        component: 'number',
        disabled: (formData) => !formData.name
    }
}

自定义布局是比较棘手的,如果全靠配置实现,会导致配置很繁琐,也无法实现一些特殊的样式,所以考虑通过占位符的形式,在Dom中配置每个表单项的位置和样式,但是表单项是渲染成input还是select,仍然由配置决定。

<template>
    <config-form
        :model="formData"
        :fields="fields"
    >
        <el-row>
            <el-col :span="24">
                <!--这里只需要占位,具体渲染成input还是别的,由配置决定-->
                <config-form-item prop="name"/>
            </el-col>
        </el-row>
        <el-row>
            <el-col :span="12">
                <config-form-item prop="age"/>
            </el-col>
            <el-col :span="12">
                <config-form-item prop="sex"/>
            </el-col>
        </el-row>
    </config-form>
</template>
<script>
export default {
    name: 'App',
    data() {
        return {
            formData: {
                name: ''
            },
            fields: {
                name: {
                    label: '姓名',
                    component: 'input'
                },
                age: {
                    label: '年龄',
                    component: 'input'
                },
                sex: {
                    label: '性别',
                    component: 'radio',
                    componentProps: {
                        options: [
                            {label: '男', value: 0},
                            {label: '女', value: 1},
                        ]
                    }
                }
            }
        };
    }
};
</script>

在布局中,通过config-form-item对每个表单项进行占位,但是不编写具体实现代码,这样能让布局相关的Dom更加简洁可读,Dom的作用就是设定表单的样式,具体表单如何渲染,仍然由配置控制,通过这种方式,即保留了通过Dom编写布局的优势,又保留了通过数据驱动渲染的优势。

库的开发难点

配置表单主要还是在一些常见UI库上(如ElementUI)进行二次封装,但又不是简单的封装,比如对于Select、Radio、CheckBox等组件,我们期望它们能够通过配置的options来驱动渲染,options可以是数组或函数,而在ElementUI中,是不支持这种方式的,所以要在原有组件基础上进行封装。

const fields = {
    sex: {
        label: '性别',
        component: 'radio',
        componentProps: {
            //支持配置一个数组,然后驱动radio的渲染
            options: [
                {label: '男', value: 0},
                {label: '女', value: 1},
            ]
        }
    }
}

但是配置表单这个库不应该强制依赖具体某个UI组件库,不能把库和ElementUI绑死,所以考虑通过插件的形式进行组件库扩展,针对某种组件库封装一个插件,然后传递给ConfigForm库。

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'


import ConfigForm from "@config-form/v3";

//将ElementUI 和 ElementPlus 封装成一个插件
import ConfigFormPluginElement from '@config-form/plugin-element';

const app = createApp(App)

app.use(ElementPlus)

app.use(ConfigForm, {
    //以插件形式传递给配置表单
    presets: [ConfigFormPluginElement],
})
app.mount('#app')

如果要支持其他不同类型的组件库,只需要参照@config-form/plugin-element实现一份即可,目前仅实现了ElementUI和ElementPlus插件

源码地址:

因为只花了一天时间进行实现,肯定还有很多不足,如果大家觉得这种方式确实对自己有用,后续我会逐渐优化,喜欢的同学麻烦帮点个小赞~~

你觉得这种方式开发表单更优雅吗?或者有什么好的想法欢迎一起讨论。