在 Vue 3 项目里引入规则引擎,本质是把原先写在业务代码里的 if-else 判断,抽象成可由非开发人员维护的数据结构。借助组合式 API 的响应式特性,我们能把规则配置、表达式求值与界面渲染解耦,既保证运行效率,也方便后期扩展。下面从建模、可视化与执行三个层面展开说明。

规则节点的数据建模与响应式设计
规则引擎的第一步是定义清楚“条件”“运算符”和“动作”这三种基础节点。在 Vue 3 中,我们可以用普通的 JavaScript 对象来描述它们,再配合 reactive 或 ref 让配置面板任何改动都实时映射到最终生成的规则树。例如一个条件节点包含字段名、比较符和期望值,逻辑节点则持有子节点数组与连接类型(and 或 or)。
这种建模方式的好处是序列化和反序列化极其简单,后端存进数据库就是一段 JSON,前端加载后直接赋值给响应式变量即可恢复整棵规则树。相较于传统流程图里用唯一 ID 互相关联的写法,嵌套结构更贴合 Vue 的组件递归渲染模式,我们可以用一个 RuleNode 组件通过 v-for 不断渲染自己的子节点,界面与数据始终保持一致。
需要注意,如果规则里允许嵌套很深,直接用 reactive 深层代理可能在超大配置下产生性能损耗。此时可把静态配置部分用 shallowRef 包裹,只在提交或切换时整体替换引用,而把当前正在编辑的单个节点用 ref 单独暴露给表单,既保留响应能力又控制代理开销。
// 规则节点基础结构示例
const ruleTree = ref({
type: 'logic',
op: 'and',
children: [
{
type: 'condition',
field: 'age',
compare: '>=',
value: 18
},
{
type: 'condition',
field: 'vip',
compare: '==',
value: true
}
]
});
function addCondition(parent) {
parent.children.push({
type: 'condition',
field: '',
compare: '==',
value: ''
});
}
基于组件递归的可视化配置面板实现
可视化配置的核心是把上面那棵树画出来,并且让产品人员能拖拽、增删、修改。Vue 3 的递归组件非常适合做这件事:我们写一个 RuleEditor 组件,当遇到逻辑节点时渲染添加按钮与子节点列表,遇到条件节点时渲染字段下拉框与比较符选择框。由于数据本身是响应式的,任何输入都会同步回 ruleTree。
为了提升体验,可以给逻辑节点增加“切换 and/or”的开关,给条件节点增加错误校验,比如字段不能为空、数值类型要匹配。这些校验可以写在组件的 computed 里,一旦不合法就在界面用红色提示,但不阻塞整体数据结构,方便用户边填边改。相比于引入大型低代码平台,这种轻量做法在中小项目里维护成本更低。
如果希望支持拖拽排序,可以引入 HTML5 的拖放 API 或者小型库如 vuedraggable。但要注意拖拽结束后的数据归一化:由于 Vue 的响应式数组在 splice 时可能触发大量更新,建议在拖拽过程中只操作一个临时副本,松手后再一次性赋值回 ruleTree.value,避免中间状态造成画布闪烁。
<template>
<div class="rule-node">
<template v-if="node.type === 'logic'">
<select v-model="node.op">
<option value="and">并且</option>
<option value="or">或者</option>
</select>
<div v-for="(child, i) in node.children" :key="i">
<RuleEditor :node="child" />
<button @click="node.children.splice(i, 1)">删除</button>
</div>
<button @click="addCondition(node)">加条件</button>
</template>
<template v-else>
<input v-model="node.field" placeholder="字段" />
<select v-model="node.compare">
<option value="==">等于</option>
<option value=">=">大于等于</option>
</select>
<input v-model="node.value" />
</template>
</div>
</template>
表达式的安全执行与沙箱化求值
配置好的规则最终要变成判定逻辑。最简单粗暴的做法是用 new Function 把条件拼成字符串执行,但这等于把脚本执行权完全交给配置端,一旦有人写了 while(true) 或访问 window 就会造成安全问题。正确思路是先遍历规则树生成抽象语法树或中间表示,再用一个纯函数解释执行。
我们可以写一个 evaluate(node, context) 函数:遇到条件节点就从 context 里取对应字段,按比较符计算布尔值;遇到逻辑节点就递归处理子节点并按 and/or 汇总。这样完全不依赖动态编译,也不会触碰全局对象。若业务真的需要复杂表达式,可引入轻量解析器如 expr-eval,并显式禁用其访问全局变量的能力,把上下文只限于传入的数据对象。
在性能方面,解释执行比编译成函数慢一些,但规则引擎通常不在高频热路径上,而且可以通过缓存编译结果来优化:只有当 ruleTree 引用变化时才重新生成执行体,平时直接跑缓存好的判定函数。下表列出两种方案的差异。
| 方案 | 安全性 | 性能 | 维护成本 |
|---|---|---|---|
| new Function 动态执行 | 低,易注入 | 高 | 低 |
| 树遍历解释执行 | 高,无全局访问 | 中 | 中 |
function evaluate(node, ctx) {
if (node.type === 'condition') {
const left = ctx[node.field];
switch (node.compare) {
case '==': return left == node.value;
case '>=': return left >= Number(node.value);
default: return false;
}
}
if (node.type === 'logic') {
if (node.op === 'and') {
return node.children.every(c => evaluate(c, ctx));
}
if (node.op === 'or') {
return node.children.some(c => evaluate(c, ctx));
}
}
return false;
}
// 使用示例
const ctx = { age: 20, vip: true };
console.log(evaluate(ruleTree.value, ctx));
通过上述三部分配合,Vue 3 项目可以用较小成本落地一套规则引擎:界面负责让业务人员可视化维护,数据层用响应式保证同步,执行层用解释器守住安全底线。后续若需接入远端规则中心,只需把 ruleTree 的读写换成接口调用,核心逻辑无需改动。