Flutter鸿蒙化工程硬编码字符串扫描工具适配实践 作为一个常年跟 Flutter 工程打交道、最近又扎进鸿蒙化整改的开发者我这两年对一句话的体会越来越深没有硬编码字符串扫描流程的国际化改造就是给未来埋雷。起初团队里很多人觉得国际化嘛不就是把中文文案抠出来塞进 ARB 文件再跑一遍 flutter gen-l10n 的事。可真等代码搬上鸿蒙、要同时维护多语言资源的时候大家才发现散落在业务代码里的各种文案——按钮标题、Toast 提示、错误信息、日志前缀——根本数不清。这时候string_literal_finder这类工具就成了救命稻草。它能把工程里所有 Dart 源文件中的字符串字面量自动拎出来按照过滤规则筛掉噪声告诉你哪些硬编码字符串值得被立即收编进国际化体系。但这篇文章我想重点聊的不是工具本身怎么用而是它在鸿蒙化工程里该怎么落地。因为 string_literal_finder 毕竟是个 Flutter/Dart 生态的工具鸿蒙工程尤其是 OpenHarmony 侧、使用 ArkTS Flutter 混合架构的项目相比纯 Flutter 工程目录结构、Dart SDK 版本、构建链路都有不少差异。直接拿现成工具去扫大概率会踩到路径扫不到、analyzer 版本冲突、报告格式不匹配之类的坑。下面我从源码原理、适配改动、CI 集成、踩坑记录四个维度把这套鸿蒙化适配方案完整拆开讲。1. 硬编码字符串为什么成了燎原之火——先说清楚这个工具的价值链条1.1 硬编码字符串的典型现场我见过太多 Flutter 工程的文案现场举几个真实例子// 场景一按钮文案直接写在 widget 里 ElevatedButton( onPressed: () {}, child: const Text(登录), ) // 场景二错误提示散落在网络层 throw ApiException(请求超时请稍后重试); // 场景三列表空态文案 Center(child: Text(暂无数据))这些代码在单语言版本里完全没问题但只要进入国际化阶段问题就立刻爆发产品经理告诉你俄罗斯市场需要俄语你只能全局搜索登录两个字逐个文件替换。更糟的是有些字符串是拼接出来的比如第 page 页翻译得考虑语序纯靠人工根本拦不住后续新增的硬编码。所以团队基建里必须有一道自动化关卡专门回答三个问题代码里到底有多少硬编码字符串它们分布在哪些文件、哪些行哪些是真需要国际化的哪些是 JSON key、正则、路径这种无需处理的噪声1.2 从 AST 层面捞字符串的原理string_literal_finder之所以比简单正则靠谱是因为它走的是 Dart 官方 analyzer 的解析链路。大致流程如下用analyzer包读取指定目录下所有.dart文件。通过parseFile或DartAnalysisContext生成抽象语法树AST。遍历语法树里的每一个节点遇到StringLiteral类型就取出来。对拿到的字符串套用过滤规则判断是否属于需要人工确认的硬编码。拼装报告输出文件路径、行号、字符串内容和匹配规则名。因为是基于 AST 而不是文本匹配所以它天然能区分字符串是写在变量里、函数参数里还是注释里。这一点很关键注释里的中文如果被无差别扫出来报告会变成灾难。1.3 过滤规则不是所有字符串都该被揪出来做适配的时候最值得深挖的是它的过滤策略。默认规则一般包括过滤类型示例原因空串与纯空白、 没有翻译价值纯数字/纯标点/纯符号123、:)不需要翻译URL、文件路径、协议头https://...、assets/nav_icon.png不随语言变化正则表达式r^\d$属于逻辑代码JSON/Map 的 keyuser_name属于数据协议而非界面文案单字母、占位符%s、$name一般是格式化模板的组成部分这套规则决定了工具的误报率。适配鸿蒙工程时我不建议一上来就放宽规则宁可多报 20%也不要漏扫——多报了可以人工排除漏扫了就直接漏到线上。2. 鸿蒙化适配的真正难点不在跑通而在工程边界2.1 Flutter 模块在鸿蒙工程里的落位纯 Flutter 工程结构固定lib/目录就是源码根目录。但鸿蒙工程不一样。以 DevEco Studio 创建的工程为例典型的混合结构是这样的MyHarmonyApp/ ├── AppScope/ ├── entry/ │ └── src/ │ └── main/ │ ├── ets/ # ArkTS 原生页面 │ ├── resources/ # 鸿蒙侧资源 │ └── module.json5 ├── flutter_module/ # Flutter 模块 │ └── lib/ # 真正的 Dart 代码 ├── oh_modules/ ├── build/ └── .dart_tool/string_literal_finder 默认扫的是运行目录的lib到了鸿蒙工程里这个路径就失效了。你需要通过参数显式指定-d flutter_module/lib。别小看这一步工具在根目录什么都扫不到时最容易让你误判为代码很干净。2.2 纯 ArkTS 页面要不要纳入扫描如果你团队是 Flutter 为主、ArkTS 只用来搭原生壳和少量系统交互页面我建议 ArkTS 也纳入管理。原因很朴素鸿蒙侧如果出现硬编码中文它同样会漏掉多语言适配。string_literal_finder 本身是 Dart 工具要直接扫描.ets文件不太可能但在适配时可以用两条路解决方案 A独立一个简单脚本用正则提取ets/目录下的字符串字面量。需要注意的坑是ArkTS 里有//行注释、/* */块注释、模板字符串${}正则写得不好会误伤一片。方案 B暂不接入 ArkTS但约定所有文案统一走resources/base/element/string.json。鸿蒙侧代码里只通过$r(app.string.xxx)引用资源。我个人推荐方案 B 优先、方案 A 兜底。团队人力有限时先把 Flutter 侧的存量治理干净ArkTS 侧靠规范约束等规范失效了再上脚本扫描也不丢人。2.3 环境差异analyzer 版本与鸿蒙 Flutter SDK 的匹配这是最容易让工具直接跑不起来的点。OpenHarmony 里使用的 Flutter SDK 通常带了自己的 Dart SDK而 String literal finder 依赖的 analyzer 版本如果高于当前 Dart SDK运行时就会报类似The current Dart SDK version is 3.x.xx. This package requires SDK version 3.x.xx处理方式有两种将工具源码 clone 到本地修改pubspec.yaml中的analyzer版本和鸿蒙 Flutter SDK 自带的 analyzer 保持一致。注意environment.sdk也要调整到匹配范围。更稳妥直接使用 Flutter SDK 内置的 Dart 来跑工具脚本。flutter_module/bin/cache/dart-sdk/bin/dart run tool/string_literal_finder.dart这种方式充分利用了模块自带的 SDK不太会出现版本错位。这也是我最终的做法。3. 适配实战从源码阅读到改完跑通的完整链路3.1 拿到源码后先摸清三条主线如果你准备自己改这个工具建议先沿着三条主线读源码不要上来就改主线一入口与参数解析。一般集中在main.dart负责收集命令行参数如扫描目录、排除规则、报告输出路径。主线二扫描与过滤引擎。这是核心目录AST 遍历、规则过滤都在这里完成。主线三报告生成。决定输出格式纯文本、JSON、汇总表和退出码。我读源码的经验是先跑通一个最小 demo随便建一个带硬编码字符串的 Dart 文件跑一遍工具输出正常了再去逐行理解。直接用大工程调试你分不清是路径问题还是规则问题。3.2 路径规则与编译选项的鸿蒙化调整适配时我在源码里做的最核心改动就是目录收集逻辑。原本它可能只有这么一段伪代码FutureListString collectDartFiles(String rootPath) async { return rootPath .list(recursive: true) .where((entity) entity.path.endsWith(.dart)) .toList(); }鸿蒙工程里我建议增强两点。第一默认扫描多个根目录比如dart run bin/main.dart -d flutter_module/lib,entry/src/main/ets第二跳过干扰目录。鸿蒙工程里最容易拖慢扫描、制造误报的是这几类build/ oh_modules/ .dart_tool/ **/generated/**这些目录里存的是缓存、第三方依赖和代码生成产物扫进去只会让报告充满噪声。源码里要么在目录遍历时排除要么在正则/文件收集器中过滤。3.3 给报告加上 ArkTS 扫描通道下面这段是我用 Dart 写的一个最小可用的.ets字符串提取逻辑只做参考团队可以按需扩展final etsRegex RegExp(r([^\\\n]*)|\([^\\\\n]*)\); void scanEtsFile(File file) { final content file.readAsStringSync(); final lines content.split(\n); for (var i 0; i lines.length; i) { final line lines[i].trim(); // 跳过注释行与资源引用行 if (line.startsWith(//) || line.contains(*)) continue; if (line.contains(r$r()) continue; for (final match in etsRegex.allMatches(line)) { final str match.group(1) ?? match.group(2) ?? ; if (str.isEmpty || str.trim().isEmpty) continue; print(${file.path}:${i 1}: ${str.trim()}); } } }注意这个简化版没有处理模板字符串里的${}嵌套也没处理连续多行字符串拼接只适合 80% 场景。真要做得严谨还是老老实实走 ArkTS 的 parser或者多配一个专门扫 ArkTS 的开源扫描器。3.4 报告输出规范化行号、绝对路径、扫描概率我强烈建议把输出格式从人类可读文本改成JSON。文本格式肉眼看看还行但一旦 CI 集成、机器人评论、数据看板都要消费这份结果JSON 的可解析性完胜。典型字段可以设计成这样{ files: [ { path: flutter_module/lib/pages/LoginPage.dart, line: 42, column: 18, content: 请输入手机号, matches: [cjkChar], level: REVIEW } ], summary: { totalFiles: 128, totalStrings: 54, uniqueStrings: 37 } }程序拿到这份 JSON可以精确到行号插入评论产品团队也能按字符串维度排优先级。4. 让扫描结果强制生效CI 阶段把硬编码拦截在仓库外4.1 本地命令行接入适配完成后的工具最理想的调用方式是作为项目内依赖或独立脚本。我在工程根目录放了一个tool/scan_hardcode.sh内容大概是#!/usr/bin/env bash set -e DART_BINflutter_module/bin/cache/dart-sdk/bin/dart $DART_BIN run \ tool/string_literal_finder.dart \ -d flutter_module/lib,entry/src/main/ets \ --exclude **/generated/**,**/*.g.dart \ --ignore assets,url,regexp,mapKey,singleChar \ -o report.json \ --fail-on-found--fail-on-found这个开关很关键一旦扫描发现硬编码就让命令以非零状态退出。本地跑是为了看结果CI 跑是为了拦人。4.2 CI Pipeline 集成不管是 Jenkins 还是 GitLab CI核心逻辑都差不多——在编译前加一道硬编码扫描关卡。下面是一个 GitLab CI 片段hardcode-scan: stage: lint script: - chmod x tool/scan_hardcode.sh - ./tool/scan_hardcode.sh only: - merge_requests allow_failure: false这里有个团队协作上的技巧初期不要开allow_failure: false否则存量硬编码太多会让 MR 直接红牌、开发者集体愤怒。先开着 allow_failure 跑两周把报告当参考等存量清理得差不多了再强制拦截团队的阻力会小很多。4.3 差异基线策略先让工具闭嘴再逐步放行我在不同项目里落地过一套效果不错的策略叫基线放行。原理不复杂第一次完整扫描时生成baseline.json里面记录所有已有硬编码字符串及位置。之后的 CI 扫描只检查新增项先跑本次扫描。将结果与baseline.json比对。只对 base 之外的增量告警。这么做的好处是老代码不用一次性清偿团队可以按迭代节奏慢慢把存量消化掉。等存量趋近于零直接把基线删除开启全量拦截。4.4 与本地 lint 和提交流程配合CI 毕竟是事后拦截代码已经被推上去了才发现问题体验并不好。更好的配合是在开发者侧加一道pre-commit钩子类似 Husky 在 Flutter 工程里的用法{ scripts: { precommit: ./tool/scan_hardcode.sh --quick --fail-on-found } }本地快速扫描可以只扫本次改动涉及的几个文件速度控制在 1 秒内开发者不会反感。具体做法用 git diff 拿到改动文件列表只把列表传给工具扫描——这一步比全量扫描快得多也精准得多。5. 踩坑实录鸿蒙化过程中最容易翻车的几个地方5.1 analyzer 版本冲突是最难受的没有之一第一次拿到鸿蒙 Flutter SDK 时我直接在本机全局 Dart 环境跑了工具立刻遇到版本报错。查了半天发现工具那边要求的 analyzer 版本远远高于鸿蒙 SDK 内置版本。最后只能把 pubspec 里的依赖改成 SDK 对应版本折腾了大半天。建议不折腾本机环境直接把工具挂在flutter_module/下用 Flutter 自带的 Dart SDK 来执行基本上不会遇到版本错位问题。这也是所有 Flutter/Cocos 跨端工程里通用的思路——用工程锁定的运行时去跑工程内的脚本。5.2 Windows 下的路径分隔符会污染报告团队里有同事是 Windows 环境全量扫描出的报告路径是flutter_module\lib\pages\LoginPage.dart反斜杠在 Linux CI 上解析直接用不了。解决方案很粗暴但有效工具内部统一用package:path库把路径标准化为正斜杠或者直接在输出前做一次replaceAll(\\, /)。5.3 生成代码和构建产物是误报重灾区Flutter 工程里有一堆自动生成的文件.dart_tool缓存、*.g.dartjson_serializable 生成、.freezed.dart、国际化工具生成的AppLocalizations.dart。如果不排除工具会把大量自动生成的字符串当硬编码报给你真的会淹没核心问题。我在适配时直接把这几类路径写进默认排除列表**/.dart_tool/** **/build/** **/generated/** **/*.g.dart **/*.freezed.dart **/l10n/** **/AppLocalizations.dart5.4 扫描速度慢用并发和增量解决全量扫一个 20 万行 Flutter 模块纯单线程遍历加 AST 解析可能要几分钟。一次能忍每次 CI 都等几分钟就有点肉疼。改造思路有三层并发解析Dart 的 isolate 拆文件列表并发跑把物理核数用满。增量扫描在本地 pre-commit 和 CI 的 MR 检查中只扫 git diff 涉及的文件跑一次 1 秒内结束。缓存 AST同一文件在多个规则间复用解析结果别反复 parse。如果只是本地全量排查我用并发改造后20 万行代码大概 40 秒能出报告完全可接受。我个人在实际落地这个工具后的最大体会是硬编码扫描这类工作工具只能解决发现环节真正决定治理成效的是团队什么时候开始把新代码零硬编码当成默认规范。string_literal_finder 的鸿蒙化适配本身不难难的是让每个开发者习惯在写完文案的那一刻顺手去 string.xml 或 ARB 里登记一把。扫描工具真正有价值的地方恰恰在于它能持续提醒你某个地方还有一句漏掉的文案没进国际化体系。建议你接完这套流程后抽一个迭代周期专门把存量清零之后维护成本会低到出乎你的意料。