最近我花了两周时间做了一件事:让内部持续在用的 Flutter 架构脚手架 CLI 能够稳定生成鸿蒙工程。改造对象是 flutter_architect_cli,一个把架构模板资产、脚手架治理绑在一起的 CLI 工具。这件事听起来只是给工具加个平台开关,真正做起来却牵扯到模板资产、构建编排、依赖映射和生成后校验四个层面,任何一个环节偷懒,后面都会以"新项目跑不起来"的形式找补回来。这篇内容不是套话式科普,而是把完整适配路径、背后逻辑和踩坑过程摊开来讲,适合正在做 Flutter 脚手架治理、或者准备把 Flutter 项目送上鸿蒙平台的团队参考。
1. 先讲清楚:为什么模板资产的适配,不只是"加个平台目录"
很多 Flutter 团队接触鸿蒙时的第一反应是:代码不是跨平台吗?把原有工程拿过来重新打包不就行了。实际上一旦涉及 Flutter 要跑在鸿蒙生态里,工程形态会发生四个变化,任何一个变化都会倒逼脚手架工具跟着改。
1.1 鸿蒙对 Flutter 工程形态带来的四个变化
第一个变化是宿主工程目录从两个变成三个。传统 Flutter 工程里有android/和ios/,这是原生平台宿主。鸿蒙化之后会多出一个ohos/目录,它承担的是鸿蒙侧应用入口、资源配置和系统能力声明。CLI 如果还停留在"只生成 android 和 ios",那么生成出来的项目在鸿蒙侧就是残缺的。
第二个变化是构建产物从 APK/IPA 变成了 HAP。HAP 的构建链路、签名方式和 Gradle/Xcode 都不一样,模板里原来内嵌的构建脚本、CI 配置、产物检查逻辑全部要适配。
第三个变化是依赖插件不再天然可用。Flutter 插件提供的是平台通道实现,鸿蒙侧有没有对应实现,决定了这个插件在生成模板里能不能默认启用。CLI 如果继续无脑把dio、shared_preferences、path_provider等写进 pubspec,生成出来的工程很可能在鸿蒙上编译失败。
第四个变化是包名和签名策略。鸿蒙应用有自己的一套 bundleName、签名证书和设备关联逻辑,模板里的占位符替换规则要同步扩展。
1.2 CLI 工具到底在团队里承担什么角色
flutter_architect_cli 这类工具不是普通的"new project"生成器。它背后是一整套架构模板资产:应用骨架、业务模块模板、页面模板、状态管理方案、路由组织方式、网络层封装,全部以模板资产的形式沉淀。团队里新项目从这套资产里长出来,而不是每个开发者从零搭一套。
同时它还承担脚手架治理职责。没有治理的话,团队里很快会出现几十个结构各异的 Flutter 项目:有人用 Bloc、有人用 Provider、有人把网络请求写在页面里。CLI 的价值就是把架构规范和模板资产绑定,让所有项目从出生起就遵循同一套约束。
所以当鸿蒙平台出现时,真正需要适配的不只是"某个项目工程",而是整个团队的项目起点。这也是我坚持从 CLI 层面做改造、而不是临时再造一个鸿蒙专用模板的原因——后者会让治理体系分裂成两套,长期看维护成本更高。
2. 拆解 flutter_architect_cli 的模板资产与脚手架链路
动手改造之前,必须先把这个 CLI 的运行机制摸清楚。它的核心链条是:模板资产 → 变量解析 → 文件渲染 → 钩子执行 → 工程产出。下面用实际例子拆开讲。
2.1 模板资产目录长什么样
flutter_architect_cli 的模板资产通常组织成下面这种形式:
templates/ ├── app/ # 完整应用骨架 │ ├── metadata.yaml # 模板元数据:变量、平台、默认依赖 │ ├── android/ │ ├── ios/ │ ├── lib/ │ │ ├── app.dart │ │ ├── core/ # 核心层:网络、路由、主题 │ │ ├── features/ # 业务模块 │ │ └── shared/ # 共享组件 │ ├── pubspec.yaml.tmpl # 带占位符的 pubspec │ └── README.md.tmpl ├── feature/ # 单个业务模块的骨架 │ ├── metadata.yaml │ ├── lib/features/__name__/ │ └── test/__name__/ └── page/ # 单个页面骨架 ├── metadata.yaml └── lib/features/__name__/pages/每个模板都带一个metadata.yaml,这里面定义了模板支持的变量、可选平台、依赖列表和钩子命令。CLI 在生成时先读取元数据,再做渲染。
2.2 渲染机制不是简单的字符串替换
这里值得多说一句。模板渲染看着是字符串替换,实际要考虑三种情况:
第一种是变量替换。{{project_name}}、{{org}}、{{package_name}}这类占位符会在生成时换成用户输入值。第二种是条件渲染。某些片段只有特定条件才生成:比如用户选择riverpod状态管理方案时,会在 pubspec 里加入对应依赖,并生成 Provider 配置;选择go_router时,会生成对应路由文件。第三种是文件级命名规则。模板里有些文件带__feature_name__前缀,渲染时需要重命名到目标位置,比如__feature_name___page.dart要变成order_list_page.dart。
渲染完成之后,CLI 还会执行一批钩子(hooks):初始化 Git 仓库、执行flutter pub get、生成 IDE 配置文件等。这些钩子一般也是写在模板元数据里,同一个模板在不同平台上的钩子可能不一样。
2.3 架构模板与平台的耦合点到底有哪些
很多人以为架构模板是纯 Dart 代码,跟平台无关。这个理解只对了一半。纯逻辑层确实无关,但一个能跑的 Flutter 工程必须在平台目录里有宿主配置,而架构模板里通常还锁定了插件版本和构建脚本,这些都与平台强相关。
我在梳理 flutter_architect_cli 的适配工作时,把耦合点整理成了四类:
| 耦合层面 | 现状 | 鸿蒙化之后 |
|---|---|---|
| 宿主目录 | android/、ios/ 由模板固定生成 | 需新增 ohos/ 目录模板 |
| 包名与配置 | AndroidManifest、Info.plist 里的占位符 | 需支持 module.json5、bundleName 等鸿蒙配置 |
| 默认依赖 | pubspec 里写死了一批插件 | 插件需要支持 ohos 平台实现,或提供兼容替换 |
| 构建钩子 | 脚本里执行 flutter build apk/ios | 需支持 HAP 构建、签名配置 |
2.4 适配挂载点清单
基于上述梳理,我列了一张改造清单,后面所有开发都围绕它展开:
- 模板层:在
app/模板中新增ohos/宿主目录,并纳入变量渲染体系。 - 元数据层:
metadata.yaml的 platforms 枚举增加ohos。 - 依赖层:新增一套"Flutter 插件 × 鸿蒙可用性"映射表,生成时做依赖选择。
- 构建层:CLI 命令增加鸿蒙构建支持,模板钩子适配 HAP 产物。
- 校验层:生成后自动检查 ohos 关键文件、插件兼容性、签名配置。
有了这份清单,改动范围就明确了:不需要动渲染引擎核心,主要工作在模板资产和元数据层面。这也是 flutter_architect_cli 这类模板驱动 CLI 的优势——把平台差异收敛到模板资产的扩展上。
3. 鸿蒙化适配的四大改造点:模板、构建、依赖、校验
这一节展开讲四大改造点的具体做法和设计考量。
3.1 模板层:新增 ohos 宿主目录并纳入渲染体系
鸿蒙宿主目录的结构在工程里大致是:
ohos/ ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ │ ├── entryability/ │ │ │ └── pages/ │ │ ├── resources/ │ │ └── module.json5 │ └── build-profile.json5 ├── build-profile.json5 ├── hvigorfile.ts └── oh-package.json5这个目录不能直接复制粘贴到模板资产里,因为里面有很多文件需要接入 CLI 的变量渲染。比如 module.json5 里的 bundleName、entry 的名称、依赖版本号,都要从{{org}}、{{package_name}}、{{ohos_sdk_version}}等占位符中解析出来。
我是这样处理的:把整个 ohos 目录作为模板的独立子模块放在templates/app/ohos/下,然后把会变化的字段全部改成占位符,并在模板元数据里声明ohos_min_sdk、ohos_target_sdk、bundle_name等变量。CLI 渲染时统一解析,生成结果跟 Flutter 官方脚手架在鸿蒙侧的结构保持一致。
这里有一个设计取舍:ohos 目录是尽量精简,还是尽量完整。我的建议是初始模板保持最小可运行集,只包含 entry、页面对应 ets、资源配置和构建配置。等业务接入后,团队再在项目层面按需补充系统能力声明。模板资产太大反而不好维护。
3.2 构建层:把 HAP 构建编排进脚手架
Flutter 在鸿蒙侧的构建,一般依赖于特定的 Flutter 鸿蒙发行版。它的构建命令和标准 Flutter 有差异,常见的是通过flutter build hap一类指令完成,具体命令以所用发行版的文档为准。但不管命令怎么写,CLI 里必须做两件事:
第一是模板生成后的构建引导。当用户执行flutter_architect_cli create --platform ohos时,生成完成后应提示下一步构建命令,或者在钩子阶段直接执行一次构建校验。
第二是签名信息预留。HAP 构建需要配套签名配置,鸿蒙开发工具的签名体系与 Android 不一样。我在模板里预留了signingConfigs相关配置位,CLI 支持通过参数注入签名文件路径,这样持续集成时不用手工操作开发工具。
实际开发中还要注意:鸿蒙构建通常需要开发工具链参与,也就是要求开发者本机安装了完整的鸿蒙 SDK 和对应 IDE,CI 容器里也要预装。CLI 在构建前先做环境探测,缺东西直接输出失败原因,比构建到一半报错体验好得多。
3.3 依赖层:插件兼容映射表决定生成质量
这是整个适配过程中最容易被低估的环节。Flutter 项目依赖的插件,在鸿蒙上不一定是开箱即用的。我在改造时建了一张"插件鸿蒙可用性映射表",放在 CLI 的配置中心里,每条记录长这样:
plugin_mapping: dio: ohos_support: true notes: "" shared_preferences: ohos_support: true fallback: shared_preferences_ohos path_provider: ohos_support: partial fallback: path_provider_ohos some_device_plugin: ohos_support: false notes: "需要自研鸿蒙实现,生成时给出警告"生成工程时,CLI 会遍历模板依赖列表,逐个查映射表。遇到不支持或部分支持的插件,要么自动替换为兼容方案,要么在生成完成后输出警告,提示开发者提前评估。这一步极大减少了"新项目鸿蒙构建失败的次数"。
这里再强调一个容易被忽略的点:依赖映射不是静态的。插件社区对鸿蒙的支持度持续变化,建议把这张映射表做成独立配置远端下发,而不是硬编码在 CLI 二进制里。这样每次插件新增支持时,团队只需更新配置,不需要重新发版 CLI。
3.4 校验层:生成后自动检查,防止"看起来成功实际不可用"
模板生成成功不等于项目可用。我在 CLI 里加了一个verify阶段,专门在生成之后做一套快速检查:
- 检查
ohos/目录是否真实存在,关键文件如module.json5、build-profile.json5是否已渲染。 - 检查 pubspec.yaml 中所有依赖是否都在插件映射表里有记录,没有记录的给出警告。
- 检查 bundleName 是否合法,是否符合鸿蒙命名规范。
- 检查构建所需 SDK 版本是否与模板元数据声明一致。
这套校验不是替代真正的编译,而是把明显错误拦截在生成阶段。实测下来,它能拦掉大概七八成由于模板占位符漏替换导致的问题,节省大量联调时间。
4. 实操:在一套模板上完成鸿蒙化改造并验证跑通
理论讲完,回到操作层面。我以 flutter_architect_cli 的app/模板为例,完整走一遍适配流程。
4.1 环境准备与版本对齐
开始前先确认环境。鸿蒙化的 Flutter SDK 与官方 Flutter SDK 不完全一样,它是带鸿蒙引擎实现的分支版本。我的做法是为 CLI 增加一个--flutter-sdk参数,让使用者能指定鸿蒙化 Flutter SDK 路径,同时 CLI 内部会做版本探测。
工具准备清单:
- 鸿蒙化 Flutter SDK(从公开渠道获取对应版本)
- 鸿蒙开发 IDE 与配套命令行工具
- 鸿蒙 SDK 和模拟器或真机
- flutter_architect_cli 源码或本地 fork
版本对齐原则:模板元数据里必须写清支持的 Flutter 和鸿蒙 SDK 版本,生成时做一次版本协商,不匹配就中止并提示,避免玄学报错。
4.2 改造模板变量文件
我给templates/app/metadata.yaml增加了鸿蒙相关变量:
name: app platforms: - android - ios - ohos variables: project_name: required: true org: required: true bundle_name: description: "鸿蒙应用唯一标识,一般由 org + project 组成" default: "{{org}}.{{project_name}}" ohos_min_sdk: default: "5.0.0(12)" ohos_target_sdk: default: "5.0.0(12)" hooks: after_generate: - command: "flutter pub get" platforms: [android, ios, ohos] - command: "git init" platforms: [android, ios, ohos]这里有一个关键细节:bundleName 的默认值设计成了{{org}}.{{project_name}},意味着变量之间可以互相引用。渲染引擎需要支持这种链式解析,否则模板配置会很啰嗦。我的实现是先解析所有变量定义,再做两层替换,第一层把变量引用替换为变量值,第二层把所有占位符替换为最终值。
4.3 生成工程并构建 HAP
环境就绪、模板改造完成后,我用下面命令生成一个测试工程:
flutter_architect_cli create my_business \ --platform ohos \ --org com.example \ --template app \ --flutter-sdk /path/to/ohos-flutter-sdk生成完成后,CLI 自动进入校验阶段,输出检查结果。接着进入构建环节:
cd my_business flutter build hap --release如果构建过程中报签名错误,检查模板里预留的签名配置是否已填写;如果报插件找不到,回到依赖映射表确认对应插件的鸿蒙实现。
整个链路跑通后,把生成的 HAP 安装到模拟器或真机,验证 Flutter 页面能正常渲染、路由能跳转、网络请求能通。
4.4 验证结果
我建议在验证阶段不要只看"能装能开"。至少检查三件事:一是 Flutter 页面纹理是否正常渲染,鸿蒙侧的 Flutter 引擎和官方 Flutter 引擎在部分机型上有渲染差异;二是平台通道调用是否正常,比如读取本地存储、获取设备信息;三是热重载是否能工作,这直接影响后续开发效率。
第一次跑通的时候,我把验证结果记录成一份 checklist,后续每次模板改动都回归一遍。这个习惯后来帮了大忙。
5. 脚手架治理视角:模板资产的可维护性与版本化管理
适配鸿蒙只是第一步。真正让模板资产长期可用,靠的是治理机制。这一节讲我在 flutter_architect_cli 改造过程中沉淀的治理经验。
5.1 模板资产语义化版本
模板资产跟业务代码一样需要版本管理。我给每个模板增加版本字段,采用语义化版本号:
- 主版本号:模板结构不兼容,升级后需要重新生成工程。
- 次版本号:新增可选能力,比如增加一个新的架构模板,或新增默认依赖。
- 修订号:修复模板细节,不改变生成结果结构。
CLI 生成工程时,会在工程根目录写入"来自哪个模板的哪个版本"。这样后续做批量升级时,可以快速知道哪些项目落后了多少个版本。
5.2 向后兼容:模板演进和双轨
鸿蒙化适配期间,模板会频繁调整。我的经验是保持过渡期双轨运行:旧项目继续用旧模板的小版本,新项目用适配鸿蒙的新版本。不要一上来就强制所有项目迁移到新版模板,否则治理阻力会很大。
模板演进时还要特别注意"重命名代价"。模板里一个目录名或文件名的变更,会影响所有已生成项目。因此模板资产需要一套迁移脚本:记录每个版本的变更操作,老项目升级时自动执行迁移。flutter_architect_cli 的元数据里支持migrations字段,我在里面放了一系列脚本:
migrations: - from: "1.0.0" to: "1.1.0" steps: - type: "add_directory" path: "ohos" source: "templates/app/ohos" - type: "update_dependency" name: "shared_preferences" version: "2.3.0"5.3 质量卡点与灰度发布
模板资产的改动必须经过质量卡点,不能直接推到团队所有人面前。我的做法是:
- 每个模板变更合并前,自动生成一个最小示例工程并执行构建冒烟测试。
- 变更发布后,先让内部一个种子项目组试用,收集反馈后再全量推广。
- 模板仓库里维护变更日志,生成工程时提示"当前模板版本与最新版差异"。
这套流程最早用于 Android 和 iOS 模板,鸿蒙化适配时同样沿用。治理机制的好处在于:即使适配工作再复杂,也不会因为人为操作失误造成模板混乱。
6. 踩坑记录与排查链路
适配过程不可能一帆风顺。下面记录几个我实际遇到的高频问题,以及一次典型问题的完整排查链路。
6.1 三个真实高频踩坑
第一个坑是ohos 目录漏进模板渲染。最初我把 ohos 目录直接放进模板资产,但没有把所有字段都改成占位符,导致生成出来的工程 bundleName 全部一样,装上真机后应用互相冲突。排查时逐个文件对比才发现 module.json5 里的 bundleName 还是模板默认值。
第二个坑是插件依赖版本冲突。鸿蒙化 Flutter SDK 对某些插件的原生实现版本有要求,官方最新版插件可能还没有适配,于是构建时会出现链接错误。解决方式是把插件版本锁定在一份经过验证的兼容清单里,而不是用any或发布版最新。
第三个坑是构建时芯片架构不匹配。鸿蒙模拟器和真机可能分别是不同芯片架构,Flutter 引擎 so 库要选对应架构产物。CLI 构建前必须做架构探测,否则装到设备上闪退。
6.2 以"生成后构建失败"为例的完整排查链路
有一次生成工程后执行flutter build hap失败,报错信息是找不到某个模块。我没有直接去搜报错,而是按下面链路一步步排查:
第一步,确认 CLI 渲染产物。生成一个"最小对照工程",用官方脚手架在相同环境下生成同样配置的鸿蒙 Flutter 工程,构建通过。这说明问题出在模板渲染,不是环境问题。
第二步,对比目录结构。把 flutter_architect_cli 生成的 ohos 目录和官方脚手架的 ohos 目录做递归 diff。这一步很快定位到差异:模板里少了一个hvigorfile.ts文件,导致构建系统找不到任务入口。
第三步,核对模板元数据。发现app/模板的文件列表里确实漏掉了ohos/下部分顶层文件。补上之后重新生成,构建恢复正常。
这个排查过程的核心思路是:先隔离变量,再对比差异。盲改模板很可能陷入反复试错的循环,而对比 diff 能快速锁定问题文件。
6.3 排查工具箱
我把常用的排查命令和工具整理成一个清单,方便复用:
- 目录对比:递归 diff 生成工程与对照工程。
- 文件渲染检查:grep 所有占位符残留,特别是
{{、}}和__。 - 依赖冲突检查:在生成工程里执行依赖分析,查看是否有冲突的插件版本。
- 构建日志分级:把构建输出分成错误、警告、信息三层,重点排查错误日志。
有了这套工具,模板改动后的问题定位时间从小时级降到分钟级。
7. 最后分享几个适配过程沉淀的小技巧
东西写完,再分享几个我自己觉得性价比很高的做法。
第一个技巧是用一个适配过鸿蒙的样板项目反推模板,而不是凭空写模板。我先把一个手动的鸿蒙 Flutter 项目调到可运行状态,再把它逐步"模板化":把固定值改成占位符、把平台相关代码隔离出来、把版本号抽到元数据里。这样推出来的模板比设计出来的更贴近真实工程,少走很多弯路。
第二个技巧是把插件映射表做成远端配置。CLI 每次生成工程时拉取最新映射表,这样插件兼容性更新不需要重新发版 CLI,团队可以直接受益。远端配置生效前做好版本管理,避免生成结果不可复现。
第三个技巧是在 CI 里加一个"鸿蒙冒烟验证"任务。每次模板变更后,自动生成一个最小鸿蒙工程,尝试构建 HAP,并记录构建耗时和失败信息。这个任务不用跑全量测试,只要保证模板在鸿蒙侧的基本可运行性,成本低、收益高。
根据我的实际经验,Flutter CLI 的鸿蒙化适配并不需要重写工具,它更像是一次模板资产的重构:把平台差异收敛到模板资产和配置映射中,用校验机制兜底,用治理机制保证演进。这套思路不仅适用于 flutter_architect_cli,换成其他 Flutter 脚手架工具同样成立。希望这篇记录能帮到正在做同类改造的团队。