做工具类应用这么多年,我频繁遇到同一个场景:手上一段正则表达式,需要反复改、反复测,但电脑不在旁边,手机里又没有趁手的调试器。在线工具倒是有不少,可很多要么把系统权限收得太死,要么对长文本和分组明细的支持一言难尽。磨蹭了几次之后,我干脆用 Flutter 在 OpenHarmony 设备上实现了一个正则表达式测试器 App,顺手把输入、解析、高亮、分组明细这一整条链路完整跑通。这个项目看着不大,但对想深入了解 OpenHarmony 生态开发的人来说,每一步都踩在实打实的技术点上。
这篇文章我会把整个实现过程拆开讲:从 Flutter 如何在 OpenHarmony 上运行,到正则引擎的差异、UI 高亮方案、数据收集与构建发布,再到我实际遇到的一系列问题和排查思路。里面涉及到的代码都是我自己实测后保留下来的版本,可以直接拿去参考。
1. 项目整体拆解:Flutter 迁移到 OpenHarmony 的底层逻辑
1.1 OpenHarmony 上的 Flutter 适配原理与版本选择
先说结论:OpenHarmony 不是“换个壳的 Android”,它有自己的应用模型、Ability 概念、包管理方式和权限体系。Flutter 能在这套系统上跑起来,靠的不是官方默认分支,而是社区维护的 OpenHarmony 适配分支。这个分支把 Flutter 引擎重新绑定到 OpenHarmony 的图形栈和输入事件上,让 Dart 层的开发者几乎感觉不到底层差异。实际表现就是:你在 Dart 里写的界面、状态管理、正则逻辑,跟 Android 或 iOS 上几乎一致,但最终生成的可安装包是 OpenHarmony 体系里的 HAP 格式。
我当时面临三条技术路线选择,这里顺便给大家做个对照:
ArkTS 原生开发:上手快,系统能力调用直接,组件风格也贴合设备端视觉,但应用模型只围绕 OpenHarmony 生态,将来要兼顾移动端的话代码复用率偏低。
HTML5 套壳方案:写起来最简单,可一旦涉及系统剪切板、文件存储、复杂渲染,就需要频繁走桥接层,调试链路拉得很长,性能也受限。
Flutter 跨平台方案:Dart 层的正则引擎是独立实现的,UI 采用自绘渲染,适配分支能产出可运行的 HAP 包,同时保留了 Android/iOS 的编译通道,复用价值最高。
我最终选了第三条,原因是工具类应用对“通用性”有天然需求,我不想只绑在一个生态里。另一个关键点在于版本匹配:不能用官方最新版 Flutter 直接去编译 OpenHarmony 产物,必须在工程配置里指定专门适配的 SDK 路径,并在命令参数中声明目标系统类型。这一点踩坑概率极高,后面第 4 节我会细说。
1.2 为什么把正则测试器作为首个实战项目
很多人第一次接触新系统时,习惯用一个带网络请求、多页面跳转的“完整业务应用”去试水,我对此持保留态度。新平台最大的不确定性往往来自基础设施,而不是业务复杂度。正则表达式测试器表面上只是个输入输出工具,但它几乎覆盖了一个工具类应用的全部关键链路:文本输入事件、实时解析、复杂富文本渲染、长文本定位、非 ASCII 字符处理,甚至输入法兼容性都能碰到。
这类项目的另一个优势是“可观测性好”。正则测试的结果非对即错,输入和输出一目了然。如果匹配结果不对,我可以很快判断问题是出在正则语法理解偏差、Dart 引擎与常见工具链的差异,还是 UI 渲染层的 bug。这种问题可分性在验证新平台时极其宝贵。我建议任何刚接触某个新系统的人都采取这个策略:先用一个几十个接口就能实现的小工具打通链路,而不是一上来就追求业务广度。
2. 正则表达式测试器的功能设计与技术方案
2.1 核心功能设计:从输入到结果的全流程闭环
正则测试器的功能看起来简单,但真要在移动端做得顺手,需要拆分得足够细。我把主界面分成四个区域:正则输入区、测试文本区、模式开关区、结果展示区。四个区域纵向排列,键盘弹出后整体滚动,避免失去焦点。
功能清单我做了这样的划分:
| 功能模块 | 具体能力 | 实现说明 |
|---|---|---|
| 正则输入 | 输入/编辑正则表达式,错误提示 | TextField + 编译异常捕获 |
| 测试文本 | 支持长文本输入与粘贴 | TextField 多行模式 |
| 模式控制 | 忽略大小写、多行模式、全局匹配 | FilterChip 开关 |
| 高亮预览 | 匹配部分用主题色标出 | RichText/TextSpan |
| 匹配明细 | 展示位置、分组内容 | ListView.builder 懒加载 |
| 替换测试 | 输入替换串,预览替换结果 | RegExp.replaceAll |
| 历史收藏 | 保存常用的正则 | 本地 JSON 文件持久化 |
匹配明细是我强烈建议保留的模块。日常调试日志抽取时,只看到高亮还不够,你往往需要知道第 2 个捕获组到底匹配到了什么,起始位置在哪里。明细列表里把这些信息逐条展示出来,能省掉大量肉眼排查的时间。
2.2 Dart 正则引擎的差异与必须绕开的坑
Dart 的 RegExp 类参照 ECMAScript 正则语法,底层本质上是相对轻量的自动机实现,和 PCRE 这类支持丰富断言、反向引用的“重量级”引擎并不完全等价。很多在桌面调试器里能够正常运行的写法,拿到 Dart 里可能直接不通过,或者得到不符合预期的结果。
我遇到比较典型的是分组命名和某些零宽断言的兼容问题。不同平台对这些结构的容忍度不一致,最稳妥的策略是让使用者优先尝试官方文档明确支持的语法。很多开发者在这上面栽跟头,因为他们在其他语言里形成的肌肉记忆不适用于 ECMAScript 风格。
另外有一个中文场景的经典问题:\w匹配的是 ASCII 字符、数字和下划线,默认情况下并不会匹配中文。如果你要匹配中文,不要在正则里用\w,而是直接写明字符范围,比如[\u4e00-\u9fff]。这一点在中文网络环境下的开发者工具里非常重要,我特意在测试器内置的帮助页里做了提示,因为几乎所有第一次使用的人都会踩到。
正则输入框还需要做一层容错。用户输入非法模式时,RegExp 的构造会抛异常,但异常信息并不友好,很多是底层符号和英文专业词,普通用户根本看不懂。最简单的做法是封装一个安全编译函数,任何异常都转成中文提示:
/// 安全编译正则,返回 null 表示输入非法 RegExp? safeCompile(String? pattern) { if (pattern == null || pattern.trim().isEmpty) { return null; } try { return RegExp(pattern); } catch (e) { // 这里把异常交给 UI 层做中文映射 return null; } }真正展示错误时,我把常见异常关键词做映射,比如“unterminated group”映射成“正则分组缺少右括号”,用户一眼就能明白问题出在哪个位置。
2.3 高亮、明细与组件选型
高亮是正则测试器体验的核心,我对比过 WebView、CustomPaint、RichText 三种方案。WebView 实现简单但性能开销大,在长文本场景里滚动明显掉帧;CustomPaint 很灵活,但要自己处理文本换行、字体测量、坐标变换,工程量不小;最终我选了 RichText + TextSpan 的方案,性能足够,代码量也适中。
RichText 的构建逻辑其实不难,核心是遍历所有匹配,把文本切成多个片段,匹配区间用高亮样式,其余片段用普通样式。需要注意匹配结果可能是零宽匹配,这种情况要特别处理,防止无限循环或死循环期间的界面卡死,我在后续版本里对零宽匹配做了单独的提示。
明细列表用ListView.builder做懒加载,每条只展示匹配文本、起始位置、结束位置和捕获组数,点击后可以展开查看分组细节。整体状态管理我用一个ChangeNotifier,输入变化时通知结果区刷新,而不是在 build 方法里去做高成本的全量遍历。
3. 核心实现细节与实操记录
3.1 输入区控制器与快速校验逻辑
输入区部分,我用了两个TextEditingController分别管理正则是测试文本。监听它们的办法不是每次键盘输入都立刻全量解析,这样长文本场景会卡顿。正确做法是引入防抖机制,停止输入 300ms 后再触发解析。
final patternController = TextEditingController(); final sampleController = TextEditingController(); Timer? _debounce; void _onFieldChanged() { _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 300), _updateResults); }_updateResults内部拿到两个 controller 的 text,先做空值判断,再用RegExp(pattern)编译。编译失败时结果区显示错误提示,成功时调用匹配与高亮方法。整个过程不到 60 行代码,但交互手感比每次击键都刷新稳妥太多。
3.2 匹配高亮文本的构建
文本高亮的关键在于把原始字符串分段。我封装了一个函数,输入测试文本和一个已编译的 RegExp,输出一组 TextSpan:
List<TextSpan> buildHighlightSpans( String text, RegExp regex, { required TextStyle normalStyle, required TextStyle highlightStyle, }) { final spans = <TextSpan>[]; var current = 0; for (final match in regex.allMatches(text)) { if (match.start > current) { spans.add(TextSpan( text: text.substring(current, match.start), style: normalStyle, )); } spans.add(TextSpan( text: text.substring(match.start, match.end), style: highlightStyle, )); current = match.end; } if (current < text.length) { spans.add(TextSpan( text: text.substring(current), style: normalStyle, )); } return spans; }这个函数有几个细节值得注意:match.group(0)拿到的整体匹配虽然好用,但在构建 span 时直接用start和end切割最可靠,因为高亮要保持原始文本的格式和长度。match.end在零宽匹配时和match.start相同,会导致空 span,我在实际版本里加了长度判断,遇到零宽匹配直接在结果区给出提示,避免用户误以为匹配到了内容。
UI 部分我用SelectableText.rich而不是普通RichText,这样用户可以直接选中高亮后的结果复制,对测试器场景非常实用。长文本时也别担心,SelectableText.rich的构建成本是一次性的,滚动性能可以直接接受。
3.3 捕获组明细的收集与展示
捕获组明细是正则调试的重要辅助。我在收集结果时构造一个简单结构体,把每次匹配的完整文本、起止位置和所有分组内容都存下来:
final matches = regex.allMatches(sampleText); final rows = <Map<String, Object>>[]; for (final match in matches) { rows.add({ 'full': match.group(0) ?? '', 'start': match.start, 'end': match.end, 'groups': List<String>.generate( match.groupCount + 1, (index) => match.group(index) ?? '', ), }); }注意一个容易出错的点:match.groupCount表示捕获组数量,但group(0)是完整匹配,所以遍历时候要加 1。我第一次写的时候图省事,直接用groupCount作为循环次数,结果每组都漏掉了整体匹配,排查了好一会儿才意识到。
明细展示我用了一个可折叠的列表项,默认只显示“整体匹配 + 位置”,点击展开后展示各个捕获组的编号和内容。这个设计在长日志场景下非常有用,屏幕空间有限,不需要把所有信息一次性铺开。
3.4 从 Dart 到 HAP:OpenHarmony 产物构建
工程能跑 UI 只是第一步,最终能不能装到设备上看效果,才是这个项目的真正目标是重头戏。这里重点说构建链路,因为新手最容易在最后一步迷失。
使用 Flutter 适配分支后,流程大概是:先在 IDE 里打开 OpenHarmony 侧工程并配置好 SDK 路径,然后在 Flutter 工程执行构建 HAP 的专用命令。第一次执行时大概率会遇到设备类型不匹配的问题,因为 OpenHarmony 设备可能有不同的 CPU 架构和支持能力,命令里没有指定目标架构时,构建工具可能无法识别。
解决方法是显式声明目标平台参数,例如指定 arm64 架构,重新执行构建。构建成功后产物是 HAP 文件,再通过设备连接工具部署到 OpenHarmony 设备或模拟器上。这一步我在日志里多次遇到签名失败,原因是新工程的调试证书没有给到应用,需要在基础设施配置里申请调试证书并用工具完成签名。
整个过程最忌讳的一点是:混用官方 Flutter 的编译缓存。适配分支和官方分支是两套不同的引擎产物,如果共享同一个缓存目录,会出现白屏、运行时报错这类很难排查的症状。我后来设置了独立的缓存环境变量给适配分支专用,从此这类问题再没有出现过。
4. 常见问题与排查实录
4.1 编译版本不匹配导致的白屏问题
项目刚跑通时我遇到过白屏,而且是设备上直接闪退。典型特征是在 Android 上正常的代码,打包成 HAP 后一到设备就 Crash。这通常不是业务代码的问题,而是 Flutter 引擎与应用包版本不一致。
我当时的排查步骤是这样:先看 Log 里有没有“engine mismatch”之类的关键词,如果有,确定是 SDK 分支和构建参数不匹配;然后把官方 Flutter 的缓存目录全部清掉,只保留适配分支相关的目录,重新构建。清完缓存后白屏消失,界面能正常渲染了。
这类问题最大的困扰在于复现不规律,有时第一次构建能起来,第二次就崩。我的经验是:把两条体系完全隔离,不要共用环境变量和缓存目录;整个项目固定使用同一套 Flutter 适配版本,不要中途随意升级。
4.2 中文与特殊字符的显示问题
我在测试文本里输入一段中文时,高亮结果部分汉字出现了异常换行和位置偏移。这个现象一开始以为是 substring 索引算错,后来定位到是字体样式问题。OpenHarmony 对默认字体的映射和 Android 不一样,某些 span 样式没有显式指定字体家族时,系统字体退化逻辑会干扰文本排版。
解决办法是在全局主题里统一指定一个不含英文大小的通用字体族,并给文本部件设置合适的行高和文本对齐方式。经过这些调整,中文高亮和换行表现稳定了。
这里还有一个容易忽略的点:编码。正则测试器一定要保证输入字符串能被系统正确处理,虽然 Dart 字符串本身是 UTF-16,但涉及系统剪贴板或文件读写时,编码不一致也会出现“看起来正常但匹配不上”的问题。我的建议是文件读写统一使用 UTF-8,避免平台默认编码差异介入。
4.3 正则错误提示不友好的优化方案
RegExp 构造抛出的异常对普通开发者并不够直观,例如某个匹配标记不受支持,异常信息是英文的底层描述,不熟悉正则术语的人很难快速定位。
我实现了一个错误映射函数,用关键词匹配的方式把常见异常转换成“正则第几个字符附近可能存在未闭合字符类”“分组缺少右括号”“量词前面缺少匹配对象”这种直接提示。同时,我会在输入框下方实时显示错误,并用抖动动画引起注意。这个功能虽然不涉及复杂逻辑,但对工具类应用的体验提升非常明显。
4.4 匹配量过大导致界面卡顿
有一次我拿一段比较长的日志文本测试,用户不小心输入了一个非常宽泛的模式,结果一瞬间匹配出几万条结果。高亮部分还好,但明细列表一口气构建几万个对象,直接把 UI 线程拖死,界面卡了好几秒。
我做了两层保护:
- 对匹配结果数量做了上限,最多构建 2000 条明细,超出部分在列表中提示“结果过多,已截断展示”。
- 将正则匹配和明细数据的收集放到单独的 isolate 中执行,避免在主 Isolate 里做大量循环。
实现思路是用 Flutter 的compute函数,把测试文本和模式字符串传入后台 isolate,让它完成遍历并返回一个列表。这样即使超长文本和超宽泛的模式同时出现,界面也只会轻量提示,不会死等。
4.5 常见问题速查表
| 现象 | 可能的原因 | 快速解法 |
|---|---|---|
| 构建后设备白屏 | 缓存混用、SDK 分支不一致 | 清理并隔离缓存目录,固定版本 |
| 中文高亮错位 | 字体映射与默认样式问题 | 全局指定字体族和行高 |
| 正则匹配不上中文 | \w不覆盖非 ASCII | 使用[\u4e00-\u9fff]等显式范围 |
| 明细列表卡顿 | 匹配结果过多 | isolate 收集数据 + 条数上限 |
| 非法正则无提示 | 未捕获异常 | 安全编译函数 + 中文错误映射 |
| 部署提示设备类型错误 | 未指定目标系统架构 | 添加目标平台参数重新构建 |
5. 从测试器到完整工具集的延伸建议
5.1 继续扩模块:下一批工具怎么加
正则测试器跑通后,我发现当初为它设计的页面骨架完全可以复用。同一个输入区、结果区、模式开关、历史收藏的结构,稍微改造就能承载 JSON 格式化/压缩、Base64 与 URL 编解码、时间戳转换、颜色值换算这类开发工具。
实际做法是配置化:定义一份“工具页面描述表”,把每个工具的字段、按钮动作、结果渲染样式声明进去,用一个通用页面工厂去渲染。新增一个工具模块的时候,只需要注册描述,不用重新写一套页面。这样扩展速度快,维护成本也低。
5.2 结果分享与收藏的体验优化
正则测试器还有一个很有使用价值的场景:把测试结果复制出去,或者把某条常用正则收藏下来。复制功能直接用系统剪贴板,不过 OpenHarmony 适配分支对某些插件的支持程度不同,我建议优先使用系统 API 而不是依赖第三方插件。收藏数据我选择存成简单的本地 JSON 文件,不引入本地数据库。理由很简单:这类数据量很小,每次读写就是几十条 JSON 记录,文件方案最可控,也能避免适配分支对某些数据库的兼容性问题。
5.3 个人经验:这个项目给我带来的最大收获
这个项目最有价值的不是“正则表达式测试器”这个功能本身,而是它让我彻底摸清了 Flutter 在一个新系统生态里从代码到真机的完整链路。它证明了一个道理:想验证一个跨平台框架对新操作系统的适配程度,最有效的方法不是大量阅读文档,而是挑一个输入输出明确、依赖较少、可观测性强的工具应用,实打实地跑一遍。
如果你也正准备在 OpenHarmony 上做 Flutter 开发,我的建议是别急着铺开功能。先让一个最小用例“输入到结果展示”形成闭环,再把版本、缓存、签名这些基础设施问题逐个解决掉。等这些底层能力稳定了,后续无论做软件开发助手、文本处理工具,还是更复杂的业务应用,都会顺畅很多。