我今年年初接了一个任务:把一套跑在 Flutter 里的业务规则引擎,原封不动地搬到鸿蒙环境里。这里说的“搬”,不是用 ArkTS 重新写一套,也不是把业务 if-else 捞出来做配置化,而是让一套用 Dart 写的声明式规则库,在鸿蒙的 Flutter 容器里跑起来,同时还要能被 ArkTS 原生侧顺畅调用。刚开始我以为这事很轻松,毕竟这个名叫 rules 的包是纯 Dart 实现,没有原生依赖;真上手之后才发现,纯 Dart 只是入场券,桥接方式、运行性能、规则组织和条件链治理才是真正要花时间的部分。
这篇指南写给两类人。第一类是在做 Flutter 鸿蒙化适配的开发者,你需要知道怎么搭工程、怎么桥接原生数据、怎么把规则引擎打包成鸿蒙侧的库;第二类是刚接触规则引擎的人,你需要理解为什么用声明式规则比堆 if-else 强,以及规则量上来之后怎么保持可维护。规则引擎和“逻辑断言”听起来很工程化,但落地本质就是把业务判断从代码大山上刨出来,变成一条条可以被编排、被测试、被替换的规则。
1. 先弄明白 rules 包到底解决什么问题
1.1 当业务逻辑变成“规则散弹”
业务代码最怕的不是逻辑复杂,而是逻辑散落。我见过很多项目里,同一个风控判断散落在订单页、支付页、后台任务三个地方,各自条件还长得不一样。等产品说要改规则时,开发只能全局搜索关键词,漏掉一处就出线上事故。这还没算测试人员,每条 if 分支都要手动造数据、走流程,规则越多,团队越疲惫。
规则引擎就是把这类“判断”从业务代码里剥出去。你先声明一组事实,比如用户等级、订单金额、历史行为、城市区域;然后定义一组规则,每条规则都是“当事实满足某个条件,就执行某个动作”;引擎负责把规则串成一条执行链,最终结果继续写回事实。这样,业务逻辑不是散弹,而是一张可读、可测、可动态调整的规则表。
我自己碰到的场景是信贷审批。审核员需要判断用户年龄是否达到 18 岁、所在地区是否支持、收入是否覆盖月供、近三个月是否有逾期。之前代码里全是一层层 if 套 if,后来改用规则引擎后,每个审核节点就是一条规则,新同事看规则列表像是在看事件流程单,不用再推理整个函数调用链。
1.2 rules 包的核心模型:Fact、Rule、Engine
rules 包把规则引擎抽象得极其克制,核心就是三个概念。Fact,本质是一个 Map 结构的业务数据,例如 {'age': 30, 'income': 12000, 'isBlacklist': false}。Rule,包含一个 when 条件和一个 then 动作,条件接收 Fact 返回布尔值,动作也接收 Fact,在里面写业务副作用。RuleEngine,持有规则列表,按加入顺序依次执行,并允许每条规则把新数据写回同一个 Fact。
一个非常短小的示例:
dart复制import 'package:rules/rules.dart';
final engine = RuleEngine();
engine.add(
Rule(
name: 'adult_check',
when: (facts) => facts['age'] >= 18,
then: (facts) => facts['isAdult'] = true,
),
);
engine.add(
Rule(
name: 'salary_check',
when: (facts) => facts['salary'] >= 5000,
then: (facts) => facts['loanAmount'] = facts['salary'] * 0.3,
),
);
final fact = {'age': 30, 'salary': 12000};
await engine.execute(fact);
print(fact['loanAmount']); // 3600
这个 API 设计很聪明,它没有发明新的 DSL 语法,就是让你用 Dart 闭包写规则。好处是零学习成本,坏处也很明显:闭包能捕获外部变量,如果规则内部不小心引用了外部状态,规则就不再是纯规则,后面我专门讲怎么治理这种隐性耦合。
1.3 选 rules 而不是其他方案的理由
当时我在做技术选型时,把能走的路都摆出来比过一轮。自研 if-else 硬编码,最后一定变成规则散弹;引入 JS 脚本引擎,开放能力过强且性能方差大;接入 Drools 那类重型引擎,规则描述文件和引擎本身都很重,在鸿蒙生态里得不偿失。相比之下,rules 包有三个硬优势。
第一,纯 Dart 实现,没有 C/C++ 原生库,跨鸿蒙 Flutter 基本没有底层迁移障碍。第二,API 极简,核心就三个类,团队半天能掌握。第三,Rule 本身是普通对象,规则集可以序列化成 JSON 放在远端,方便做版本管理和灰度。对中型商城、金融 App、内部工具类应用来说,这是性价比很高的组合。
| 方案 | 引入成本 | 跨端能力 | 适合场景 |
|---|---|---|---|
| 自研 if-else 硬编码 | 低 | 差 | 规则数量极少且固定 |
| JS 等脚本引擎 | 中 | 中 | 需要服务端下发动态脚本 |
| Drools 等重型引擎 | 高 | 低 | 大型 BRMS 平台建设 |
| rules 纯 Dart 包 | 低 | 高 | Flutter 多端复用,鸿蒙集成 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 鸿蒙化适配方案的整体设计
2.1 鸿蒙 Flutter 生态与运行形态
在做适配之前,先要把鸿蒙 Flutter 的运行方式搞清楚。鸿蒙应用的基本交付物是 HAP(HarmonyOS Ability Package),里面可以包含 ArkTS 写的 UI 和业务逻辑,也可以嵌入 Flutter 引擎,用 Flutter 页面渲染部分界面。Flutter 插件在鸿蒙侧的落地形态,是插件工程里多一个 ohos 平台目录,里面放 ArkTS 原生实现,Dart 代码通过 MethodChannel 和原生侧通信。
构建方式也与传统 Flutter 不太一样。安卓用 Gradle 构建 APK/AAR,鸿蒙用 hvigor 构建 HAP/HAR。HAR 是 HarmonyOS Archive,相当于鸿蒙的库包,可以把一个 Flutter 插件封装成 HAR 供多个鸿蒙应用复用。这意味着,你不仅要在 Dart 侧保证代码可编译,还要在 ohos 目录里写对原生壳子,保证插件能被 hvigor 正常打进包。
工具链这块目前差异较大。不同版本的 Flutter SDK、鸿蒙 SDK 对 ohos 目录的识别规则并非完全一致。我的经验是尽量让团队锁死一套经过交叉验证的组合,不要轻易升级,否则很可能出现“本机编译通过,同事环境一拉就崩”的问题。
2.2 三条适配路线,我为什么选第三条
-
路线 A:在鸿蒙 Flutter 工程里直接引入
rules包,不写任何原生代码。这个方案最简单,适用于规则执行所需的数据全部在 Flutter/Dart 侧的场景。但问题在于,很多鸿蒙应用真正的业务数据在原生侧,比如账号系统、本地数据库、地理位置,全量搬到 Dart 侧不现实。 -
路线 B:用 ArkTS 重写一套规则引擎。工程量巨大,还要重复解决规则序列化、执行优先级、异常处理等已经解决的问题,除非有自研可控的硬需求,否则不推荐。
-
路线 C:做一个 Flutter 插件,Dart 侧使用
rules包作为规则执行内核,ohos 目录提供原生桥接,把 ArkTS 侧数据注入 Dart 规则引擎,执行结果再返回原生侧。这个方案兼顾逻辑复用与数据可达性。
最终我选择的是路线 C。核心考量是:规则执行是纯计算逻辑,放在 Dart 侧没有任何问题,而且还能在其他 Flutter 端继续复用;原生侧只负责数据采集和结果接收,职责单一,桥接层代码量小,后续维护压力低。
2.3 整体架构和模块划分
整个插件包我命名为 harmony_rule_engine,内部拆成四层。入口层是 Dart 侧对外的 HarmonyRuleEngine,业务只需要注册规则和发起执行;内核层是 rules 包,负责实际的条件匹配与动作执行;桥接层是 MethodChannel,负责把原生侧的事实数据拉进来;原生侧是 ArkTS 实现的插件壳子,获取系统数据和业务数据后返回给 Dart。
分层的好处是每一层都能单独测试。桥接层出问题不用怀疑规则逻辑,规则逻辑出问题也不用动原生代码。尤其在做鸿蒙适配时,Dart 与 ArkTS 之间通信本来就容易踩坑,分层清晰能帮你快速定位问题。
3. 鸿蒙化适配的环境准备与工程搭建
3.1 准备开发环境
适配鸿蒙 Flutter 插件,基础环境有四个部分:DevEco Studio,用于查看和编译鸿蒙侧代码,通常需要附带对应版本的 HarmonyOS SDK;Flutter SDK,建议使用社区维护的鸿蒙支持分支,并确认 Flutter 版本与鸿蒙 SDK 版本匹配;Dart SDK 随 Flutter SDK 一起提供;hvigor 构建工具,由 DevEco Studio 或鸿蒙工程模板自动管理。
这个环境组合没有绝对标准答案,因为生态变化太快。我之前就踩过坑:DevEco Studio 升级后,原有工程的 ohos 目录无法被 hvigor 识别,报错信息完全看不出来是版本不匹配,折腾了一天才想起去查兼容矩阵。建议把以下环境信息写进项目 README,新同事入职第一件事就是按文档装环境。
| 标题 | 版本建议 |
|---|---|
| DevEco Studio | 与 HarmonyOS SDK 配套的正式版本 |
| Flutter SDK | 鸿蒙支持分支,固定 commit |
| hvigor | 跟随 DevEco 工程模板 |
| Dart SDK | 随 Flutter SDK 锁定 |
3.2 创建支持 ohos 的 Flutter 插件工程
创建工程的第一步,是用 Flutter 命令行生成插件骨架。传统模式是 flutter create --template=plugin,生成后会得到 lib、android、ios 等目录,接下来手动添加鸿蒙支持的目录结构。
一个典型鸿蒙 Flutter 插件的目录结构大致如下:
text复制harmony_rule_engine/
├── lib/
│ ├── harmony_rule_engine.dart
│ └── src/
│ ├── engine_proxy.dart
│ ├── fact_bridge.dart
│ └── rule_serializer.dart
├── ohos/
│ ├── entry/
│ │ └── src/main/ets/
│ │ └── plugin/
│ │ └── RuleEnginePlugin.ets
│ ├── build-profile.json5
│ ├── hvigorfile.ts
│ └── oh-package.json5
├── example/
│ ├── lib/
│ ├── ohos/
│ └── pubspec.yaml
├── pubspec.yaml
└── README.md
ohos 目录本质上是一个独立的鸿蒙模块工程,里面有自己的构建配置。你要确保插件根目录的 pubspec.yaml 里 flutter 插件声明部分包含 ohos 平台,这样 Flutter 工具链才能把原生代码注册成插件:
yaml复制flutter:
plugin:
platforms:
android:
package: com.example.harmony_rule_engine
pluginClass: HarmonyRuleEnginePlugin
ohos:
pluginClass: RuleEnginePlugin
3.3 pubspec 修改与依赖锁定
修改 pubspec.yaml 时,除了 rules 包,还要把鸿蒙相关依赖加进来。当时我的依赖配置如下:
yaml复制name: harmony_rule_engine
description: A HarmonyOS adapted Flutter plugin wrapping the rules package.
version: 1.0.0
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.16.0"
dependencies:
flutter:
sdk: flutter
rules: ^0.0.5
path: ^1.8.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
plugin:
platforms:
ohos:
pluginClass: RuleEnginePlugin
这里特别提醒:rules 包版本较老时,对 Dart SDK 有版本要求,如果是高版本 Flutter 工程,依赖解析可能会冲突。我当时的处理办法是先 flutter pub get,解析失败就查看锁文件冲突项,用 dependency_overrides 指定 rules 版本。但 override 是最后手段,能用正常解析解决就不要强制。
4. 核心适配实操:把 rules 引擎吃进鸿蒙工程
4.1 把规则引擎包进自己的封装层
直接让业务代码操作 RuleEngine 不算错,但不太利于长期维护。我选择在 rules 外面包一层 HarmonyRuleEngine,把注册和执行的接口收敛成自己团队的风格,这样未来底层引擎即使更换,业务代码也不用大面积改动。
封装后的接口大概是这样的:
dart复制import 'package:rules/rules.dart';
typedef RulePredicate = bool Function(Map<String, dynamic> facts);
typedef RuleAction = void Function(Map<String, dynamic> facts);
class HarmonyRuleEngine {
HarmonyRuleEngine({Map<String, dynamic>? baseFacts})
: _facts = {...?baseFacts};
final Map<String, dynamic> _facts;
final RuleEngine _engine = RuleEngine();
void register(String name, RulePredicate when, RuleAction then) {
_engine.add(
Rule(name: name, when: when, then: then),
);
}
Future<void> execute() async {
await _engine.execute(_facts);
}
T? result<T>(String key) => _facts[key] as T?;
}
这个封装层价值在鸿蒙化场景里尤其明显。因为后面我们要做数据桥接、序列化、规则分组,这些都是底层引擎不关心的横向能力,正好放在封装层里统一处理。对于业务方来说,他们看到的只有 register 和 execute,心智负担小很多。
4.2 原生数据桥接:把 ArkTS 侧事实喂给 Dart 规则
规则执行需要事实,事实来源却常在鸿蒙原生侧。比如用户是否VIP、当前GPS定位城市、上次登录时间,这些数据在 ArkTS 侧最方便获取。如果让 Dart 侧去逐个模块要数据,既绕又慢。正确做法是通过 MethodChannel 把原生侧数据一次性拉过来。
Dart 侧定义一个事实桥接类:
dart复制import 'package:flutter/services.dart';
class FactBridge {
static const MethodChannel _channel = MethodChannel(
'harmony_rule_engine/facts',
);
Future<Map<String, dynamic>> fetchFacts(List<String> factKeys) async {
final raw = await _channel.invokeMethod('getFacts', {
'keys': factKeys,
});
return Map<String, dynamic>.from(raw as Map);
}
}
鸿蒙侧的 ArkTS 插件实现,重点是在回调里把字段组装成一个 Map 返回:
typescript复制import { MethodCall, Plugin } from '@kit.PluginKit';
export class RuleEnginePlugin extends Plugin {
async onCall(call: MethodCall): Promise<Object | null> {
if (call.method === 'getFacts') {
const keys: Array<string> = call.args['keys'];
const facts: Record<string, Object | null> = {};
keys.forEach((key) => {
facts[key] = this.fetchNativeFact(key);
});
return facts;
}
return null;
}
private fetchNativeFact(key: string): Object | null {
// 从用户会话、数据库、系统API获取数据
if (key === 'isVip') {
return this.isCurrentUserVip();
}
if (key === 'location') {
return this.getLastLocation();
}
return null;
}
}
这里有个经验:桥接拿到的事实,原则上是只读的。规则执行过程中尽量不要写回原生侧,而是等整条规则链执行完,再把最终结果一次性回传。否则一条规则触发一次 Channel 调用,性能会肉眼可见地变差。
4.3 JSON 事实注入与序列化性能
鸿蒙原生侧拿到的数据,很多时候是 JSON 字符串;Dart 侧 Fact 又要求是 Map。频繁做 jsonEncode 和 jsonDecode 是有开销的,尤其是当事实里包含时间、BigInt、嵌套对象时,序列化不一致还会引发诡异问题。
我建议在桥接层做一次“事实统一”处理。原生侧返回的字段,凡是确定不会被规则当字符串使用的,就转成标准类型;凡是会被当作条件比较的,确保是数字或布尔。这样规则写起来也不用反复做类型转换。
dart复制Future<Map<String, dynamic>> fetchFactsWithNormalization(
List<String> keys,
) async {
final facts = await FactBridge().fetchFacts(keys);
return facts.map((key, value) {
if (value is String) {
final parsed = num.tryParse(value);
if (parsed != null) {
return MapEntry(key, parsed);
}
}
return MapEntry(key, value);
});
}
这个细节看起来不起眼,但确实能避免一批“规则判断不准”的隐性 bug。尤其是后台下发的数字字段有时是字符串,前端拿来做 > 比较时永远为 false,排查起来相当浪费时间。
4.4 把规则集打包进 HAR 并集成
插件开发完成后,你不仅要在 Flutter 工程里引用它,很多时候还要让鸿蒙原生应用整体集成这个 Flutter 插件模块。这就要把插件打成一个 HAR(HarmonyOS Archive)。
打包过程主要在两个层面。首先是 Dart 侧,执行 flutter build har --release 或对应构建命令,生成包含 Dart 产物和原生插件的鸿蒙归档包。然后是鸿蒙侧,确认 ohos 模块的 oh-package.json5 里已经声明好依赖,再通过 DevEco Studio 的 hvigor 构建流程产出 HAR。
集成到鸿蒙 App 时,有两种方式:一种是把 HAR 文件直接复制到目标工程的 oh_modules 目录,并在 oh-package.json5 里声明依赖;另一种是使用源码依赖,直接把插件工程目录挂到目标工程下。源码依赖调试方便,正式交付用 HAR 更干净,避免对方拿到你的源码。
5. 性能调优:逻辑断言不能拍脑袋
5.1 先做基准测试,再谈优化
我见过不少人一上来就嚷着要加多线程、要并行执行,结果连当前瓶颈在哪都不知道。规则引擎的性能优化第一件事永远是采集基线。我在适配时写了一个非常简单的基准脚本,跑一万次规则执行:
dart复制final engine = RuleEngine();
engine.add(
Rule(
name: 'vip_check',
when: (facts) => facts['isVip'] == true,
then: (facts) => facts['canUseVoucher'] = true,
),
);
engine.add(
Rule(
name: 'amount_check',
when: (facts) => facts['amount'] >= 500,
then: (facts) => facts['discount'] = 0.9,
),
);
final facts = {'isVip': true, 'amount': 800};
final stopwatch = Stopwatch()..start();
for (var i = 0; i < 10000; i++) {
await engine.execute(facts);
}
stopwatch.stop();
print('avg execute: ${stopwatch.elapsedMicroseconds / 10000} us');
实测下来,几十条规则的场景,单次执行基本在几十微秒量级,完全不需要优化。一旦规则数量涨到几百上千条,且每条条件里都有 list.contains、string.split 这种重操作,单次执行就可能飙到几十毫秒,这时候才需要考虑优化方案。
5.2 规则预编排:让执行顺序可预期
rules 包本身按添加顺序执行规则,这在规则少时没问题,规则多时就会出现性能问题。有些规则只依赖事实里的 userLevel,有些只依赖 orderAmount,如果每次执行都让全部规则过一遍,等于把所有无关条件判断都跑了一次。
我在封装层里加入了“按事实维度索引规则”的机制。注册规则时,要求调用方声明这条规则关注哪些事实字段;执行时根据传入事实里发生变化的字段,只触发关注这些字段的规则。
dart复制class IndexedRule {
final String name;
final Set<String> dependsOn;
final Rule rule;
}
class HarmonyRuleEngine {
final Map<String, List<IndexedRule>> _ruleIndex = {};
void registerIndexed(
String name,
Set<String> dependsOn,
RulePredicate when,
RuleAction then,
) {
final indexed = IndexedRule(
name: name,
dependsOn: dependsOn,
rule: Rule(name: name, when: when, then: then),
);
for (final key in dependsOn) {
_ruleIndex.putIfAbsent(key, () => []).add(indexed);
}
}
}
这个预编排带来的收益,在复杂条件链场景下特别明显。比如风控规则有 50 条只看用户信用分,有 30 条只看订单金额,用户登录时只需要跑前 50 条,下单时才追加后 30 条。执行总量可能减少一半以上。
5.3 用 Isolate 隔离执行,避免卡 UI
规则执行是纯计算任务,但也有可能在某个对象深度嵌套或条件复杂时拖慢 UI 帧率。鸿蒙 Flutter 的 Dart isolate 机制和原生 Flutter 一致,所以完全可以用 Isolate.run 把规则执行放到后台。
dart复制Future<Map<String, dynamic>> executeInIsolate(
Map<String, dynamic> facts,
Map<String, dynamic> ruleConfig,
) async {
return Isolate.run(() async {
final engine = RuleEngine();
for (final item in ruleConfig['rules'] as List) {
final rule = Rule(
name: item['name'],
when: (f) => _parseCondition(item['when'], f),
then: (f) => _parseAction(item['then'], f),
);
engine.add(rule);
}
await engine.execute(facts);
return facts;
});
}
使用 Isolate 有一个隐藏成本:传入和返回的数据都要跨 isolate 拷贝一次。如果事实很大,或者规则链执行本身只要几百微秒,那么 isolate 拷贝的开销可能比执行本身还大。所以我的建议是:事实超过一定规模或规则链超过百条,才值得用 Isolate;轻量规则直接在 UI isolate 里跑就好。
5.4 避免桥接层成为性能瓶颈
规则执行过程中的数据流动,要尽量减少跨层调用。最理想的模式是:执行前一次性拉取所需原生事实,执行过程中事实在 Dart 侧内部流转,执行完成后只把结果字段返回原生侧。
我见过一个反面案例,规则里每次判断用户位置时,都通过 MethodChannel 去调用 GPS 模块拉数据,一条规则链跑完,桥接调用了几十次。改进方法很简单:在规则链启动前,先批量把 GPS、用户画像、订单信息全部拉进 Fact,规则内部不再做任何异步桥接调用。这个改动让整体耗时下降了接近一个数量级。
6. 复杂条件链治理:从能跑到能维护
6.1 优先级、分组与终止策略
规则数量多了以后,最大的问题不是性能,而是“规则执行结果是否正确”。比如风控场景里,黑名单检测必须最先执行,一旦命中就终止后续规则的放款动作;优惠券场景里,最低折扣规则优先级最高,不能被普通规则覆盖。
rules 包的执行模型是按添加顺序串行执行,这本身就给了你秩序控制权。我在封装层做了一层简单的优先级队列:每条规则注册时带一个 priority 数字,数字越小越先执行。执行完优先级为 critical 的规则后,如果事实里出现终止标记,整条链立即停止:
dart复制Future<void> executeWithPriority() async {
final sortedRules = _indexedRules.values
.expand((list) => list)
.toList()
..sort((a, b) => a.priority.compareTo(b.priority));
for (final indexed in sortedRules) {
if (_facts['__terminated__'] == true) break;
await indexed.rule.execute(_facts);
}
}
这个思路比在规则 then 里写 return false 更清晰,因为业务规则不需要感知引擎控制逻辑,只需要在自己的动作里设置业务状态。
6.2 规则之间只能通过事实通信
规则引擎踩坑最多的地方,就是规则闭包捕获了外部变量,导致规则与规则之间产生隐形依赖。比如规则 A 里写了一个计数器,规则 B 读取这个计数器决定是否执行。看着没问题,但一旦规则顺序变化,或者 A 被删除,B 就静默失效。
正确做法是:规则之间不共享任何闭包变量,只通过 Fact 读写数据。规则 A 需要计数,就把计数写进 facts['counter'];规则 B 判断时只读取 facts['counter']。这样规则变成纯函数,可以随意排序、单条测试、甚至单独下发。
我在团队里定了一条规矩:规则文件内不允许出现规则闭包之外的可变状态。所有临时变量必须以 facts['tmp_xxx'] 的形式存在。这条规矩执行后,规则链的可调试性提高了非常多,出问题基本看一遍 facts 日志就能定位。
6.3 规则序列化与远端下发
规则不该硬编码在 App 里。把规则序列化成 JSON,由服务端远程下发,是规则引擎的重要价值。rules 包的闭包规则天然无法序列化,所以我在准备远端下发时,对规则做了一层协议:远端下发 when 和 then 的 JSON 描述,Dart 侧再解析成闭包。
一份简单的规则 JSON 如下:
json复制{
"rules": [
{
"name": "blacklist_check",
"priority": 1,
"when": {
"field": "isBlacklist",
"operator": "eq",
"value": true
},
"then": {
"action": "set",
"field": "approval",
"value": "reject"
}
}
]
}
解析层只支持有限的算子,比如 eq、neq、gt、lt、in,足够覆盖大多数业务判断。这样做的好处是,运营人员可以在后台配置规则,App 启动时拉取最新规则集,不用等发版。灰度阶段可以只对 10% 用户下发新规则,出现异常再一键切回上一版。
7. 常见问题与排查实录
7.1 典型问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
构建时 ohos 目录未识别 |
插件 pubspec 未声明 ohos 平台 | 检查 flutter.plugin.platforms 配置 |
| 原生侧调用 Flutter 通道无响应 | 插件类未注册,或模块名不一致 | 确认插件注册名和 Dart 侧 channel 名一致 |
| 规则执行后结果不正确 | 原生数据字符串被当作数字比较 | 在桥接层统一做数值解析 |
| 依赖安装报版本冲突 | rules 包要求低版本 Dart SDK | 加 dependency_overrides 临时解决 |
| 大批量规则执行卡 UI | 规则过多或规则内部重操作过多 | 用 Isolate 隔离执行,或规则预编排 |
| 真机上桥接返回 null | 原生侧返回类型不是合法 Map | 检查 ArkTS 返回值是否是可被 MethodChannel 序列化的类型 |
| HAR 集成后插件方法找不到 | 目标工程缺少插件依赖声明 | 检查 oh-package.json5 的 dependencies |
7.2 排查思路:先分层,再合并
适配期遇到问题,我的排查顺序永远是:先确认数据能不能到 Dart 侧,再确认规则逻辑有没有执行,最后才看结果有没有回传原生。如果桥接层都还没通,就纠结规则表达式对不对,那是浪费时间。
具体做法是三层日志:Dart 侧进入方法时打一条,标注收到的事实 key 列表;规则引擎执行完打一条,记录每条规则名称和执行耗时;原生侧收到结果回调再打一条。三层日志时间轴对齐后,问题基本一眼就能看出来。
还有一个小技巧:鸿蒙原生侧日志用 DevEco Studio 的 Log 面板查看,Dart 侧用 flutter logs 或控制台打印。两个日志的时间戳可能不同步,建议在两边同时打印当前时间或同一请求 ID,方便关联定位。
8. 再往前走一步
规则引擎这个方向,做完适配和性能优化只是开始。我在实际项目中体会到,真正能拉开差距的是规则治理生态:规则能不能可视化管理、能不能做版本比对、能不能在线上动态调整。如果你现在刚把 rules 包适配进鸿蒙,下一步可以做三件事:第一,把规则执行耗时上报到监控平台,建立规则性能基线;第二,给规则写单元测试,每条规则至少覆盖一个命中场景和一个不命中场景;第三,设计一套规则版本号规范,确保远端下发的规则集能平稳切换。
我在适配过程中踩过最大的坑,就是低估了数据桥接的复杂度,以为规则库能跑通就万事大吉。后来被线上问题教育过才明白,规则引擎的核心价值不在引擎本身,而在围绕引擎搭起的那套工程化能力:数据如何进来、规则如何管理、结果如何被信任。希望这篇文章能让你少绕一些弯路。
