React项目优化(6)-表单中组织表单元素(FormItem)的三种方案

1,430 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

上一篇: React项目优化(5)-使用PlopJs生成模板文件


概述

在笔者使用React开发PC端后台管理系统和资金业务系统的过程中,使用最普通,最常见的组件就是表格组件Form及表单组件Table
由于业务比较复杂,导致每一个表单或表格都会有很多元素组成。而系统中又又很多的表单或表格。那么,如何组织表单元素,看似简单,实则对整个项目的开发过程影响重大。合理的表单组织方式,能够减少繁琐的工作量,能够快速的定位问题,处理问题。

本文中,笔者结合自己的项目实践,向大家展示三种在复杂表单中组织表单项的方式。

基础说明

  1. 我们在项目中使用的是 TinperNext 组件库。其中的表单组件和Antd-design中的表单基本一致。
  2. 我所说的表单项包括(文本框【Input】,下拉选项【Select】,单选组件【Radio】,日期组件【DatePicker】,数组框组件【NumberInput】),还有一些扩展的自定义组件(如:参照类型【TableFormRef】,档案类型等)。
  3. 一个表单中包括两种状态:编辑态,阅读态。
  4. 表单中还需要处理的逻辑如下:
    • 是否必填
    • 是否禁用
    • 是否显示
    • 是否满足自定义的校验条件
    • 表单联动逻辑的处理
    • 表单数据的初始化
    • 表单提交数据

基础方案

这是使用表单元素的最基础方式。

核心代码示例

// 表单组件定义
class MainForm extends Component {
    constructor(props) {
        super(props);
        this.state = {}
    }
    render() {
        // do something ...
        return (<div className="edit-collapse">
            <Row className='form-panel'>
                <FormItem>
                    // 普通文本框
                    <FormControl disabled={true} placeholder="系统自动生成"
                        {...getFieldProps('billNo', {
                            initialValue: formData.billNo,
                        })}
                    />}
                    <FormError errorMsg={getFieldError('billNo')} />
                </FormItem>
                <FormItem>
                    // 下拉选择
                    <Select mode='multiple' placeholder=''>
                        <Option value='red'>Red</Option> 
                        <Option value='green'>Green</Option> 
                        <Option value='blue'>Blue</Option> 
                    </Select>
                </FormItem>
                <FormItem>
                    // 日期组件 
                    <DatePicker>
                        // ...
                    </DatePicker>
                </FormItem>
                <FormItem>
                    // 多选框
                    <CheckBox
                      // ....
                    ></CheckBox>
                </FormItem>
                ... 诸如此类,还有很多很多...
            </Row>
        </div>)
    }
};
export default Form.createForm()(MainForm);

基础方案的特点

优点: 逻辑简单,入手容易,直接使用组件,不需要二次封装,有问题时能够很快的定位组件,找问题。也是官方示例中使用的。
缺点:

  1. 代码繁琐,开发效率低。如果表单元素较多,那页面动辄上千行代码。如果项目节点多,那前端组织表单和表格页面将是一项繁重的体力劳动。
  2. 代码维护困难,尤其是开发完成后,有问题需要调整时,可能要一处处调整。
  3. 代码风格不统一,每个开发根据自己的能力随意发挥, 代码质量难以保证。

进阶方案

基于基础方案的一些问题, 我们进行了调整,封装了组件,组成了此进阶版本的方案。

实现思路

  1. 将表单元素封装层组件(Field),在此组件内部负责渲染组件,根据传入的参数,判断渲染为:文本框、下拉选组件、选框组件等等。 责初始化数据。负责提供组件数据合法性校验等等。
  2. 封装了(FormGoup)组件,这是一个容器组件,用于包装所有的表单元素。作用:负责控制表单渲染几列,表单元素是否有标题。表单元素的宽度等等。
  3. 在表单外部,所有的表单配置项作为一个JSON格式的大对象,传递给封装的组件中。所有的逻辑都在组件内完成。 在此方案中,开发人员只需要负责一下两件事: a. 组装表单元素的JSON数据,传入表单组件。 b. 处理一些表单联动逻辑。 c. 处理特殊的页面样式,根据需求调整配置项。

