QueryBuilder 是一个常用的过滤器的 UI 组件,本文从前后端和数据库查询的角度总结了一些使用经验,包括一些踩坑的心得。
QueryBuilder 是什么?
引用 jQuery QueryBuilder 的定义
QueryBuilder 是一个用于创建查询和过滤器的 UI 组件。
- 它可以用于高级搜索的引擎页面、管理端等。
- 它是高度可定制的,并可插入许多小部件,如 sliders 滑块和日期选择器。
- 它输出一个结构化的 JSON 规则,可以很容易地解析来创建 SQL/NoSQL/ 任何查询。
- 它还附带了一些插件,并有一个完整的事件系统来支持更多的功能。
QueryBuilder 组件一般多用于数据筛选,它以 AND OR NOT
的嵌套组合,让非专业的人也能构造复杂的数据查询语句。在问卷系统中,就有不少的地方需要使用到这个组件,本文就从最开始的技术选型到上线总结一下其中的一些关键技术点。
需求场景
一般来说,一个专业的问卷系统都需要满足大量的数据筛选和清洗的工作,而 QueryBuilder 正是交互的第一步。在问卷的回收过程中,我们需要直接根据用户设置的条件进行答案的过滤,如下图:
在答题者提交问卷之后,便会直接在后台根据 QueryBuilder 生成的规则进行 运算,并且标记该份答案是 "有效/无效",一般多用于根据答题者的答题认真程度进行发奖、招募等场景。因为这种筛选是在 api 侧实时运算的,需要直接根据答案的值解析 QueryBuilder 规则。
而数据清洗的功能则是在管理端异步任务中计算的,一般用于生成报表或者批量导出部分数据使用,它是针对所有回收的问卷进行清洗,所以需要将 QueryBuilder 规则转换成相应的查询语句,比如我们主要的分析工具是 es ,那么就要转换成 es 对应的 DSL 语句。
技术调研
通过需求场景可以看出,虽然是同样的交互,但是不同的使用场景,底层需要做的事情是完全不一样的,所以我们技术调研时需要考虑的核心点就是扩展性,其一是 UI 组件是否能方便扩展新的规则(例如问卷中需要计算2个数组交集、字符串长度等等); 其二是 QueryBuilder 规则存储的数据结构能否便捷的转换成对应的语法,如 mongo、es 等;最后还有非常重要的一点就是,是否有后端解析库的支持,比如支持在我们使用的主要语言 go
中直接计算出结果。
综上,最终我们确定使用的是 react-awesome-query-builder,它不仅能通过简单配置扩展 UI 规则,还内置了很多转换器,可以直接将 UI 组件的数据转换成 mysql/mongo/es
的查询语句。而且还可以将 QueryBuilder 规则转换成 jsonLogic,这是一种用 json 构造的语法树,最主要优势是语言无关、前后端通用,jsonLogic
虽然不支持复杂的语法:setters、循环、函数等,但对于 QueryBuilder 这种主要为 AND OR NOT
的语法完全够用了,看看它的语法:
var rules = { "and" : [
{"<" : [ { "var" : "temp" }, 110 ]},
{"==" : [ { "var" : "pie.filling" }, "apple" ] }
] };
var data = { "temp" : 100, "pie" : { "filling" : "apple" } };
jsonLogic.apply(rules, data); // true
非常简单明了,jsonLogic
官方有 js/php/python/ruby
对应的解析库,只可惜没有 go
的。
难点解决
在实际的开发过程中,我们还是遇到了不少的问题。
go 解析 jsonLogic 规则
因为 jsonlogc
官方并没有相应的 go
版本,最开始我打算自己实现,在调研过程中,发现 github 上确实也有几个不错的开源项目,其中 https://github.com/diegoholiveira/jsonlogic 入参和返回值的设计最符合我们的使用场景,能减少很多的开发量。唯一遗憾的是它不支持自定义操作符,这对于我们的需求是必须实现的,好在作者比较活跃,我提了一个 PR implement AddOperator method,他很快的合入了版本。
最终,我在项目中引用了该库的最新版,并增加了字符长度比较(用于填空题)、数组是否存在交集(用于多选题)。
benchmark:
代码语言:txt复制# 跑了 1==1 和 reduce 函数的benchmark
goos: darwin
goarch: amd64
pkg: git.code.oa.com/mur-survey/survey-api-v2/internal/pkg/json-logic
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSampleRule-12 4209722 289.0 ns/op 896 B/op 4 allocs/op
BenchmarkReduceFunc-12 3978484 292.7 ns/op 896 B/op 4 allocs/op
QueryBuilder 扩展 es 的转换规则和语法,优雅修改 node_modules
react-awesome-query-builder 虽然内置了 es 语法的转换,但是只支持比较常见的一小部分,之前有提到,我们有不少自定义的规则。举个例子:gte_strlen(文本长度大于),像这个操作符就需要新增转换规则。react-awesome-query-builder 为 mongo 提供了 mongoFormatOp 这样的函数,可以针对特殊的操作符配置不同的语法,比如:
代码语言:txt复制{
equal: {
label: 'equals',
mongoFormatOp: (field, op, value) => ({ [field]: { '$eq': value } }),
},
}
但是,没有为 es 提供相应的函数,而且我又不想提 PR(没时间,还得写test,还得等作者合并),所以直接修改 node_modules
的代码是最快的方式。那么,如何优雅的修改呢?
patch-package 可能是最好的方式,patch-package
可以在修改完 node_modules
文件之后,根据当前库的版本生成一份补丁,在其他人 npm install
之后,执行一下 patch-package
就可以应用到修改后的代码,非常方便和安全。
所以,我修改了 react-awesome-query-builder 转换函数中的源码,让其可以支持这样配置:
代码语言:txt复制{
gte_strlen: {
label: '文本长度大于',
labelForFormat: 'gte_strlen',
esFormatOp: (field, op, values) => ({
script: {
script: `doc['${field}_text.keyword'].value.length() > ${values[0] ?? 0}`,
},
}),
}
}
例子中,将 gte_strlen
转换成了 es 的 painless 脚本,让其支持查询文本的长度是否大于某值。
vue2 兼容 react 组件
虽然 react-awesome-query-builder 这个库很完善很好用,但是我们的问卷管理端是早期使用 vue2 搭建的,所以重点还需要解决如何在 vue2 中使用 react 组件的问题。其实理论上,build 之后的代码都只是原生的创建 UI 的函数,已经框架无关了,只是像 props/event 这种需要手动处理,vuera 就提供了这样的 react/vue 相互转换的 wrapper,让我们可以混用 Vue 和 React 的组件,不需要额外的脚本配置(webpack/babel等),是一个改造成本比较低的方式,如下:
代码语言:txt复制<template>
<my-react-component :passedProps="passedProps"></my-react-component>
</template>
<script>
import { ReactWrapper } from 'vuera'
import MyReactComponent from './MyReactComponent.jsx'
export default {
data () {
message: 'Hello from React!',
},
computed: {
passedProps () {
return {
message: this.message,
reset: this.reset,
}
},
},
components: { 'my-react-component': MyReactComponent },
}
</script>
减少包的体积
可以想象,不仅除了组件的代码还有 react 源码,所以整体包的体积 gzip 之后增加了 1.34 mb,虽然是管理端,但作为有追求的前端,我还是希望能将包的体积减少一些。首先,我移除了 react-awesome-query-builder 所有依赖的 UI 库(它适配了 antd/material),其次最核心的是使用只有 3kb 的 preact 替代 react。
代码语言:txt复制// vue.config.js
config.resolve.alias.set('react', resolve('node_modules/preact/compat'))
config.resolve.alias.set('react-dom', resolve('node_modules/preact/compat'))
最终体积从 1.34 mb 变成了 0.29mb ,减少了 78%的大小,效果非常明显,就在我准备开开心心提交代码的时候,发现了一个严重的问题,使用 preact 之后,子组件不渲染了。更准确的说,是在 Group.jsx 中的这行代码没有生效:
代码语言:txt复制renderChildren() {
const {children1} = this.props;
return children1 ? children1.map(this.renderItem.bind(this)).toList() : null;
}
主要的原因是 react 支持 iterator 的遍历,比如 immutable.js 中的 List 类型,但 preact
没有支持,虽然有人提过相关的 PR,Add support for all iterable children, not just Arrays,但由于体积的考虑,最终没有 merged,所以我又修改了对应的源码,使用 Array.from
转换成 js 原生数组:
renderChildren() {
const {children1} = this.props;
return children1 ? Array.from(children1.map(this.renderItem.bind(this)).toList()) : null;
}
总结
其实,类似的组件有一些设计、文档比较好的,都是需要收费的,比如 Essential JS 2,在开源项目中 react-awesome-query-builder 只能说相对而言是比较不错的,在看源码过程中,只能说中规中矩,当然它最大优点就是功能齐全,帮助我们减少了很多的开发时间。如果让我重新设计,我可能更多会考虑 UI 无关的部分,先从数据结构,树的变换算法开始做一个由纯数据驱动的库,然后再考虑上层 UI ,跟 vue/react 等适配,这也是我们之前重构问卷系统所思考的方式。