可视化搭建工具技术探索之表单

做者:橙子前端

前言

说到页面可视化搭建,想必不少同窗都有所了解,业内已有很是多文章介绍,具体能够查看底部传送门,本文仅从 如何搭建一个易用、可扩展的通用可视化搭建工具 出发,探索技术思路,以及在实际实践中思考,欢迎互相探讨。vue

关于相关工具,业内开源及商业化产品很是多,可是通用的、知足定制化业务的却很难找到,缘由有不少:react

  • 用户不一样,相同功能的组件运营同窗与研发同窗诉求不一样,运营但愿简单,研发但愿二次开发能力
  • 场景不一样,配运营活动与配流程表单,使用的组件几乎彻底不一样。
  • 设计器要求不一样,不一样系统对设计器界面要求不一样,面板能力也不一样。
  • 开发者偏好不一样,有人偏向react开发,有人偏向vue,使用组件库也不尽相同。
  • ...

因为以上某些缘由,致使一些开源工具不能很好的在实际业务落地,不少时候就只能本身开发,或基于开源二次改造。git

为何要尝试作一个通用的可视化搭建工具呢?github

可视化配置工具做为一种提效工具,若是只是为了知足自身业务就搞一套,从更大范围看,是提效了仍是减效了?即便一个团队、一个部门能够作到通用,整个公司却不必定,就会遇到常被DISS的“重复造轮子”,解释起来基本就是有自身定制的需求,别的工具不能知足。所以也很难造成统一的可视化配置组件及规范。web

虽然有不少定制化场景及偏好问题,但从技术层面来看,有不少类似的地方:json

一、须要设计器,可添加、拖拽、配置组件
二、提供渲染能力
三、组件间可通讯
四、表单场景可联动等markdown

假设设计器能定制,不一样组件库实现的可视化组件可在不一样设计器中运行,经过底层一套schema或DSL规范约束。这样就能很大程度解决组件共享问题,从而大幅减小重复开发成本。实现一个设计器及定制组件并不难,难的是如何达成这样的规范,同时支持扩展。组件和设计器只是上层实现而已。app

借用一句毛爷爷的话:异步

道路是曲折的,前途是光明的

下面具体来看本身在尝试实现过程的一些思考

1、划分

一、按场景分

如下为比较典型的业务可视化配置场景:

场景 用户 特色 用途
运营活动 运营同窗 数量多,定制化强、需快速上线 通常配置运营活动、落地页、抽奖等
流程表单 流程实施 对表单能力要求高,表单内外联动、公式计算等 配合流程设计,实现业务流转
业务报表 产品、运营等 以查询表单+可定制列的表格以及图表配置为表明 对流程及业务结果展现
个性化页面 普通用户 配置应更简单,交互要求高 个性化诉求。如用户主页、定制工做台等
中后台页面 前端研发 须要有代码扩展能力、专业性强 前端提效。解决繁琐、重复开发

固然以上也只是可视化配置的几种典型场景,若是配置化能力足够强,或许基于此,解决前端大部分开发工做也不是不可能。

二、按用户专业性分

从设计器开发到最终使用,涉及不一样角色的用户:

  1. 设计器开发者:保证设计器的独立与业务解耦,关注底层能力、设计器通用性、灵活性
  2. 组件开发者:通用组件、业务组件开发。关注组件用处、业务定制性等
  3. 配置人员:添加、拖拽配置、发布等。关注配置难度、灵活性、组件是否丰富。
  4. 最终用户:使用最终发布的页面。关注使用体验,打开是否快、功能是否正常等

从配置难度来看,可视化工具一般有如下几种:

  • NoCode:顾名思义,彻底不须要编码能力,好比运营活动配置、用户个性化主页、流程表单、业务报表等。一般须要基于特定场景定制化组件
  • LowCode:大部分界面和功能可经过可视化方式配置,可是完整功能还须要借助少许代码完成。如定制的表单关联、表单的提交逻辑等。
  • ProCode:需具有专业前端代码能力,对应传统研发。特色是交互周期长,研发成本高

能够看下【可视化搭建工具与页面】

工具与页面实现关系

能够发现,LowCodeNoCodeProCode都能实现最终页面(蓝色)。ProCode能力是最强的,能够实现所有场景功能,同时还能实现LowCodeNoCode平台或工具自己

三、按典型交互分

  • 表单交互:涉及表单校验、联动、提交、值回显等
  • 展现页:较少或无需用户输入,以展现为主,部分个性化配置。如业务报表、用户个性化主页、运营活动等。

