公式编辑器怎么用(低代码之路 —— 公式编辑器)
什么是公式编辑器?
讲了这么多,那么公式编辑器 究竟是个啥子呢 ?请看下图:
我们先解释一下图中的三个部分:
- 区域 1 是公式编辑器的输入部分,这个部分是一个代码编辑器,这个代码编辑最核心的特性是,它可以对 “表单字段” 和 “函数” 进行联想输入。
- 区域 2 是公式编辑器的可用参数列表,这些字段都是从表单设计器中动态获取来的。
- 区域 3 是公式编辑器为提供的逻辑组合公式,你可以把它理解成一个 js 工具库,只不过我们现在把它可视化了,它里面涵盖了数字处理(例如:求平均数、求和、求绝对值等等)、文本处理和时间处理等等。
为什么说它是逻辑可视化的一小步呢 ?我们可以来模拟一种场景,我们现在使用低代码设计器设计了一张表单,这个表单里在提交之前需要对所有的字段都做一层验证,但是我们事先都不知道验证的规则,只有在设计这张表单时才知道,所以,我们现在有两条路:第一,手写逻辑代码注入;第二,利用 公式编辑器 编写规则注入。
其实对于编码人员来说,手写逻辑代码更灵活,但是 公式编辑器 的这种形式让更多人能参与这部分的编码成为了可能,这是低代码关于逻辑可视化的一个好的开始。
公式编辑器的执行原理:
这部分业务的核心在于 编辑 与 翻译。
formula-editor-react
作为公式的输入端,公式编辑器其实是比较重要的一环,完善的编辑器甚至能检验出输入规则的符合法性。这部分呢,我并没有花太多时间去造轮子,而是直接在 npm 库里面物色了一个:formula-editor-react。
作者大哥贴的实际效果图(实际跑起来也是保真的):
这个插件实现了关键字唤起,字段联想等,对于使用来说已经足够了,但是这个插件本身还是有一些坑,我们来细说一下。
formula-editor-react 的问题与对策
使用
插件的使用比较简单,像普通组件一样引入使用就行:
// 模拟的表单字段
const fieldList = [
{ name: "销量", value: "xl"},
{ name: "单价", value: "dj"},
{ name: "批发价", value: "pfj"},
{ name: "采购价", value: "cgj"},
{ name: "零售毛利", value: "lsml"},
]
// 模拟的公式库
const methodList = [
{ name: "平均值", value: "平均值(,)", realValue: "avg" },
{ name: "最大值", value: "最大值(,)", realValue: "max" },
{ name: "最小值", value: "最小值(,)", realValue: "min" },
{ name: "求和", value: "求和(,)", realValue: "sum" }
]
<FormulaEdit
className="code-editor"
ref={editRef} // 需要调用组件中插入value的方法时使用,提供insertValue()方法
theme="day" // 主题
height={200} // 高度
defaultValue={defaultCode} // 初始化值
fieldList={fieldList} // @唤起
methodList={methodList} // #唤起
normalList={normalList} // 自定义无需校验关键词
readOnly={false} // 是否只读
lineNumber={true} // 是否显示列数
onChange={getCode} // 回调
></FormulaEdit>
问题1:代码编号被覆盖
插件在常规启用之后,样式会出现一点问题:
编辑器的行号会被内容遮盖,需要稍微调一下:
.CodeMirror-line {
padding-left: 30px !important;
box-sizing: border-box;
}
问题2:API 不够用
下面是作者贴出来的 API,这个插件的底层还是依赖的大名鼎鼎的 codeMirror,还不知道 codeMirror 的同学可以去百度了解一下,可能以后就会派上用场。
为啥说它不够用呢 ?我们有两个很常规的场景它无法满足:第一,我希望每次打开编辑器弹窗的时候将编辑器清空;第二,我希望每次打开编辑器时编辑器能自动聚焦并开启光标闪烁。
这两个场景是不是再普通不过了?但是这位作者大哥的文档不支持,于是,我就把插件的实例打印出来瞅了瞅:
然后发现,这个 CodeMirrorEditor 那是异常眼熟,但是很遗憾,它里面也没有什么可用的突破口。直到,我点开了它的原型对象:
大家请看,这不就是 codeMirror 的 API 吗,也就是说,这个 CodeMirrorEditor 就是 codeMirror 实例。
但是这个地方,还有个问题大家要注意一下,我去翻了一下他的 package.json 文件,他这个地方用的是 5.x 版本。codeMirror 在 6.x 之后进行了大刀阔斧的更新,用法与 5.x 是完全不一样的,所以,这个时候我们要去翻 5.x 的 API。
过程我就省略了,这里我直接告诉大家结果吧:
// 编辑器聚焦
editRef.current.focus()
// 编辑器清空上一次输入
editRef.setValue('')
问题3:动态更改字段后,语法校验失效
什么意思呢 ? 这个插件本身做了一层比较简单的输入语法校验,比如说,我们现在表单里有两个字段:
const fieldList = [
{ name: "销量", value: "xl"},
{ name: "单价", value: "dj"}
]
现在,我们在编辑框里输入 @销量 ,编辑器的语法验证是通过状态。但是我如果胡乱输入了一些其他字符 @销量balabala ,这个时候编辑就会反馈说,你输入的字符不符合规则,你拿到这个提示之后呢就可以反馈给使用者,或者在保存之前做一些拦截性的措施。
那么,这个警告信息在哪儿看呢 ?
const getCode = (code, e) {
...
}
<FormulaEdit
...
onChange={getCode} // 回调
></FormulaEdit>
onChange 钩子函数会在用户每次进行输入时调用,第一个参数 code 能即时获取到当前编辑器的内容,而参数 e 中则会反馈出插件的详细信息,包括输入代码是否有语法问题。
那么,我们要讲的问题是什么呢 ?问题是,我们现在更新了表单字段,往里面加了一个字段:
const fieldList = [
{ name: "销量", value: "xl"},
{ name: "单价", value: "dj"},
{ name: "批发价", value: "pfj"}
]
此时,插件的验证语法验证功能对 @批发价 是失效的,当你输入 @批发价 时,onChange 中会报告代码错误。
这个问题有点棘手,我需要去看一下他的更新逻辑去让插件重载一下参数才行,但是大哥并没有提供源码,只有一个打包后的文件。
github 上我也去翻找了一遍,大哥并没有把项目传上去 ...
咋个办 ?我把上面插件实例里的可用方法翻了两三遍,把所以带 up、update 等等跟更新沾边的方法都试了一遍,还是没个结果,于是乎,我动起了打包后的代码的主意,好在确实让我发现了端倪:
好家伙,他居然把这些数据存进了 localStorage 里,于是我赶忙翻了一下 localStorage
好了,故事讲到这里,往往是我要说结论了。没错,我们只要在每次更新完表单字段之后,顺便把它往 localStorage 存一下,它这个语法验证就会生效了。
其实它这个验证,并不是真正的语法验证,只是对既有的表单字段和公式库做了一个字符对比,没有深奥的语法分析等等。
语法翻译的新思路
这一节其实是这篇文章真正要讲的内容,当我们走完规则的编辑和保存流程,来到对规则数据的使用这一步的时候,我们要怎么去做?
我们还是来举个例子,比如,我们在表单设计之初,编写了下面这样一条规则:
#AVERAGE(@参数1,@参数2,@参数3)
这条规则在我们从数据库拿出来时,它只是一个字符串,而我们要做的,就是把它翻译成 js 代码去执行。
面对这个问题,我的第一个念头是,第一步,我需要从公式库中识别出 AVERAGE 这个函数,然后通过正则拿到 (...) 里的所有参数,然后把参数传入,并执行函数 AVERAGE 就可以了。直白来讲,就是我得先拆分字符,并将公式函数与表单字段一一翻译解析,最后求出结果。
但是,公式编辑器其实是支持 与 或 与 四则 的,也就是,极端状况下,我们需要解析的公式有可能会变成这样:
(#AVERAGE(@参数1,@参数2,@参数3) > @参数4) 或 (@参数5 === @参数6)
这个可就麻烦了,麻烦的不在于参数,而在于运算符号,我们如果要去翻译这些运算符号,那可就不止一点两点的坑了。
咋个办 ?
好,讲到这里,又到了我揭晓结论的时候了。大家想一想,这个公式字符串,和 js 代码是不是很像 ?
get 到没有 ?
是的,我们完全可以不用去翻译它,我们就去执行它!
执行字符串式的 js 代码,我们有两种方案:第一,eval 函数;第二,Function 构造函数。在这个,我选择了第二种,原因有两点:第一,eval 无法传承,而 Function 可以;第二:eval 无法 return 执行结果,而 Function 可以。
Function() 构造函数创建了一个新的 Function 对象。直接调用构造函数可以动态创建函数,但可能会经受一些安全和类似于 eval()(但远不重要)的性能问题。然而,不像 eval(可能访问到本地作用域),Function 构造函数只创建全局执行的函数。
———— MDN
Function 使用示例:
const sum = new Function('a', 'b', 'return a + b')
console.log(sum(2, 6))
// Expected output: 8
具体怎么做呢?
- 第一步,我们扫描一遍规则,拿到我们需要准备的 公式集 和表单 参数集
- 第二步,我们替换掉公式中的中文参数,使用我们实际拿到的参数名,替换掉 或 与 等中文逻辑字符,使用 js 逻辑字符 || && 等
- 第三步,我们传入准备好的 公式集 和表单 参数集 以及调整过的公式字符串,并执行
code = 'retrun ' + code
const res = (new Function(code)).call({
...params,
...funcs
})
这里要注意一点的是,我们这里使用了 call 方法来注入参数,我们需要把 #AVERAGE 和 @参数 都替换成 this.AVERAGE 和 this.参数