1. 为什么在 OpenHarmony 上更需要一道“本地质量门禁”
先说一下背景。前阵子我帮一个团队调整 Flutter 跨端工程的交付流程,工程已经同时出 OpenHarmony 包了。每次代码评审,总有人在群里喊“这段代码没格式化吧?lint 也没过”——这几乎是所有同时维护移动端和 OpenHarmony 端 Flutter 工程的同学都会撞上的痛点。代码质量门禁大家都认可,可门禁如果只放在 CI 上,等 push 上去再跑,一个小时的排队时长足够让效率感彻底归零。
于是我们把 dart_pre_commit 这套基于 Dart 的 Git Hooks 方案搬了进来,针对 OpenHarmony 的工程结构做了一轮适配,让 commit 本身变成一道必须跨过的质量关卡。这篇博文就是把整个适配过程拆开讲清楚:从 dart_pre_commit 的核心机制,到 OpenHarmony 工程里的路径、命令、排除规则,再到一份可以直接复用的配置,最后是我踩过的一些坑。
1.1 把质量检查从 CI 挪到 commit 前,问题就少了一半
团队里最让人头疼的循环通常长这样:本地写完代码、自我感觉良好、push、CI 排队、CI 报错、翻日志、修完再 push、继续排队。如果 CI 排队时间短还好,遇到构建压力大或者工程庞大,光排队加打包的时间就能吃掉半天。
而 Git Hooks 的思路是:在git commit这个动作发生的瞬间,把 format、analyze、甚至单测全部跑一遍,任何一项不过,这个 commit 就不准产生。这样所有问题都被拦截在本地,CI 只做最后的确认,而不是第一道防线。
所谓“极客级”,我的理解是:尽量靠机制而不是靠自觉。不依赖某个人记得跑dart format,不依赖评审人去盯风格,直接把工具的检查结果做成提交的前置条件。谁都能绕过,但绕过需要主动操作,这本身就提高了门槛。
1.2 OpenHarmony 工程的“双重复杂度”,让门禁下移更有必要
OpenHarmony 上的 Flutter 工程,比普通 Flutter 工程多了一层复杂度。
普通 Flutter 工程是单栈,代码基本只有 Dart,工具链只要伺候好 Flutter 就能覆盖大部分质量诉求。而 OpenHarmony 工程是双栈:一侧是 ArkTS/ETS 代码,由 hvigor、ohpm 这套工具链负责;另一侧是 Flutter 模块,由 Dart/Flutter 工具链负责。工程树里既有entry、feature这样的鸿蒙侧模块,又有flutter_module这样的 Flutter 模块。
双栈带来的问题很直接:出问题的面积翻倍,回归成本翻倍,CI 构建链路更长。如果不在本地拦截一道,任何一次低级错误都会消耗掉昂贵的远程构建资源。最开始我们只给 CI 配置了质量门禁,结果构建排队一次 40 分钟起步,其中一半的失败原因是dart format没跑、flutter analyze有 warning。这类问题完全可以在 commit 前用不足十秒的时间拦住。
1.3 选型:dart_pre_commit 为什么比手写 shell 脚本靠谱
在做这套门禁之前,我也考虑过手写 Git Hooks。思路很简单:写一个pre-commitshell 脚本,里面跑git diff --cached、过滤.dart文件、再逐个执行dart format、flutter analyze。听起来不难,但实际维护起来全是坑。
首先是跨平台。团队里有人用 macOS,有人用 Windows,Git Bash 和 cmd 的语法差异就够喝一壶的。其次是逻辑复杂度。要正确处理“暂存区里哪些文件需要检查”“format 之后是否要重新 add”“analyze 的输出怎么解析”这些细节,shell 脚本很容易写得又长又脆。最后是安装。让每个新人手动把一个pre-commit文件复制到.git/hooks下,还要保证文件有执行权限,这件事在团队协作里基本等于没做。
dart_pre_commit 解决了这些问题。它是 pub 包,用 Dart 写的,天然跨平台;挂在 Flutter 模块的dev_dependencies里,不需要额外装 Node 或者 Python;配置文件是 YAML,团队里任何一个人都能看懂并修改。它甚至可以自动把 hook 安装到.git/hooks里,新人拉完代码跑一次命令就完事。这些特征让它成为 Flutter 侧质量门禁的最佳候选。
2. dart_pre_commit 的运行机制与配置项拆解
2.1 从 commit 到 hook:它到底做了什么
如果你对 Git Hooks 还不熟,先花三十秒补齐背景。Git 在.git/hooks/目录下预埋了一批可执行脚本模板,pre-commit就是其中之一。每次执行git commit时,Git 会先找到并运行pre-commit脚本,脚本退出码是 0 则提交继续,非 0 则提交被中断。
dart_pre_commit 做的事情,就是帮我们把pre-commit这个脚本生成好。安装之后,.git/hooks/pre-commit的内容非常简单,本质上就是调用 Dart 去执行工具本身:
#!/bin/sh dart run dart_pre_commit当git commit被触发时,dart_pre_commit 会做下面这几件事:
- 读取当前 Git 仓库的暂存区信息,拿到“即将提交的文件列表”;
- 过滤出其中的
.dart文件; - 按配置文件里的规则,对这批文件执行
dart format和flutter analyze; - 如果有自定义 hook 命令,也会一并执行;
- 任何一步失败,都把失败信息打印到控制台,并让整个
git commit中止。
这里值得注意的一点是,dart_pre_commit 默认聚焦的是“暂存区里的 Dart 文件”,而不是全量扫描整个仓库。这个设计非常合理:一次 commit 可能只改了一个文件,如果每次都全量跑 analyze,代码一多速度会非常感人。按暂存文件来处理,命中检查的永远是本次提交相关的内容,速度更快,规则也更精准。
2.2 配置文件里的几个关键字段
dart_pre_commit 的行为由仓库根目录下的dart_pre_commit.yaml控制。第一次跑的时候如果没找到这个文件,它会基于默认值运行;但要做 OpenHarmony 适配,这个文件基本是必写的。
一个典型的配置长这样:
dialog: false format: enabled: true args: ["--set-exit-if-changed", "."] analyze: enabled: true args: ["--fatal-infos", "--fatal-warnings"] exceptions: - "**/*.g.dart" - "**/*.freezed.dart" - "**/generated/**" hooks: pre-commit: - command: "dart format --set-exit-if-changed lib test" - command: "flutter analyze --no-pub"逐个字段说一下我的理解。
dialog表示运行的时候要不要弹出交互式选择,比如“只检查暂存文件还是全量检查”。本地开发时交互其实挺有用,但在 CI 或团队统一流程里会阻塞自动化,我习惯直接设成false。
format是内置的格式化检查。注意--set-exit-if-changed这个参数很关键:如果dart format发现代码需要重新格式化,它默认会帮你改,但退出码是 0,这样 hook 根本拦不住。加上这个参数后,只要存在需要格式化的文件,退出码就变成非 0,commit 会被中断。这正好是我们需要的“门禁”语义。
analyze是内置的静态分析。--fatal-infos把 info 级别的问题也升级为错误,--fatal-warnings把警告升级为错误。对于要求严格的团队,这两个参数可以拉满;如果团队里的存量代码历史包袱比较重,可以先只开--fatal-warnings,给旧代码留出缓冲期。
exceptions是排除规则。这一步在 OpenHarmony 工程里尤其重要,后面会有专节细说。简单来讲,所有自动生成的代码(.g.dart、.freezed.dart)都不应该进入 analyze 的检查范围,因为它们的风格由生成器决定,你手动去改反而是负优化。
hooks.pre-commit是自定义命令列表。在这里可以写任意 shell 命令,dart_pre_commit 会顺序执行并把退出码纳入门禁判断。
2.3 横向对比:和 husky、lint-staged、lefthook 怎么选
不是说 dart_pre_commit 天下第一,其他方案各有各的适用场景。但在“OpenHarmony 双栈 Flutter 工程”这个特定背景下,它的优势非常明显。我做了一个简单的对比:
| 方案 | 技术栈依赖 | 配置方式 | 跨平台 | 自定义命令 | 适合场景 |
|---|---|---|---|---|---|
| 手写 Git hooks | 无 | Shell 脚本 | 一般 | 自由但要自己维护 | 极简单文件仓库 |
| husky + lint-staged | Node.js | package.json | 较好 | 支持 | 已有 Node 生态的前端/RN 工程 |
| lefthook | 无(Go/Rust 二进制) | YAML | 很好 | 支持 | 多语言 monorepo |
| dart_pre_commit | Dart | YAML | 很好 | 支持 | Flutter/Dart 主导的工程 |
我们选 dart_pre_commit 的核心原因有两个。第一,纯 Flutter 团队不想为了一个 hook 工具再引入 Node 运行时,维护成本会变成双份;第二,dart_pre_commit 本身就是 Dart 生态的一等公民,和pubspec.yaml、dart format、flutter analyze的配合是原生级别的,不需要再包一层适配器。
3. OpenHarmony 适配的关键改造点
3.1 环境准备:Flutter OHOS 分支与依赖源
在 OpenHarmony 上跑 Flutter,首先得有一套支持 OpenHarmony 目标平台的 Flutter SDK。社区维护的分支一般会带flutter build hap --target-platform ohos这类命令,用之前需要先确认自己当前的 Flutter 版本能识别 OpenHarmony target。
确定 SDK 没问题之后,在 Flutter 模块的pubspec.yaml里加上依赖:
dev_dependencies: dart_pre_commit: ^2.2.0然后执行flutter pub get完成解析。
这里有一个实操要点:如果你的开发机网络访问默认 pub.dev 不稳定,需要提前配好公共镜像源。做法是在环境变量里设置:
export PUB_HOSTED_URL=https://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn当然具体用哪个镜像,按团队所在网络环境来定,原则就是保证团队成员在flutter pub get这一步不会因为依赖下载问题卡住。这个配置应该写进团队的开发环境初始化脚本里,而不是靠每个人手动敲。
3.2 工程树下的路径问题:从仓库根目录到 Flutter 模块
这是整个适配过程中最容易踩坑的地方。OpenHarmony 工程的根目录和 Flutter 模块目录通常不是同一个。
一个典型的 OpenHarmony + Flutter 仓库结构如下:
harmony_shop/ ├── AppScope/ ├── entry/ ├── feature/ ├── flutter_module/ │ ├── lib/ │ ├── test/ │ ├── pubspec.yaml │ ├── dart_pre_commit.yaml │ └── ... ├── build-profile.json5 ├── hvigorfile.ts └── oh-package.json5Git 仓库的根目录是harmony_shop/,但真正的 Flutter 工程在flutter_module/下面。默认情况下,Git 执行pre-commit时,当前工作目录是 “触发 git 命令的那个目录的根”,也就是harmony_shop/。而dart run dart_pre_commit跑到这里会找不到pubspec.yaml,于是 hook 直接报错。
解决办法有两个方向。
一个方向是在 hook 脚本里显式cd到 Flutter 模块目录再执行。比如:
#!/bin/sh cd "$(dirname "$0")/../flutter_module" || exit 1 dart run dart_pre_commit另一个方向是使用core.hooksPath,把 hooks 统一放到仓库根目录下的.githooks/目录,并在入口脚本中处理路径跳转。我个人更推荐后者,因为.githooks/目录可以提交到 Git 仓库,所有团队成员 clone 下来之后,只需要执行一次配置就能使用同一套 hook,不存在“我的.git/hooks里忘了放脚本”这种问题。
初始化方式:
git config core.hooksPath .githooks然后写.githooks/pre-commit:
#!/bin/sh set -e cd "$(dirname "$0")/../flutter_module" dart run dart_pre_commitset -e保证任何一条命令失败都立即中断脚本,避免出现“dart format 失败了但 commit 还继续”的诡异情况。
3.3 命令与排除规则:针对 OHOS 产物的调优
适配 OpenHarmony 过程中,第二个大头是命令本身。
dart format和dart run dart_pre_commit一样,命令由 Dart 运行时提供,不区分你跑在哪个 target 上,所以 format 这条命令几乎不需要改动。但flutter analyze在 OpenHarmony 分支下不一定像标准 Flutter 分支那样稳定输出。尤其是工程里如果包含 OpenHarmony 自动生成的一些 Dart 文件,analyze 很可能会把它们也纳入检查范围。
解决这个问题的办法是“排除规则 + 精确目录”。
先看排除规则。我们的配置里加了这几条:
exceptions: - "**/*.g.dart" - "**/*.freezed.dart" - "**/generated/**" - "**/oh_modules/**"oh_modules是 OpenHarmony 工程解析依赖后生成的目录,里面偶尔会出现一些 Dart 相关的中间产物,这些东西既不该被格式化,也不该被静态分析,必须排除出检查范围。
再看精确目录。dart format默认可以在配置里写成:
format: enabled: true args: ["--set-exit-if-changed", "lib", "test"]把检查范围收窄到lib和test,避免误扫工程根目录下的临时脚本或配置文件。同理,analyze 的命令也可以收窄:
analyze: enabled: true args: ["--no-pub", "--fatal-infos", "--fatal-warnings", "lib"]这里加--no-pub是为了让 analyze 不触发依赖更新解析,否则每次提交时它都可能尝试访问网络,严重影响体验,而且在一些受限网络环境下会直接卡死。
3.4 把 ArkTS 侧检查也纳入同一门禁
适配过程中我一直在想:既然 Flutter 侧已经做了本地门禁,那 OpenHarmony 侧的 ArkTS 代码要不要也纳入 pre-commit?
答案是:应该,但要选好时机。OpenHarmony 的 hvigor 构建链路比 Flutter 重不少,如果每次 commit 都跑完整assembleHap,那这个 hook 的耗时会把所有人逼疯。所以我们只把轻量级检查纳入 pre-commit,把重量级构建留给 CI。
在自定义命令里加一个 hvigor 的 lint 或类型检查脚本,比如:
hooks: pre-commit: - command: "cd ../ && hvigorw --mode module -p product=default sync -p module=entry@default" - command: "cd ../ && hvigorw --mode module -p product=default assembleHap -p module=entry@default --build-mode=debug"稳妥做法是先跑一次hvigorw的轻量编译,确认改了 ArkTS 侧代码不会立刻把工程编译炸掉。如果仓库里 ArkTS 代码很少,这步耗时可以接受;如果 ArkTS 代码量很大,建议只把hvigorw的 lint 命令放进 pre-commit,完整构建留在 CI。
说到底,门禁本身讲究一个“分层”。Commit 前做轻量快速检查,门禁做得像安检;CI 上做完整构建和回归,门禁做得像法院终审。两者职责分明,而不是把所有检查都堆到一个环节。
3.5 monorepo 与多模块场景的特殊处理
如果仓库里不止一个 Flutter 模块,比如feature/a_flutter和feature/b_flutter各是一个 Flutter 工程,那 hook 入口脚本就得写成“遍历 + 逐个执行”的模式:
#!/bin/sh set -e for module in feature/a_flutter feature/b_flutter; do echo "==> checking $module" (cd "$(dirname "$0")/../$module" && dart run dart_pre_commit) done注意每个模块都必须有自己的dart_pre_commit.yaml。这样每个模块可以有不同的 analyze 规则、不同的 exclude 配置,互不干扰。这个思路也让 dart_pre_commit 能应对不同团队的不同约束,不需要一个配置文件包打天下。
4. 实战:一个可复用的 OpenHarmony + Flutter 门禁配置
4.1 安装 hooks 全流程
先把整个工程准备好,假设仓库结构是前面说的harmony_shop/+flutter_module/。
第一步,进入 Flutter 模块,添加依赖:
cd harmony_shop/flutter_module flutter pub add --dev dart_pre_commit如果你是手动编辑pubspec.yaml,记得随后执行flutter pub get。
第二步,在仓库根目录创建.githooks并配置 hooks 路径:
cd .. git config core.hooksPath .githooks mkdir -p .githooks第三步,编写.githooks/pre-commit脚本,并确保它有可执行权限:
#!/bin/sh set -e cd "$(dirname "$0")/../flutter_module" dart run dart_pre_commitchmod +x .githooks/pre-commit到这里,一个最小可用的本地门禁已经架好了。随便改一个 Dart 文件,故意不格式化,然后git add、git commit,就能看到它被拦下来。
4.2 编写一份适配 OpenHarmony 的配置
下面这份 YAML 是我们在真实工程里跑过一段时间后定下来的,你可以直接复制,再按自己仓库的结构微调。
# dart_pre_commit.yaml dialog: false format: enabled: true args: ["--set-exit-if-changed", "lib", "test"] analyze: enabled: true args: ["--no-pub", "--fatal-infos", "--fatal-warnings", "lib"] exceptions: - "**/*.g.dart" - "**/*.freezed.dart" - "**/generated/**" - "**/oh_modules/**" hooks: pre-commit: - command: "dart format --set-exit-if-changed lib test" - command: "flutter analyze --no-pub --fatal-infos --fatal-warnings lib"说几个细节。
dialog: false很重要。dart_pre_commit 默认可能存在交互式行为,但 hook 场景下每个人都希望“提交时不要让我选来选去”,通不通过给个明确结果就好。把它固定为 false,行为就变得可预期。
format 命令写了两遍,一次是在内置format配置里,一次是在自定义命令里。这里看起来重复,但其实是刻意的:如果你想把某个目录排除在 format 之外,或者对部分文件做不同的格式处理,自定义命令的灵活度更高。内置format应付常规情况,自定义命令用来兜底和收窄检查范围。
flutter analyze最后明确加上了lib参数。这会带来一个效果:只分析lib目录。新增的test文件暂时不参与静态分析,因为写测试代码时有时候会有意使用一些测试框架的“魔法字符串”,过早开启 fatal 反而会增加团队挫败感。等测试代码量大了,再逐步收紧。
4.3 验证拦截效果:故意制造一次违规
安装完配置后,建议立即做一次拦截验证,确认 hook 真正生效。
操作是这样的:打开lib/main.dart,删除一个 import 行,或者在文件末尾加两个空行让格式不标准。然后:
git add lib/main.dart git commit -m "chore: 验证 hook 拦截"预期输出大概是这样的(一个合规 hook 的反馈过程):
Running dart_pre_commit... Formatting changed dart files... [FAIL] lib/main.dart is not formatted correctly - line 12: 多余的换行符 Run `dart format --set-exit-if-changed` to fix the format. Aborting commit due to pre-commit hook failure.如果只删了 import,format 可能不会报错,但flutter analyze一定会报出 unused import 的 warning。在--fatal-warnings的加持下,这个 warning 会直接变成红色错误,commit 同样被中断。
修复方式也很简单:跑一次dart format lib/main.dart,把多余 import 删掉,重新git add、git commit,这次钩子会静静放行,不打印多余信息,只让你看到“提交成功”。
4.4 与 CI 无缝衔接:本地和流水线共用同一套规则
Hook 本地拦截只是第一道闸,CI 上仍然需要一套同样的检查。这里的思路是:让 CI 和本地使用同一个 YAML 配置,而不是在流水线里再维护一套独立的命令行。
在 CI 脚本里,进入 Flutter 模块后执行:
dart run dart_pre_commit --ci当然,如果你们用的 CI 环境没有事先准备 Git 暂存区,或者不希望 hook 依赖“暂存文件”这个概念,也可以直接在 CI 里跑等价命令:
dart format --set-exit-if-changed lib test flutter analyze --no-pub --fatal-infos --fatal-warnings lib两条思路都行。第一种的好处是“一份配置跑天下”,本地行为和线上行为完全一致;第二种的好处是 CI 不依赖 Git 状态,更接近“全量检查”,适合那种已经过了 hook 但还想在整个仓库范围再做一次全量扫描的场景。
我自己的实操建议是:本地用 dart_pre_commit,CI 上做全量扫描。两者形成互补——本地管增量,CI 管全量。
5. 踩坑记录与问题排查速查
配置过程不是一帆风顺的。下面这些坑都是我们在实际使用中遇到过的,按出现频率从高到低排,每个都给出了排查思路和解决方式。
5.1 hook 脚本报dart: command not found
这是最常见的坑。终端里手动执行dart没问题,一跑git commit就报找不到命令。
原因:Git 执行 hook 时的环境变量跟你交互终端里的不一样。很多开发机把 Dart SDK 的路径加到 shell profile 里,但 hook 脚本默认只继承了一段精简的 PATH。
解决方式:在 hook 脚本开头显式把 Dart SDK 的路径补进去。如果你不知道 dart 在哪个绝对路径,先执行:
which dart然后写进脚本:
#!/bin/sh set -e export PATH="$PATH:/path/to/dart-sdk/bin" cd "$(dirname "$0")/../flutter_module" dart run dart_pre_commit有一点要注意:团队里如果有人用的是 OpenHarmony 专用的 Flutter SDK 分支,Dart SDK 的位置可能和标准安装路径不一样,不要假设所有人的路径都和你相同。可以考虑在项目文档里写清楚“本仓库要求 Dart SDK 可通过 PATH 访问”,然后脚本里做一个检测,找不到就打印提示并退出。
5.2 仓库根目录不对导致 hook 静默失败
表现是 hook 没报错,但 dart_pre_commit 实际上没有跑过任何检查,commit 直接通过。
排查方法:在预提交脚本里加上调试输出,先打印当前目录:
pwd如果是harmony_shop/,而 Flutter 模块在flutter_module/,那dart run dart_pre_commit确实找不到配置,也不会扫描任何文件。更隐蔽的是它还会有“成功退出”的假象,因为默认空跑可能不算错误。
解决方式就是前面说的:在脚本里cd到 Flutter 模块目录。改成正确目录后,随便改一个文件做一次提交,确认拦截输出真的出现了再收工。
5.3 analyze 被 OHOS 产物干扰,误报一堆
第一次在 OpenHarmony 工程里跑flutter analyze时,我们的 CI 一度被刷屏,满眼都是“undefined class”或者“unused import”之类的报错。
原因:oh_modules目录和 build 产物中存在生成的 Dart 文件;另外某些 OpenHarmony 分支下的平台桥接代码如果写得不规范,也会被 analyze 顺手当成问题报出来。
解决方式就是配置里的 exclude:
exceptions: - "**/oh_modules/**" - "**/generated/**"如果 analyze 依然有噪音,可以把分析参数里最终带上的目录从“整个 Flutter 模块”收窄到lib,我们最后就是固定成flutter analyze --no-pub --fatal-infos --fatal-warnings lib,噪音基本消失,剩下的都是真实问题。
5.4 Windows 团队的 CRLF 换行符问题
Windows 环境下,Git 默认可能把文件标记成 CRLF 行尾,而dart format --set-exit-if-changed对换行符敏感。这就出现一个很尴尬的场面:代码明明格式没问题,但 hook 报“needs formatting”,因为 Windows 提交时把每一行的换行都改了。
解决方式:在仓库根目录放一个.gitattributes文件,统一文本文件的换行策略:
*.dart text eol=lf *.yaml text eol=lf加上之后,无论提交者用 Windows 还是 macOS,Git 都会在暂存时把 Dart 文件统一转成 LF。这样 format 的检查结果在任何平台上都是一致的。这个文件要尽早提交到仓库里,否则历史提交里的换行风格已经乱了,改动会铺开。
5.5 绕过机制:--no-verify的合理使用
门禁是门禁,但总有人要临时绕过。git commit --no-verify可以直接跳过 pre-commit hook,这一点无法禁止,也不建议试图禁止。
我的态度是:把--no-verify视为“紧急逃生通道”,但不能滥用。可以在代码评审规范里约定一条:使用--no-verify提交的人,必须在 commit message 里标明原因,例如:
git commit --no-verify -m "fix: 临时修复构建产物,提交后立即补跑全量检查"从工程实践来看,一旦这个约定建立起来,--no-verify的使用频率会快速下降。因为它从“偷偷绕门禁”变成了“全组可见的公开例外”,需要承担对应的责任。
最后分享一点我在实际操作中的体会。把 dart_pre_commit 真正用起来之后,最明显的变化是:CI 的失败率直线下降,代码评审里很少再出现“格式化一下”“删掉未使用的 import”这类琐碎评论。团队不再为一个空格或者一个告警来回拉扯,能把精力放到真正值得讨论的架构和逻辑上。
再提供一个我后来加上的小技巧:在.githooks/pre-commit脚本里用time命令包一层,记录每次 hook 的耗时。如果某一天大家普遍觉得提交变慢了,看一眼耗时统计就能定位是flutter analyze太慢,还是单测太重。门禁不是越严越好,找到那个“拦截了问题但又不烦人”的平衡点,才算真正落地。