做者:橙子前端
说到页面可视化搭建,想必不少同窗都有所了解,业内已有很是多文章介绍,具体能够查看底部传送门,本文仅从 如何搭建一个易用、可扩展的通用可视化搭建工具 出发,探索技术思路,以及在实际实践中思考,欢迎互相探讨。vue
关于相关工具,业内开源及商业化产品很是多,可是通用的、知足定制化业务的却很难找到,缘由有不少:react
因为以上某些缘由,致使一些开源工具不能很好的在实际业务落地,不少时候就只能本身开发,或基于开源二次改造。git
为何要尝试作一个通用的可视化搭建工具呢?github
可视化配置工具做为一种提效工具,若是只是为了知足自身业务就搞一套,从更大范围看,是提效了仍是减效了?即便一个团队、一个部门能够作到通用,整个公司却不必定,就会遇到常被DISS的“重复造轮子”,解释起来基本就是有自身定制的需求,别的工具不能知足。所以也很难造成统一的可视化配置组件及规范。web
虽然有不少定制化场景及偏好问题,但从技术层面来看,有不少类似的地方:json
一、须要设计器,可添加、拖拽、配置组件
二、提供渲染能力
三、组件间可通讯
四、表单场景可联动等markdown
假设设计器能定制,不一样组件库实现的可视化组件可在不一样设计器中运行,经过底层一套schema或DSL规范约束。这样就能很大程度解决组件共享问题,从而大幅减小重复开发成本。实现一个设计器及定制组件并不难,难的是如何达成这样的规范,同时支持扩展。组件和设计器只是上层实现而已。app
借用一句毛爷爷的话:异步
道路是曲折的,前途是光明的
下面具体来看本身在尝试实现过程的一些思考
如下为比较典型的业务可视化配置场景:
场景 | 用户 | 特色 | 用途 |
---|---|---|---|
运营活动 | 运营同窗 | 数量多,定制化强、需快速上线 | 通常配置运营活动、落地页、抽奖等 |
流程表单 | 流程实施 | 对表单能力要求高,表单内外联动、公式计算等 | 配合流程设计,实现业务流转 |
业务报表 | 产品、运营等 | 以查询表单+可定制列的表格以及图表配置为表明 | 对流程及业务结果展现 |
个性化页面 | 普通用户 | 配置应更简单,交互要求高 | 个性化诉求。如用户主页、定制工做台等 |
中后台页面 | 前端研发 | 须要有代码扩展能力、专业性强 | 前端提效。解决繁琐、重复开发 |
固然以上也只是可视化配置的几种典型场景,若是配置化能力足够强,或许基于此,解决前端大部分开发工做也不是不可能。
从设计器开发到最终使用,涉及不一样角色的用户:
从配置难度来看,可视化工具一般有如下几种:
能够看下【可视化搭建工具与页面】
能够发现,LowCode
、NoCode
、ProCode
都能实现最终页面(蓝色)。ProCode
能力是最强的,能够实现所有场景功能,同时还能实现LowCode
、NoCode
平台或工具自己
以上按不一样方式对可视化配置工具进行了分类,不必定很是准确,但基本都有所覆盖。从技术出发,结合特色和诉求,如何实现这样一个通用工具是一个值得探索的问题。
如下暂且列了部分表单问题、自定义诉求、通用能力三个方面典型问题
以上只是工具形式提供能力时可能碰见的几个典型问题,固然问题远远不止这些,升级到平台会涉及更多的问题。篇幅有限如下主要探索 表单场景 的典型问题,其余问题留给后续探索。
以 轮播图 组件为例,不一样专业程度用户但愿配置的属性不一样。
虽然最终展现结果相同,可是配置却不一样,这种状况下是否能够作成一个组件呢?我的以为是能够的,好比把更多属性配置放在高级里,或者让组件之间能够继承等。
那么一个组件应该具有哪些部分呢?
从示例能够发现,首先须要有最终展现部分,其次须要有配置部分,还须要定义配置项。这里将最终展现的部分称View
,配置部分称Setting
,定义配置称为Schema
。其关系大体以下:
Setting
与View
经过Schema
关联起来,Schema
实例化后为json
数据可保存到服务端。Setting
表单修改Schema
,Schema
变化影响View
变化。
表单规则定义探索及如何自定义规则?
从主流组件库来看,不考虑联动校验规则状况下,输入框比下拉框、单选、时间等组件的规则要复杂些。后者只须要作选择,通常增长是否必填规则便可,而前者除了必填,还有对字符作特别校验。一般 用户可输入的组件比提供选项选择的组件在规则上要复杂些。
表单组件通常都至少有一条规则,如 必填,固然也有例外,好比 开关组件(
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
组件时,能够看见增长了 手机号 规则这里先给一个我的理解的联动定义
表单联动通常是指 一个或多个表单字段 的 值或属性 发生 变化,使其余 一个或多个表单字段 的 值或属性 变化的交互。
这里有几个关键点:一个或多个表单字段、值或属性、变化。
好比能够为如下任意联动关系:
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描述会有如下很差的地方:
一、会让hidden
原本为boolean
类型,却变成了字符串表达式。
二、若是hidden
原本就是字符串类型的字段,又怎么区分是具体值仍是表达式呢?固然也能够在扩展字段
三、不一样字段属性逻辑比较分散,不方便统一管理
以上示例联动中,影响字段
经过改变自身的表单值
来触发联动逻辑。这里的值能够是等于
关系,也能够是包含
、小于
等关系,取决于值类型。如:
等于
、不等于
等于
、不等于
、包含
、不包含
等于
、不等于
、大于
、小于
、大于等于
、小于不等于
因为不一样表单组件值类型可能不一样,因此能够做为静态属性定义到组件的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
事件
如下以开发 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
作一个可视化配置工具并不难,可是既要保证通用,又能保证扩展性,同时统一标准一块儿共建却不容易。须要创建一套统一Schema
或 DSL,不一样开发者能认同,可根据须要扩展定制,进而达到快速实现业务交付目标。
传送门:
滴滴效能平台前端团队EFE,感召于经过技术持续提高组织效能的组织使命,致力于打造技术领先的前端技术团队,深耕于性能监控、质量监控、低代码配置、文档协做、微前端、webIDE等多个领域,技术方向广阔,探索空间和成长空间极大。
咱们是一个充满激情和有梦想的团队,期待您的加入。感兴趣的可联系 dumingtan@didiglobal.com