可拖拽条件规则编辑器:从逻辑树 AST 到表达式生成
条件规则编辑器先用多选框收集条件,再把条件拖入 AND/OR 分组,最后输出一段可执行或可保存的表达式。整条链路已经具备小型编译器的结构。
可视化拖拽、嵌套逻辑和表达式优先级需要始终描述同一件事。递归逻辑树把交互到输出的这条编译链连接起来。
用 AST 表达规则
假设用户要配置:
1 | (A AND B) OR (C AND (D OR E)) |
如果状态直接保存为字符串,拖拽一个条件、把一个分组嵌入另一个分组,都会变成脆弱的文本替换。更合适的数据结构是抽象语法树(AST):
1 | type ConditionItem = { |
每个逻辑组同时容纳两类成员:直接条件 items 和子逻辑组 children。logical 定义当前这一层如何把它们连接起来。
graph TD
Root[OR]
Root --> A[AND]
Root --> B[AND]
A --> C1[条件 A]
A --> C2[条件 B]
B --> C3[条件 C]
B --> D[OR]
D --> C4[条件 D]
D --> C5[条件 E]
条件和值、逻辑运算符、父子关系都保存在结构里,不隐含在展示文本中。
表达式生成:一次后序遍历
逻辑树通过一次后序递归生成表达式:先生成子节点表达式,再在当前节点合并。
1 | function buildExpression(node: LogicalNode): string { |
以 (A AND B) OR (C AND D) 为例,执行顺序是:
- 左子树生成
(A AND B); - 右子树生成
(C AND D); - 根节点以
OR合并,得到((A AND B) OR (C AND D))。
括号略显冗余,但每层逻辑的优先级明确。规则编辑器优先保证语义正确,不追求最短字符串。
复杂度与树高
设整棵树包含 N 个逻辑组和条件项,最大嵌套层级为 H:
- 每个节点只访问一次,遍历成本为
O(N); - 递归调用栈最深为
O(H); - 最终字符串的构造还会受到条件名称总长度影响。
通常规则树并不深,递归足够直观。但如果产品允许无限嵌套,应该限制最大层级,或者在序列化时改用显式栈,避免异常输入占满调用栈。
拖拽操作变换树
拖拽库告诉我们“什么被拖到了哪里”,业务树的更新由数据层处理。把条件拖入逻辑组时,数据层至少要完成三步:
1 | 从旧分组移除条件 |
逻辑组拖入另一个逻辑组时,同样是「剪下子树 → 挂到新父节点 → 更新 parent → 清理空组」。这些操作可以复用一组递归函数:
1 | function addItem(tree: LogicalNode[], targetKey: string, item: ConditionItem) { |
这个模式把拖拽交互翻译成可验证的数据变换。UI 组件可以换成任何 DnD 库,树变换规则仍然成立。
两个边界
单根约束
有些实现只在根逻辑组数量恰好为 1 时生成表达式:
1 | if (logical && logical.length === 1) { |
输出表达式的语义需要一个唯一根节点。多个根组可以表示,但当前模型没有定义它们之间默认是 AND 还是 OR。
这种约束需要在 UI 中显式呈现:禁止创建多个根组,或提供根级运算符,把多个根组包进一个虚拟根节点。
用字符串内容决定括号并不可靠
expression.includes(' AND ') 或 expression.includes(' OR ') 能在大多数情况下判断是否需要括号,但它把结构判断交给了字符串内容。若条件文本本身包含 AND 或 OR,例如某个业务条件名是 Brand OR Channel,就可能被误认为是一段复合逻辑。
括号可以根据 AST 结构判断:
1 | function print(node: LogicalNode): string { |
进一步还可以区分运算符优先级,只有子表达式优先级低于父表达式时才加括号。但在规则编辑器中,保守地保留冗余括号通常更容易维护。
从展示树到持久化数据
数据流从条件表流向图节点 data:
1 | 条件表 |
结构化 AST 保留编辑能力,字符串表达式适合展示和执行。只存字符串,下一次打开编辑器时就得反向解析;只存树,又会让执行方承担序列化工作。两者一起保存,用少量存储空间换取编辑和消费两端的明确边界。
递归更新也要保持不可变
树更新函数经常写成 map/filter + 递归,看上去像不可变更新;但若递归过程中直接修改未命中节点的 children,React state 的引用语义仍可能被破坏。
更新时应始终返回新对象:
1 | return { |
这样能避免旧状态被意外改写,也便于引入 memo、撤销快照或状态对比。
总结
可拖拽条件编辑器是一条小型编译链:
1 | 可视化交互 |
结构作为事实、字符串作为输出时,拖拽、嵌套、撤销和重新编辑共享同一份真相来源。