做 Flutter 工程化的人应该都有这种感觉:一个 App 从三五个页面膨胀到几十个页面,架构从单 Feature 撑到十几个模块,靠人肉去维持目录结构和样板代码的一致性,基本等于自我消耗。morpheme_cli 就是为了终结这种状态而存在的——它把 Flutter 项目里 feature、data、domain、presentation 这些分层骨架的创建、model 的生成、依赖的装配全部收进命令行,一条命令拉出一个符合团队规范的模块。最近我在做 morpheme_cli 的鸿蒙化适配,目标很简单:让这套脚手架能力在鸿蒙生态里也能开箱即用,生成的工程直接可以进鸿蒙的构建链路,而不是换一个平台就让团队退回人肉搭架构的原始状态。这篇文章会把适配过程中的设计思路、实操步骤和踩坑记录都摊开讲,给准备做 Flutter 跨端工程迁移的朋友一个完整参考。
内容适合三类人:正在做 Flutter 应用鸿蒙化落地的客户端工程师,准备把团队内部脚手架带上新平台的架构组同学,以及对 OpenHarmony 生态里三方库工程化有兴趣的开发者。不管你是来抄作业还是来找思路,下面这些内容应该都有点用。
1. 先搞懂 morpheme_cli 在工程化里的生态位
1.1 脚手架工具的三代演进
很多团队对脚手架的理解还停留在“复制模板工程”这个阶段,但这件事其实已经进化过好几轮了。
第一代是裸工程阶段,典型代表就是flutter create。它只能生成一个标准的壳子,页面目录、网络层、状态管理、路由都给你留着空白,具体怎么搭全靠新项目的开发者自己发挥。一个人搭一个风格,三个项目三种结构,架构约束基本靠开会强调。
第二代是模板仓库阶段。把团队验证过的工程复制一份到新项目里,一开始效率确实高,但问题很快暴露:项目之间的定制化差异会慢慢侵入模板,今天有人加了日志组件,明天有人改了目录结构,模板仓库很快就成了一堆补丁的集合体,没人敢动它。
第三代是代码生成器阶段,morpheme_cli 就是这一代的典型形态。它的核心不是“复制”,而是“声明式生成”。你告诉它项目叫什么、需要用哪些层、走什么样的架构风格,它按照内置的模板引擎把对应的代码结构、样板文件、依赖声明一次性生成出来。模板本身是活的,参数化程度高,团队规范真正落在了工具链里,而不是落在人的自觉上。
理解了这三代演进,你就能明白 morpheme_cli 这类工具的价值:它把架构约束从“文档 + 口头传承”变成了“工具自动保证”。这条价值在跨平台适配时会被放大——鸿蒙化不是换皮,是整个构建链路和生态体系的替换,如果没有脚手架层面的支撑,团队在鸿蒙侧很容易出现架构断层。
1.2 morpheme_cli 的核心能力拆解
要谈适配,得先拆清楚这个 CLI 到底做了什么。按我实际使用的体验,它的能力可以分成五块:
| 能力模块 | 具体行为 | 对应产出 |
|---|---|---|
| 项目初始化 | 交互问答选择架构风格、状态管理方案 | 完整 Flutter 工程骨架 |
| 模块生成 | feature 命令创建业务模块 | screen/widget/bloc/repository/service 等文件 |
| 模型生成 | 根据字段定义生成实体类 | 带 fromJson/toJson 的 model 文件 |
| 依赖装配 | 按需写入依赖声明 | 更新后的 pubspec.yaml |
| 规范约束 | 自动处理命名、目录、文件关系 | 符合团队规范的模块结构 |
这里最值得玩味的是依赖装配这一块。大多数脚手架只管生成代码文件,morpheme_cli 还会替你决定“这个模块应该引入哪些依赖、什么版本”。在鸿蒙化的时候,这一块恰恰是最大的差异点:平时从 pub.dev 拉依赖没问题,但在鸿蒙构建链路里,不是所有 pub 包都有鸿蒙侧的平台实现,替换哪些、锁定什么版本,需要 CLI 在生成阶段就做对,而不是等开发者编译报错再去手改。
1.3 鸿蒙化适配为什么绕不开
先说结论:鸿蒙化适配不是“加分项”,而是一个必然会遇到的工程化命题。
原因一,多端产物交付正在变成常态。一套 Flutter 代码同时产出 Android、iOS、鸿蒙三个端的安装包,已经不是个别团队在尝试的玩法。只要你有这个交付目标,脚手架就必须支持在生成阶段指定目标平台。
原因二,架构一致性不能因为换平台就断裂。团队花了大半年沉淀出来的分层规范、命名规则、模块划分方式,如果在鸿蒙侧全部靠手工重建,等于让工程化水平倒退到第一代脚手架时代。工具链层面的适配,本质上是在保住团队已有的工程化投资。
原因三,鸿蒙工程的构建体系跟 Flutter 默认工程差异巨大。Gradle 换成了 hvigor,Android 清单换成了 module.json5,pub.dev 依赖有一部分要换成 OpenHarmony 三方库中心里的适配版本。这些差异如果不在 CLI 生成时处理掉,就会散落在每个开发者的本地环境里,变成一个个说不清楚的人肉补丁。
2. 鸿蒙化适配的分层思路:从 CLI 执行环境到工程构建链路
2.1 鸿蒙生态里 Flutter 工程到底变了什么
要适配,先要知道鸿蒙侧跟 Flutter 默认工程比,哪些东西变了。我梳理下来主要有三个层面的差异。
第一个是构建系统。Flutter 标准工程在 Android 侧依赖 Gradle,通过android/app/build.gradle描述构建逻辑。鸿蒙侧用的是 hvigor,对应的描述文件是hvigorfile.ts,构建任务是基于ohos插件体系跑的。这意味着 CLI 生成的工程里必须多出这一套描述文件,否则工程在 DevEco Studio 里根本构建不起来。
第二个是模块描述方式。Android 用 AndroidManifest.xml 声明权限和组件,鸿蒙用module.json5来描述 module 的基本信息、能力入口和设备类型,权限声明放在requestPermissions数组里。CLI 如果生成的是标准 Flutter 工程,这些鸿蒙侧的描述文件是不会出现的,必须额外补。
第三个是依赖生态。这个差异最隐蔽也最致命。pub.dev 上的纯 Dart 包在鸿蒙上基本能直接编译,但凡是依赖了平台通道的包,都需要鸿蒙侧有对应的原生实现。实际适配中,fluttertoast、shared_preferences、path_provider这类常用插件都要去找 OpenHarmony 生态的适配版本,或者改走鸿蒙原生的能力。morpheme_cli 在生成项目、装配依赖的时候,必须把这一层差异内建到模板和依赖策略里,否则生成的工程大概率拉不起来。
2.2 适配的三个层级与影响面
鸿蒙化适配不是一个单点改动,我把它拆成了三个层级,每一层解决不同的问题。
第一层是 CLI 执行环境层。morpheme_cli 本身是 Dart 写的命令行程序,Dart 虚拟机在 macOS、Windows、Linux 开发机上都能跑,这一层基本不需要重写。需要处理的是两件事:识别用户想要生成鸿蒙目标平台,以及把鸿蒙侧构建命令的调用方式接进 CLI 的流程里。
第二层是模板生成层。morpheme_cli 内部用的是模板渲染引擎,代码文件都是基于模板 + 变量渲染出来的。鸿蒙化适配的重头就在这里:在模板里增加平台条件块,当目标平台包含 ohos 时,额外渲染ohos/目录下的工程描述文件、权限配置和依赖清单。
第三层是产物构建层。生成的工程要能在鸿蒙侧真正跑起来,依赖的是 hvigor 构建链路是否完整。CLI 在这个层面的工作是把签名配置、SDK 版本、模块入口这些参数生成到位,让开发者拿到的工程不是“半成品”,而是一个可以直接交给构建系统消费的完整项目。
这三个层级是有依赖关系的。第一层没有识别到鸿蒙目标,第二层的模板就不会触发;第二层没有渲染出正确的ohos/目录结构,第三层就没有可构建的产物。适配过程只要有一层断掉,最终效果就是“生成了工程但构建不过”。
2.3 方案取舍:为什么不需要另起一套 CLI
规划阶段最容易纠结的问题就是:要不要维护一套独立的“鸿蒙版 morpheme_cli”?我的结论是不要,理由很实际。
代码复用率在这里高达九成。项目初始化、模块生成、命名规范、model 工具这些核心逻辑跟目标平台没有强耦合,真正需要差异化的只有平台描述文件、依赖策略和构建配置这几部分。如果拆成两套工具,意味着每一次模板调整、每一个 bug 修复都要同步改两遍,维护成本翻倍,而且两套工具很容易演进出一个不一致的分叉。
更好的做法是在现有 CLI 里增加平台开关,把鸿蒙作为一个新的 target platform 接入。模板层用条件渲染来处理差异,依赖层用平台感知的依赖解析策略,命令入口加一个--platforms=ohos参数。这样改动范围可控,老用户不受影响,新需求也能平滑吸收,是投入产出比最高的路径。
3. 核心适配实操:让一条命令生成鸿蒙友好工程
3.1 CLI 入口层的平台识别与参数透传
第一步是让 CLI 知道“用户在生成一个鸿蒙目标”。morpheme_cli 的初始化命令本来就有平台参数,常见的是--platforms=android,ios。鸿蒙化适配就是把这个平台列表扩展出ohos这个选项,同时做好平台识别逻辑。
实际操作中,平台来源有两个:命令行参数和本机环境。命令行参数最直接,用户在初始化时显式声明目标平台;本机环境属于兜底,如果机器上配置了鸿蒙 SDK 相关环境变量,CLI 也可以主动感知。
import 'dart:io'; List<String> resolveTargetPlatforms(List<String> args) { if (args.contains('--platforms=ohos')) { return ['ohos']; } if (args.contains('--platforms=android,ios,ohos')) { return ['android', 'ios', 'ohos']; } // 兜底:检测本机是否具备鸿蒙构建环境 if (Platform.environment['OHOS_SDK_HOME'] != null) { return ['ohos']; } return ['android', 'ios']; }这层改动不复杂,但要想清楚一件事:平台列表的解析逻辑应该收敛到一个统一的入口。CLI 后续初始化模板、依赖装配、目录生成都会依赖这个平台列表,入口收得干净,后面各层取数据就省心。
第二个要处理的是自动化场景下的非交互能力。morpheme_cli 支持交互式问答,但在 CI 或者脚本里跑的时候交互是走不通的,所以要保证--platforms=ohos配合其他非交互参数可以一把生成,不需要人工介入。我实际测试下来,这一点在鸿蒙适配中尤其重要——因为鸿蒙工程的联调往往要跨多个环节,如果每次生成都要人肉确认一轮,自动化链路就断了。
3.2 模板层的鸿蒙差异化改造
模板层是整个适配最核心的部分。morpheme_cli 的模板引擎基于 mustache 语法,原来模板里基本只有项目名、包名、架构风格这些变量,鸿蒙化之后需要加入平台条件块。
思路是这样:在模板里包一个isOhos的布尔变量,当它被渲染为 true 时,输出鸿蒙侧专属的工程文件;为 false 时保持原有行为。以工程描述文件为例,模板可以写成下面这样:
{{#isOhos}} // 生成 ohos/AppScope/app.json5 和 ohos/entry/src/main/module.json5 export default { app: { bundleName: "{{bundleName}}", vendor: "example", versionCode: 1000000, versionName: "{{versionName}}", icon: "$media:app_icon", label: "$string:app_name" } } {{/isOhos}}这里有一个实操要点:鸿蒙侧ohos/目录结构跟 Android 的android/、iOS 的ios/是平级的,所以在生成目录结构的时候要按平台分支来铺。Flutter 默认模板里没有ohos/,我们的适配模板里要显式创建这个目录并填充内容,包括:
ohos/AppScope/app.json5:应用级配置ohos/entry/src/main/module.json5:模块级配置和权限声明ohos/entry/src/main/ets/entryability/EntryAbility.ets:UIAbility 入口ohos/hvigorfile.ts:hvigor 构建任务入口ohos/oh-package.json5:鸿蒙侧包配置
还有一个容易被忽略的点:入口文件的命名。Flutter 标准工程的入口是 main.dart,鸿蒙侧作为宿主壳需要有一个 UIAbility 来承载 Flutter 视图。模板层生成的 EntryAbility 里要正确引用 Flutter 引擎的启动逻辑,这个文件的模板化程度比 Android 的 MainActivity 高不少,因为鸿蒙侧的 UI 描述用的是 ArkTS,跟 Java/Kotlin 不是一回事。
3.3 依赖装配与构建配置生成
依赖装配是 morpheme_cli 鸿蒙化里差异最大的部分。普通 Flutter 项目从 pub.dev 拉依赖,但在鸿蒙构建链路里,部分插件的坐标要切换到 OpenHarmony 三方库中心,或者使用鸿蒙社区的 Flutter 适配版本。
以代码生成器实际产出的pubspec.yaml为例,适配后的模板会有明显的平台感知:
name: {{projectName}} description: Generated by morpheme_cli version: 1.0.0+1 environment: sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter # 以下依赖需要根据目标平台做差异化选择 dio: ^5.4.0 # 鸿蒙适配版的基础库,位于 OpenHarmony 三方库中心 # 由 CLI 在生成阶段自动切换坐标与版本 flutter_ohos: 1.0.0这里有一个经验:依赖版本不应该写成永远向上兼容的宽松范围。鸿蒙侧的适配版本往往跟着特定的 API Level 走,版本放宽了,反而会让生成出来的工程在某个 API Level 上编译失败。CLI 在写入依赖时应该锁定经过验证的版本区间,并且把 API Level 跟依赖版本做关联。
除了 pubspec.yaml,CLI 还需要生成鸿蒙侧的oh-package.json5。它是鸿蒙工程的包管理描述文件,类似 npm 的 package.json。生成模板大概是:
{ "name": "{{projectName}}_ohos", "version": "1.0.0", "description": "Generated by morpheme_cli", "main": "", "author": "", "license": "", "dependencies": { "@ohos/axios": "^1.0.0" } }hvigorfile.ts 的生成也要纳入模板。它负责把鸿蒙的构建任务串起来,适配后的模板如下:
import { appTasks } from '@ohos/hvigor-ohos-plugin'; export default { system: appTasks, plugins: [] };3.4 资源与权限适配
鸿蒙工程的资源和权限声明跟 Flutter 默认工程差别很大,不处理的话编译能过,但一上真机就会出现各种奇怪问题。
权限声明这块,Flutter Android 工程是通过 AndroidManifest.xml 里的 uses-permission 声明,鸿蒙侧是在module.json5里用requestPermissions数组声明。CLI 生成模板时要区分基础权限和业务权限,像网络权限这种基础能力应该默认生成:
{ "module": { "name": "entry", "type": "entry", "srcEntrance": "./ets/entryability/EntryAbility.ets", "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": ["phone", "tablet"], "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }图标和字符串资源也建议在生成阶段就铺好。鸿蒙的资源目录结构与 Android 资源有些接近,但命名和引用方式有差异,CLI 模板里按鸿蒙的 media/string 资源规范生成占位资源,开发者拿到项目后直接替换资源文件即可,不用自己摸索目录结构。
有一点务必注意:bundleName是鸿蒙侧唯一标识,格式要求跟 Android 包名不太一致,不要从 Android 的 applicationId 直接抄过来。CLI 应该在生成时单独生成一套符合鸿蒙规范的 bundleName,不要复用 Android 的包名规则,否则上架审核阶段会非常被动。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
适配过程中我遇到不少问题,把典型场景整理成一张速查表,方便后面同学直接对照:
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 生成的工程里没有 ohos/ 目录 | CLI 平台列表未包含 ohos | 初始化时显式传--platforms=ohos,并确认模板层已注册鸿蒙平台分支 |
| pub.dev 上的包在鸿蒙编译报错 | 依赖内部包含平台通道实现 | 到 OpenHarmony 三方库中心找对应适配版本,替换 pubspec 里的依赖坐标 |
| hvigor 构建报 SDK 版本不一致 | DevEco Studio 配置的 API 版本与命令行 SDK 不匹配 | 统一本机 SDK 路径,检查 hvigor 插件版本是否匹配当前 API Level |
| module.json5 解析失败 | 权限名称拼写错误或设备类型声明不规范 | 对照官方文档逐项核对requestPermissions和deviceTypes |
| 生成后 Flutter 页面无法拉起 | EntryAbility 里 Flutter 引擎初始化缺失 | 检查模板生成的 EntryAbility 是否正确加载 Flutter 容器 |
| bundleName 与签名不符 | 复用了 Android applicationId | 单独生成鸿蒙 bundleName,并在签名配置里保持一致 |
4.2 我踩过的坑和解决过程
第一个坑是路径大小写。我在 macOS 上开发,文件系统默认大小写不敏感,生成出来的目录结构里Ohos和ohos混着来,本机跑得好好的,一到 Linux 构建机上就报 module 找不到。这个问题的根源在于 hvigor 对 module 路径是大小写敏感的。解决方式是 CLI 模板里对路径统一走小写,并且在输出时对目录名做一次强制规范化,不允许模板变量里出现大写目录名。
第二个坑是依赖版本锁得太松。第一版适配的时候我图省事,把 flutter_ohos 的版本写成^1.0.0,结果不同开发者拉到的版本不一致,行为表现也各异,有人跑通有人跑不通,排了半天发现是版本漂移。后来改成锁定精确版本,并加了一条规则:当 API Level 变更时,依赖版本跟着一起变更,不允许出现“API Level 升了但依赖没锁住”的情况。
第三个坑是 CI 环境里的非交互初始化。morpheme_cli 原本有交互问答流程,在本地开发机上跑没问题,但在 CI 容器里跑就卡死在等待输入。适配过程中我把鸿蒙初始化相关的参数做成了全量可传参,任何一步都可以用命令行参数覆盖,生成过程不再依赖键盘输入。这个改动不仅服务了鸿蒙场景,整个自动化链路都跟着受益了。
5. 写在最后的工程化建议
适配过程最大的体会是:工具链适配最难的地方往往不是代码改造,而是团队信心。业务研发最怕的就是“换个平台,以前的开发方式全作废了”。鸿蒙化这件事,恰恰容易被包装成这种印象。但实际上,只要脚手架层面把差异兜住,业务同学写 Flutter 的方式几乎不变,变的只是底层构建链路和依赖坐标,这些应该由工具去消化,而不是让每个开发者去记忆。
最后分享一个小技巧:在 CI 里加一个每晚的冒烟任务,用 morpheme_cli 生成一个包含鸿蒙目标平台的样本工程,然后直接跑 hvigor 构建。这个任务不跑业务用例,只验证“模板生成 + 依赖装配 + 构建链路”这条主链路是否健康。坚持跑一段时间你就会发现,90% 的鸿蒙化回归问题都能在构建阶段暴露出来,根本轮不到业务联调阶段去发现。工程化适配的价值,恰恰就藏在这些不起眼的自动化细节里。