规则引擎在前端的应用
Published in:2024-04-19 | category: 前端 解决方案

1. 前言

规则引擎常作为一个独立服务运行在一定体量的产品之中,通过接收有效的数据来做出对业务的合理决策。在前端项目的常年迭代下,对于某些重要的或频繁扩展改造的业务模块由于时间长、注释少、不易阅读等客观问题的遗留会对后期的迭代造成一定困扰,也不免会造成额外的测试压力。所以轻量的、可运行在浏览器端得规则引擎将彻底消灭这样问题的存在。

2. 规则引擎初探

适用于浏览器端的规则引擎在开源社区已有实现,这里我们就通过json-rules-engine开源项目来走进规则引擎的世界~

json-rules-engine由 JSON 数据格式来组合规则,这样既便于规则的阅读也便于规则的持久化存储,还有轻量也是它的特点之一~

2.1 制定游戏规则

在某一运动赛事规则的制定时共同约定:
1 当比赛进行到 40 分钟时,如果运行员犯规次数达到 5 次及以上的因被罚出场;
2 当比赛进行到 48 分钟时,如果运动员犯规次数达到 6 次及以上的因被罚出场;

2.2 搭建规则引擎

2.2.1 创建引擎

通过简单的模块导入并实例化引擎对象即完成了引擎的创建工作:

1
2
let { Engine } = require('json-rules-engine');
let engine = new Engine();

2.2.2 添加规则

通过 event 对象来定义当规则被命中后所触发的信息,通过conditions对象来定义具体的规则,每个规则至少包含factoperatorvalue三部分,分别定义事实名称、操作符和当前规则的阈值或范围,规则和规则之间使用 any 来表示逻辑或和使用 all 来表示逻辑与的关系,这样就组成了这次赛事的具体规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let event = {
type: 'fouledOut',
params: {
message: 'Player has fouled out!'
}
};
let conditions = {
any: [
{
all: [
{ fact: 'gameDuration', operator: 'equal', value: 40 },
{ fact: 'personalFoulCount', operator: 'greaterThanInclusive', value: 5 },
]
}, {
all: [
{ fact: 'gameDuration', operator: 'equal', value: 48 },
{ fact: 'personalFoulCount', operator: 'greaterThanInclusive', value: 6 },
]
}
]
};
engine.addRule({ conditions, event });

下图是这个赛事规则的可视化规则示意图,在文末将会讲解如何利用可视化工具来高效阅读所定义的规则~

2.2.3 定义 Facts

这里使用对象来表示一个Fact,让它进入到规则引擎并经过每一条规则来判断是否允许通过:

1
2
3
4
let facts = {
personalFoulCount: 6,
gameDuration: 40,
}

2.2.4 运行引擎

通过引擎校验后将在控制台输出 Player has fouled out!的信息,说明这个运动员已经命中了被罚出场的规则~

1
2
const { events } = await engine.run(facts);
events.map(event => console.log(event.params.message));

3. 线上项目分析应用

3.1 场景 1&多属性控制菜单

在线上项目的某一个菜单处出现了同时由 8 个属性共同控制一个菜单的显示与隐藏,猜想在最初也只是仅包含一个用户权限和数据状态的控制吧,但随着业务的持续变动导致这块的控制属性持续增加,也是在最近的迭代中又为这个菜单补充了一个属性的控制,其实这些属性中也不全是 Boolean 类型的判断,下面通过对比源代码和应用规则引擎后的代码来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
源代码:

// item.error_type !== 53
// !item.not_foreign_nationality_settle &&
// !item.not_hongkong_macao_taiwan_settle &&
// !item.is_excess_single &&
// isSuperAdmin!= 'groupmanager' &&
// !isMissingPayMode(item) &&
// (item.status === 0 || item.status === 4 || item.status === 9) &&
// showCreateBillBtn(item) && // 忽略,因函数逻辑不易拆解
// !item.bankerr"
*/

// 引擎规则:
let conditions = {
all: [
// 非此状态的允许通过
{ fact: 'error_type', operator: 'notEqual', value: 53 },
// 非组织管理员允许通过
{ fact: 'isSuperAdmin', operator: 'notEqual', value: 'groupmanager' },
// 状态符合:0 4 9 允许通过
{ fact: 'status', operator: 'in', value: [0, 4, 9] },
// 非外籍人员允许通过
{ fact: 'isForeignNationality', operator: 'equal', value: false },
// 非港澳台人员允许通过
{ fact: 'isHongkongMacaoTaiwanRegion', operator: 'equal', value: false },
// 未超出单笔限额允许通过
{ fact: 'isExcessSingle', operator: 'equal', value: false },
// 支付方式未丢失用户允许通过
{ fact: 'isMissingPayMode', operator: 'equal', value: false },
// 银行卡号未丢失时允许通过(当支付方式为银行卡且卡号丢失时此节点为true)
{ fact: 'isMissingBankCardNumber', operator: 'equal', value: false },
]
}

