乐趣区

关于前端:基于分步表单的实践探索

咱们是袋鼠云数栈 UED 团队,致力于打造优良的一站式数据中台产品。咱们始终保持工匠精力,摸索前端路线,为社区积攒并流传教训价值。。

本文作者:修能

以下内容充斥个人观点。◡ ヽ(`Д´)ノ ┻━┻

前言

基于散布表单的需要,在中后盾治理中是一个十分常见的需要,通常具备如下布局:

其中,自定义需要度从高到低为,注释 > 按钮区 > 步骤条。

尽管布局相似,然而实现的形式却是天差地别,这里就探索一下到底怎么样实现能够兼具代码的可维护性和可读性呢?

指出问题

Container

咱们这里,以「指标 - 数据模型」的代码为例。

首先先来看看数据模型这里的代码是如何实现的?

export default () => {
  ...
  return (
    <>
      <header>
        <Steps current={current}>
          {['tab1', 'tab2', 'tab3', 'tab4', 'tab5'].map((title, index) => (<Step key={index} title={title} />
             )
          )}
        </Steps>
      </header>
      <Spin>
        {stepRender(current, {
           childRef,
           modelDetail,
           globalStep: globalStep.current,
           mode,
           isModelTypeDisabled,
           setModelDetail,
           setDisabled,
           onModelNameChange: handleModelNameChange,
        })}
        <Modal>...</Modal>
      </Spin>
      <footer>
        {current === EnumModifyStep.tab1 ? (
           <Button
             onClick={() => router.push('/url')}
           >
             勾销
           </Button>
        ) : null}
        ...
      </footer>
    </>
  )
}

这是数据模型编辑页面 Steps所在的容器组件的 DOM 局部的代码。

能够看进去,设计者的思路是比拟明确的,通过 header,content,和 footer 进行分层,减少代码的可读性。

在 header 中,通过申明 title 数组的形式 创立 Steps 的形式简洁又不失可读性。

在 content 中,有几个问题的存在:

  1. 既然 header 和 footer 都有语义化的标签强化可读性,我认为这里其实也能够 增加语义化的标签强化可读性 ,譬如 main 或者section,当然同时还须要思考会不会造成过深的层级。
  2. stepRender函数的实现把一大堆 params 传到子组件是否适合。
  3. 为何 content 区域内,会存在 Modal?对于没有设置 getPopupContainer 的 Modal 来说,其会通过 createPortal在 body 上创立,那么在这里不论是写在 content 还是 header,都不会影响它的渲染,所以我举荐把 Modal 写到最角落里,不影响可读性。
  4. 在 footer 中,通过 current === 步骤 的形式去定义按钮,我认为这种形式会使代码显得较为冗余。

    Tab1

    咱们这里以指标相干代码为例,以简见深,以小见大

export default (props) => {
  ...
 const {cref, modelDetail, mode, onModelNameChange} = props;

  useImperativeHandle(cref, () => {
    return {validate: () => {...},
      getValue: () => {...},
    }
  });

   useEffect(() => {
     setFieldsValue({
       a: modelDetail.a,
       b: modelDetail.b,
       c: modelDetail.c,
     });
    }, [modelDetail]);

  return (
    <Form>
      <Row gutter={40}>
        <Col span={12}>
            ...
        </Col>
        <Col span={12}>
            ...
        </Col>
    </Row>
    <Row gutter={40}>
      <Col span={12}>
         ...
      </Col>
    </Row>
    </Form>
  )
}

这里我想指出的第一个问题是,ref 的应用,因为 ref 无奈在 props 中传递,须要通过 forwardRef 能力拿到。然而这里 通过 cref 这种比拟 hack 的形式 进行一个操作。我认为这是一个不举荐的做法,如果须要拿 ref 我倡议是老老实实通过 forwardRef 拿。

其次是 Row 和 Col 的应用,并不是说 Col 达到 24 之后就须要再写一个 Row,你能够持续写的呀,童鞋!

这里须要提出来的一个论点是,每一个子组件里去写 Form 的形式好(即下面的这种写法),还是总体写一个 Form 的形式更好?集体认为前者存在的问题如下:

  1. 因为子组件写 Form,然而提交(或下一步)按钮在里面,那么必然须要用 ref 拿到子组件的实例,并调用相干办法。(下面是 validate 和 getValue 别离对应下一步和上一步调用)
  2. 没有遵循 single source of truth(繁多事实起源)
  3. 如果多层级构造,例如 RelationTableSelect 的话,每一层都有填写内容,那么须要大量 Form + ref,升高可维护性。

除此之外,因为根底信息比较简单,所以不存在 props 层层往下传递的问题,然而简单组件就会存在层层往下传递的状况,那么就波及到是否须要 context 的问题了。当然,我举荐是须要 context 的。

Tab2

这里再看一眼第二步关联表的设计

interface ITab2Props {
  cref: IModifyRef;
  modelDetail?: Partial<IModelDetail>;
  mode: any;
  globalStep: number;
  updateModelDetail: Function;
  setDisabled?: Function;
}

const RelationTableSelect = (props: ITab2Props) => {}

首先,这里须要反对的一个设计思路是,通常状况下,切忌间接把 dispatch 传递给子组件

关联表这里的设计因为层级嵌套很深,子组件十分多,导致 updateModelDetail 一直往下传递,你齐全不晓得哪层组件在什么状况下会去批改这个值!!! 这对于 SSOT 来说,是毁灭性的打击。

再加上 modelDetail 是一个很简单的数据,对于可维护性来说,属于是力中暴力地打击了。

解决问题

综上,咱们设计散布表单的时候,须要躲避以上的问题,遵循如下准则:

  1. SSOT
  2. 可维护性
  3. 可扩展性

首先实现如下组件:

<StepsForm
  current={current}
  onChange={setCurrent}
  titles={['tab1', 'tab2', 'tab3', 'tab4', 'tab5']}
/>

这一块代码比较简单,无非就是投传几个值到对应的组件中去。

接下来思考底部按钮的可扩展性。

通过 submitter 属性反对定制按钮的交互属性。

<StepsForm
  current={current}
  onChange={setCurrent}
  submitter={[
    {[StepsForm.PREV]: {children: '勾销',},
    },
    null,
    {[PREVIEW]: {
        danger: true,
        children: '预览',
      },
    },
  ]}
/>

接下来要解决按钮的事件,这里有两种计划,一种是将事件挂载在 Container 上(即这里的 StepsForm 组件),通过诸如 onCancel,onSubmit,onPrev等形式进行反馈。
我认为这种形式不够好,起因有如下几点

  1. 通常咱们会把子组件提出来,不会和 Container 组件写在一起,这就会使得咱们须要在不同的组件中写按钮的交互逻辑和 UI 逻辑,存在 隔离感
  2. 有时候咱们须要把 Select.Option 相干的数据一起放到数据里给到服务端,这种形式交互须要把 Option 的数据提取到 Container 中
  3. 须要通过 ref 去子组件获取值

而目前我思考通过事件订阅对按钮事件触发,通过 useEffect 监听事件,然而这种形式的毛病如下:

  1. 不够直观,和咱们通常来说的组件开发有肯定相悖的思路

除了以上两种形式以外,其实还有一种形式,即通过实现 Children 组件,将 Children 组件作为 StepsForm 的子组件,从而使得将每一步相干的 title 和 onSubmit 等形式都挂载在 Children 组件上。即 ant-design-pro 中的 StepsForm 的实现形式。我认为这种形式的长处在于直观,不割裂。毛病在于如下:

  1. 为了获取 title 不得不先渲染子组件,从而导致 DOM 先渲染进去,而后通过 active 判断表单是否渲染。
  2. 导致子组件无奈通过 useEffect获取数据

其中第二点我认为是无法忍受的,这和开发组件的思路齐全相悖,故摒弃这种形式
临时思考不分明是第一种好还是第二种好。

这里先思考实现第二种形式后组件书写的成果:

export function () {
  ...
  StepsForm.useFooterEffect(({ prev}) => {prev(() => {});
    },
    [StepsForm.PREV],
  );

  StepsForm.useFooterEffect(() => {message.info('预览')
  }, [PREVIEW]);

  StepsForm.useFooterEffect(({ next}) => {next(() => {return new Promise((resolve) => {setTimeout(() => {resolve();
          }, 1000);
        });
      });
    },
    [StepsForm.NEXT],
  );

  return (...)
}

hook 的实现形式也比较简单,基于事件订阅,联合每一个按钮都赋予一个惟一值。
实现按钮交互触发后,通过事件散发,触发以后渲染的组件中的监听 hook。

总结

本文意在摸索分步表单的最佳实际,避免不同的同学在开发该类型的需要会写出形形色色的代码,从而导致升高可维护性。

本文提到的解决方案也不认为是最佳实际,其中不同的办法通过剖析都存在长处和毛病。在理论的开发过程中,依然须要依据具体的需要进行调整。

然而基于分步表单的个性和应用场景,总结出实用大部分状况下的方法论是有必要的。


最初

欢送关注【袋鼠云数栈 UED 团队】~
袋鼠云数栈 UED 团队继续为宽广开发者分享技术成绩,相继参加开源了欢送 star

  • 大数据分布式任务调度零碎——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据畛域的 SQL Parser 我的项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实际文档——code-review-practices
  • 一个速度更快、配置更灵便、应用更简略的模块打包器——ko
退出移动版