1. 为什么我在 Flutter 鸿蒙工程里坚持引入 dart_pre_commit
如果你在 Flutter 团队里待过一阵子,大概见过这种场面:某天合入代码后发现格式乱七八糟,或者一个flutter analyze没跑过的代码被推到了远端,紧接着 CI 挂了、别人git pull下来编译失败,耗时半小时查了一个本来十秒就能发现的问题。我在 OpenHarmony 侧的 Flutter 工程里也踩过类似的坑,而且踩得更狠——因为鸿蒙工具链版本变化快、环境差异大,人工检查的可靠性几乎为零。
后来我引入了dart_pre_commit这个三方库,把 Git Hooks 变成一套"代码质量门禁":commit 之前自动跑格式化、静态分析、甚至自定义检查脚本,无论如何都会被拦住。项目标题里的适配指南,说白了就是我在把这套机制从普通 Linux/macOS Flutter 工程搬到 OpenHarmony 开发环境时,完整记录下的一线实战经验。今天这篇文章适合谁?你在 Flutter 生态里做鸿蒙适配,或者团队刚切到 OpenHarmony 平台,想从第一天就开始管代码质量,那这篇可以直接拿去参考复现。
我非常建议你先理解它的本质,它不是一个"装完就能用"的工具,而是一个由 Dart 编写的 pre-commit 钩子框架,专门为 Flutter 项目定制了代码检查、格式化、测试等逻辑。最大的价值在于:它把质量门禁放到离开发者最近的位置,不必等 CI 跑完才告诉你哪里错了。
2. Git Hooks 拦截机制:为什么它能真正拦住"手贱"的提交
2.1 一次 commit 背后,Git 到底给了你几次拦截机会
先快速对齐基础。Git Hooks 是 Git 内置的事件回调,分布在 commit、merge、push 等流程的关键节点。以一次本地git commit为例,触发顺序大致是pre-commit→prepare-commit-msg→commit-msg→post-commit。其中pre-commit是真正能拦住这次提交的钩子,它会拿到暂存区(staging)里的文件清单,如果检测不通过就返回非零退出码,commit 直接被终止。
这个机制本身不复杂,核心在于:提交是否被允许,完全取决于钩子脚本的返回值。0 就是放行,非 0 就是拦截。所以任何质量检查只要包装成一个返回正确退出码的脚本,就能成为门禁。这里也提醒一句,纯手工写 shell 钩子很随意,而且在 Windows 和 Linux 上行为完全不一致,这正是 dart_pre_commit 这类工具存在的价值。
2.2 dart_pre_commit 帮你做了什么
dart_pre_commit本质上是一个用 Dart 语言实现的钩子管理器和执行器。你安装它之后,它会往.git/hooks/pre-commit里写入一个 Dart 脚本作为入口。接下来每一次 commit,Git 都会调用这个 Dart 脚本,脚本再去读取项目根目录下的配置文件,依次执行配置里声明的检查项。
这些检查项包括但不限于:
- 格式检查与自动修复:等价于
dart format - 静态分析:等价于
flutter analyze或dart analyze - 自定义脚本运行:比如调用你自己写的 Python 脚本检查资源文件命名
- 测试执行:按配置决定是全部测试还是只跑增量测试
你只需要在配置文件中决定"启用哪些、禁用哪些"即可。它不再要求你手工维护一堆 shell 片段,设计和机制是针对 Flutter 场景量身定做的。我第一次用它时最大感受是:终于有个工具默认就懂 pubspec.yaml、懂 Flutter 目录结构、懂怎么跳过.dart_tool之类的生成目录。
2.3 OpenHarmony 上为什么依然适用
有人问:鸿蒙应用主要语言是 ArkTS,Flutter 在这里属于跨平台方案,引入 dart_pre_commit 是否有意义?我直接在项目里用了之后发现,不仅有意义,而且比纯 Flutter 工程更有价值。
原因是 OpenHarmony 端的 Flutter 工程,往往包含一个标准 Flutter 模块(dart 代码)、鸿蒙原生工程(ArkTS 代码)、以及大量的脚本和资源配置文件。dart_pre_commit 对 Dart 侧代码的检查本来就走标准流程,而它支持的自定义脚本则能覆盖 ArkTS 代码的检查、资源文件命名检查、甚至有前后端接口字段一致性的脚本校验。门禁的对象不只是 Flutter 代码,而是整个仓库里的关键文件。这是它能成为团队质量基石的真正原因,因为它的设计允许你以任意语言实现检查逻辑,只要求你能写出返回码。
3. 鸿蒙适配指南:方案选型与核心难点拆解
3.1 适配的本质:不是改 Dart 逻辑,而是打通执行环境
拿到 dart_pre_commit 源码之后,你会发现它的核心逻辑和平台关系不大,真正的挑战在三点:
第一,Dart SDK 怎么找到。在普通 Linux 开发机上,开发者可能把 Flutter SDK 放在任意路径。在 OpenHarmony 开发环境里,你可能是从某个内部镜像解压了 DevEco Studio 自带的 Flutter SDK,或者手动下载了鸿蒙版本的 Flutter SDK,环境变量各不相同。dart_pre_commit 在初始化时必须正确定位dart可执行文件的路径,否则钩子脚本跑不起来。
第二,shell 解释器的差异。传统 pre-commit 钩子大量依赖 bash 特性,但 OpenHarmony 开发环境下,开发机往往是 Windows 和 Linux 混用。Windows 上大家用 PowerShell 或 cmd,bash 脚本容易直接躺尸。
第三,可执行权限。在 Windows 上不存在这个问题,但 Linux 和 macOS 上,.git/hooks/pre-commit文件必须带有可执行权限,否则被 Git 直接忽略。
我的策略是这样:完全借助 dart_pre_commit 本身的设计优势——它的入口脚本是 Dart 文件,通过dart run执行,这就绕开了对 bash 的依赖。整条流程内我对开发者的要求只剩一个:系统里有可用的dart命令。这个要求相比"你要装对 Python 版本、配好 sh 环境、考虑 sed 和 awk 的差异"来说,简单了不止一个量级。
3.2 方案A:改造入口脚本,让 Dart 帮你找到自己
如果你看过dart_pre_commit生成的钩子内容,会发现它通常是这样的:
#!/bin/sh dart run dart_pre_commit这里最大的问题是:它假设dart已经在 PATH 里。对于 OpenHarmony 环境,有的开发机连 Flutter 都没装全局,DevEco Studio 里套了一个 SDK,但没有写进 PATH。那执行 commit 时就会直接报dart: command not found。
适配方案是:在钩子脚本里加一段自动定位逻辑。不依赖用户手动去改系统环境变量,这样对团队新成员最友好。
#!/bin/sh # dart_pre_commit 鸿蒙适配入口 # 依次从常见位置探测 dart 可执行文件 if command -v dart >/dev/null 2>&1; then DART_BIN="dart" elif [ -f "$LOCALAPPDATA/Programs/flutter/bin/dart" ]; then # Windows 用户级安装 DART_BIN="$LOCALAPPDATA/Programs/flutter/bin/dart" elif [ -d "/opt/flutter/bin" ]; then DART_BIN="/opt/flutter/bin/dart" else echo "dart_pre_commit: 未找到 dart 可执行文件,请将 flutter/bin 加入 PATH" exit 1 fi exec "$DART_BIN" run dart_pre_commit这个脚本的核心思想是"降级处理":能全局找到就用全局,找不到就兜底探测几个常见安装目录。不同团队的 SDK 路径五花八门,我建议你维护一个数组,把你团队实际用到的路径都写进去。脚本的逻辑并不复杂,核心是把这个文件配置成git config core.hooksPath指定的目录,并给它可执行权限。
3.3 方案B:强制走 pub cache 路径,避免版本漂移
另一个非常容易踩坑的点是:dart run dart_pre_commit运行前,Dart 会去解析当前工程里的dart_pre_commit依赖版本。如果第一次运行时没有任何 lock 文件,它会去 pub.dev 拉取最新版。在 OpenHarmony 内网开发环境里,公网可能根本不通,这个操作会直接超时。
我的做法比较"笨"但非常稳:在 CI 服务器或一位团队成员的机器上先dart pub get,把生成的pubspec.lock完整提交进仓库。这样所有其他成员的环境里都会锁在同一个版本上。同时团队成员本地已经把依赖下载到 pub cache 里,钩子执行时不会再发起网络请求。
如果某些开发机确实没缓存,我会额外在钩子脚本里加一层"依赖预检查":
# 检查 pub cache 里是否有 dart_pre_commit PUB_CACHE_DEFAULT="$HOME/.pub-cache" if [ ! -d "$PUB_CACHE_DEFAULT/hosted/pub.dev/dart_pre_commit" ]; then echo "请先运行 flutter pub get 该项目的依赖" fi这个检查不是可选的。我实际遇到的 OpenHarmony 开发机,很多是刚从 CI 镜像分出来的,干净得连 dev_dependencies 都没装。与其让开发者面对Could not find package dart_pre_commit in the dependency list这一类困惑,不如一开始就告诉他该执行什么。
3.4 方案C:把 bash 专属配置替换成跨平台实现
之前我说过,真正的适配挑战在于避免依赖 bash 或 Python 环境。如果反过来问:为什么很多项目不用 dart_pre_commit?我见到最多的情况一是团队不知道有这个工具,二是团队尝试后发现要装 Python 依赖、要配 pre-commit 框架,觉得太重。
dart_pre_commit 的优势在于它完全避开了这个问题的根源。它的钩子本身就是 Dart 程序,跨平台性天然好。它唯一的"系统依赖"是 Flutter/Dart SDK,而这本来就在每个 Flutter 开发者的机器上。换句话说:如果你的 Flutter 工程开发环境是 OpenHarmony 工具链,那么 dart_pre_commit 的执行环境也一定是存在的,没有额外负担。
我在配置里把默认模板里的dart test禁掉(因为鸿蒙工程的测试可能需要连接真机),换成自定义的 ArkTS 静态检查脚本。这件事在 dart_pre_commit 的配置项里做起来极其顺手,因为自定义脚本的输入可以是"被修改的文件路径列表",你的脚本拿到这个列表之后,只对变更文件做增量检查,效率很高。
4. 实操演示:在 OpenHarmony Flutter 工程里搭建代码质量门禁
4.1 前置准备与安装
假设你已经有一个 OpenHarmony Flutter 工程的代码仓库,里面至少包含pubspec.yaml和 Flutter 模块。第一步是在pubspec.yaml的dev_dependencies中加入:
dev_dependencies: dart_pre_commit: ^2.0.0然后执行:
flutter pub get完成之后,你需要初始化钩子。运行:
dart run dart_pre_commit init这一步会自动在.git/hooks/pre-commit写入入口脚本。注意,如果你团队已经设置了git config core.hooksPath,你可能需要确认 init 命令写到了正确的位置,或者手动把入口脚本放到对应的 hooks 目录。否则你会遇到"明明初始化了但提交时没有任何反应"的诡异情况。
完成后,我会建议你立即看看生成的钩子文件是否带可执行权限。 Linux 环境下的常见问题是,文件内容正确但-rw-r--r--没执行位,Git 静默跳过。修复方法非常简单:
chmod +x .git/hooks/pre-commit4.2 编写适配鸿蒙场景的配置文件
dart_pre_commit的默认配置文件是.dart_pre_commit.yaml,位于项目根目录。下面是我在当前鸿蒙 Flutter 工程里实际使用的配置(已脱敏并简化注释):
# dart_pre_commit 配置模板 # 仅保留适用于 OpenHarmony Flutter 工程的检查项 format: enabled: true options: line-length: 120 analyze: enabled: true fatal-infos: true fatal-warnings: true shell-check: enabled: false custom: enabled: true scripts: - name: "arkts-analyze-check" language: "python" working-directory: "." command: "scripts/check_arkts.py" additional-dependencies: []这里解释几个关键点:
format和analyze是必开项,它们是 Flutter 代码门禁的基本盘。fatal-infos和fatal-warnings我强烈建议打开,因为 OpenHarmony 场景下,有些 API 在 Flutter 层是废弃的,但你会通过条件编译代码引用来兼容不同平台。如果不把 warning 视为错误,这些废弃调用会在不知情时流入主干,后面升级 SDK 时被坑得很惨。
shell-check我关闭了,因为在 Windows 开发机上它经常误报。如果你团队全是 Linux 环境,可以把这项打开,它用来检查 shell 脚本语法,属于额外保险。
自定义脚本arkts-analyze-check是我写的针对鸿蒙模块的检查脚本。这里我强调一下additional-dependencies:如果你的自定义脚本是 Python 写的,又依赖了第三方包,你可以列在这里,这样 dart_pre_commit 在首次运行时就会尝试安装。如果在内网环境装不了第三方包,就保持空列表,脚本本身只依赖标准库。
4.3 验证拦截效果:故意制造一次格式错误
配置文件写完后,你需要实测整套流程,不要等到团队第一个成员踩坑了才发现机制没生效。验证方法极其简单:
- 修改一个 Dart 文件,故意把缩进搞乱。
git add这个文件。- 运行
git commit尝试提交。
正确的结果是:commit 被阻止,终端输出格式化修复提示,告知你文件已被自动格式化。你会发现git commit不提交,而是打出dart format的差异说明,然后告诉你检查失败。这一步通过,说明你的门禁已经从"纸面规则"变成了"机器强制"。
我当年的项目上,这一步验证出了两个问题:一是 Windows 开发机无法运行exec "$DART_BIN" run dart_pre_commit(因为exec在某些 sh 实现下行为怪异),二是因为没有先flutter pub get,导致钩子找不到依赖。这些都可以提前在配置脚本里规避,但不实际跑一遍根本发现不了。
4.4 性能优化:如何保证门禁不要拖累日常开发
质量门禁最怕的一件事,就是"每次提交等 30 秒"变成团队的日常噩梦。dart_pre_commit 有一个非常实用的机制:它默认只对暂存区里发生变更的文件做检查。这对 Flutter 工程特别重要,因为大型工程里flutter analyze全量扫描可能要一两分钟,而增量分析往往几秒就能完成。
然后还有一条配置路径值得推荐:把不必要的插件排除掉。工程里的三方插件代码你基本不碰,却会被dart format整个目录扫过。在配置里可以明确排除:
format: enabled: true excluded: - "lib/third_party/*"同理,flutter analyze时也可以借助工程本身的分析选项排除第三方代码。注意,这里的排除路径配置必须和工程里的 pubspec 依赖包路径一致,否则排除不生效。我实际用下来,把build目录、.dart_tool目录、以及third_party目录加进排除列表之后,提交耗时从原来的 40 秒降到了 5 秒左右,团队接受度明显提升。
5. 自定义检查脚本:把鸿蒙特有规则装进门禁里
5.1 为什么必须自定义脚本:内置检查覆盖不住 ArkTS 侧问题
dart_pre_commit 内置的 format/analyze/test 解决的是 Dart 代码,但 OpenHarmony 工程里 ArkTS 代码才是鸿蒙原生逻辑的主体。你完全可以在 Dart 代码符合规范、但 ArkTS 模块里却埋着一堆any类型滥用和资源文件命名错误时,照样提交成功。
要堵住这个口子,必须把 ArkTS 的检查逻辑挂到自定义脚本里。dart_pre_commit 的自定义脚本机制非常开放,本质就是"执行一段任意命令,拿到退出码和输出"。它的调用方式类似于:在暂存文件变更后,节点会以变更文件列表为参数调用你的脚本,你的脚本负责决定返回 0 还是 1。
5.2 一个可复用的 ArkTS 变更文件检查脚本
下面是一个简化但可直接参考的 Python 例子。它遍历 dart_pre_commit 传给它的暂存文件列表,筛出其中 ArkTS 文件(.ets),再对这些文件做几类规则检查:命名风格、禁止any类型、TODO 注释标记。
#!/usr/bin/env python3 import sys import re from pathlib import Path def check_arkts_rules(file_path: str) -> list[str]: errors = [] with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() for line_no, line in enumerate(lines, 1): stripped = line.strip() # 规则1:禁止显式 any 类型 if re.search(r"\bany\b", stripped): errors.append(f"{file_path}:{line_no}: 禁止使用显式 any 类型") # 规则2:TODOs 必须带责任人 if "TODO" in stripped and "@" not in stripped: errors.append(f"{file_path}:{line_no}: TODO 必须标注责任人邮箱") # 规则3:类命名必须 PascalCase match = re.match(r"^\s*export\s+class\s+(\w+)", stripped) if match and not re.match(r"^[A-Z]", match.group(1)): errors.append(f"{file_path}:{line_no}: 类名 {match.group(1)} 应使用 PascalCase") return errors def main(): # dart_pre_commit 通过 stdin 传入变更文件列表,每行一个 changed_files = [line.strip() for line in sys.stdin if line.strip()] all_errors = [] for f in changed_files: if not f.endswith(".ets"): continue all_errors.extend(check_arkts_rules(f)) if all_errors: for err in all_errors: print(err) sys.exit(1) print("ArkTS 自定义检查通过") sys.exit(0) if __name__ == "__main__": main()这里有一个很重要的技术细节:自定义脚本的输入方式。不同版本的 dart_pre_commit 传参形式不完全一样,有的是通过命令参数传文件列表,有的通过 stdin 传,有的通过环境变量传。我的经验是先读官方文档对应的版本说明,再写脚本。上面这个例子用的是 stdin 形式,实测在 2.x 版本下工作正常,但如果你用的是 1.x 版本或特别定制过的版本,大概率需要调整。
5.3 测试脚本时的几个要点
自定义脚本最容易出的问题是"你本地跑通了,但钩子环境下跑不通"。钩子环境下有几个典型差异:
- 环境变量不完整,
PYTHONPATH没设置 - 工作目录不在项目根目录,相对路径失效
- 使用了 PyYAML 等第三方库但环境里没装
应对这些问题的思路是:脚本里第一件事,把自己所在目录切到项目根目录。可以利用.git目录向上回溯,或者直接通过环境变量获取 Git 仓库的顶层目录。
import subprocess repo_root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()这样不管钩子在哪里被触发,你的脚本可以在任意目录下运行。
6. 常见问题与排查技巧实录
6.1 钩子完全不生效
这个问题的排查路径我建议固定为:
- 确认
.git/hooks/pre-commit文件存在且非空。 - 检查文件是否具备可执行权限(Linux/macOS)。
- 在终端手动运行这个文件,看输出是否正常。
- 检查
git config core.hooksPath是不是被设置成了别的目录。
我见过最有迷惑性的一次是:团队里有人用 GUI 工具(如某些桌面 Git 客户端)提交代码,而 GUI 工具默认不跑 pre-commit 钩子。处理办法只能是团队纪律+文档说明,这个工具层面解决不了。当然,更推荐的做法是把它放到 CI 的流水线里,形成第二道防线,避免钩子被绕过后的风险。
6.2 钩子运行时报 "dart: command not found"
这个在 OpenHarmony 开发环境里出现过不止一次。开发机装的是 IDE 内置的 Flutter SDK,没有配过 PATH。解决方案就是前面写的方案A:入口脚本里做路径探测,不要依赖全局 PATH。
我建议把探测路径按command -v→ 用户级安装目录 → 特定 IDE 内置 SDK 目录依次查找。不同 IDE 的目录结构差异较大,最好提前问清楚团队成员的 SDK 位置,写死在探测列表里,这个工作花半天就能完成,却能省一整个适应期。
6.3 格式检查自动修改了文件但 commit 还是失败
这是 dart_pre_commit 的一个"特性":它会自动格式化你的代码,但git commit依然会因为原文件与暂存区状态不一致而中止。有些初学者会误以为工具坏了,其实这是设计使然——它强制你git add一次格式化后的结果。
我之前也想过是不是自己用法不对,后来查了实现才明白这个行为是对的。一个完整的提交是:修改代码 →git add→ 触发钩子 → 自动格式化 → 再次git add→git commit成功。第一次提交失败是正常的流程的一部分。如果团队觉得这一步体验太反直觉,可以在文档里写明:看到"格式化失败"时不要慌,那是工具在帮你改代码,你把改动 add 进来再提交即可。
6.4 大量文件提交时性能明显下降
dart_pre_commit 默认对暂存区所有文件做检查,如果这次提交涉及几百个文件,dart format和flutter analyze的耗时就会很可观。处理方式有三层:
- 在配置里排除生成目录和第三方目录(前面提过)
- 把增量检查逻辑写进自定义脚本(只检查变更列表,不检查整个工程)
- 实在不行,把
analyze从 pre-commit 阶段移到 CI 阶段,本地只保留 format 和自定义快速检查
我不建议一上来就禁掉 analyze,因为分析类错误在本地发现的成本最低,移到 CI 之后,一会儿本地、一会儿远程,上下文切换反而更耗时。先把增量优化做到位,再看是否需要调整。
6.5 Windows 开发机上的额外注意点
Windows 上跑 dart_pre_commit 总体来说问题不大,因为核心入口是 Dart 脚本,跨平台性很好。但自定义脚本如果是 shell 写的,在 Windows 上就会直接失效。解决方案很简单:自定义脚本尽量用 Dart 或 Python 这些有跨平台运行时支撑的语言;Python 脚本要被钩子调用,需要开发机上有可用的python命令。很多开发机装了但没加入 PATH,这又回到了环境探测的老话题上。
另外一个容易被忽略的问题:路径分隔符。Python 在 Windows 上拿到的路径可能带反斜杠,拿到的 stdin 内容格式也可能带\r\n。所有解析逻辑里务必先做strip(),否则你排查半天发现是字符串匹配失败,那就冤枉了。
7. 实际投入产出比:这套门禁值不值得做
从我的实际体验来看,在 OpenHarmony Flutter 工程里接入 dart_pre_commit 的初始成本大概是一到两天:第一天研究配置、写自定义脚本,第二天做团队推广、处理个别成员环境的兼容问题。之后的日常维护成本几乎为零,偶尔新版本更新或团队成员增多时需要看一眼配置。
收益却很直接:代码评审里再也看不到"格式化错误"这种毫无意义的评论;CI 的失败率里彻底移除了"Dart 代码风格不合规"这一类问题;ArktTS 资源命名这类低级错误也能在本地被强制提醒。这相当于把你的代码质量底线从"依靠自觉"变成了"依靠系统"。
我个人在踩过几次钩子不生效、路径找不到这些坑之后,最大的体会是:这类工具的价值不取决于它有多少功能,而取决于团队是否真的把它当成开发流程的一部分。配置、脚本、权限这些细节你只要搞定一次,后续所有人都会在不知不觉中被它保护。
如果你团队里还没有任何人提过代码质量门禁,这篇文章里的方案可以直接拿来用。先从小范围试用开始,让几位核心成员跑一周,再根据实际报错调整配置,基本不会出大问题。