let event = {
type: 'showAllowSettlement',
params: {
message: 'You can display the settlement menu.!'
}
}
engine.addRule({ conditions, event });
下图是上述规则的可视化示意图,通过图所示,当着 8 个属性均符合条件后才允许通过,这个时候对应的菜单才允许被显示:<br />![](https://img02.sogoucdn.com/v2/thumb/retype_exclude_gif/ext/auto/q/95/crop/xy/ai/t/0/?appid=122&url=cdn.nlark.com/yuque/0/2022/png/2373519/1662190407772-b7e15ca1-0ebf-4211-be60-0e7d262c4a63.png)

3.2 场景 2&多属性多分支控制菜单

上面的场景 1 你可能看不出来规则引擎带来的便利,可能觉得原来的代码看起来也说的过去,那接着看这个包含多个分支的控制案例。
商户开票的功能设计由于不同的开票方式等其他的一系列因素导致这块有 4 个比较大的分支处理,这里我拆分出其中的一个分支来利用规则引擎简单的实现一下:

3.2.1 源项目逻辑分析

下面是摘自源项目的部分逻辑控制,其中部分的属性和函数背后还有很多的逻辑处理,对于代码的阅读和功能的测试都会造成困扰:

1
2
3
4
5
6
7
申请开票方式1:
控制菜单显示:
!permissionSwitch() &&
((isSuperAdmin && projectListCode && is_allow_apply_invoice) || (projectListCode == '' && is_allow_apply_invoice))
控制菜单触发:
(settlement_business_scenario_switch && invoiceDealObj.isShowHistory && this.invoiceDealObj.historyCanInvoice !== 0) ||
(invoiceDealObj.isShowHistory && invoiceDealObj.merchantHistoryCanInvoice !== 0)

3.2.2 引擎规则编写

通过源代码分析控制此菜单的数据达 10 个,通过逻辑与和逻辑或共同控制着 16 条规则的运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
let conditions = {
all: [
{ fact: 'invoice_way', operator: 'notEqual', value: 1 },
{
any: [
{
all: [
{
any: [
{ fact: 'isSuperAdmin', operator: 'equal', value: 'superadmin' },
{ fact: 'isSuperAdmin', operator: 'equal', value: 'groupmanager' },
{
all: [
{ fact: 'isSuperAdmin', operator: 'equal', value: 'projectmanager' },
{ fact: 'project_invoice_switch', operator: 'equal', value: true },
]
},
{
all: [
{ fact: 'isSuperAdmin', operator: 'equal', value: 'financial' },
{ fact: 'hasCreatePower', operator: 'equal', value: 1 },
]
},
]
},
{ fact: 'projectListCode', operator: 'notEqual', value: '' },
{ fact: 'is_allow_apply_invoice', operator: 'equal', value: true },
]
},
{
all: [
{ fact: 'projectListCode', operator: 'equal', value: '' },
{ fact: 'is_allow_apply_invoice', operator: 'equal', value: true },
]
},
]
},
{
any: [
{
all: [
{ fact: 'settlement_business_scenario_switch', operator: 'equal', value: true },
{ fact: 'isShowHistory', operator: 'equal', value: true, },
{ fact: 'historyCanInvoice', operator: 'notEqual', value: 0, },
]
},
{
all: [
{ fact: 'isShowHistory', operator: 'equal', value: true },
{ fact: 'merchantHistoryCanInvoice', operator: 'notEqual', value: 0 }
]
}
]
}
]
}

let event = {
type: 'allowInvoicing',
params: {
message: 'Invoicing method 1 allowed!'
}
}

3.2.3 规则示意图

当规则变多以后,纯代码的阅读也会变得更加费劲,所以这个时候就可以利用可视化的工具来通过图的方式阅读,下面这几张图就是对上面案例规则的描述:

4. 规则可视化方案

规则可视化的前提就需要将编写的引擎规则转为 JSON 对象,通过加载 JSON 对象实现规则的可视化工作。

4.1 规则 JSON 化处理

json-rules-engine模块中的 Rule 对象提供了 toJSON()函数,我们直接编写脚本来得到一份 json 化后的规则数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const { Rule } = require("json-rules-engine");
const json = new Rule({
conditions: {
any: [
{
all: [
{ fact: "gameDuration", operator: "equal", value: 40 },
{
fact: "personalFoulCount",
operator: "greaterThanInclusive",
value: 5,
},
],
},
{
all: [
{ fact: "gameDuration", operator: "equal", value: 48 },
{
fact: "personalFoulCount",
operator: "greaterThanInclusive",
value: 6,
},
],
},
],
},
event: {
type: "fouledOut",
params: {
message: "Player has fouled out!",
},
},
}).toJSON();
console.log(json);

在得到规则数据后需要对数据做简易处理,通过name来命名这个规则的名称以示区分,在decisions中插入得到的规则数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"name": "rule",
"decisions": [
{
"conditions": {
"priority": 1,
"any": [
{
"priority": 1,
"all": [
{
"operator": "equal",
"value": 40,
"fact": "gameDuration"
},
{
"operator": "greaterThanInclusive",
"value": 5,
"fact": "personalFoulCount"
}
]
},
{
"priority": 1,
"all": [
{
"operator": "equal",
"value": 48,
"fact": "gameDuration"
},
{
"operator": "greaterThanInclusive",
"value": 6,
"fact": "personalFoulCount"
}
]
}
]
},
"priority": 1,
"event": {
"type": "fouledOut",
"params": {
"message": "Player has fouled out!"
}
}
}
]
}

4.2 规则可视化处理

json-rule-editor开源项目可以快速完成引擎规则的可视化,json-rule-editor 是该项目部署的在线页面,通过选择规则文件夹、上传规则文件、更新,在选择 rule 规则后右侧的 Decisions 页签可以看到下面的可视化规则:

5. 总结

使用 json-rules-engine 开源规则引擎可以实现在复杂逻辑处的优化处理,配合可视化的方案可以更方便的阅读引擎规则。

Prev:
如何衡量一个网页的性能
Next:
qiankun 微前端落地实践