前端配置化开发

178 阅读3分钟

背景: 

工作中有许多逻辑冗杂、迭代频繁的业务代码,随着迭代将越来越难以维护,一些场景适合通过配置化的方式来处理便于维护。

eg. 表单项非常多并且需要跟随用户选中条件动态切换的场景:

  • 当选中设备类型为空调时,有开启和关闭两种状态
    • 当选中状态为开启时,又有温度、风速和模式表单项
    • 当选中状态为关闭时,则不显示其他表单项
  • 当选中设备类型为其他时,又有其他非常多的状态

image.png

image.png

方案设计:

根据业务场景使用配置化的 Object|Array|Map 处理条件判断逻辑,通常需要配置文件 CONFIG.json。 配置化思路:

  • 表驱动法,使用表数据,存储对应的状态处理
  • 对表单项进行抽象,制定一份协议去描述每个表单项
  • 本质上 if/else 逻辑是一种状态匹配, 我们用动态配置取代很多的状态匹配判断

解决方案:

  1. 设计协议,把开发巨型表单系统转换成编写 JSON,即我们的配置config:
// DynamicFormOption
id: 树状层级,比如 1-1
type: 组件类型,比如 input、select、inputNumber、还有自定义组件 sliderPercent...
label: 表单项的名称/描述。
prop: 我们提交数据到后段的字段名,比如 [form.name](http://form.name/) 的 'name'
validator: antd formItem 校验器格式
hidden?: 不显示该组件,一般用于需要用到表数据存储对应的状态,但又不需要展示给用户时使用
conditions?: 动态后续条件, 比如select 选中值为 percent 时,会再显示一个表示窗帘开合度的slider滑动条的子表单项
needUnit?: 是否需要一个hidden的formItem上报所需单位
unitProp?: unit上报的字段名
attrs?: 表单项的额外参数
eg: {
    placeholder?: 缺省提示
    options?: select的选项
    type?: 'text' | 'phone' | 'email', input的类型
    addonAfter?: input的后缀
    stringMode?: inputNumber开启高精度小数支持
    ....
}

eg:

// CONFIG.json
// 根据用户选中的设备类型(1/2/3/...),返回对应的 DynamicFormOption[]

image.png

image.png

  1. 动态组件:根据配置化数据结构,递归动态渲染联动的组件。
// AsyncFormItems.tsx

const lazyComponent = (componentName?: string) => {
  return React.lazy(() => import(`./components/${componentName}`));
};

function dynamicRendeControl(item: DynamicFormOption) {
  const componentName = item.type;
  const DynamicFormItem = lazyComponent(componentName);
  return DynamicFormItem ? <DynamicFormItem {...item.attrs} /> : null;
}

  // 递归渲染联动的组件
  function renderSubItems(items: DynamicFormOption[]) {
    return items.map((item) => {
      return (
        <React.Fragment key={item.id}>
          <Form.Item
            label={item.label}
            className={styles.inline_form_item}
            name={[field.name, item.prop]}
            fieldKey={[field.fieldKey, item.prop]}
            rules={parseValidator(item.validator)}
            key={item.id}
            preserve={false}
            initialValue={item.defaultValue}
            hidden={item.hidden}
            validateFirst={true}
            // 由于InputNumber组件不支持pattern校验所以转换为stringMode,填入form中时再转换成数字
            normalize={item.type === 'input.number' ? (value) => Number(value) : undefined}
          >
            {dynamicRendeControl(item)}
          </Form.Item>
          {item.needUnit && (
            <Form.Item
              hidden
              name={[field.name, item.unitProp || 'unit']}
              fieldKey={[field.fieldKey, item.unitProp || 'unit']}
              initialValue={item.suffix}
              preserve={false}
            >
              <Input />
            </Form.Item>
          )}
          <Form.Item
            noStyle
            shouldUpdate={(pre, next) =>
              pre[formListName][index]?.[item.prop] !== next[formListName][index]?.[item.prop]
            }
          >
            {({ getFieldValue }) => {
              const value = getFieldValue([formListName, index, item.prop]);
              if (!value) return null;
              const subItems = item.conditions?.[value];
              if (!subItems) return null;
              return renderSubItems(subItems);
            }}
          </Form.Item>
        </React.Fragment>
      );
    });
  }
  
  function parseValidator(rules?: BaseRule[]): ValidatorRule[] | undefined {
    if (!rules) return rules;
    const newRules = rules.map((rule) => {
      if (rule.pattern) {
        try {
          // 实践上传递的是正则的字符串,所以需要再转一遍
          return {
            ...rule,
            pattern: new RegExp(rule.pattern.slice(1, -1)),
          };
        } catch (error) {
          console.log('parseValidator erro', error);
          return { ...rule, pattern: undefined } as ValidatorRule;
        }
      }
      return rule as ValidatorRule;
    });
    return newRules;
  }

成效(优点):

高复用,可读性好,可维护性高。提升开发效率,解放生产力。

  • 高复用性: 相似的业务逻辑进行统一处理,复用在相似领域的业务场景。

  • 可读性好: 减少了繁杂嵌套的 if-else,读取配置,逻辑更清晰

  • 可维护性高: 实现配置代替开发,逻辑分支的增删只是 CONFIG 的增删。即使把配置抽离,交到非技术人员处,其根据协议一样能实现表单项的增删,完成业务。让复杂的表单变得清晰好维护