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有人用 WindowsGit 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 根本拦不住。加上这个参数后只要存在需要格式化的文件退出码就变成非 0commit 会被中断。这正好是我们需要的“门禁”语义。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-stagedNode.jspackage.json较好支持已有 Node 生态的前端/RN 工程lefthook无Go/Rust 二进制YAML很好支持多语言 monorepodart_pre_commitDartYAML很好支持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_URLhttps://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URLhttps://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 productdefault sync -p moduleentrydefault - command: cd ../ hvigorw --mode module -p productdefault assembleHap -p moduleentrydefault --build-modedebug稳妥做法是先跑一次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.如果只删了 importformat 可能不会报错但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_commitCI 上做全量扫描。两者形成互补——本地管增量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 顺手当成问题报出来。解决方式就是配置里的 excludeexceptions: - **/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 eollf *.yaml text eollf加上之后无论提交者用 Windows 还是 macOSGit 都会在暂存时把 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太慢还是单测太重。门禁不是越严越好找到那个“拦截了问题但又不烦人”的平衡点才算真正落地。
阅读完成 · 觉得有帮助?