接手这个任务前,我团队里新开一个 Flutter 项目的标准流程,还是从 Git 仓库拉一个老工程,全局替换包名,然后祈祷 replace 别把注释里的示例代码也改了。这种"复制粘贴式初始化"撑了三年,终于把问题摆到了台面上:有的项目用的是旧版路由,有的项目还在用已被废弃的依赖,CI 脚本各写各的,架构规范全靠老员工口口相传。后来我们把工程初始化这件事做成一个专用工具,也就是标题里的这个 Flutter 三方库 easy_init_cli,目的就是管住"工程初始化资产"。再后来,团队开始接鸿蒙侧的适配需求,我又花了三周把 easy_init_cli 做了鸿蒙化适配。这篇文章就围绕这次适配,把我做过的事、踩过的坑、以及最后沉淀下来的那套"精密架构治理"思路完整写出来,给同样要做跨端脚手架或者工具链改造的团队一个参考。
先说结论:easy_init_cli 并不是一个"模板拷贝器",它管的是工程初始化资产——模板、目录骨架、依赖清单、CI 脚本、代码规范配置、多语言资源,这些都属于初始化资产。鸿蒙化适配也绝不是在模板目录里多塞一个ohos/文件夹那么简单。真正难的是,你要让同一套脚手架逻辑,在两个构建体系、两套配置文件、两套生命周期模型、两套资源规范之间都能输出合格产物,还要保证长期演进不腐化。
1. 从复制粘贴一个新工程说起:easy_init_cli 到底管住了什么资产
1.1 初始化漂移是怎么发生的
先描述一个几乎所有中大型 Flutter 团队都经历过的场景。某个业务线要开新项目,负责人打开老项目的 Git 地址,clone 下来,全局搜索替换包名,改一下应用名,push 到新仓库,完事。这个流程看起来快,实际上给后续埋了大雷:
- 老项目里某个已经被废弃的依赖,因为"能跑就行"被原封不动继承了下来;
- 目录结构完全取决于当初第一个项目创建者的个人习惯;
- 代码规范配置、lint 规则、CI 脚本在不同项目之间出现任意漂移;
- 更隐蔽的是,有些团队在某个项目里沉淀了初始化基建,但没有任何机制把它回流到其它新项目。
这就是"初始化漂移"。新项目不是一个拼装过程,而是一个抽样复制过程——每个新项目都带着前一个项目的随机历史包袱。
我们开发 easy_init_cli 的初衷,就是把"初始化新工程"从人工操作变成一条确定性流水线。它不负责业务代码,只负责工程骨架和初始资产的生成。所谓"工程初始化资产",具体包含这几类:
- 工程目录骨架与平台壳工程(Android / iOS 原生目录、入口文件、资源占位);
- 核心配置文件(
pubspec.yaml、分析选项、格式化配置、CI 工作流); - 基础代码模板(应用入口、路由表、网络层封装骨架、本地化资源);
- 依赖清单与版本基线(哪些库默认引入、版本号锁在哪个区间);
- 工程元信息(项目名、组织名、包名、多语言默认值)。
1.2 easy_init_cli 的资产清单
easy_init_cli 的核心并不是"生成文件",而是"维护一套资产的单一事实来源"。所有模板文件统一收编在一个模板仓库里,模板的每一次变更都有提交记录。生成新工程时,CLI 做四件事:
- 读取用户输入的参数(项目名、组织名、平台范围等);
- 把参数注入到模板引擎中,渲染目录和文件;
- 执行文件级校验(包名合法性、目录冲突检查);
- 进入后置任务阶段,比如自动拉取依赖、生成锁文件、初始化 Git。
这一步把"复制粘贴"升级成了"受控渲染"。模板仓库里的任何一个文件都像一条资产记录,既可以被审计,也可以被版本回溯。
1.3 四段式生成管线
这里说细一点,因为后面鸿蒙化适配的所有工作,都是围着这条管线展开的。easy_init_cli 的生成过程可以拆成四个阶段:
- 参数收集:命令行参数 + 交互式提问,统一收敛成一份
ProjectSpec; - 模板渲染:按照
ProjectSpec遍历模板目录,用渲染引擎填充变量; - 差异处理:检查目标目录是否已存在同名文件,决定覆盖、跳过还是合并;
- 后置任务:执行依赖安装、格式化、静态检查、Git 初始化等。
鸿蒙化之后,管线没有变,变的是模板源、参数语义、校验规则和后置任务的实现。这个设计帮了大忙。后面所有"适配工作"都能在四个阶段里找到对应位置,不会出现"为了兼容鸿蒙把整体逻辑揉成一团"的情况。
2. 鸿蒙化适配的第一要务:盘点两边工程的六个关键差异
真正动手之前,我先花了两天时间把鸿蒙工程和 Flutter 工程的差异点列了个清单。这一步非常值得,因为差一点在哪、差异的粒度是什么,直接决定了模板怎么组织、适配层长什么样。我按影响面从大到小排了六项。
2.1 目录结构不是一个层级的事
一个标准 Flutter 工程,顶层是lib/、android/、ios/、pubspec.yaml。一个鸿蒙工程,顶层则是AppScope/、ohos/或entry/、build-profile.json5、hvigorfile.ts、oh-package.json5。
关键差异在于:鸿蒙的模块(Module)和 Flutter 的 package(包)不是同一个概念。鸿蒙工程里可以有多个 module,entry只是最常见的一个,每个 module 都有自己的module.json5、自己的hvigorfile.ts,编译时按模块维度组织任务。这意味着模板库里不能只用一份扁平目录结构覆盖两端,必须按平台分叉。鸿蒙侧还需要维护AppScope里的应用级配置,这一层在 Flutter 工程里没有对应物。
2.2 配置体系与依赖归因完全不同
Flutter 的依赖写在pubspec.yaml,解析交给 Pub;鸿蒙的依赖写在oh-package.json5,解析交给 ohpm;构建脚本上,Flutter 走flutter build,鸿蒙走hvigor。
我见过有人图省事,把鸿蒙相关文件直接塞进 Flutter 工程目录里,"反正都是配置文件"。这个想法很危险。因为两端构建工具都会扫描工程根目录,oh-package.json5的存在会让 Flutter 工具链误判工程结构,反过来pubspec.yaml也会干扰鸿蒙构建扫描。所以模板设计里,两端要么完全独立生成,要么在单一工程中通过规范约定切分干净,绝不能让两套配置互相暴露在对方的扫描路径里。如果一个工程要同时保留 Flutter 端和鸿蒙端,我建议鸿蒙目录统一收敛在某个专用的子目录内,并且把该目录写进 Flutter 的分析排除列表,避免双方构建时互相干扰。
2.3 生命周期事件要从 Ability 转发到 Flutter 引擎
这一点是最容易翻车、但模板阶段又最容易忽略的。Flutter 应用在 Android/iOS 上有着比较成熟的生命周期对接,鸿蒙侧则完全不同。鸿蒙应用的生命周期主体是 UIAbility,onCreate、onForeground、onBackground、onDestroy这些事件,必须通过 Flutter 引擎适配层再转交给 Flutter 的帧渲染与状态管理机制。
在模板代码里,我没有把这些事件写死在某个页面里,而是抽了一个LifecycleBridge辅助类,集中负责鸿蒙 Ability 生命周期事件到 Flutter 引擎的转发。同时把 Flutter 侧的AppLifecycleListener和鸿蒙侧的 Ability 状态建立明确映射:onForeground对应 resumed,onBackground对应 inactive/paused,onDestroy对应 detached。这一步没做好的话,应用切后台再恢复时会出现画面冻结、定时器失效等诡异问题,而且非常难排查。
2.4 权限、签名与 bundleName 的规则差异
Flutter 工程里权限写在原生各自的配置里,Android 用 AndroidManifest,iOS 用 Info.plist。鸿蒙工程里,权限声明集中在module.json5的requestPermissions节点。签名信息则是独立的证书配置,在build-profile.json5里按构建产物分别声明。
另外还有一个不可忽视的细节:鸿蒙的bundleName是应用级唯一标识,命名规则比 Android 包名更严格,必须是反向域名格式,且不能以下划线开头,每一段还有字符限制。模板校验规则必须针对这套规则重写,生成前发现非法命名直接报错,而不是等编译期才暴露。这个后面我会专门展开。
2.5 资源体系与多语言文件不通用
Flutter 通常把资源收敛在assets/下,多语言用.arb文件配合 intl 工具链;鸿蒙的资源在resources/目录下,分base/element、base/media、base/profile等子目录,多语言靠en_US、zh_CN这类限定目录。图标、启动页、字符串占位文件完全不是一套体系。
模板设计里,资源目录也必须分叉渲染。同一个"应用名"变量,在 Flutter 端要进pubspec.yaml和 Android 资源,在鸿蒙端则要渲染到string.json和app.json5两处。这是双平台一致性问题,不能靠复制模板文件夹解决。
2.6 一个综合对比表
| 差异项 | Flutter 工程 | 鸿蒙工程 |
|---|---|---|
| 顶层目录 | lib / android / ios / assets | AppScope / entry / resources |
| 依赖配置文件 | pubspec.yaml | oh-package.json5 |
| 模块描述 | 无统一模块清单 | module.json5 |
| 构建命令 | flutter build / test | hvigorw assembleHap |
| 应用唯一标识 | applicationId / bundle id | bundleName(规则更严) |
| 权限声明 | AndroidManifest / Info.plist | module.json5 的 requestPermissions |
| 资源目录 | assets / 各平台资源目录 | resources/base 系列目录 |
| 生命周期对接 | Flutter engine 自动处理 | Ability 事件需手动转发给引擎 |
这张表是我后面设计模板目录和适配层接口的主要依据。每一条差异都对应一处模板分支或一段适配代码,盘点得越早,后面写代码的时候越心里有底。
3. 模板资产重构:从"拷贝目录"升级为"双平台参数化渲染"
3.1 模板目录如何分层
原来的 easy_init_cli,模板目录本质上是一份扁平的快照,生成时整体拷贝。鸿蒙化适配过程中,我做的第一个结构性调整,就是把模板目录改成三分:
templates/shared/:两端共用的文件,比如项目说明、通用 gitignore、基础目录占位;templates/flutter/:Flutter 专属模板,保留原来的内容并重新参数化;templates/harmony/:鸿蒙专属模板,包括AppScope、entry、oh-package.json5等。
渲染时,CLI 根据用户指定的平台组合,依次叠加 shared 和对应平台模板。shared 目录中的文件凡是涉及名称、包名、标识符的,一律参数化,不允许出现硬编码。这样做的直接好处是:新增平台时,不需要修改 shared 里的任何模板,只新增平台目录和该平台的适配器。
3.2 关键变量 schema 与 bundleName 校验
参数化听起来简单,真正落地时要设计好变量 schema。我定义了一份ProjectSpec,字段大概是这样:
class ProjectSpec { final String projectName; // 工程目录名 final String appTitle; // 显示名,会进多语言资源 final String orgName; // 组织名,如 com.example final String bundleName; // 鸿蒙专用,orgName + '.' + projectName 的拼接 final String moduleName; // 鸿蒙模块名,默认 entry final List<String> platforms; // flutter / harmony final bool withCI; // 是否生成 CI 配置 }这里有一点必须提醒:bundleName不要直接拿 Android 的applicationId复用。鸿蒙对 bundleName 的校验更严格,尤其是下划线问题。Android 包名允许分段之间用下划线,鸿蒙的 bundleName 每段不能以数字开头、不能包含下划线。我在生成前加了一段校验逻辑:
bool isValidBundleName(String name) { if (name.isEmpty || name.length > 200) return false; final segments = name.split('.'); for (final seg in segments) { if (seg.isEmpty) return false; if (!RegExp(r'^[a-zA-Z][a-zA-Z0-9_]*$').hasMatch(seg)) return false; if (seg.startsWith('_')) return false; } return true; }别小看这段校验。没有它,模板能正常渲染,但编译期会收获一条让人摸不着头脑的错误。提前在 CLI 阶段拦截,把错误提示写成"bundleName 分段不能以下划线开头,请修改 orgName 或 projectName",使用体验完全不同。
3.3 条件渲染和文件过滤
参数化之后,第二个问题是条件渲染。鸿蒙模板里有些文件只在对外开放时才有意义,有些只在调试包时需要。我不打算引入复杂的条件语法,只做文件级过滤:
- 模板文件名中的
.skip.标记:渲染时如果命中未选中的平台,直接跳过; - 目录级
_partials/约定:里面放代码片段而非整文件,由主模板按变量引用嵌入; - 根据
withCI参数决定是否渲染 CI 工作流文件; - 根据
platforms参数过滤平台专属目录,比如只选 harmony 时不生成 Flutter 目录。
这样设计之后,同一个模板仓库能应对多种组合输入,而不是"harmony 一套模板、flutter 一套模板、flutter+harmony 再一套模板"。
3.4 交互默认值的原则:少问问题,给对的默认
很多 CLI 工具死在了交互步骤太多上。我的原则是:默认值能覆盖绝大多数场景的问题绝不弹出,弹出来的问题一定是有歧义且影响不可逆的。easy_init_cli 的交互流程,我把平台选择、项目名、组织名、模块名四个问题放在必问列表里,其余全部给默认值。平台选择我额外提供了一个快捷参数--platform all,一次性生成 flutter + harmony 两端骨架。
另外,自动生成的bundleName默认为orgName.projectName,但如果用户输入的 orgName 本身包含非法字符,校验失败时我会打印一条提示,并让用户重新输入,而不是把错误吞掉继续生成。
4. 让鸿蒙构建链在脚手架里跑通:命令改造、环境检测与后置任务
4.1 命令接口设计:平台参数化而不是复制命令
最容易踩的设计坑,就是图省事为鸿蒙单独加一套命令,比如easy_init_cli create-harmony。短期看是快,长期看必然导致两套命令的参数体系、默认值、输出结构慢慢分叉。
我选择的是平台参数化。原命令保留,增加一个平台枚举参数:
easy_init_cli init --platform flutter easy_init_cli init --platform harmony easy_init_cli init --platform all参数层面,原来以 Flutter 为中心的字段被重新归类。--org变成通用字段,新增--bundle-name和--module-name给鸿蒙用,未传入时从org和projectName推导。所有平台相关参数都要求显式声明,不搞"悄悄猜测"。
4.2 生成前的工具链检测
这一步是我在适配过程中加上去的。原版 CLI 不关心用户机器上装了什么,反正复制模板不需要。但鸿蒙工程生成后要能直接构建,环境不对会浪费大量时间。
生成前我加了环境检测,逻辑很简单但很实用:
# 检查项目根目录是否存在 hvigorw / hvigorw.bat # 启动时执行 hvigorw --version 校验可用性 # 检查 ohpm 是否在 PATH 中 # 检查 DevEco 工具链的环境变量是否已配置检测结果分为 fatal 和 warning 两档。缺少鸿蒙构建工具链属于 fatal,直接中止生成并给出安装引导;检测到多个工具链版本时属于 warning,打印提示但不阻塞。
环境检测还有一层意义:它把脚手架从"文件生成器"推向了"工程装配器"。生成出来的工程如果不能跑通后续构建命令,那这个脚手架的价值就大打折扣。
4.3 生成后的自动装配 Pipeline
模板渲染完毕后,原来 easy_init_cli 只做一次flutter pub get。鸿蒙化之后,后置任务扩展为一条有序 Pipeline:
- 在鸿蒙侧执行
ohpm install拉取依赖; - 执行
hvigorw assembleHap做一次完整构建; - 构建成功后,再回 Flutter 侧执行
flutter analyze做静态检查; - 全部通过后初始化 Git 并创建首个提交。
Pipeline 的设计重点是失败可重入。因为鸿蒙构建耗时比 Flutter 依赖拉取长得多,网络错误、签名配置缺失都可能让任务挂掉。我在实现上把每个后置任务都设计成可单独重跑的子命令:
easy_init_cli run --task ohpm-install easy_init_cli run --task build-hap这样 CI 或者脚手架本身遇到失败,不需要重新生成整个工程,只要定位到具体任务重跑即可。
4.4 给存量工程补鸿蒙壳:add-ohos 的幂等设计
新工程用init命令生成,那已经存在的 Flutter 工程呢?团队里有大量存量 Flutter 项目要加鸿蒙支持,不可能重新初始化。所以我给 easy_init_cli 加了一个add-ohos子命令,专门负责给存量工程补鸿蒙壳。
这个命令的实现有一个敏感性很强的点:幂等性。反复执行应该得到相同结果,已有文件不能被无脑覆盖。我的策略是:
- 先扫描目标工程是否已存在
ohos/或AppScope/目录; - 存在则进入增量合并模式,逐文件对比模板与现状,输出 diff;
- 用户自定义过的文件(比如改过
module.json5里的权限声明)会被判断为"本地修改",默认保留; - 只有模板源文件才允许覆盖,并且覆盖前先备份。
这个逻辑最难的部分在于"判断一个文件是否被用户改过"。我没有采用复杂的智能比较,而是约定:生成时在每个模板文件头部写入一段声明注释,包含模板版本号。增量合并时,如果文件中的版本号和当前模板版本不一致,就认为是本地修改,保留不动。这个方案虽然简陋,但非常可靠,而且用户也容易理解。
4.5 适配层代码的大致结构
适配层的核心是平台适配器。我抽象了一个接口,Flutter 和鸿蒙各自实现:
abstract class PlatformAdapter { String get platformName; Future<void> renderTemplates(ProjectSpec spec, RenderEngine engine); Future<void> runPostTasks(ProjectSpec spec); List<String> get requiredToolchains; } class HarmonyAdapter implements PlatformAdapter { @override String get platformName => 'harmony'; @override Future<void> renderTemplates(ProjectSpec spec, RenderEngine engine) async { await engine.renderDirectory('templates/harmony', spec); } @override Future<void> runPostTasks(ProjectSpec spec) async { await ToolchainRunner.run('ohpm', ['install']); await ToolchainRunner.run('hvigorw', ['assembleHap']); } @override List<String> get requiredToolchains => ['hvigorw', 'ohpm']; }命令层根据用户传入的 platforms 组装适配器列表,逐个执行渲染和后置任务。这份拆分的收益在后续维护期非常显著:每次鸿蒙构建方式调整,修改范围都收拢在HarmonyAdapter内部,不会波及 Flutter 侧逻辑。
5. 精密架构治理实战:适配层隔离、可插拔与双端一致性
5.1 鸿蒙逻辑不能塞进 Flutter 路径里
刚接到适配需求时,有人给我提过一个"省事方案":在原来的模板渲染流程里加一个 if,判断到是鸿蒙平台就多拷贝几个目录。我没采纳。原因很直接:短期省事,长期必然腐化。
if 分支一旦增多,函数会越来越长,平台专属逻辑和公共逻辑纠缠在一起。等维护者换了一批人,没人敢动这段代码,脚手架就成了新项目的起点,但也是下一个技术债的起点。"精密架构治理"说的就是这个:工具本身要成为架构约束的示范。
我采用的架构是清晰的分层,从上到下依次是:命令层 -> 平台适配器 -> 渲染引擎 -> 模板仓库。每一层只依赖下一层的抽象接口,不跨层调用。
5.2 平台能力表取代 if else
新增平台时,与其到处写 if else,不如在适配器里声明"平台能力"。我在适配层加了一个能力描述结构:
class PlatformCapabilities { final bool supportsHapBuild; // 是否支持构建 HAP final bool supportsOhpm; // 依赖管理是否走 ohpm final bool requiresBundleName; // 是否强制校验 bundleName final List<String> lifecycleEvents; // 需要桥接的生命周期事件列表 }命令层拿到一份能力描述,据此决定流程的走向。比如"是否执行 bundleName 校验"不再写在生成主流程里,而是读到适配器的requiresBundleName字段后统一执行。这样平台间的差异被显式建模,闹钟代码肉眼可见地变得规律了。
5.3 同一份语义,两套模板渲染
双端一致性是我在适配过程中一直绷着的一根弦。同一个appTitle,在 Flutter 端渲染到pubspec.yaml的description和 Android 的strings.xml,在鸿蒙端渲染到string.json。同一个版本号,Flutter 端进pubspec.yaml,鸿蒙端进app.json5和module.json5两处。
我用的做法是"同一份变量定义,按平台渲染不同模板"。变量 schema 是唯一的,渲染目标是分叉的,这样可以从源头保证两端语义一致。版本号升级时,只改一处输入,脚手架负责同步到两端的所有目标文件。这个机制对长期维护极有价值。新人的典型错误是"两端各自维护一套值",一段时间后必然出现 Flutter 端 1.2.0、鸿蒙端 1.1.0 的尴尬局面。
5.4 快照测试守住脚手架本身
脚手架是生成工具,工具本身也要有回归测试。我给 easy_init_cli 加了快照测试机制:用一个固定的ProjectSpec生成整套工程文件,然后和 git 里保存的"黄金快照"做逐文件比对。任何模板改动导致输出变化,测试都会失败,必须人工确认是预期变更后更新快照。
具体做法是:
- 测试环境准备一个干净目录,执行
init --platform all; - 生成结果与
test/golden/下已有快照逐文件 diff; - 唯一的例外是时间戳、随机 ID 这类动态内容,用占位符处理;
- 新增模板文件时,必须同步更新黄金快照。
这个机制让我后面改模板时底气足了很多。有一次我把鸿蒙模板里module.json5的一个字段名拼错了,快照测试在 CI 上第一时间暴露了差异,避免了一个"生成时完全正常、编译时莫名报错"的坑。
6. 实测埋坑记录与验收清单:从"能生成"进步到"生成即可用"
6.1 我踩过的七个坑
适配过程和真实落地过程中,我踩了一批不算深但足够耽误时间的坑,挑有代表性的列出来。
| 坑 | 现象 | 处理方式 |
|---|---|---|
| Windows 路径分隔符 | 模板渲染后路径变成反斜杠,鸿蒙构建脚本不认 | 渲染引擎统一输出正斜杠路径,只在文件系统写入时做转换 |
| bundleName 下划线 | build 阶段报签名错误 | 生成前校验并拦截,提示用户修改 orgName |
| 引擎产物未就位 | 运行时找不到 Flutter 引擎动态库 | 模板里把引擎产物路径声明成变量,适配器填充实际路径 |
| 资源文件重名覆盖 | 应用图标被 Flutter 模板里的占位资源覆盖 | 两端资源目录彻底分离,禁止同名文件跨越模板目录互相覆盖 |
| 生命周期事件未转发 | 后台恢复后画面冻结 | 集中用 LifecycleBridge 转发,模板内置实现 |
| hvigor 版本漂移 | 新工程构建失败 | 工具链检测时记录版本,生成时在 README 标注已知兼容版本 |
| 增量补壳覆盖用户改动 | 用户配置被模板覆盖 | 模板文件内写版本注释,版本不一致视为本地修改并保留 |
6.2 健康度基线怎么定
脚手架做完,只靠"看起来能生成"是不够的。我定义了一条健康度基线,作为每次模板变动的验收标准,共五步,全部通过才算合格:
- 快照测试全部通过,输出文件和黄金快照一致;
- 新生成的 Flutter 工程
flutter analyze零告警; - 新生成的鸿蒙工程
hvigorw assembleHap构建通过; - 鸿蒙工程能正常安装到模拟器,启动后前后台切换生命周期事件无异常;
- 存量 Flutter 工程执行
add-ohos后,原有 Flutter 功能不受影响。
前四步可以在 CI 里自动化,第五步需要人工复核。我建议团队把这条基线写进脚手架仓库的 PR 检查清单,任何模板改动都必须附上健康度验证结果。这也是"从能生成到生成即可用"的关键动作。
6.3 团队落地的节奏建议
我个人的体验是:鸿蒙化适配这种改造,不适合一口气把模板铺满全部存量项目。比较稳妥的节奏是分三波走。
第一波,先拿一个内部工具型的 Flutter 项目做试点,用add-ohos补鸿蒙壳,跑通构建、安装、基础生命周期验证,把暴露出来的模板缺漏补齐。第二波,把模板里已经验证稳定的部分推广到两三个业务项目,同时开始收集不同项目的差异化诉求。第三波,等模板稳定了,再把脚手架作为新项目的默认初始化工具,并在团队文档里废除"复制老工程"的旧流程。
这样推进的优点是每一波都能形成反馈闭环,不会出现大规模迁移后才发现模板设计有根本性问题的情况。工具链改造天然带有很强的惯性,节奏宁慢勿快。
最后聊一点我自己的体会。这次鸿蒙化适配,技术难度不算高,真正难的其实是"克制"——克制住抄近路的冲动,克制住只改模板不改结构的想法。easy_init_cli 之所以能顺利承接双平台生成,靠的不是适配阶段灵光一现,而是它从一开始就把"模板即资产""渲染而非拷贝""管线四段式"这些原则立住了。鸿蒙化适配本质上是给这套治理框架增加一种新的平台实现,而不是推翻重来。如果你也在做类似的脚手架或工具链迁移,我的建议是:动手改模板之前,先花时间把平台差异盘点清楚,把适配层接口设计好。这些前置工作会节省你后面至少三倍的返工时间。另外,给存量工程做增量补壳时,幂等性是底线,模板文件里的版本注释这个土办法,真的比任何 fancy 的 diff 算法都可靠。工具会越用越顺手,但只要一开始设计歪了,后面每加一个平台都会痛一次。