什么是规则引擎
业务场景一般都是杂糅繁复的,于是代码很容易就互相嵌套、错综复杂、结构不清晰,同时维护成本高,可读性可拓展性差
规则引擎:整合了传入系统的Fact集合和规则集合,以推出结论。可以理解为当下一些状态集合(有限状态机)的情况下,去触发(推论出)一个或多个业务操作,可以表示为“在某些条件下,执行某些任务”
在拥有大量规则和Fact对象的业务系统中,可能会出现多个Fact输入都会导致同样的输出,这种情况通常称作规则冲突。规则引擎可以以下冲突解决方案来确定冲突规则的执行顺序:
正向链接:(基于“数据驱动”的形式),规则引擎利用可用的Fact推理规则来提取出更多的Fact对象,直到计算出最终目标,最终会有一个或多个规则被匹配,并执行。因此,始于事实,始于结论。
反向链接:(基于“目标驱动”的形式),从规则引擎假设的结论开始,如果不能够直接满足这些假设,则搜索可满足假设的子目标。规则引擎会循环执行这一过程,直到证明结论或没有更多可证明的子目标为止
规则引擎可以被视为复杂的if / then语句解释器。if部分表示处理条件;then部分表示执行的操作
举个栗子(json-rules-engine)
let a = 3;
if (a > 2) {
console.log('a大于2');
} else {
console.log('a小于等于2');
}
// 用规则引擎怎么写
// 描述 if (a > 2) console.log('a大于2')
const rule0 = {
// 描述 if (a > 2)
conditions: {
all: [
{
fact: 'a',
operator: 'gt',
value: 2
}
]
},
// 描述 console.log('a大于2')
event: {
type: 'console',
params: {
message: 'a大于2'
}
}
// 描述 if (a <= 2) console.log('a小于等于2')
const rule1 = {
// 描述 if (a <= 2)
conditions: {
all: [
{
fact: 'a',
operator: 'lt&equal',
value: 2
}
]
},
// 描述 console.log('a小于等于2')
event: {
type: 'console',
params: {
message: 'a小于等于2'
}
}
规则写好了,怎么添加上去呢?
import { Engine } from 'json-rules-engine';
const engine = new Engine();
engine.addRule(rule0);
engine.addRule(rule1);
除了添加规则,其实还能自定义一些FACT、操作符等
engine.addFact('account-type', function getAccountType(params, almanac) {
// ...
})
engine.addOperator('startsWithLetter', (factValue, jsonValue) => {
if (!factValue.length) return false
return factValue[0].toLowerCase() === jsonValue.toLowerCase()
})
使用 engine.run()
将规则引擎驱动起来
可以通过 engine官方文档 查看更多用法
实用场景:权限控制
config.beforeEach((to, from, next) => {
(async() => {
try {
// 获取当前用户信息
const user = await getCurrUserInfo();
if (to.path.startsWith('\/a')) {
// 只有管理员可以打开 a 页面
if (user.isAdmin()) {
next();
} else {
next({ name: 'Denied', message: '您不是管理员无法打开此页面' });
}
} else if (to.path.startsWith('\/b')) {
// 团队1、2、3 内成员可以打开 b 页面
if (
user.isMemberOf('团队1') ||
user.isMemberOf('团队2') ||
user.isMemberOf('团队3')
) {
next();
} else {
next({ name: 'Denied', message: '您不是团队1、2、3中任意一个团队的成员,无法打开此页面' });
}
} else if (to.path.startsWith('\/c')) {
// 团队C内成员,可以打开处于可用状态的 c 页面
const c = await getInfoForWarPage();
if (
user.isMemberOf('团队C') &&
c.isAble()
) {
next();
} else {
next({ name: 'Denied', message: '您不是团队C的成员,或 c 页面当前禁用,无法打开该页面' });
}
} else {
// 没有做权限控制的页面
next({ name: 'Denied' });
}
} catch (err) {
next({ name: 'Denied' });
}
})();
});
从上述代码可以看出其拓展性差,if 嵌套深,功能杂糅。现在可以根据引擎模板的思路改造一下:
FACT和规则集合:to.path.startsWith('\/a')
、to.path.startsWith('\/b')
、to.path.startsWith('\/c')
、user.isAdmin()
、user.isMemberOf('团队1') || user.isMemberOf('团队2') || user.isMemberOf('团队3')
、user.isMemberOf('团队C') && c.isAble()
推论:{ name: 'XXX', message: 'XXX' }
(next()是vue-router的用法)
// oper/starts_with.js'
// 操作符 operate
export function operStartsWith(factValue, jsonValue) {
return factValue.startsWith(jsonValue);
}
// fact/is_admin.js'
// 异步事实 fact 可以通过 [almanac官方文档](https://github.com/CacheControl/json-rules-engine/blob/master/docs/almanac.md) 查看更多用法
const user = await getCurrUserInfo();
export async function factIsAdmin(params, almanac) {
const user = await almanac.factValue('user');
return user.isAdmin();
}
// fact/is_member_of.js'
const user = await getCurrUserInfo();
export async function factIsMemberOf(params, almanac) {
const teams = params.teams || [];
const user= await almanac.factValue('user');
return teams.some(team => user.isMemberOf(team));
}
// fact/is_c_able.js'
const user = await getCurrUserInfo();
export async function factIsCAble(params, almanac) {
const c = await getInfoForCPage();
return c.isAble();
}
// rule/a.js'
const ruleFoo0 = {
conditions: {
all: [
{
fact: 'path',
operator: 'startsWith',
value: '/a'
},
{
fact: 'isAdmin',
operator: 'equal',
value: true
}
]
},
event: {
type: 'auth',
params: {
perm: true
}
}
}
const ruleFoo1 = {
conditions: {
all: [
{
fact: 'path',
operator: 'startsWith',
value: '/a'
},
{
fact: 'isAdmin',
operator: 'notEqual',
value: true
}
]
},
event: {
type: 'auth',
params: {
perm: false,
msg: '您不是管理员无法打开此页面'
}
}
}
// rule/b.js'
const ruleB0 = {
conditions: {
all: [
{
fact: 'path',
operator: 'startsWith',
value: '/b'
},
{
fact: 'isMemberOf',
params: {
teams: [
'团队1',
'团队2',
'团队3'
]
},
operator: 'equal',
value: true
}
]
},
event: {
type: 'auth',
params: {
perm: true
}
}
}
const ruleB1 = {
conditions: {
all: [
{
fact: 'path',
operator: 'startsWith',
value: '/b'
},
{
fact: 'isMemberOf',
params: {
teams: [
'团队1',
'团队2',
'团队3'
]
},
operator: 'notEqual',
value: true
}
]
},
event: {
type: 'auth',
params: {
perm: false,
msg: '您不是团队1、2、3中任意一个团队的成员,无法打开此页面'
}
}
}
// rule/c.js'
const ruleC0 = {
conditions: {
all: [
{
fact: 'path',
operator: 'startsWith',
value: '/c'
},
{
fact: 'isMemberOf',
params: {
teams: [
'团队C'
]
},
operator: 'equal',
value: true
{
fact: 'isCAble',
operator: 'equal',
value: true
}
]
},
event: {
type: 'auth',
params: {
perm: true
}
}
}
const ruleC1 = {
conditions: {
all: [
{
fact: 'path',
operator: 'startsWith',
value: '/c'
},
{
fact: 'isMemberOf',
params: {
teams: [
'团队C'
]
},
operator: 'notEqual',
value: true
},
{
fact: 'isCAble',
operator: 'notEqual',
value: true
}
]
},
event: {
type: 'auth',
params: {
perm: false,
msg: '您不是团队C的成员,或 c 页面当前禁用,无法打开该页面'
}
}
}
// 入口
import operStartsWith from './oper/starts_with';
import factIsAdmin from './fact/is_admin';
import factIsMemberOf from './fact/is_member_of';
import factIsCAble from './fact/is_c_able';
import { ruleFoo0, ruleFoo1 } from './rule/a';
import { ruleBar0, ruleBar1 } from './rule/b';
import { ruleWar0, ruleWar1 } from './rule/c';
async function getPerm (to, from) {
const engine = new Engine();
engine.addOperator('startsWith', operStartsWith);
engine.addFact('isAdmin', factIsAdmin);
engine.addFact('isMemberOf', factIsMemberOf);
engine.addFact('isCAble', factIsCAble );
engine.addRule(ruleA0);
engine.addRule(ruleA1);
engine.addRule(ruleB0);
engine.addRule(ruleB1);
engine.addRule(ruleC0);
engine.addRule(ruleC1);
const user = await getCurrUserInfo();
const ret = await engine.run({ user, path: to.path })
if (ret.events.length) {
return ret.events[0].params;
} else {
return { perm: false, msg: '没有做权限控制的页面禁止打开' }
}
}
// router.config.js
import getPerm from './auth/index.js'; // 入口
config.beforeEach((to, from, next) => {
try {
const ret = await getPerm(to, from);
if (ret.perm) {
next();
} else {
next({
name: 'Denied',
params: {
title: '访问受限',
message: ret.msg || '请检查您的权限以确保可以打开此页面'
}
});
}
} catch (err) {
console.error(err);
next({
name: 'Denied',
params: {
title: '访问受限',
message: `发生未知错误 ${err.message}`
}
});
}
});
之后新增路由只需要新增其对应的文件以及规则即可,后续如果后台支持的情况下,这些配置文件可直接入库(JSON格式),前端需要调整的地方不多,可延展性好,当然还有改进的点。。。
补充:priority 对于提升规则匹配的性能十分显著。
如果没有设置 priority,所有规则都相当于一个个微任务,并发执行。当设置了 priority,那么相同 priority 的规则会组合成一个新的队列,根据优先级执行。因此将重要的规则排在前面,就可以很好的优化性能
参考文档:json-rules-engine