去年年中,我决定把 easy_init_cli 从 Flutter 单一平台,改造成能同时服务 Flutter 和鸿蒙工程的初始化工具。easy_init_cli 这个三方库的价值大家应该能感受到:新建 Flutter 项目时,它能把 pubspec、目录、依赖、平台配置一次性生成到位,不用再从老项目复制改包名。现在团队同时要维护 Flutter 和鸿蒙两条产品线,初始化资产如果各搞各的,早晚变成两套规范、两套维护成本。这才有了这次鸿蒙化适配的立项。
适配这件事,远不是把模板目录里的 android、ios 文件夹换成 ohos 那么简单。鸿蒙工程在配置格式、依赖管理、命名规范、构建脚本上和 Flutter 有本质差异,如果不从架构层面做抽象,最后生成的工程不是构建失败,就是埋下一堆技术债。这篇文章把这条鸿蒙化适配路径完整捋一遍:先讲 easy_init_cli 的底层资产模型,再讲模板层和命令层怎么改,最后是架构治理和排坑实录。适合正在做客户端工程基建、想给团队搭一套跨 Flutter/鸿蒙初始化工具的同学参考,也适合被“模板复制”折磨到想彻底治理工程资产的团队。
1. 为什么需要鸿蒙化适配:脚手架工具的资产盲区
1.1 easy_init_cli 在 Flutter 侧解决的核心问题
先说 easy_init_cli 在 Flutter 侧到底做了什么。早期团队新建项目的方法,和大多数小团队一样:复制上一个项目,全局替换包名,改应用名,改图标。这个做法看起来快,但三个问题立刻暴露。
第一个问题是目录遗产。历史项目里残留着旧的模块、旧的思路,甚至有几代人的临时调试文件,复制得越多,项目结构就越散。第二个问题是依赖版本各自为政。有的人用网络库 4.x,有的人用 5.x,一旦要统一升级,全团队的时间成本非常高。第三个问题最隐蔽,新建项目的命名、目录、构建配置这些“初始化资产”从来没有被系统管理过,没有人能回答“当前最新的工程规范长什么样”。
easy_init_cli 把“初始化工程”这件事抽象成了四类资产:目录与文件资产,也就是模板树;配置资产,也就是 pubspec、分析规则、忽略清单这些文件;依赖资产,也就是版本集合;交互资产,也就是命令行参数和选择项。工具本身不关心业务代码,它只负责把四类资产组装成一个标准工程,保证每次生成的结果一致、可复现、可审查。这才是脚手架工具该有的样子。
1.2 鸿蒙工程初始化的差异与适配任务拆解
鸿蒙那边的工程模型和 Flutter 非常不一样。我一开始也想着“映射一套目录就完事”,真正打开一个标准鸿蒙工程看目录结构之后,才发现这是一次架构级改造,不是表层翻译。
差异集中在四个维度。配置格式上,Flutter 用 YAML,鸿蒙用 JSON5,JSON5 虽然宽松,但很多编辑器里注释支持不统一,手写容易出错。依赖管理上,Flutter 是 pub,鸿蒙是 ohpm,两者仓库、锁文件格式、版本约束语法都不一样。工程结构上,Flutter 是单一 package 加 android/ios 平台目录,鸿蒙是 hvigor 工程模型,包含应用级配置、模块级配置、构建脚本三层结构。命名规范上,Flutter 包名强制小写加下划线,鸿蒙 bundleName 要反向域名,ArkTS 源文件要小驼峰,模块名要符合 ohpm 的包命名规则。
我列过一张对比表,实测下来每一行都会影响生成的代码正确性:
| 维度 | Flutter 工程 | 鸿蒙工程 |
|---|---|---|
| 配置格式 | YAML | JSON5 |
| 依赖声明 | pubspec.yaml | oh-package.json5 |
| 模块模型 | 单一 package | 工程级 + 模块级(entry 等) |
| 包名规则 | 小写下划线 | 反向域名 / 小驼峰 |
| 应用入口 | main.dart | EntryAbility.ets |
| 资源索引 | pubspec assets | resources 目录 + 索引文件 |
| 构建工具 | flutter pub get | ohpm install + hvigor |
基于这些差异,我把适配任务拆成了四层:命令行层要新增平台参数和鸿蒙专属参数;模型层要补齐鸿蒙工程需要的配置结构;模板渲染层要抽象渲染规则;后处理层要加命名规范、编码、产物校验。四层缺一不可,只有这样才能让 easy_init_cli 输出的鸿蒙工程,和官方模板逐字段对齐。
2. 适配方案选型:从命令层到模板层的四层改造
2.1 模板源与渲染器分离
第一次做适配时,我的直觉是给 easy_init_cli 加一套“鸿蒙模板目录”,里面直接放好一个标准的鸿蒙工程,然后做字符串替换。真正跑起来才发现,Flutter 模板和鸿蒙模板的渲染规则完全不同:Flutter 侧要把{{projectName}}换成 snake_case 的包名,鸿蒙侧却要生成 pascalCase 的类名和 reverse domain 的 bundleName,如果用一套替换逻辑,生成的 ArkTS 文件基本都会挂。
所以我把模板源和渲染器拆开了。模板源只负责提供模板文件树,渲染器负责解释模板中的占位符、条件块和过滤函数。目录结构大概长这样:
templates/ ├── flutter/ │ ├── pubspec.yaml.tmpl │ └── lib/main.dart.tmpl └── ohos/ ├── AppScope/app.json5.tmpl ├── entry/src/main/module.json5.tmpl ├── entry/src/main/ets/EntryAbility.ets.tmpl └── oh-package.json5.tmpl渲染器我在内部定义了一个统一的TemplateRenderer接口,只暴露一个render(source, context)方法。Flutter 和鸿蒙各自实现一套渲染规则,但对外表现一致。这样做的好处是,后续再要接第三个平台,只要新增一套模板和渲染实现,命令层、配置层、校验层完全不用动。
2.2 统一配置模型:一个命令入口管住所有参数
easy_init_cli 原来的入口参数很简单,就是create加项目名。鸿蒙化之后,参数一下子多了很多,包括平台类型、模块名、bundleName、API 版本、签名配置等。如果一股脑全塞给命令,用户根本记不住,团队里又会开始流传各种“正确命令”的截图。
我最后定的方案是三层配置来源,优先级从高到低:命令行参数优先,其次是环境变量,再其次是配置文件easy_init.yaml。实际创建一个鸿蒙工程的命令长这样:
easy_init create demo_app \ --platform ohos \ --module-name entry \ --bundle-name com.example.demo \ --api-version 12 \ --keep-flutter false如果不想敲这么多参数,可以在当前目录放一个easy_init.yaml:
project: name: demo_app platform: ohos moduleName: entry bundleName: com.example.demo apiVersion: 12统一配置模型的意义不只是少敲几个参数。所有参数进入同一个 Model 对象后,模板渲染、文件生成的逻辑只认这个 Model,不会再从命令行临时拿值。这就让“传入参数”和“生成动作”解耦了,后续做集成测试、批量生成、参数校验都变得非常容易。
2.3 资产映射表:把 Flutter 配置翻译成鸿蒙配置
鸿蒙化适配中最容易被低估的部分,是配置字段的映射。很多人以为模板渲染就是把{{projectName}}替换成实际名字,其实真正麻烦的是 pubspec 里的依赖和鸿蒙的 oh-package.json5 之间没有一一对应的关系。
比如 Flutter 侧的网络库、状态管理库,在鸿蒙侧可能是完全不同的包名和版本号。如果人工去翻译,每次生成都要核对一遍,漏掉一个版本号就是一次构建事故。我就在模板仓库里维护了一张asset_map.yaml,专门做这一层映射,模板渲染时由适配层自动逐条翻译:
flutter: dio: ohos: http defaultVersion: 5.0.0 riverpod: ohos: state_management defaultVersion: 2.5.0 ohos: bundleNamePrefix: com.example apiCompatibleVersion: "12"这张映射表本身也是资产,要纳入版本控制。每次鸿蒙 SDK 升级、依赖版本更新,都先改这时表,再重新生成工程验证。实测下来,把映射逻辑集中在一处,比散落在模板里好维护得多。
3. 实操:一步一步把 easy_init_cli 跑通鸿蒙工程
3.1 环境准备与工程目录设计
动手改造之前,建议先把环境核对一遍。跑鸿蒙工程生成和构建,至少需要以下环境:鸿蒙 SDK 的 API 版本要与生成参数里--api-version一致;官方 IDE 提供的 hvigor 命令行工具要能正常执行构建;ohpm 依赖管理命令要在 PATH 里;如果还要交叉验证 Flutter 侧,Flutter SDK 当然也得装好。
我建议用一个干净目录做实验。生成前,工具只依赖一个空目录,生成后的结构是这样:
demo_app/ ├── AppScope/ │ ├── app.json5 │ └── resources/ ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ │ ├── entryability/ │ │ │ └── pages/ │ │ ├── module.json5 │ │ └── resources/ │ ├── build-profile.json5 │ ├── hvigorfile.ts │ └── oh-package.json5 ├── build-profile.json5 ├── hvigorfile.ts ├── oh-package.json5 └── AppScope 和模块的配置文件目录设计好了,生成逻辑就聚焦在“产出这棵目录树”。工具维护一个内部的文件清单,记录每个模板文件对应输出的位置,而不是靠遍历目录复制,这样能精确控制哪些文件要渲染、哪些文件要原样拷贝,避免把模板仓库里的辅助文件带进生成结果。
3.2 命令入口和参数校验改造
命令入口这块,我在原有基础上加了平台枚举和鸿蒙专属参数。参数校验是重点,很多生成问题其实不是模板写错,而是参数一开始就非法。团队里的人如果传了一个带中文的项目名,生成的模块名在鸿蒙侧可能直接编译不过。
我加了几条硬性校验规则,实测下来能挡住 80% 的错误输入。
projectName必须是小写字母和数字组合,可以包含下划线,但不能包含连字符和中文。bundleName必须是反向域名格式,至少两段,且每一段都以字母开头。apiVersion必须在支持列表内,不能随意传一个未来版本。moduleName不能和工程名相同,避免 hvigor 解析时出现模块冲突。
代码实现上用一组正则和取值列表就够了,我在命令行解析后立即执行校验,校验不过直接提示退出,不进入模板渲染阶段。这样省去了后续一堆无效构建。
3.3 渲染器兼容层和模板编写
渲染器兼容层是这次适配改动最大的地方。原来的渲染逻辑会把所有占位符做全局替换,鸿蒙模板里很多字段要经过不同的转换函数,比如:
- 工程名
demo_app转模块文件名,要用小驼峰demoApp。 - 包名反向域名
com.example.demo,转 ArkTS 命名空间时要保留层级。
模板变量我用的是双大括号语法,条件块用 section 语法。一个典型的 module.json5 模板片段长这样:
{ "module": { "name": "{{moduleName}}", "type": "entry", "deviceTypes": ["{{#phone}}phone{{/phone}}"], "deliveryWithInstall": true, "abilities": [ { "name": "EntryAbility", "srcEntry": "ets/entryability/EntryAbility.ets", "exported": true } ] } }注意模板文件本身是 JSON5,里面不要用//注释,因为渲染后的一些检查工具不认注释,这我后面还会提到。渲染器会在模板编译前解析变量,调用对应的转换函数,再输出到目标文件。
3.4 生成后处理:命名规范、编码与文件权限修正
模板渲染完成不等于工程能用,还有一堆后处理要做。我总结下来,至少有三件事是每次生成后必须执行的。
第一件是文件重命名。模板里放的是通用文件名,比如entryability,生成后要按鸿蒙规范改成EntryAbility.ets,同时引用它的位置也要跟着改。第二件是编码修正。Windows 环境下写文件默认可能带 BOM,module.json5 这类配置被 BOM 干扰后 IDE 可能直接解析失败,所以我在生成器里统一强制输出无 BOM 的 UTF-8。第三件是行结束符统一,全部转成 LF,避免团队里换行符混乱。
做完这三步,我再执行一次ohpm install做冒烟验证。能装依赖、能解析配置,就算一次基本成功的工程生成。这一步很关键,它把问题挡在生成阶段,而不是等开发者打开 IDE 才发现工程是坏的。
4. 架构治理实战:不让脚手架变成技术债入口
4.1 依赖版本治理与锁文件策略
脚手架工具如果只负责生成,不负责治理,它很快就会变成新的技术债入口。最容易失控的是依赖版本:模板里写死的版本号一旦更新,所有老工程不会跟着变,时间一长,线上工程用的依赖版本五花八门。
我在 easy_init_cli 里加了一个版本中心versions.yaml,所有模板引用的依赖版本都写在这里,模板渲染时只认这个版本中心里的值。哪个依赖升了级,只改版本中心,重新生成或跑一次升级命令,就能把最新版本同步到工程里。
鸿蒙工程侧对应的是oh-package-lock.json5锁文件。我要求生成后必须保留这个锁文件,并且纳入版本控制。这样能保证同一模板生成的工程,依赖树完全一致,排查问题时不用先怀疑“依赖不一致”。
4.2 目录收敛与文件资产清单
脚手架生成的工程,哪些文件是模板来的,哪些文件是开发者后来改的,这个问题如果没有答案,后面升级模板就会很痛苦。我做的方案叫“文件资产清单”,生成完工程后,工具会遍历所有输出文件,计算每个文件的哈希,写进project_asset.yaml。
generated_at: 2024-11-30T10:00:00Z template_version: 2.3.0 files: - path: entry/src/main/module.json5 hash: a3f2b81c9d... source: templates/ohos/entry/module.json5.tmpl - path: entry/src/main/ets/EntryAbility.ets hash: 4f8e3a2d77... source: templates/ohos/entry/EntryAbility.ets.tmpl有了这份清单,工具就能区分“模板管理的文件”和“开发者自建的文件”。升级模板时,只覆盖清单里的文件,开发者自己的业务代码不受影响。这让架构治理从口号变成了可操作的手段。
4.3 增量生成与幂等性校验
幂等性是我对脚手架工具的执念。同一个工程,同一份参数,生成两次,结果必须完全一致。如果生成两次差异巨大,说明模板里混入了随机值或者依赖了可变状态,这种工具没法用。
为了验证幂等性,我写了easy_init validate命令。它会用当前模板重新生成一份完整的工程到临时目录,然后和现有工程做逐文件对比,只输出差异,不实际覆盖。团队在 CI 里可以定期跑这个命令,一旦模板改动导致生成结果变化,diff 就能自动暴露。
增量生成也很重要,不是每次都要覆盖全部文件。按资产清单的哈希比对,只有模板源文件变化了,才重新生成对应文件。这套机制跑下来,脚手架才真正变成“可控的代码生成系统”,而不是一次性初始化工具。
5. 常见问题排查与避坑速查
5.1 高频问题实录
这一个多月里,我遇到过的问题不少,挑几个典型的高频问题列出来,基本覆盖了“能生成、但构建失败”的绝大多数场景。
第一个是 module.json5 解析失败。症状是 IDE 打开工程提示格式错误,但看起来内容没什么问题。排查下来基本是两个原因:一是文件带了 BOM,二是模板里写了注释,JSON5 解析器在某些模式下不认注释。对策是我在后处理时强制无 BOM UTF-8,同时规定模板文件禁止写注释,要写说明就放在模板仓库的独立文档里。
第二个是 API 版本不匹配。生成参数传的 API 版本和本机安装的鸿蒙 SDK 不一致,构建到一半报一堆晦涩错误。对策是生成器在 install 冒烟前先检查 SDK 版本列表,不在支持列表里直接报出来,比让开发者去猜快得多。
第三个是 Windows 路径过长。鸿蒙工程模板里有几层很深的目录,Windows 默认路径上限会截断文件。这个问题不是改代码能根治的,我的处理方式是在模板设计中控制目录深度,把 ets 下的子目录减少一层,同时提醒 Windows 用户开启长路径支持。
第四个是 ohpm install 拉不到依赖。大多数情况是依赖源没有配置或者配置错了,生成器里我会在生成后主动输出当前依赖源,让开发者自己核对。
第五个是命名转换错位。工程名里带了下划线,生成 ArkTS 文件时如果只做了简单替换,可能生成entry_ability.ets这种不符合小驼峰规范的文件名。对策是 ArkTS 侧的文件名统一走camelCase转换函数,不走默认替换。
5.2 几处值得注意的设计细节
除了上面的高频问题,还有三个设计层面的细节,是我这次适配中体会最深的。
模板变量要区分“全局变量”和“平台变量”。全局变量是工程名、bundleName 这类所有平台通用的,平台变量是 apiVersion、moduleName 这类只影响鸿蒙的。两者混在一起,渲染时会非常难维护。
dry-run模式一定要做。生成器支持--dry-run时只打印将要生成的文件列表和关键配置,不实际写盘。我每次改完模板,都会先跑 dry-run 看输出,确认无误后再真实生成,省了无数次删除目录重来的操作。
后处理中要加入权限修正。生成的文件有时候会带上模板仓库里的只读属性,导致后续在 IDE 里无法修改。我在写文件后统一设置可写权限,这个细节不处理,开发者第一次打开工程就会遇到很奇怪的“文件只读”问题。
adaptation 这项工作做到这里,easy_init_cli 已经从单纯的 Flutter 脚手架,变成了跨 Flutter/鸿蒙的工程初始化底座。我个人在实际操作中最大的体会是,适配一个平台并不是“加一套模板”,而是要重新梳理一次你对工程资产的理解。鸿蒙的命名规范、配置结构、依赖模型,每一项都逼着你把之前“复制项目”时期模糊处理的问题,变成显式的规则和代码。这套适配方案后续还可以继续扩展,比如把模板源放到私有仓库做多版本管理,加入更多强制性的架构检查规则,甚至和 CI 流水线做深度整合。工具本身不复杂,复杂的是把团队里那些“约定俗成但没有落地”的规范,真正变成可以自动执行的东西。