以上按不一样方式对可视化配置工具进行了分类,不必定很是准确,但基本都有所覆盖。从技术出发,结合特色和诉求,如何实现这样一个通用工具是一个值得探索的问题。

2、问题探索

如下暂且列了部分表单问题、自定义诉求、通用能力三个方面典型问题

  • 表单问题
  1. 表单校验,表单规则定义探索及如何自定义规则?
  2. 表单联动,表单内字段如何联动?表单内值变动或者触发事件,如何联动表单外?表单外事件如何联动表单内组件?
  • 自定义诉求
  1. 自定义组件,如何自定义一个普通组件?如何自定义容器组件?组件基础配置不知足时从新开发仍是扩展配置?
  2. 自定义设计器,当设计器嵌入业务系统时,设计器应具有怎样的开放能力以实现低成本、无缝衔接?
  • 通用能力
  1. 国际化,设计器国际化、翻译预料管理
  2. 自定义样式
  3. PC端与H5同时配置

以上只是工具形式提供能力时可能碰见的几个典型问题,固然问题远远不止这些,升级到平台会涉及更多的问题。篇幅有限如下主要探索 表单场景 的典型问题,其余问题留给后续探索。

一、典型组件应该具有哪些部分

轮播图 组件为例,不一样专业程度用户但愿配置的属性不一样。

  • 对于 NoCode 用户而言,可能只须要配置以下属性:轮播图个数、拖动添加图片、配置图片跳转连接、输入轮播时间间隔、选择轮播切换动画等
  • 对于 LowCode 用户而言,除了以上配置之外,还能够配置图片上传接口、请求方式、上传请求参数、接口返回转换脚本等,这样在更大程度上复用,同时使用难度也增长了。

虽然最终展现结果相同,可是配置却不一样,这种状况下是否能够作成一个组件呢?我的以为是能够的,好比把更多属性配置放在高级里,或者让组件之间能够继承等。

那么一个组件应该具有哪些部分呢?

从示例能够发现,首先须要有最终展现部分,其次须要有配置部分,还须要定义配置项。这里将最终展现的部分称View,配置部分称Setting,定义配置称为Schema。其关系大体以下:

组件内部关系

SettingView经过Schema关联起来,Schema实例化后为json数据可保存到服务端。Setting表单修改SchemaSchema变化影响View变化。

二、表单场景典型问题

1)、表单验证

表单规则定义探索及如何自定义规则?

从主流组件库来看,不考虑联动校验规则状况下,输入框比下拉框、单选、时间等组件的规则要复杂些。后者只须要作选择,通常增长是否必填规则便可,而前者除了必填,还有对字符作特别校验。一般 用户可输入的组件比提供选项选择的组件在规则上要复杂些

表单组件通常都至少有一条规则,如 必填,固然也有例外,好比 开关组件(switch),无论是true 仍是 false 必填对其来讲都没有意义

所以可在组件schema上能够定义required字段表示时候必填,如:

{
  "name": "firstName",
  "required": true,
  "errorMessage": "这是必填项"
}
复制代码

对于只须要必填规则的组件来讲,这样定义彷佛并无什么问题。然而不少时候一个组件每每有多个规则同时生效,如:但愿该字段必填,能配置对应错误信息,同时还要求字符串长度有限制,对应过长或太短都能给相应的错误提示。用以上定义就不太好知足了,因而能够升级一下:

{
  "name": "firstName",
  "rules": [
    { "required": true, "message": "这是必填项" },
    { "min": 3, "message": "最小长度不能小于3" },
    { "max": 10, "message": "最大长度不能超过10" }
  ]
}
复制代码

这样看起来清晰了不少,同时支持多条规则组合。这也是主流UI组件库都在用的表单校验 async-validatorrules字段应与 async-validator 在使用上保持一致,这样就能够利用第三方库作规则校验了,

由于表单基本都有一条必填规则,能够约定rules字段第一个规则为必填,其他规则根据实际状况由配置人员动态添加。

注意schema.rules中的每条规则字段类型与async-validator并不是一一对应,缘由是咱们的schema将以json的形式保存到服务端或本地,因此一些特殊字段如自定义校验函数或正则等,就必须转成相应字符串了。

  • async-validator 字段规则描述:
{
  "type": "string",
  "validator": (rule, value) => value === 'test',
  "message": "请输入 test"
}
复制代码
  • schema.rules中单条规则描述
{
  "type": "string",
  "validator": "(rule, value) => value === 'test'",
  "message": "请输入 test"
}
复制代码

