elementui联动显示-后台推荐! 阿里高性能表单解决方案——Formily

加强自我学习,提高自我素质。 积累工作经验,改进工作方法,向身边的同志学习,关注别人的长处,学习他们处理问题的方法,查找不足,提高自己。

哈喽大家好,我是小智,明天是我们的分享时间。 之前跟大家分享过很多可视化后端项目和工程实践,明天继续给大家分享一个特别有价值的开源项目Formily,它可以帮助我们更高效地开发任何复杂的表单,但是支持可视化表格的构造如下:

接下来我就带大家了解一下这个开源项目。

背景介绍

众所周知,表单场景仍然是后端领域最复杂的场景,主要有以下几个方面:

等等这么多问题,除了想办法解决之外,还需要高尚地解决。 经过中后台的大量实践和研究,阿里巴巴数字供应链团队沉淀了Formily表单解决方案,这些都是上面提到的。 这个问题,经过UForm到Formily1.x,直到Formily2.x终于实现了崇高的解决。 Formily2.x是如何解决这个问题的呢?

解决方案剖析

为了解决上述问题,我们可以进一步细化问题,提出突破方向。

准确渲染

实现React场景中的一个表单需求,因为需要收集表单数据并实现一些联动需求,所以大多使用setState来实现数组数据收集,实现起来极其简单,心理成本很低,但是它引入了性能问题。 由于每次输入都会导致所有数组被完整渲染,即使在 DOM 更新级别存在 diff,diff 也有一个预估成本,这会浪费大量预估资源。 如果从时间复杂度来看,表单的初始渲染是O(n),数组输入也是O(n),这显然是不合理的。

历史经验总是对人类有帮助的。 几六年前,人类创造了MVVM设计模式。 该设计模式的核心是将视图模型可视化,然后在DSL模板层进行消费。 DSL采用一定的依赖收集机制,然后在视图模型中统一调度,保证每个输入都被准确渲染。 这是工业级GUI形式!

恰逢其时,github 社区针对这样的 MVVM 模型具体化了一个名为 Mobx 的状态管理解决方案。 Mobx的核心能力是依赖跟踪机制和响应式模型的具体能力。

因此,使用Mobx可以完全解决表单数组输入过程中的O(n)问题,并且可以非常甜蜜地解决,而Formily2.x发现Mobx仍然存在一些与核心思想不兼容的问题​​正式地。 最终我们只能重新造一个轮子,延续Mobx@formily/reactive的核心思想

这里要提一下react-hook-form,它非常流行,堪称业界性能最好的表单解决方案。 让我们看一下它最简单的情况:

import React from 'react'
import ReactDOM from 'react-dom'
import { useForm } from 'react-hook-form'

function App({
  const { register, handleSubmit, errors } = useForm() // initialize the hook
  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstname" ref={register} /> {/* register an input */}
      <input name="lastname" ref={register({ required: true })} />
      {errors.lastname && 'Last name is required.'}
      <input name="age" ref={register({ pattern: /d+/ })} />
      {errors.age && 'Please enter number for age.'}
      <input type="submit" />
    </form>

  )
}

ReactDOM.render(<App />document.getElementById('root'))

虽然值管理实现了精确渲染,但是当触发校准时,仍然会导致表单的全渲染。 由于错误状态的更新,需要整体受控渲染的同步。 这只是标定和完整渲染,虽然还是有联动的,而react-hook-form需要实现联动,这也需要整体受控渲染才能够实现联动。 因此,想要真正实现精准渲染,就必须使用Reactive!

领域模型

正如下面问题提到的,表单的联动是非常复杂的,包括数组之间的各种关系。 让我们想象一下,大多数表单链接基本上都是基于各个数组的值的链接。 然而实际的业务需求可能会不太舒服。 除了基于单个数组值引起链接外,它还会基于其他副作用值引起链接,例如应用程序状态、服务器数据状态、页面 URL、数组 UI 组件的内部数据以及当前的其他数据状态。数组本身。 个别特殊的异步扰动等。 用一张图来描述一下:

从上图可以看出,要实现联动关系,核心是将数组的各个状态属性与各个数据关联起来。 这里的个体数据可以是外部数据,也可以是自身的数据,比如数组的显示/隐藏以及个体数据。 数据的关联,例如数组的值与个体数据的关联,以及数组的禁用/编辑与个体数据的关联,只是三个示例。 虽然我们已经体现了最简单的 Field 模型:

