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 脚本作为入口。接下来每一次 commitGit 都会调用这个 Dart 脚本脚本再去读取项目根目录下的配置文件依次执行配置里声明的检查项。这些检查项包括但不限于格式检查与自动修复等价于dart format静态分析等价于flutter analyze或dart analyze自定义脚本运行比如调用你自己写的 Python 脚本检查资源文件命名测试执行按配置决定是全部测试还是只跑增量测试你只需要在配置文件中决定启用哪些、禁用哪些即可。它不再要求你手工维护一堆 shell 片段设计和机制是针对 Flutter 场景量身定做的。我第一次用它时最大感受是终于有个工具默认就懂 pubspec.yaml、懂 Flutter 目录结构、懂怎么跳过.dart_tool之类的生成目录。2.3 OpenHarmony 上为什么依然适用有人问鸿蒙应用主要语言是 ArkTSFlutter 在这里属于跨平台方案引入 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 或 cmdbash 脚本容易直接躺尸。第三可执行权限。在 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 21; then DART_BINdart 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, encodingutf-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 类型) # 规则2TODOs 必须带责任人 if TODO in stripped and not in stripped: errors.append(f{file_path}:{line_no}: TODO 必须标注责任人邮箱) # 规则3类命名必须 PascalCase match re.match(r^\s*export\sclass\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], textTrue).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 资源命名这类低级错误也能在本地被强制提醒。这相当于把你的代码质量底线从依靠自觉变成了依靠系统。我个人在踩过几次钩子不生效、路径找不到这些坑之后最大的体会是这类工具的价值不取决于它有多少功能而取决于团队是否真的把它当成开发流程的一部分。配置、脚本、权限这些细节你只要搞定一次后续所有人都会在不知不觉中被它保护。如果你团队里还没有任何人提过代码质量门禁这篇文章里的方案可以直接拿来用。先从小范围试用开始让几位核心成员跑一周再根据实际报错调整配置基本不会出大问题。