基于分步表单的实践探索

基于,分步,表单,实践,探索 · 浏览次数 : 307

小编点评

**分步表单最佳实践** **SSOT可维护性** * 使用子组件实现步骤,将组件与状态分离。 * 通过 useEffect 监听事件,更新表单状态。 * 使用 memo 缓存数据,避免重复渲染。 **性能** * 使用 event 的分发,减少事件处理数量。 * 使用 React 的 memo 缓存,减少组件渲染数量。 * 使用 lazy loading 加载表单数据,避免延迟加载。 **可扩展性** * 使用组件库,将组件模块化和可扩展。 * 使用 React 的 hooks,管理组件状态和性能。 * 使用 React 的 memo 和 lazy loading,优化组件性能。 **代码组织** * 使用模块化设计,将代码模块化。 * 使用命名空间,提高代码可读性。 * 使用代码评审,确保代码质量。 **事件处理** * 使用事件分发,将事件处理到组件上。 * 使用 React 的 useEffect 和 React Hook,管理组件状态和性能。 * 使用 React 的 useMemo 和 React Hook,缓存组件状态和性能。 **优化** * 使用 React 的 lazy loading,优化表单加载性能。 * 使用 React 的 memo 和 React Hook,缓存组件状态和性能。 * 使用 event 的分发,减少事件处理数量。 * 使用 React 的 hooks,管理组件状态和性能。

正文

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。

本文作者:修能

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

前言

基于分布表单的需求,在中后台管理中是一个非常常见的需求,通常具有如下布局:

file

其中,自定义需求度从高到低为,正文 > 按钮区 > 步骤条。

虽然布局类似,但是实现的方式却是天差地别,这里就探究一下究竟怎么样实现可以兼具代码的可维护性和可读性呢?

指出问题

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

与基于分步表单的实践探索相似的内容:

基于分步表单的实践探索

>我们是[袋鼠云数栈 UED 团队](http://ued.dtstack.cn/),致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。 >本文作者:修能 ***以下内容充满个人观点。◡ ヽ(`Д´)ノ ┻━┻*** # 前言 基于分布表单的需求,在

一文带你实现云上部署轻量化定制表单Docker

本文分享自华为云社区 《【华为云云耀云服务器L实例评测|云原生】自定制轻量化表单Docker快速部署云耀云服务器 | 玩转华为云》,作者:计算机魔术师。 华为云的云耀云服务器L实例备受推崇,以其高效、可靠和安全的基础设施服务而闻名。本文将为展示在该服务器上部署轻量化定制表单服务,这是一款基于pywe

基于改进MFCC特征和卷积递归神经网络的心音分类

具体的软硬件实现点击http://mcu-ai.com/MCU-AI技术网页_MCU-AI人工智能 心音分类在心血管疾病的早期发现中起着至关重要的作用,特别是对于小型初级卫生保健诊所。尽管近年来心音分类取得了很大进展,但其中大多数都是基于传统的分段特征和基于浅层结构的分类器。这些传统的声学表示和分类

Python遥感影像叠加分析:基于一景数据提取另一数据

本文介绍基于Python中GDAL模块,实现基于一景栅格影像,对另一景栅格影像的像元数值加以叠加提取的方法。 本文期望实现的需求为:现有一景表示6种不同植被类型的.tif格式栅格数据,以及另一景与前述栅格数据同区域的、表示植被参数的.tif格式栅格数据;我们希望基于前者中的植被类型数据,分别提取6种

聊聊自然语言处理NLP

## 概述 自然语言处理(NLP)的正式定义:是一个使用计算机科学、人工智能(AI)和形式语言学概念来分析自然语言的研究领域。不太正式的定义表明:它是一组工具,用于从自然语言源(如web页面和文本文档)获取有意义和有用的信息。NLP工具的实现一般是基于机器学习与深度学习、其它算法(Lucene Co

基于深度卷积神经网络的时间序列图像分类,开源、低功耗、低成本的人工智能硬件提供者

具体的软硬件实现点击 http://mcu-ai.com/ MCU-AI技术网页_MCU-AI人工智能 卷积神经网络(CNN)通过从原始数据中自动学习层次特征表示,在图像识别任务中取得了巨大成功。虽然大多数时间序列分类(TSC)文献都集中在1D信号上,但本文使用递归图(RP)将时间序列转换为2D纹理

如何使用前端表格控件实现数据更新?

前言 小编之前分享过一篇文章叫《如何使用前端表格控件实现多数据源整合?》。今天,继续为大家介绍如何使用前端表格控件来更新已连接的数据源信息。 环境准备 SpreadJS在线表格编辑器: SpreadJS 前端表格控件新版本新增了一款报表插件,该插件基于 SpreadJS 本身强大的表格能力,在 Da

SMOGN算法Python实现:解决回归分析中的数据不平衡

本文介绍基于Python语言中的smogn包,读取.csv格式的Excel表格文件,实现SMOGN算法,对机器学习、深度学习回归中,训练数据集不平衡的情况加以解决的具体方法~

基于纯前端类Excel表格控件实现在线损益表应用

财务报表也称对外会计报表,是会计主体对外提供的反映企业或预算单位一定时期资金、利润状况的会计报表,由资产负债表、损益表、现金流量表或财务状况变动表、附表和附注构成。财务报表是财务报告的主要部分,不包括董事报告、管理分析及财务情况说明书等列入财务报告或年度报告的资料。 为了全面系统地揭示企业一定时期的

基于EasyCode定制Mybatisplus全自动单表实现:新增/批量新增/修改/批量删除/分页查询/ID查询

基于EasyCode定制Mybatisplus全自动单表实现CRUD接口 分页查询 ID查询 新增 批量新增 修改 批量删除 注意使用了MybatisPlus的自动填充功能,和insertBatchSomeColumn扩展批量插入功能,分页插件 需要几个增加插件实现类 自动填充 package co