规则配置系统设计与实战踩坑经验分享

西门艳花 工具 阅读 1,381
赞 82 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

去年底接了个配置化表单的需求,客户希望运营人员能自己搭表单,不用每次都找开发改代码。一开始我以为就是拖拽组件 + JSON 配置,简单得很。结果需求文档里写着“支持字段联动、条件显隐、校验规则动态组合”,我当场就意识到这玩意没那么简单。

规则配置系统设计与实战踩坑经验分享

最开始想用现成的低代码平台,但评估后发现要么太重,要么定制性差。后来决定自己撸一套轻量级规则引擎,核心思路是:把每个字段的行为抽象成“规则”,规则之间可以互相引用。比如“当A字段等于‘是’时,B字段必填且显示”。这样配置起来灵活,代码也容易维护。

规则怎么写?JSON 还是函数?

第一个纠结点:规则用 JSON 描述还是写成 JS 函数?JSON 好处是纯数据,方便存储和传输;坏处是表达能力弱,复杂逻辑写起来像绕口令。函数灵活但不好序列化,还可能引入安全风险(比如 eval)。

折中了一下,最后采用“类表达式”的 JSON 结构。比如一个简单的显隐规则:

{
  "target": "field_b",
  "action": "setVisible",
  "condition": {
    "field": "field_a",
    "operator": "equals",
    "value": "yes"
  }
}

校验规则类似:

{
  "target": "email",
  "action": "setRequired",
  "condition": {
    "field": "contact_method",
    "operator": "equals",
    "value": "email"
  }
}

这样结构清晰,解析也简单。但很快问题来了——条件嵌套怎么办?比如“当 A=1 且 (B=2 或 C=3) 时触发”。这时候 JSON 层级一深,解析逻辑就得递归处理,代码瞬间变复杂。

最大的坑:性能问题

上线前压测,表单有 30+ 字段,每字段平均绑了 3-4 条规则。用户一操作,页面直接卡死。Chrome DevTools 一看,光规则计算就占了 800ms+。问题出在哪?

原来每次字段值变化,我都遍历所有规则,逐条判断 condition 是否满足。这在小表单没问题,但字段一多,O(n²) 的复杂度就爆了。更糟的是,有些规则会触发其他字段变化,导致连锁反应——改一个字段,整个表单重新跑一遍规则,死循环边缘试探。

折腾了半天,发现关键不是优化单条规则执行速度,而是减少无效计算。于是做了两件事:

  • 规则索引:预处理所有规则,建立“依赖关系图”。比如 field_a 的变化只会影响依赖它的规则,不用全表扫描。
  • 防抖 + 批量更新:字段值变化先收集变更,等同步任务结束再统一触发规则计算,避免中间状态反复触发。

改造后的核心逻辑长这样:

class RuleEngine {
  constructor(rules) {
    this.rules = rules;
    this.dependencyMap = this.buildDependencyMap();
    this.pendingUpdates = new Set();
  }

  buildDependencyMap() {
    const map = {};
    this.rules.forEach(rule => {
      const deps = this.extractDependencies(rule.condition);
      deps.forEach(field => {
        if (!map[field]) map[field] = [];
        map[field].push(rule);
      });
    });
    return map;
  }

  extractDependencies(condition) {
    // 递归解析 condition 中涉及的字段名
    const fields = [];
    const walk = (obj) => {
      if (obj.field) fields.push(obj.field);
      if (obj.and) obj.and.forEach(walk);
      if (obj.or) obj.or.forEach(walk);
    };
    walk(condition);
    return [...new Set(fields)];
  }

  onFieldChange(fieldName, newValue) {
    this.pendingUpdates.add(fieldName);
    // 防抖处理,确保同一 tick 内多次变更只触发一次
    Promise.resolve().then(() => {
      this.executePendingRules();
      this.pendingUpdates.clear();
    });
  }

  executePendingRules() {
    const affectedRules = new Set();
    this.pendingUpdates.forEach(field => {
      const rules = this.dependencyMap[field] || [];
      rules.forEach(rule => affectedRules.add(rule));
    });

    affectedRules.forEach(rule => {
      const shouldApply = this.evaluateCondition(rule.condition);
      this.applyAction(rule.target, rule.action, shouldApply);
    });
  }

  evaluateCondition(condition) {
    // 实现 operator 的判断逻辑,比如 equals, gt, in 等
    if (condition.field) {
      const fieldValue = this.getFieldValue(condition.field);
      switch (condition.operator) {
        case 'equals':
          return fieldValue === condition.value;
        // 其他 operator...
      }
    }
    // 处理 and/or 嵌套
    if (condition.and) {
      return condition.and.every(c => this.evaluateCondition(c));
    }
    if (condition.or) {
      return condition.or.some(c => this.evaluateCondition(c));
    }
    return false;
  }
}

加了索引和批量更新后,30字段的表单响应时间从 800ms 降到 30ms 以内,肉眼几乎无感。

另一个头疼事:规则冲突

规则多了难免打架。比如两条规则同时控制同一个字段的显隐:一条说“显示”,另一条说“隐藏”。谁优先?

一开始想按规则定义顺序执行,后定义的覆盖先定义的。但运营反馈说不好调试——改了后面一条规则,前面的效果突然没了,根本想不到是冲突。

后来加了个“优先级”字段,允许配置数字。高优先级规则生效时,低优先级直接忽略。但又带来新问题:优先级设多少合适?运营根本不会估。

最后妥协方案:冲突时前端打 warning 日志,提示“字段 X 被多条规则控制,请检查配置”。虽然没自动解决,但至少能让问题暴露出来。实际用下来,90% 的冲突都是配置错误,比如复制粘贴忘了改字段名。

最终的解决方案

整套东西跑了几个月,现在支撑着十几个动态表单。核心就三点:

  • 规则用结构化 JSON 描述,支持基础逻辑组合
  • 依赖索引 + 批量更新解决性能瓶颈
  • 冲突检测靠日志提醒,不自动兜底

配套还做了个简易可视化配置器,运营拖拽选择字段、操作符、值就行,生成的 JSON 直接存数据库。API 示例大概长这样:

// 提交表单时获取规则配置
fetch('https://jztheme.com/api/form-rules/123')
  .then(res => res.json())
  .then(rules => {
    const engine = new RuleEngine(rules);
    engine.onFieldChange('user_type', 'premium');
  });

回顾与反思

这套方案最大的优点是轻量,没引入任何重型库,核心代码不到 200 行。而且规则和 UI 完全解耦,换个框架也能用。

但也有硬伤:复杂条件还是得写 JSON,运营搞不定就得开发介入。理想情况是配个图形化逻辑编辑器,像流程图那样连线,但工期不允许,只能先凑合。

另外规则引擎本身没做缓存优化。比如同一个 condition 在多个规则里重复出现,其实可以 memoize 结果。不过目前字段不多,影响不大,暂时没动。

总的来说,这个方案在“灵活性”和“维护成本”之间找到了平衡点。如果你也在搞动态表单,不妨试试类似思路——别一上来就想造轮子,先用最糙的方式跑通,再根据实际问题迭代。

以上是我踩坑后的总结,希望对你有帮助。规则配置这块水挺深,有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论