核心代码示例

// Field.js
class Field extends Component {
    render() {
       if (!isEdit) {
       // 阅读状态,根据返回值分情况渲染结果
       if (renderType === GRID_DIC_SELECT) {
	   value = formatDicOption(renderOption, value);
       }
       if (renderType === GRID_OTHER_SELECT) {
	   value = getOptionValue(renderOptionKey, renderOptionValueKey)(renderOption, value);
       }
        if (renderType === GRID_DATE_PICKER){
	  value = typeof value === "object" ? value.format(format) : value;
        }
	return value ? value : "";
       }else{
         // 编辑态,根据参数渲染不通组件(文本框,还是下拉选,日期组件等等)
           switch (renderType) {
		case GRID_TEXT:
		formContent = <FormControl showClose={isCanEdit} {...itemProps} />;
		break;
		// 文本域
		case GRID_TEXTAREA:
		formContent = <FormControl componentClass="textarea" showClose={isCanEdit} {...itemProps} />;
		break;
		// 数字
		case GRID_NUMBER:
                formContent= <NumberInput></NumberInput>
                break;
		//枚举
		case GRID_SELECT:
                // ...
		break;
		//日期
		case GRID_DATE_PICKER:
		formContent = <DatePicker {...itemProps} />;
		break;
		// 此处省略更多类型的判断及渲染;
            };
           return formContent ? formContent : "";       
       } 
    }
}

// 组件调用示例:BaseColumn 是一个配置对象,如下图
<FiledGroup
    columns={baseColumns}
    form={form}
    mode={!isEdit ? "read" : "edit"}
/>

image.png

进阶方案中的一些问题

目前我们的项目中表单及表格的实现逻辑都是基于如上的进阶方案进行的。 当然在使用中,根据实际的开发体验,我们也在不断单探索新方案。

在进阶方案中,所有的表单配置对象都是在前端页面中写的,虽然从组件层面分离出来了,但是,仍然是比较庞大的数据对象。那这些庞大的配置数据能不能进一步优化?

项目上线后,可能会有一些微调整,比如:字段是否显示、字段是否必填,字段是否被禁用,以及字段显示顺序跳转。由于配置对象是写在前端代码中固定的,每次修改都需要开发人员修改前端代码。然后重新发版,才能生效? 那有没有什么方式简化这些操作?

配置方案

基于进阶方案实践中的一些问题。我们尝试探索更加优雅的解决方案。

实现思路

  1. 将表单中元素的详细信息包括(字段名称,编码,类型,是否显示,是否必填,默认值)等,作为独立的因素保存在后端数据库。
  2. 通过一个可以拖拽的表单设计器,将独立的表单元素,拖拽到表单中。然后根据这些信息,可以生成一个表单元素的JSON格式的对象。然后将该对象保存到数据库中。
  3. 在使用表单组件,区别于进阶方案的是,我们从数据库获取表单元素的配置对象。而非在前端页面获取。
  4. 如果需要对表单元素进行微调,只需要录入在表单设计器中调整,理论上,不需要在代码层面调整。

效果示例

  1. 表单元素配置界面

image.png

  1. 表单设计器配置字段

image.png

  1. 生成的表单

image.png

针对配置方案,我们尝试做了几个功能点。 有以下一些感受:

  1. 针对一下使用频繁的场景有必要这样操作。我们是在配置产品报价方案时,使用了此功能。 如果只是能简单的场景,可能在表单中很快就实现了,如果使用配置,会适得其反。
  2. 看起来更加灵活了,一切可以配置。但实际上,正因为这种可配置性,会导致很多额外的问题产生。所以使用配置时,需要了解规范,按规范操作。
  3. 对于此方案是否通用。我们也在进一步琢磨中。

总结

  1. 基于表单及表格元素的组织方式仁者见仁,智者见智。笔者也仅仅结合自己的项目特点进行一些思考和改进。并不是所有的项目都应该这样做,也不存在说哪种方案就是最有方案。我也会继续不停的探索。
  2. 至于表单数据的初始化,表单项的校验,表单的数据提交等。之后有时间将继续总结。
  3. 表格元素的组织与表单元素的组织基本一致。再次不做展开论述。