尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

dcli_scripts鸿蒙化适配实战:hdc/hvigor/ohpm工具链迁移指南

dcli_scripts鸿蒙化适配实战:hdc/hvigor/ohpm工具链迁移指南 鸿蒙生态这几年的节奏大家都有体会Flutter 开发者一边要跟上主流的版本演进一边还要面对“这个三方库能不能在鸿蒙上跑”的灵魂拷问。我所在的小组被拉去做鸿蒙适配之后最先遇到的就是命令行工具链的断档Flutter 生态里大量的自动化脚本、脚手架工具都依赖 dcli 这类 Dart 命令行库而鸿蒙侧的工具链从 adb 换成了 hdc从 Gradle 换成了 hvigor从 npm 换成了 ohpm原来的 dcli_scripts 直接拿过来用基本是废的。这篇内容就是我们把 dcli_scripts 这一整套基于 dcli 的脚本库做鸿蒙化适配的完整记录。我不会泛泛讲“鸿蒙化的重要性”而是把思路、改动点、踩过的坑都摊开说。已经做完 Flutter 鸿蒙化接入的团队可以照着抄作业正准备开始的也能在动手前把坑位看清至少省掉一周的试错时间。1. 先想明白一件事dcli_scripts“鸿蒙化”到底改的是什么很多人一听“鸿蒙化适配”第一反应是把三方库的 API 签名改成鸿蒙 SDK 的 API。这个直觉在原生插件、UI 组件库上是成立的但对 dcli_scripts 这类命令行脚本库来说方向完全不对。dcli 脚本跑在开发者的宿主机上它不跑在鸿蒙设备里所以你的 Dart 代码一行都不用因为“鸿蒙”而改写。真正要动的是脚本内部对“设备、构建、签名、包管理”这四个对象的调用方式。1.1 dcli_scripts 的真实定位它是“开发机上的自动化中枢”dcli 是 Dart 生态里的命令行脚本库你可以把它理解成“用 Dart 写 shell 脚本”。比纯 bash 强的地方是它有类型、有包管理、能解析参数还能直接复用你用 Dart 写的那堆业务逻辑。dcli_scripts 则是我们团队维护的一组基于 dcli 的脚本集合覆盖了模板代码生成、工程初始化、版本号注入、构建发布这类日常高频动作。在 Flutter 时代这套脚本依赖的是 adb、Gradle、Flutter SDK它们之间通过 Process 调用来协作比如脚本生成好一个 Android 工程结构然后调 gradlew 去打包。鸿蒙化之后底层工具链全换了但脚本的“骨架”——参数解析、流程控制、日志、文件处理——完全没变。所以我在接手时给团队定的调子是不要重写脚本只换“与外部工具链的适配层”。1.2 鸿蒙给 Flutter 开发者的命令行工具链长什么样搞 Flutter 的人都熟悉这么一套链子flutter pub get拉依赖、flutter build apk构建、adb install安装、adb logcat看日志。鸿蒙侧对应的三件套是用途Flutter/Android 时代的命令鸿蒙侧的命令/工具差异点设备连接与安装adbhdcHarmonyOS Device Connector命令结构类似但输出格式和授权机制不同工程构建gradlew / flutter buildhvigorw / hvigorHAP 产物路径和构建参数体系完全不同包管理pub / npmohpm配置文件从 pubspec.yaml 变成了 oh-package.json5产物签名apksigner / 手工 keystorehap-sign-tool 或 DevEco 签名配置更依赖工程里的构建配置文件Flutter 应用鸿蒙化的整体构建链路一般是先用 Flutter 工具链编出鸿蒙适配的 framework 产物再交给 hvigor 把工程打包成 HAP。这意味着 dcli_scripts 里的构建脚本可能要同时调两套工具链顺序和条件判断都要梳理清楚不然就会出现“Flutter 侧编完了hvigor 侧不知道给它塞到哪个模块”的尴尬。1.3 先用一张适配评估矩阵盘家底别急着动手我建议第一步不是改代码而是把 dcli_scripts 仓库里每个脚本按“依赖的外部工具”拆开做一次影响面评估。我用的矩阵大概是这样的脚本类型是否受影响说明纯文件处理、模板渲染不受影响只依赖 Dart 标准库和 pubspec 里的依赖和鸿蒙无关路径解析、工程结构识别轻微影响要识别鸿蒙的 module 结构和 HAP 输出目录调 adb 的安装/日志脚本重写适配层换成 hdc 命令输出解析逻辑全要换调 flutter build 的脚本需要扩展保留 flutter build后面追加 hvigor 打包步骤读 Android 的 local.properties需要扩展补读鸿蒙 SDK 路径、HarmonyOS SDK 配置依赖原生插件做运行时能力的基本重写这类脚本如果涉及设备侧执行要评估 ArkTS 侧方案当时我们盘完之后发现真正需要大改的只有 20% 左右的脚本剩下 80% 的骨架直接复用。明确这个边界之后团队的焦虑感一下就降下来了。2. 环境基线适配前的检查与准备少一项后面全部白搭dcli_scripts 之所以容易“莫名其妙失败”绝大多数时候不是脚本逻辑错了而是宿主机的环境没有达到脚本的预期。鸿蒙化的适配中环境基线要从“Android SDK 在不在”变成“hdc、ohpm、hvigor 在不在鸿蒙 SDK 路径能不能被探测到”。2.1 把鸿蒙开发环境当成可以被脚本探测的目标一个好的 dcli 脚本第一步永远是探测环境而不是直接跑命令。dcli 里有which()可以查可执行文件我用它做了一组基线检查大概长这样import package:dcli/dcli.dart; void checkHarmonyEnv() { final hdcPath which(hdc); if (hdcPath null) { printerr(未找到 hdc请确认 DevEco Studio 已安装且命令行工具已加入 PATH); exit(2); } final hvigorw which(hvigorw); if (hvigorw null) { // 鸿蒙工程根目录下一般有 hvigorw如果全局没有也不强求 print(警告未找到全局 hvigorw后续将尝试使用工程内的 wrapper); } final ohpmPath which(ohpm); if (ohpmPath null) { printerr(未找到 ohpm鸿蒙三方依赖无法安装); exit(2); } }这里有一个很容易忽略的点hdc 在 DevEco Studio 的安装目录里但默认不一定进了 PATH。很多脚本挂在“hdc command not found”上排查了一圈发现就是环境变量没配。我在基线检查时会把常见的 DevEco Studio 安装路径也加进which的搜索路径里避免让用户手动去配环境变量。2.2 识别鸿蒙工程结构的关键文件dcli_scripts 里的很多脚本要解析工程结构Flutter 时代读的是 pubspec.yaml 和 android/local.properties鸿蒙侧则要认这几类文件oh-package.json5鸿蒙工程的依赖清单相当于 package.json里面有ohos相关的三方依赖。build-profile.json5鸿蒙工程构建配置定义了 product、签名配置、模块列表是脚本最需要读取的文件。hvigorfile.ts构建脚本入口里面能看到可用的构建任务。entry/src/main/module.json5应用模块配置文件bundleName、versionCode这些关键信息都在这里。脚本里要读build-profile.json5的时候会有一个很隐蔽的坑Dart 自带的jsonDecode只认标准 JSON不认 JSON5。鸿蒙工程里这个文件经常带注释或者 key 不带引号直接解析会炸。我的处理方式是先做一次注释剥离再jsonDecodeString stripJson5Comments(String source) { // 只做简单剥离足够处理 build-profile.json5 里的 // 和 /* */ 注释 return source .replaceAll(RegExp(r(?:\\.|[^\\])*), (m) m[0]!) .replaceAll(RegExp(r//[^\n]*), ) .replaceAll(RegExp(r/\*.*?\*/, dotAll: true), ); }这个正则先匹配字符串字面量再删注释不会误伤//出现在 URL 里的情况。实测下来处理标准 DevEco 生成的工程文件够用了。2.3 我建议的基线检查表下面这张表直接贴到团队文档里任何脚本在跑之前都先过一遍检查项检查命令/方法失败的表现处理建议hdc 可用hdc list targets提示 command not found把 DevEco 的 toolchains 目录加入 PATH鸿蒙 SDK 路径可探测检查local.properties或环境变量构建脚本找不到 SDK读取 HarmonyOS SDK 路径设置DEVECO_SDK_HOME工程依赖已安装ohpm install执行过hvigor 构建时报缺依赖脚本里加ohpm install的兜底调用设备已连接并授权hdc list targets有非 unauthorized 的设备安装脚本挂起脚本检测到 unauthorized 时给出明确提示hvigorw 存在工程根目录或全局打包步骤无法执行优先用工程根目录的hvigorwwrapper现在网上很多教程直接讲“hdc 能跑就行”但我实际用下来“hdc 能跑”和“脚本能在无人值守的情况下连续跑 100 次不挂”之间差了上面这张表里的每一行。3. 实战改造让 dcli_scripts 一键完成鸿蒙的装、编、签、跑评估做完、环境基线列清楚之后就开始进入真正的改造。我拿团队里最常用的一个场景来讲完整链路一条命令把当前的 Flutter 鸿蒙工程编译、打包、签名、安装到真机。这套流程在 DevEco Studio 里手动点至少得两三分钟还容易点错脚本化之后十秒上下而且每一次的操作都是确定性的。3.1 把 dcli 脚本封装成项目级 command而不是散落的可执行文件dcli_scripts 内部我习惯用统一的入口dcli.dart通过子命令分发类似dcli install、dcli build、dcli logs。这样做的原因是鸿蒙化改造之后命令之间往往存在依赖关系统一入口方便在公共层做环境检查和日志初始化。import package:dcli/dcli.dart; void main(ListString args) { final command args.isEmpty ? help : args[0]; switch (command) { case install: installHap(); break; case build: buildHap(); break; case logs: streamDeviceLogs(); break; default: usage(); } }这里的installHap、buildHap就是我们要鸿蒙化的核心函数。很多教程会教你把每个脚本写成一个独立的.dart文件然后各自执行但我强烈建议统一入口因为环境检查、路径解析这些逻辑收敛到一处后面维护成本会低很多。3.2 在脚本里正确调用 hdc路径、超时、嵌套 shell 的处理dcli 调外部命令有两种方式run()是同步执行并拿到结果适合短命令Process.start()是异步流式适合长耗时或需要持续读输出的命令。我在调 hdc 的时候绝大多数用run()但会做两件事显式传超时时间捕获非零退出码。Futurevoid installHap() async { final hapPath await locateBuiltHap(); // 在 3.4 小节说明 final targets run(hdc list targets, timeout: const Duration(seconds: 10)); if (targets.contains(unauthorized)) { printerr(设备未授权请先在 DevEco Studio 中确认连接弹窗); exit(3); } if (!targets.contains(Connected)) { printerr(没有已连接的鸿蒙设备或模拟器); exit(3); } final result run( hdc install -r $hapPath, timeout: const Duration(seconds: 60), ); if (result.exitCode ! 0) { printerr(hdc install 失败${result.stderr}); exit(3); } print(安装完成$hapPath); }这里特别说明一下hdc install -r-r表示覆盖安装对应 adb install -r。我们第一次适配的时候漏了-r迭代开发时每次都要先hdc uninstall才能装新的体验非常差。dcli 的run在 Windows 上走的是cmd.exe如果命令里出现、|这类 shell 操作符建议改成runInShell但也要评估引入 shell 带来的转义风险。实践上我尽量不用字符串拼 shell 管道而是分两步在 Dart 里做输出处理。3.3 让脚本读懂鸿蒙的构建产物而不是凭感觉找 HAP 文件定位 HAP 文件是这次适配里最值得注意的一个点。鸿蒙工程可以有多个 module每个 module 的产物路径类似entry/build/default/outputs/default/entry-default-signed.hap但这个路径在不同 DevEco 版本里可能带不同的版本号或签名后缀。我之前踩过写死路径的坑后来改成解析build-profile.json5加目录扫描双保险FutureString locateBuiltHap() async { final projectRoot Dir.current.path; final buildProfile File($projectRoot/build-profile.json5).readAsStringSync(); final cleaned stripJson5Comments(buildProfile); final json jsonDecode(cleaned) as MapString, dynamic; // 常见配置里的 app/products取第一个 product 的名称 final products (json[app]?[products] as List?) ?? []; final productName products.isNotEmpty ? (products[0] as Map)[name].toString() : default; final outputDir Directory($projectRoot/entry/build/default/outputs/default); if (!outputDir.existsSync()) { printerr(未找到构建产物目录请先执行构建命令); exit(4); } return outputDir .listSync() .whereTypeFile() .firstWhere((f) f.path.endsWith(.hap), orElse: () { printerr(outputs 目录下没有 .hap 文件); exit(4); }).path; }代码不复杂核心思想是永远不要在生产脚本里写死产物路径。只用default是因为我们适配期一般跑的是 debug 包如果碰到release场景再把 productName 作为参数传进来路径就要相应改成release目录。3.4 构建链路的顺序安排Flutter 产物在前hvigor 打包在后Flutter 鸿蒙化工程里两种工具链是串联的关系。我们最终确定的标准顺序是flutter pub get保证 Dart 侧的依赖完整。flutter build针对鸿蒙目标的产物生成不同 Flutter 鸿蒙分支的命令参数略有差异这一步留了可配置项让不同团队填自己的分支路径。ohpm install保证鸿蒙工程侧的依赖完整。hvigorw assembleHap把上面两步的产物收拢并打包成 HAP。调用自定义签名配置或者直接使用工程里的 Debug 签名。hdc install -r安装到设备。顺序不能乱尤其是flutter pub get和ohpm install的位置我们最初把ohpm install放到了最后结果 hvigor 解析模块阶段就报缺包白白浪费了一轮构建时间。另外hvigorw在工程根目录脚本里建议用绝对路径调用因为 dcli 的工作目录不一定总在工程根目录。4. 踩坑记录hdc 输出编码、设备授权和 dcli 卡死的三板斧适配过程里真正的体感差异不是“API 能不能调通”而是“小问题会不会把你卡死”。鸿蒙工具链和 Flutter 生态之间的摩擦集中体现在下面这几个点上。4.1 中文系统下 hdc 输出 GBK/UTF-8 乱码的坑我们在 Windows 开发机上跑脚本时hdc list targets的输出偶尔会变成乱码。原因不复杂hdc 在部分 Windows 版本上输出编码跟随系统代码页而 dcli 的run默认按 UTF-8 解码。中文设备名、中文应用名最容易触发。我的解决方式是在脚本开头强制设置环境变量再执行 hdc 命令final result run( hdc list targets, timeout: const Duration(seconds: 10), environment: { PYTHONIOENCODING: utf-8, LANG: en_US.UTF-8, }, );这个办法并不能百分之百覆盖所有 hdc 版本的问题更稳妥的做法是拿到输出字节后用gbk解码兜底。dcli 提供了result.stdout的原始字节访问可以在utf8.decode失败时再尝试gbk解码。我建议在脚本里做一个decodeHdcOutput的函数统一处理不要每个命令都重复写一遍。4.2 设备授权弹窗adb 时代的老问题换了一层皮Flutter 开发者对adb devices里的unauthorized应该不陌生。鸿蒙的 hdc 也有类似的机制第一次连接真机时设备上会弹授权框没确认的话hdc list targets就显示unauthorized。但这里有个和 adb 不太一样的细节鸿蒙对“开发者模式”的开启要求更严格有些设备型号还要先在设置里打开“USB 调试”才可以。我们的安装脚本因此加了一个前置等待逻辑如果检测到 unauthorized就打印提示并轮询等待最长 30 秒给人在设备上点授权的缓冲时间。for (var i 0; i 30; i) { final output run(hdc list targets, timeout: const Duration(seconds: 5)) .stdout; if (output.isNotEmpty !output.contains(unauthorized)) { break; } stderr(等待设备授权... ${30 - i}s); sleep(1.second); }这个小逻辑上线后团队里的新人跑脚本挂掉的概率明显下降。4.3 dcli 脚本看似“卡死”的真相子进程输出缓冲和同步阻塞说实话这次适配里最有价值的教训来自一个“脚本执行到 hvigor 好像卡住不动了”的问题。排查半天问题出在 hvigor 的告警输出实在太多而 dcli 的run是同步攒输出、结束后一次性返回。构建任务明明还在跑但因为输出没刷新到终端看起来就是卡死了。解决办法是放弃run换成Process.start流式读取输出final process await Process.start(hvigorw, [assembleHap], runInShell: true); process.stdout.listen((chunk) { stdout.add(chunk); }); process.stderr.listen((chunk) { stderr.add(chunk); }); final exitCode await process.exitCode; if (exitCode ! 0) { printerr(hvigor build 失败退出码$exitCode); exit(4); }这个改动比想象中影响大。流式输出之后团队在 CI 上也能看到实时构建日志了排错体验提升了一大截。另外给 hvigor 传--no-daemon或类似参数可以减少偶发的构建挂起问题具体看 hvigor 版本支持情况。5. 让 CLI 用起来像产品而不是玩具日志规范、退出码和进度反馈我们可以把一长串 hdc 和 hvigor 命令拼凑成“能跑”的脚本但如果团队里其他人也要用、CI 也要跑那脚本就得有工程化的体面。这一节聊的是把 dcli_scripts 当产品来打磨的几条准则。5.1 给 dcli 脚本制定统一的退出码和日志分级直接 print 一大段文字在终端上人还能忍受到 CI 日志里就是灾难。我们给脚本定了一套简单粗暴的退出码约定团队里所有人都遵守退出码含义典型场景0成功构建、安装、日志轮转正常结束1参数错误传入的模块名或 product 不存在2环境缺失hdc/ohpm 未找到SDK 路径未配置3设备错误设备未连接、未授权、安装失败4构建失败flutter build 或 hvigor assembleHap 失败日志分级我推荐用 dcli 自带的Logger而不是到处print。info 留给关键步骤warn 给可能影响结果的告警error 只给真正阻断流程的信息。团队有个隐性规定error 日志里必须包含排查线索比如命令的完整调用、退出码、以及建议检查的环境变量。5.2 长耗时任务里给开发者“它还在工作”的反馈hvigor 打包可能耗时几十秒这段等待里如果在终端只看到一个静止的光标人会立刻怀疑脚本挂了。dcli 提供了progress和spinner可以在两个关键节点之间给用户反馈final spinner Spinner(); spinner.start(title: 正在执行 ohpm install ...); final installResult run(ohpm install, timeout: const Duration(minutes: 5)); spinner.stop();这里要提个经验spinner 适合短等待真正冗长的构建过程还是靠流式日志最靠谱人能看到“编到哪个模块了”才算安心。我们是 combo 用法阶段切换用 spinner长任务用流式输出。5.3 与 CI/CD 的磨合点非交互模式、彩色输出和密钥处理我们很快就把这套脚本接进了团队的 CI原本本机跑的脚本到了 CI 上就暴露出几个问题脚本里有ask()这类交互式输入CI 没有 TTY直接挂。处理方式是规定所有命令默认非交互必需的可变参数一律通过命令行参数或环境变量传入。彩色输出在 CI 日志里会变成乱码转义符。处理方式是给脚本加--no-color参数或者检测到CI环境变量就自动关闭彩色输出。签名密钥和 keystore 密码绝不能出现在脚本代码里。鸿蒙应用的签名配置优先走build-profile.json5里引用的环境变量脚本只负责读取路径不负责托管密钥。这些点如果是个人脚本基本不会想到一旦要团队化和自动化就是必踩的坑。6. 如果还想更彻底设备侧命令行能力的鸿蒙化边界到这里我们已经把“宿主机上的命令行工具链”鸿蒙化得差不多了。但经常有同事问dcli_scripts 能不能在鸿蒙设备上直接跑这个问题要分清楚两个完全不同的场景。6.1 Dart 代码能否直接跑在鸿蒙设备上取决于运行时dcli 本身依赖 Dart VM 和宿主操作系统的 Process 能力它不是为嵌入式设备设计的交互式命令环境。鸿蒙设备上的 Flutter 应用确实可以运行 Dart 逻辑但那是在集成好的 App 沙箱里不会给你一个可以随便执行hvigor这类宿主编译命令的终端环境。所以我的结论是dcli_scripts 继续留在宿主机负责调用 hdc 指挥设备。不要把“在 App 内执行 Dart 脚本”和“命令行终端化”混为一谈。前者能做但那是 Flutter 鸿蒙应用内部的功能逻辑不是开发者工具链的问题。6.2 真正的设备侧替代ArkTS 封装、hdc shell 与原子化服务有一种需求确实需要“设备内部有自动化能力”比如给门店设备批量做配置、巡检本地存储、控制外设。这种情况下我最常用的路径是在宿主机用 dcli 脚本通过hdc shell执行设备侧的命令比如检查进程、读系统信息。如果逻辑比较复杂再用 ArkTS 写一个小的本地任务模块通过 hdc 的调试接口触发。如果要做成用户可感知的维护工具就做成原子化服务在后台执行定期任务。这里给一个决策表把场景和方案对应清楚需求场景推荐方式理由开发期批量安装、抓日志宿主机 dcli 脚本 hdc最直接复用现有工具链设备侧执行 shell 过命令hdc shell轻量无需开发 App 功能设备侧周期性自检、上报ArkTS 本地任务 原子化服务要常驻后台只有系统级能力才稳给运维团队做个可视化维护面板Flutter 鸿蒙应用 项目管理是完整的应用开发交给应用层从工具链角度说设备侧方案其实是另一个生态在管的事。我们团队现在的分工也很清晰dcli_scripts 管好宿主机侧所有的自动化设备侧只暴露最少量的 hdc shell 接口需要更复杂能力再走 ArkTS。如果要给这次鸿蒙化适配打一个总结我不会说“我们完美解决了所有问题”。现实是第一版核心链路周末两天就通了但让它在各种开发机上稳定跑、让 CI 上不再突然冒出莫名其妙的乱码和授权问题前前后后用了三周。dcli_scripts 的鸿蒙化本质不是把 API 签个名那么简单而是把你对设备、构建、签名、包管理这条链路的理解重新梳理了一遍。最后分享一个小技巧动大改之前先写一个二十行的 POC——只做一件事检查 hdc 连接、选一个最小的 HAP 装上真机。这个 POC 跑通剩下的就是一层层往里加逻辑跑不通说明环境还没对齐别急着写后面的代码。
返回列表