interface Field {
  value: any
  visible: boolean
  disabled: boolean
}

其实Field模型只有这3个属性吗? 肯定不是,如果我们要表达一个数组,就必须有一个数组的路径,因为我们需要描述整个表单树结构,同时我们还需要管理对应的UI组件的属性数组,比如Input和Select都有它们的属性,比如Input的占位符是与单个数据关联的,或者Select的下拉选项是与单个数据关联的,这样就可以理解了。 因此,我们的 Field 模型可以如下所示:

interface Field {
   path:string[],
   value:any,
   visible:boolean,
   disabled:boolean,
   component:[Component,ComponentProps]
}

我们添加了Component属性,它代表了数组对应的UI组件和UI组件属性,从而实现了将单个数据与数组组件属性关联起来,甚至与数组组件关联起来的能力。 还有吗? 其实举个例子,数组的包装容器一般叫FormItem,它主要负责数组周围的交互样式,比如外键标题、错误消息样式等。如果我们想覆盖更多的联动,比如作为单独的数据和FormItem的联动,还必须添加外包装容器。 还有很多很多的属性,这里无法一一列举。

从里面的思路我们可以看到,为了解决联动问题,无论我们如何可视化,最终都会将数组模型可视化,其中包含了与数组相关的所有状态。 只要我们操作这个状态,就可以引起联动。

关于精准渲染,我们已经确定可以选择类似Mobx的Reactive方案。 虽然是新轮子,但是Reactive的这些模式还是非常适合具体的响应式模型。 因此,基于Reactive的能力,Formily经历了不断的试错和修正。 ,最终设计出真正尊贵的形态模型。 这样的表单模型解决了表单域的问题,因此也称为域模型。 有了这样的领域模型,我们就可以让表单的联动显得可枚举、可预测,这也描述了上面描述的合约的联动。 奠定了坚实的基础。

路径系统

后面会提到表单域模型中的数组模型。 如果设计更完整的话,也许不仅是阵列模型,还可以有一个表格模型作为顶层模型。 顶层模型管理所有阵列模型。 每个数组都有自己的路径,如何找到这个数组呢? 上面提到的联动关系更多的是一种被动依赖关系,在某些场景下,我们只是想根据异步风暴动作来改变数组的状态。 这里涉及到如何优雅的查找一个数组。 经过大量的试错和修正,Formily独创的路径系统@formily/path很好的解决了这个问题。 它不仅让数组搜索看起来很优雅,还可以通过重构表达式来处理前后端数据结构不一致的恶心问题。

生命周期

使用Mobx和路径系统,我们已经构建了一个相对完整的表单解决方案。 然而,具体化之后,我们的解决方案就像一个黑匣子。 外界很难感知解的内部状态流程。 实现一些逻辑是不可能的,所以这里我们需要另一个概念,生命周期,只要我们把整个表单生命周期作为storm hook暴露给外界,这样就可以实现既具体又灵活的表单解决方案。

契约驱动

如果要实现动态可配置的表单,则必须使表单结构看起来可序列化。 序列化的方法有很多种。 它可以是基于UI的UI描述契约,也可以是基于数据的数据描述。 契约,既然表单本身就是维护一份数据,自然对于表单场景来说,数据契约是最合适的,但是要描述数据结构,当今业界最流行的是JSON-Schema,因为JSONSchema 合约本身有很多与校准相关的属性,这些属性自然与表单校准相关。 UI描述契约真的不适合描述表单吗? 不,UI描述契约适合更通用的UI表达。 描述形式没有问题,但会更多是后端合约。 相反,JSON-Schema在前端模型层是可表达的,在描述数据上表现力更强。 共同点,所以两个合约都有各自的原理,但是在简单形式领域,JSON-Schema会更加面向领域。

那么,如果选择 JSON-Schema,我们如何描述 UI、如何描述逻辑呢? 单纯描述数据,想要输出可供实际业务使用的表单页面是不太现实的。

React-jsonschema-form的解决方案是数据是数据,UI是UI。 这样做的用处在于,每个合约都是非常纯粹的合约,但是带来了很大的维护成本和理解成本。 用户需要制定一种形式,就需要在两种契约心态之间不断切换。 因此,如果从技术角度来看这样的拆分似乎是非常合理的,而从产品角度来看,这种拆分则是将成本抛给了用户,因此Formily的表单合约会更倾向于在JSON-Schema上进行扩展。

那么,如何扩展呢? 为了不污染标准的JSON-Schema属性,我们统一用x-*格式表示扩展属性:

