作者:橙子
前言
说到页面可视化搭建,想必很多同学都有所了解,业内已有非常多文章介绍,具体可以查看底部传送门,本文仅从 如何搭建一个易用、可扩展的通用可视化搭建工具 出发,探索技术思路,以及在实际实践中思考,欢迎互相探讨。
关于相关工具,业内开源及商业化产品非常多,但是通用的、满足定制化业务的却很难找到,原因有很多:
- 用户不同,相同功能的组件运营同学与研发同学诉求不同,运营希望简单,研发希望二次开发能力
- 场景不同,配运营活动与配流程表单,使用的组件几乎完全不同。
- 设计器要求不同,不同系统对设计器界面要求不同,面板能力也不同。
- 开发者偏好不同,有人偏向react开发,有人偏向vue,使用组件库也不尽相同。
- ...
由于以上某些原因,导致一些开源工具不能很好的在实际业务落地,很多时候就只能自己开发,或基于开源二次改造。
为什么要尝试做一个通用的可视化搭建工具呢?
可视化配置工具作为一种提效工具,如果只是为了满足自身业务就搞一套,从更大范围看,是提效了还是减效了?即使一个团队、一个部门可以做到通用,整个公司却不一定,就会遇到常被DISS的“重复造轮子”,解释起来基本就是有自身定制的需求,别的工具不能满足。因此也很难形成统一的可视化配置组件及规范。
虽然有很多定制化场景及偏好问题,但从技术层面来看,有很多相似的地方:
1、需要设计器,可添加、拖拽、配置组件
2、提供渲染能力
3、组件间可通信
4、表单场景可联动等
假设设计器能定制,不同组件库实现的可视化组件可在不同设计器中运行,通过底层一套schema或DSL规范约束。这样就能很大程度解决组件共享问题,从而大幅减少重复开发成本。实现一个设计器及定制组件并不难,难的是如何达成这样的规范,同时支持扩展。组件和设计器只是上层实现而已。
借用一句毛爷爷的话:
道路是曲折的,前途是光明的
下面具体来看自己在尝试实现过程的一些思考
一、划分
1、按场景分
以下为比较典型的业务可视化配置场景:
场景 | 用户 | 特点 | 用途 |
---|---|---|---|
运营活动 | 运营同学 | 数量多,定制化强、需快速上线 | 一般配置运营活动、落地页、抽奖等 |
流程表单 | 流程实施 | 对表单能力要求高,表单内外联动、公式计算等 | 配合流程设计,实现业务流转 |
业务报表 | 产品、运营等 | 以查询表单+可定制列的表格以及图表配置为代表 | 对流程及业务结果展示 |
个性化页面 | 普通用户 | 配置应更简单,交互要求高 | 个性化诉求。如用户主页、定制工作台等 |
中后台页面 | 前端研发 | 需要有代码扩展能力、专业性强 | 前端提效。解决繁琐、重复开发 |
当然以上也只是可视化配置的几种典型场景,如果配置化能力足够强,或许基于此,解决前端大部分开发工作也不是不可能。
2、按用户专业性分
从设计器开发到最终使用,涉及不同角色的用户:
- 设计器开发者:保证设计器的独立与业务解耦,关注底层能力、设计器通用性、灵活性
- 组件开发者:通用组件、业务组件开发。关注组件用处、业务定制性等
- 配置人员:添加、拖拽配置、发布等。关注配置难度、灵活性、组件是否丰富。
- 最终用户:使用最终发布的页面。关注使用体验,打开是否快、功能是否正常等
从配置难度来看,可视化工具通常有以下几种:
- NoCode:顾名思义,完全不需要编码能力,比如运营活动配置、用户个性化主页、流程表单、业务报表等。通常需要基于特定场景定制化组件
- LowCode:大部分界面和功能可通过可视化方式配置,但是完整功能还需要借助少量代码完成。如定制的表单关联、表单的提交逻辑等。
- ProCode:需具备专业前端代码能力,对应传统研发。特点是交互周期长,研发成本高
可以看下【可视化搭建工具与页面】
可以发现,LowCode
、NoCode
、ProCode
都能实现最终页面(蓝色)。ProCode
能力是最强的,可以实现全部场景功能,同时还能实现LowCode
、NoCode
平台或工具本身
3、按典型交互分
- 表单交互:涉及表单校验、联动、提交、值回显等
- 展示页:较少或无需用户输入,以展示为主,部分个性化配置。如业务报表、用户个性化主页、运营活动等。
以上按不同方式对可视化配置工具进行了分类,不一定非常准确,但基本都有所覆盖。从技术出发,结合特点和诉求,如何实现这样一个通用工具是一个值得探索的问题。
二、问题探索
以下暂且列了部分表单问题、自定义诉求、通用能力三个方面典型问题
- 表单问题
- 表单校验,表单规则定义探索及如何自定义规则?
- 表单联动,表单内字段如何联动?表单内值变更或者触发事件,如何联动表单外?表单外事件如何联动表单内组件?
- 自定义诉求
- 自定义组件,如何自定义一个普通组件?如何自定义容器组件?组件基础配置不满足时重新开发还是扩展配置?
- 自定义设计器,当设计器嵌入业务系统时,设计器应具备怎样的开放能力以实现低成本、无缝衔接?
- 通用能力
- 国际化,设计器国际化、翻译预料管理
- 自定义样式
- PC端与H5同时配置
以上只是工具形式提供能力时可能遇见的几个典型问题,当然问题远远不止这些,升级到平台会涉及更多的问题。篇幅有限以下主要探索 表单场景 的典型问题,其他问题留给后续探索。
1、典型组件应该具备哪些部分
以 轮播图 组件为例,不同专业程度用户希望配置的属性不同。
- 对于 NoCode 用户而言,可能只需要配置如下属性:轮播图个数、拖动添加图片、配置图片跳转链接、输入轮播时间间隔、选择轮播切换动画等
- 对于 LowCode 用户而言,除了以上配置以外,还可以配置图片上传接口、请求方式、上传请求参数、接口返回转换脚本等,这样在更大程度上复用,同时使用难度也增加了。
虽然最终展示结果相同,但是配置却不同,这种情况下是否可以做成一个组件呢?个人觉得是可以的,比如把更多属性配置放在高级里,或者让组件之间可以继承等。
那么一个组件应该具备哪些部分呢?
从示例可以发现,首先需要有最终展示部分,其次需要有配置部分,还需要定义配置项。这里将最终展示的部分称View
,配置部分称Setting
,定义配置称为Schema
。其关系大致如下:
Setting
与View
通过Schema
关联起来,Schema
实例化后为json
数据可保存到服务端。Setting
表单修改Schema
,Schema
变化影响View
变化。
2、表单场景典型问题
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-validator。rules
字段应与 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万 |
1
与2
之间可能是 且 也可能是 或 的关系
- 多对多:
No. | 影响字段 | 关系 | 值 | 被影响字段 | 属性 | |
---|---|---|---|---|---|---|
1 | 城市 | 属于 | 中国 | --> | 学校 | 可选学校 |
2 | 在校人数 | 大于 | 1万 | 专业 | 可选专业 |
1
与2
之间可能是 且 也可能是 或 的关系
注意:
- 多个影响字段之间可能是
且
也可能是或
关系 - 影响字段可以
等于
、属于
等多关系与值建立条件 - 被影响字段之间一般不存在
且
与或
的关系 - 多级关联,如
a
字段影响b
字段,b
字段影响c
字段等,可通过多个两级关联配置
以上是基于影响字段角度考虑关联。当然也可以从被影响字段的角度考虑关联,在一些时候更直观,如:
{
"widget": "input",
"name": "c",
"hidden": "$a.hidden === false && $b.hiden === true"
}
以上schema描述会有以下不好的地方:
1、会让hidden
本来为boolean
类型,却变成了字符串表达式。
2、如果hidden
本来就是字符串类型的字段,又怎么区分是具体值还是表达式呢?当然也可以在扩展字段
3、不同字段属性逻辑比较分散,不方便统一管理
以上示例联动中,影响字段
通过改变自身的表单值
来触发联动逻辑。这里的值可以是等于
关系,也可以是包含
、小于
等关系,取决于值类型。如:
- 布尔可以是
等于
、不等于
- 字符串可以是
等于
、不等于
、包含
、不包含
- 数字可以是
等于
、不等于
、大于
、小于
、大于等于
、小于不等于
由于不同表单组件值类型可能不同,所以可以作为静态属性定义到组件的Schema
上,如:
class InputSchema extends FormSchema {}
Object.assign(InputSchema, {
logic: {
value: ['=', '!=', '<>', '><'] // [等于, 不等于, 包含, 不包含]
}
})
如果把 表单字段 当做一个对象,表单值(value
)当做一个特殊属性,还有一些普通属性,如显隐(hidden)、禁用(disabled)等,就会发现联动就是属性与属性之间逻辑绑定。如何做到监听value
变化以及普通属性变化呢?
value
之所以认为是特殊属性主要原因:该属性的变化会触发
onchange
事件。对应hidden
、disabled
等普通属性却没有,理论上也应该有onhidden
、ondisabled
相应事件。
如果把 表单字段 所有属性定义成响应式,任意属性变化时就能很方便通知到。也可以自己实现订阅发布方式,来修改表单属性。
- 一种是表单字段属性符合某种条件后,联动其他表单字段属性变化,这里称
值联动
- 另一种是表单组件发生了某个事件(如
onchange
),联动其他表单字段属性变化,这里称事件联动
从一定程度讲二者方式都能解决部分相同功能的联动,如A组件value值发生变化,也可以认为是A组件发生onchange
事件
ii、联动实现
以下以开发 epage 部分实现为例分析(暂未实现多对一、多对多关联逻辑)
首先,逻辑定义
定义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
三、总结
做一个可视化配置工具并不难,但是既要保证通用,又能保证扩展性,同时统一标准一起共建却不容易。需要建立一套统一Schema
或 DSL,不同开发者能认同,可根据需要扩展定制,进而达到快速实现业务交付目标。
传送门:
关于作者团队
滴滴效能平台前端团队EFE,感召于通过技术持续提升组织效能的组织使命,致力于打造技术领先的前端技术团队,深耕于性能监控、质量监控、低代码配置、文档协作、微前端、webIDE等多个领域,技术方向广阔,探索空间和成长空间极大。
我们是一个充满激情和有梦想的团队,期待您的加入。感兴趣的可联系 dumingtan@didiglobal.com