所以,设计器底层须要对表单规则提供解析模块(Rule)。这个只是实现规则层面,对配置层面的话,让配置人员写这些代码实在有些勉强,而提供可视化的方式选择或简单填写就颇有必要,以下图:

自定义扩展规则

经常使用规则能够内置到设计器底层。实际业务中,每每会有自定义的复杂规则,或者异步校验等,那么:

如何能配置规则的同时,还能根据不一样业务场景扩展规则呢?

这里就要求设计器对表单规则有扩展能力。一种多是在配置的时候,直接经过脚本实现规则,仅适用于前端开发。第二种是组件开发同窗,提早开发好规则,而后建立设计器时扩展规则,最后在配置规则时选择便可。这里讨论第二种实现。

  • 实现手机号规则示例
// ./PhoneRule.js
export default class PhoneRule {
  static get type () {
    return 'phone'
  }
  static get name () {
    return '手机号'  // 用于可视化显示
  }
  constructor (rule = {}) {
    const defaultRule = {
      type: 'pattern',
      pattern: '',
      message: '手机号不正确'
    }
    this.origin = Object.assign({}, defaultRule, rule)
    this.rule = {
      type: 'pattern',
      trigger: 'blur',
      pattern: /^1[3-9]\d{9}$/g,
      message: ''
    }
    this.update(this.origin)
  }

  update (rule) {
    if (rule) {
      this.rule.message = rule.message
      Object.assign(this.origin, rule)
    }
  }
}

复制代码
  • 应用规则及传入规则示例
import { Rule } from 'epage-core'
import PhoneRule from './PhoneRule.js'

Rule.set({ PhoneRule })
// 应用规则:PhoneRule的type静态属性对应phone
helper.setValidators(widgets, { input: ['phone'] })
// 传入规则
new Epage({
  Rule,
  // ...
})
复制代码
  • 最终配置input组件时,能够看见增长了 手机号 规则

自定义扩展规则

2)、表单联动

这里先给一个我的理解的联动定义

表单联动通常是指 一个或多个表单字段值或属性 发生 变化,使其余 一个或多个表单字段值或属性 变化的交互。

这里有几个关键点:一个或多个表单字段值或属性变化

i、联动示例

表单联动

好比能够为如下任意联动关系:

  • 一对一:
No. 影响字段 关系 被影响字段 属性
1 城市 属于 中国 --> 学校 可选学校
  • 一对多:
No. 影响字段 关系 被影响字段 属性
1 城市 属于 中国 --> 学校 可选学校
专业 可选专业
  • 多对一:
No. 影响字段 关系 被影响字段 属性
1 城市 属于 中国 --> 学校 可选学校
2 在校人数 大于 1万

12之间多是 也多是 的关系

  • 多对多:
No. 影响字段 关系 被影响字段 属性
1 城市 属于 中国 --> 学校 可选学校
2 在校人数 大于 1万 专业 可选专业

12之间多是 也多是 的关系

注意:

  • 多个影响字段之间多是也多是关系
  • 影响字段能够等于属于等多关系与值创建条件
  • 被影响字段之间通常不存在的关系
  • 多级关联,如 a字段影响b字段,b字段影响c字段等,可经过多个两级关联配置

以上是基于影响字段角度考虑关联。固然也能够从被影响字段的角度考虑关联,在一些时候更直观,如:

{
  "widget": "input",
  "name": "c",
  "hidden": "$a.hidden === false && $b.hiden === true"
}
复制代码

以上schema描述会有如下很差的地方:

一、会让hidden原本为boolean类型,却变成了字符串表达式。
二、若是hidden原本就是字符串类型的字段,又怎么区分是具体值仍是表达式呢?固然也能够在扩展字段
三、不一样字段属性逻辑比较分散,不方便统一管理

以上示例联动中,影响字段经过改变自身的表单值来触发联动逻辑。这里的值能够是等于关系,也能够是包含小于等关系,取决于值类型。如:

  • 布尔能够是等于不等于
  • 字符串能够是等于不等于包含不包含
  • 数字能够是等于不等于大于小于大于等于小于不等于

因为不一样表单组件值类型可能不一样,因此能够做为静态属性定义到组件的Schema上,如:

class InputSchema extends FormSchema {}

Object.assign(InputSchema, {
  logic: {
    value: ['=', '!=', '<>', '><'] // [等于, 不等于, 包含, 不包含]
  }
})
复制代码

若是把 表单字段 当作一个对象,表单值(value)当作一个特殊属性,还有一些普通属性,如显隐(hidden)、禁用(disabled)等,就会发现联动就是属性与属性之间逻辑绑定。如何作到监听value变化以及普通属性变化呢?