{
  "type""string",
  "title""字符串",
  "description""这是一个字符串",
  "x-component""Input",
  "x-component-props": {
    "placeholder""请输入"
  }
}

从这个角度来看,UI契约和数据契约是混合在一起的。 只要有统一的扩展协议,仍然可以保证两个合约责任单一。

之后,如果您想将 UI 容器包装在各个数组周围怎么办? 在这里,Formily 定义了一个名为 void 的新模式类型。 void并不陌生,W3C规范中还有voidelement,js中还有void关键字,后者代表虚拟元素,前者代表虚拟指针。 因此,在JSONSchema中,引入了void来表示虚拟的数据节点,表示该节点不占用实际的数据结构。 所以,我们可以这样做:

{
  "type""void",
  "title""卡片",
  "description""这是一个卡片",
  "x-component""Card",
  "properties": {
    "string": {
      "type""string",
      "title""字符串",
      "description""这是一个字符串",
      "x-component""Input",
      "x-component-props": {
        "placeholder""请输入"
      }
    }
  }
}

这样就可以描述一个UI容器了。 既然UI容器是可以描述的,我们就可以很方便地封装一个基于场景的组件,比如FormStep,那么我们如何描述数组之间的联动呢? 例如,一个数组需要控制另一个数组的显示和隐藏。 我们做得到:

{
  "type""object",
  "properties": {
    "source": {
      "type""string",
      "title""Source",
      "x-component""Input",
      "x-component-props": {
        "placeholder""请输入"
      }
    },
    "target": {
      "type""string",
      "title""Target",
      "x-component""Input",
      "x-component-props": {
        "placeholder""请输入"
      },
      "x-reactions": [
        {
          "dependencies": ["source"],
          "when""{{$deps[0] == '123'}}",
          "fulfill": {
            "state": {
              "visible"true
            }
          },
          "otherwise": {
            "state": {
              "visible"false
            }
          }
        }
      ]
    }
  }
}

使用 x-reactions 来描述目标数组,这取决于源数组的值。 如果值为“123”,则显示目标数组,否则隐藏。 这些联动方式都是被动联动。 如果我们想实现主动联动怎么办? ? 可以是这样的:

{
  "type""object",
  "properties": {
    "source": {
      "type""string",
      "title""Source",
      "x-component""Input",
      "x-component-props": {
        "placeholder""请输入"
      },
      "x-reactions": [
        {
          "when""{{$self.value == '123'}}",
          "target""target",
          "fulfill": {
            "state": {
              "visible"true
            }
          },
          "otherwise": {
            "state": {
              "visible"false
            }
          }
        }
      ]
    },
    "target": {
      "type""string",
      "title""Target",
      "x-component""Input",
      "x-component-props": {
        "placeholder""请输入"
      }
    }
  }
}

您只需更改 x-reactions 的位置,将其放在源数组上,然后指定目标即可。

可以看到,我们的联动,虽然核心是基于:

由于内部状态管理采用类似Mobx的@formily/reactive方案,Formily轻松实现被动和主动联动场景,覆盖大部分业务需求。

因此,我们的表单完全可以通过合约来描述,无论多么复杂的布局,或者非常复杂的联动,都可以配置。

分层架构

我们一开始就谈到了各种问题的解决方案,那么如何设计才能让Formily变得更加自洽和高贵呢?

该图主要将Formily分为内核层、UI桥接层、扩展组件层、配置应用层。

内核层是UI无关的,这保证了用户管理的逻辑和状态不耦合到任何框架elementui联动显示,这有几个好处:

JSONSchema独立存在,由UI桥接层消费,保证了不同UI框架下合约驱动的绝对一致性elementui联动显示,不需要重复实现合约解析逻辑。

扩展组件层,提供一系列表单场景组件,保证用户开箱即用。 无需花费大量时间进行二次开发。

核心优势 核心劣势

学习成本相对较高。 即使2.x已经融合了大量的概念,但仍然存在一定的学习成本。

终于

文章最后,希望这个开源解决方案能够对您有所帮助,也欢迎各位码农支持和点赞这个开源库,让它越来越好~

github:

同时,如果您对后端可视化和工程化感兴趣,还可以关注以下项目,目前这些项目正在持续迭代中:

欢迎长按图片加刷碗智为好友我会第一时间和你分享前端行业趋势,学习途径,搞怪趣事,生活中的另一面幽默等等。新的一年我们一起洗刷刷!!!!!!