为真正的大型React形式而奋斗

在其中一个项目中,我们遇到了数十个相互依赖的模块的表单。像往常一样,由于NDA,我们无法详细讨论该任务,但是我们将尝试使用一个抽象的(甚至是几乎没有生命的)示例来描述我们“驯服”这些表格的性能的经验。我将告诉您我们从带有Final-form的React项目中得出了什么结论。



图片


想象一下,该表格允许您在通过中介机构-签证中心处理申根签证的同时获取新样品的外国护照。这个例子似乎官僚主义足以证明我们的复杂性。



因此,在我们的项目中,我们面临着许多具有某些属性的块的形式:



  • 在这些字段中有输入框,多项选择,自动完成字段。
  • 这些块链接在一起。假设您需要在一个区域中指定内部护照的数据,而在下面将有一个区域,其中包含签证申请人的数据。同时,还与签证中心签有内部护照协议。

  • – , , ( 10 , ) .
  • , , . , 10- , . : .
  • . . .


最终形式在垂直方向上占据约6000像素-总共约3-4个屏幕,总共80多个不同的场。与这种形式相比,国家服务部的申请似乎并不那么好。就大量问题而言,最接近的事情可能是针对某大型公司的安全服务调查表,或者是有关视频内容偏好的无聊民意调查。



在实际问题中,大型表格并不常见。如果我们尝试“正面”实现这种形式(类似于习惯于使用小型表单的方式),那么结果将无法使用。



主要问题是,当您在适当的字段中输入每个字母时,将重新绘制整个表格,这会带来性能问题,尤其是在移动设备上。



而且,不仅对于最终用户,而且对于必须维护该表单的开发人员来说,都很难处理该表单。如果不采取特殊步骤,则代码中的字段之间的关系将很难跟踪-一处更改会带来有时难以预测的后果。



我们如何部署最终形式



该项目使用React和TypeScript(完成任务后,我们完全切换到TypeScript)。因此,为了实现表单,我们从Redux Form的创建者那里获取了React Final-form库。



在项目开始时,我们将表单分为几个单独的块,并使用了Final-form文档中描述的方法。las,这导致了一个事实,一个字段中的输入对整个大格式都进行了更改。由于图书馆是相对较新的,因此那里的文档仍然很年轻。它没有描述改善大型模具性能的最佳方法。据我了解,在项目中很少有人遇到这个问题。对于小型表单,该组件的一些额外重绘不会影响性能。



依存关系



我们首先要面对的晦涩之处是如何准确实现字段之间的依赖关系。如果严格按照文档进行操作,则由于大量相互关联的字段,表格的增长速度会开始放慢。关键是依赖关系。该文档建议在该字段旁边放置对外部字段的订阅。这就是我们项目的方式-负责连接字段的适应版本的react-final-formlisteners与组件位于同一位置,也就是说,它们位于各个角落。依赖关系很难找到。这使代码量大增-组件巨大。一切都缓慢进行。为了更改表单中的某些内容,您必须花费大量时间在所有项目文件中进行搜索(项目中大约有600个文件,其中有100多个是组件)。



我们已经做了几次尝试来改善这种情况。



我们必须实现自己的选择器,该选择器仅选择特定块所需的数据。



<Form onSubmit={this.handleSubmit} initialValues={initialValues}>
   {({values, error, ...other}) => (
      <>
      <Block1 data={selectDataForBlock1(values)}/>
      <Block2 data={selectDataForBlock2(values)}/>
      ...
      <BlockN data={selectDataForBlockN(values)}/>
      </>
   )}
</Form>


可以想象,我不得不提出自己的建议memoize pick([field1, field2,...fieldn])



所有这些结合在一起PureComponent (React.memo, reselect)导致了这样一个事实,即仅在块所依赖的数据发生更改时才重绘块(是的,我们将Reselect库引入了之前未使用的项目中,借助它,我们可以执行几乎所有数据请求)。



结果,我们切换到一个侦听器,该侦听器描述了表单的所有依赖关系。我们从final-form-calculate项目(https://github.com/final-form/final-form-calculate)中采用了这种方法的想法,并将其添加到我们的需求中。



<Form
   onSubmit={this.handleSubmit}
   initialValues={initialValues}
   decorators={[withContextListenerDecorator]}
>

   export const listenerDecorator = (context: IContext) =>
   createDecorator(
      ...block1FieldListeners(context),
      ...block2FieldListeners(context),
      ...
   );

   export const block1FieldListeners = (context: any): IListener[] => [
      {
      field: 'block1Field',
      updates: (value: string, name: string) => {
         //    block1Field       ...
         return {
            block2Field1: block2Field1NewValue,
            block2Field2: block2Field2NewValue,
         };
      },
   },
];


结果,我们得到了字段之间所需的依赖关系。另外,数据存储在一个地方,使用起来更加透明。此外,我们知道订阅以什么顺序触发,因为这也很重要。



验证方式



通过类比依赖,我们已经进行了验证。



在几乎每个领域中,我们都需要检查人员输入的年龄是否正确(例如,文档集是否符合指定的年龄)。从散布在所有表格中的数十种不同的验证器中,我们切换到一个全局验证器,将其分解为单独的块:



  • 护照数据验证器,
  • 行程数据验证器,
  • 有关以前签发的签证的数据,
  • 等等


这几乎没有影响性能,但是加速了进一步的开发。现在,进行更改时,您无需遍历整个文件即可了解各个验证器中发生的情况。



代码重用



我们从一个大的形式开始,在这个大形式上我们运行我们的想法,但是随着时间的推移,该项目不断发展-出现了另一个形式。自然,在第二种形式中,我们使用了所有相同的想法,甚至重用了代码。



以前,我们已经将所有逻辑移到了单独的模块中,那么为什么不将它们连接到新表单呢?这样,我们大大减少了代码量和开发速度。



同样,新表单现在具有与旧表单相同的类型,常量和组件-例如,它们具有一般授权。



代替总数



问题是合乎逻辑的:为什么我们不使用另一个库来存储表单,因为这个库很困难。但是无论如何,大型表格都会有自己的问题。过去,我本人曾与Formik合作。考虑到我们确实找到了问题的解决方案,因此最终表单更加方便。



总体而言,这是使用表单的绝佳工具。连同代码库开发的一些规则,他帮助我们大大优化了开发。所有这些工作的额外好处是能够使新的团队成员更快地更新。



突出显示逻辑之后,就可以更清楚地了解特定字段所依赖的内容-不必为此而阅读三层需求。在这种情况下,审核错误现在至少需要两个小时,尽管可能需要几天才能完成所有这些改进。一直以来,开发人员一直在寻找幻像错误,但根据其自身的表现尚不清楚。



本文的作者:Oleg Troshagin,Maxilekt。



PS:我们在Runet上的多个站点上发表了文章。订阅我们在VKFBInstagramTelegram频道上的页面以了解我们的所有出版物以及Maxilect的其他新闻。



All Articles