value之因此认为是特殊属性主要缘由:

该属性的变化会触发onchange事件。对应hiddendisabled等普通属性却没有,理论上也应该有onhiddenondisabled相应事件。

若是把 表单字段 全部属性定义成响应式,任意属性变化时就能很方便通知到。也能够本身实现订阅发布方式,来修改表单属性。

表单联动示意2

  • 一种是表单字段属性符合某种条件后,联动其余表单字段属性变化,这里称值联动
  • 另外一种是表单组件发生了某个事件(如onchange),联动其余表单字段属性变化,这里称事件联动

从必定程度讲两者方式都能解决部分相同功能的联动,如A组件value值发生变化,也能够认为是A组件发生onchange事件

ii、联动实现

如下以开发 epage 部分实现为例分析(暂未实现多对1、多对多关联逻辑)

首先,逻辑定义

定义schema上应该保存的逻辑结构。具体逻辑定在单个组件的Schema上仍是最外层Schema均可以,这里定义到统一的地方,方便管理。

主要定义 影响组件被影响组件:包括联动类型、影响表单组件值符合某种条件、被影响表单组件哪些属性联动、影响表单触发的什么事件等

{
  // schema 其余字段
  logics: [
    {
      "key": "kB1mKTnek", // 影响组件key
      "type": "value",    // 关联类型,值联动 或 事件联动
      "action": "=",      // 值联动是相等关系,这里定义不一样符号,应该提供符号解析能力
      "value": "show",    // 具体值
      "effects": [        // 被影响组件列表
        {
          "key": "kASJAJwRB", // 被影响组件key
          "properties": [
            { "key": "hidden", "value": true },   // 被影响组件隐藏
            { "key": "disabled", "value": true }  // 被影响组件禁用,还应能够为其余属性
          ]
        }
      ]
    }
  ]
}

复制代码

其次,逻辑解析

  • 逻辑管理

基于以上分析,应具有值逻辑事件逻辑。在渲染或预览时执行生效

import EventLogic from './EventLogic'
import ValueLogic from './ValueLogic'

class Logic {
  // 检查值逻辑配置是否合法,是否有重复逻辑等
  // 返回 { patches, scripts },对应比较结果和可能的自定义脚本
  diffValueLogics(){}
  // 同上
  diffEventLogics(){}
  // 根据以上比较结果,最终修改组件Schema属性
  applyPatches(){}
  // 检查被影响组件是否有效等
  checkEffect(){}
}
复制代码
  • 值逻辑部分实现示例

对以上生成的 逻辑关系 进行解析。如值联动中 action字段就有不少比较关系(=(等于)、!=(不等于)、>(大于)、<(小于)、<>(包含)等),以=为例:

class ValueLogic{
  constructor () {
    this.map = {
      '=': {
        key: '=',
        value: '等于',
        // left、right为用户输入值都为字符串,valueType为应该的数据类型
        // 但左右值类型与valueType不一致时,根据状况进行转换后比较
        validator: (left, right, { valueType }) => {
          const booleanMap = { true: true, false: false }
          let leftValue = left
          let rightValue = right

          if (valueType === 'number') {
            leftValue = parseFloat(left)
            rightValue = parseFloat(right)

            return (isNaN(leftValue) || isNaN(leftValue)) ? false : leftValue === rightValue
          } else if (valueType === 'boolean') {
            if (right in booleanMap) {
              rightValue = booleanMap[right]
            }
          }
          return leftValue === rightValue
        }
      },
      // ...
    }
  }
}
复制代码

为了让设计器更具备通用性,逻辑关系定义及解析也应支持组件开发者扩展。

具体值逻辑或事件逻辑的一些实现能够参考 epage#Logic

3、总结

作一个可视化配置工具并不难,可是既要保证通用,又能保证扩展性,同时统一标准一块儿共建却不容易。须要创建一套统一SchemaDSL,不一样开发者能认同,可根据须要扩展定制,进而达到快速实现业务交付目标。

传送门

关于做者团队

滴滴效能平台前端团队EFE,感召于经过技术持续提高组织效能的组织使命,致力于打造技术领先的前端技术团队,深耕于性能监控、质量监控、低代码配置、文档协做、微前端、webIDE等多个领域,技术方向广阔,探索空间和成长空间极大。

咱们是一个充满激情和有梦想的团队,期待您的加入。感兴趣的可联系 dumingtan@didiglobal.com

相关文章
相关标签/搜索