上个月,我把团队一直在用的 Flutter 代码生成脚手架 scaffoldio 做了一次鸿蒙化适配。这个工具平时负责从模板批量生成页面、路由、网络模块,是团队从零搭 Flutter 项目时最依赖的加速器。适配之前我在网上搜了一圈,发现讨论 Flutter 框架本身搬到鸿蒙的教程不少,但把“工具链”同步迁到鸿蒙生态的实战内容几乎没有,所以这次把完整过程沉淀成文,给后面做同样事情的人留一份可复现的参考。
scaffoldio 的核心定位不是 UI 组件库,而是一套“代码生成极速引擎”:开发者写好模板和配置,它负责把工程骨架、业务页面、路由注册、网络层封装一次性生成出来。我在文章开头先把结论抛出来:整个适配工作大头不在“让工具在鸿蒙上能运行”,而在于“让工具生成的产物被鸿蒙的构建体系识别”,这决定了后面所有改造的顺序和方法。
1. 整体设计与思路拆解
1.1 scaffoldio 的核心工作原理
scaffoldio 这类代码生成工具,本质上做的是三件事:读取配置、渲染模板、落盘文件。配置一般是一份 yaml 或 json,里面声明了要生成的模块名、目录结构、页面列表、依赖项;模板是一套带占位符的文件;最后通过一个命令分发器把两者组合起来,输出完整的工程代码。
它的“极速”主要来自三个设计:模板缓存、增量生成、并发写入。模板缓存避免每次生成都重新解析模板文件;增量生成只处理有变动的模块;并发写入把几十个文件的生成任务拆到多线程里。第一次跑的时候可能有点慢,第二次开始几乎是秒级完成。
我拿它和“预制菜中央厨房”做过类比:模板就是预先洗好切好的食材,配置是订单,工具只负责按订单拼盘出餐。代码生成器真正值钱的不是“生成”这个动作,而是把团队的代码规范、目录约定、初始化流程全部固化成模板,任何人用同一条命令都能产出一致的结果。
1.2 鸿蒙端 Flutter 生态的现状与适配切入点
目前 Flutter 官方并不直接支持鸿蒙平台,社区通过基于 OpenHarmony 的适配分支为鸿蒙提供 Flutter 运行环境。这套分支保留了 Dart 层和 Flutter 框架层大部分 API,但平台侧构建体系完全换了:Android/iOS 用 Gradle/Xcode,鸿蒙用 hvigor 加 DevEco Studio;依赖管理从 Gradle/Pod 换成了 ohpm。
这就引出了适配的切入点:scaffoldio 原来只感知 android/ios 两个平台,在生成插件工程时会默认创建 android/ios 目录,在 pubspec 里声明平台插件实现时也不会带上 ohos。如果不改造,即便 Flutter 鸿蒙分支能跑起来,你生成的工程也缺了鸿蒙这一侧的入口文件、构建配置和原生桥接代码。
| 层级 | Android/iOS 原有产物 | 鸿蒙需要的产物 |
|---|---|---|
| 工程结构 | android/ ios/ 目录 | ohos/ 目录,含 entry、module 配置 |
| 依赖声明 | pubspec 里 platforms: android/ios | platforms: ohos 与 oh-package.json5 |
| 原生桥接 | Kotlin/Swift 实现 | ArkTS 实现 MethodChannel/EventChannel |
| 构建脚本 | gradle/pod | hvigorfile.ts |
所以我把适配工作拆成三条线:CLI 运行环境适配、模板产物适配、生成结果的编译验证。CLI 本身跑在开发机上,鸿蒙化主要是让它能识别 ohos 平台;模板产物适配是工作量最大的部分,要新增一套 ArkTS 模板;编译验证则是把生成结果丢进 DevEco 或 hvigor 命令行去跑,确认生成的不是一堆“看上去對了”但编译不过的文件。
1.3 为什么“极速”在鸿蒙端比以往更重要
在 Android/iOS 上,手写工程骨架最多是费时间,不太容易出错。但在鸿蒙端,工程结构更复杂,entry 模块、feature 模块、共享包之间的依赖关系比较绕,手写很容易出现“忘了建 ohos 目录”“漏了某条路由注册”这类问题,编译报错后排查链路又长。
我自己实测的对比数据是:手动创建一个带 3 个页面的 Flutter 鸿蒙模块,从建目录到写路由到调通通道,大概 10 到 15 分钟;用适配后的 scaffoldio 跑一条命令,首次生成 4 秒左右,后续因为有缓存能压到 1 秒内。更重要的是生成结果不会漏文件,因为模板把该有的东西都固化了。
这也是我坚持要做这个适配的原因。鸿蒙适配不是简单“让 Flutter 应用跑起来”,而是让整个研发链路都跟上。框架能跑了,工具链还在用老一套,团队的效率会被拖在后面。
2. 鸿蒙化适配前的环境准备与工程摸底
2.1 工具链清单与版本选择
在动手改代码之前,我先把环境完整搭了一遍,避免在改造过程中反复猜测“是工具问题还是环境问题”。
| 工具 | 我用到的版本 | 用途 |
|---|---|---|
| Flutter 鸿蒙分支 | 基于 3.x 的社区适配版 | 提供 Flutter 运行环境与 dart 命令 |
| DevEco Studio | 5.x | 鸿蒙侧工程构建、签名、真机调试 |
| hvigor | 随 DevEco 配套 | 鸿蒙构建引擎,支持命令行构建 |
| ohpm | 随 SDK 配套 | 鸿蒙依赖管理 |
| scaffoldio | 团队内部维护版 | 待改造的代码生成器 |
| Dart SDK | 随 Flutter 分支携带 | 运行 scaffoldio 的 CLI 入口 |
这里有一个经验:Flutter 鸿蒙分支的版本要和你团队的目标鸿蒙 SDK 版本匹配。我一开始用了较新的分支,结果生成的工程在稍旧的 DevEco 上编译不过,后来锁了版本就好了。如果团队项目多,建议把 Flutter 鸿蒙分支版本也写进 scaffoldio 的模板变量里,生成出来的工程直接固定版本号,避免“生成一时爽,编译火葬场”。
2.2 原库中“无法直接跨平台”的点
我做了个不兼容点清单,这是改造前最重要的一步,建议所有人都先做一遍:
平台判断硬编码。模板和代码里有大量
Platform.isAndroid、Platform.isIOS的逻辑,鸿蒙既不在这两个分支里,也没有默认分支兜底。需要新增Platform.isOhos之类判断,同时把默认行为调整为“未识别平台时报清晰错误”,而不是静默生成错误内容。路径分隔符与大小写问题。原模板生成时大量使用
'/'硬编码路径,在 Windows 开发机上会出问题。鸿蒙工程的目录规范更严格,加上很多开发者用 Mac 或 Linux,但 CI 机器可能是 Windows,路径处理必须统一。另外鸿蒙模板目录我用了Ohos而不是ohos,实际工程里模块名大小写也敏感,这个不提前统一,后面找 bug 会非常痛苦。pubspec 平台声明缺失。原来的 pubspec 生成模板里,
flutter.plugin.platforms只有 android 和 ios。鸿蒙插件需要额外声明 ohos,并且要对应ohos/目录下的插件实现。命令式工具链假设。scaffoldio 内部有一些地方默认调用了 Gradle 或 Xcode 命令来做生成后校验。鸿蒙环境没有这些命令,需要改成调用 hvigor。
模板引擎缓存目录。原来的缓存目录写在用户主目录下,不区分平台版本。鸿蒙分支的 Flutter 版本更新频率高,缓存键没带版本号的话,切换分支后可能用到旧模板。建议把缓存 key 加上 Flutter 版本和 scaffoldio 版本,双保险。
2.3 改造前先跑通最小链路
我先做了个“最小链路”验证,而不是直接动手改大逻辑。
做法:从模板库里复制一个最简页面模板,放到临时目录,手动调用 scaffoldio 核心的 render 方法,只生成一个文件,然后看产出内容是否正确。这个步骤很快,十几分钟就能排除掉 Dart 层、模板引擎层、文件系统层的绝大多数问题。
做完最小链路以后,我对改造方案的信心就比较足了。因为 scaffoldio 本身是 Dart 写的,Dart VM 在鸿蒙 Flutter 分支里是完整可用的,CLI 运行不依赖 Android/iOS 工具链。真正要动手术的是平台识别、模板生成目录和后续构建校验这三块。
注意:最小链路验证时,先别打开 IDE、不加载插件、不做代码分析,只跑命令行生成。这样能最快区分是 generator 的问题还是 IDE/插件的问题。
3. 核心适配改造实操
3.1 命令分发与平台感知改造
scaffoldio 的命令入口是一个ScaffoldCommand类,内部维护了平台列表。我做的第一件事是扩展平台枚举,从android/ios变成android/ios/ohos,并且在解析命令行参数时允许用户通过--platform ohos指定目标平台。
改造后的核心逻辑:
enum ScaffoldPlatform { android, ios, ohos, } ScaffoldPlatform parsePlatform(String raw) { switch (raw.toLowerCase()) { case 'ohos': case 'harmony': case 'openharmony': return ScaffoldPlatform.ohos; case 'android': return ScaffoldPlatform.android; case 'ios': return ScaffoldPlatform.ios; default: throw ScaffoldException('Unsupported platform: $raw'); } }这一步并不复杂,但很重要。原来代码里很多if (platform == android)的散落判断,我全部收口到一个统一方法里。收口的好处是后续如果要支持更多平台,只改一处就行。
命令行里还遇到一个细节:生成鸿蒙模块时,用户经常同时传好几个平台(比如一套业务代码既要 Android 也要鸿蒙),我在参数设计上加了--platforms复数参数,用逗号分隔,内部再并行分发到不同模板目录。这里涉及并发写入,鸿蒙模板和 Android 模板如果同时生成同一个业务文件,可能产生文件锁竞争,我最后用“按文件路径加锁”的方案解决,而不是全局锁,并发度更高。
3.2 模板引擎与路径处理的改造
模板引擎层面,我保留了 scaffoldio 原来的模板语法,但重写了路径解析器。原来模板文件里的相对路径是直接拼字符串,现在改成了统一的TemplatePathResolver,负责三件事:
- 根据平台选择正确的根目录(
android/、ios/、ohos/); - 处理路径分隔符,统一使用斜杠,落盘时再根据当前系统转换成对应分隔符;
- 处理大小写约定,鸿蒙工程目录统一用小写,模块类名统一用大驼峰。
这里最容易被忽略的是“模板变量在路径里的使用”。比如lib/pages/{moduleName}/和ohos/entry/src/main/ets/pages/{ModuleName}/,目录名大小写风格完全不同。我最后在模板变量里增加了moduleDirName和moduleClassName两个独立变量,分别控制目录名和类名,从源头避免风格混用。
路径解析器核心示例:
class TemplatePathResolver { final ScaffoldPlatform platform; final Map<String, String> vars; String resolve(String templatePath) { var path = templatePath; for (final entry in vars.entries) { path = path.replaceAll('{${entry.key}}', entry.value); } if (platform == ScaffoldPlatform.ohos) { path = path.replaceAll('{moduleDirName}', toSnakeCase(vars['moduleName']!)); } return path.replaceAll('/', Platform.pathSeparator); } }这套设计跑下来,Windows、macOS、Linux 三种开发机上生成的工程结构完全一致,没有再出现路径分隔符导致的“文件生成到了错误目录”的诡异问题。
3.3 模板内容适配:从纯 Dart 产物到 ArkTS 产物
路径搞定后,重头戏在模板内容。原来 scaffoldio 生成的是纯 Dart 工程文件,比如路由表、网络层、状态管理模板,这些文件在鸿蒙 Flutter 分支下大部分可以复用,因为 Dart 代码是跨平台的。真正需要新增的是鸿蒙原生侧的 ArkTS 入口文件、构建配置和桥接代码。
我在模板库里新增了templates/ohos/目录,里面放了这些文件:
oh-package.json5.tmpl,鸿蒙包的模块声明;build-profile.json5.tmpl,构建配置;hvigorfile.ts.tmpl,hvigor 构建脚本;entry/src/main/ets/entryability/EntryAbility.ets.tmpl,应用入口;entry/src/main/ets/pages/Index.ets.tmpl,首页;entry/src/main/ets/plugins/ChannelPlugin.ets.tmpl,通道插件模板。
以路由表为例,原来只生成lib/router/app_router.dart,鸿蒙化后同时生成一个entry/src/main/ets/router/AppRouter.ets,两个文件的页面清单完全一致,由同一个配置文件驱动。这样 Android 和鸿蒙两边的路由跳转不会出现页面缺失的偏差。
我在这里踩过一个不小的坑:ArkTS 语法比 TypeScript 更严格,不允许一些 TS 的灵活性写法。模板里如果直接写 TS 风格的对象展开、任意类型转换,编译会报一堆错误。建议在写模板时就把类型写死,不要用any,尽量不用运行时反射,鸿蒙侧对这类写法容忍度很低。
3.4 通信代码生成:MethodChannel 与 EventChannel 的鸿蒙实现
scaffoldio 原本有一个很受欢迎的能力:自动生成 Flutter 与原生通信的桥接代码。开发者只需要在配置文件里声明通道名和方法列表,工具会直接生成 Android 的 Kotlin 实现、iOS 的 Swift 实现,以及 Flutter 侧的调用封装。
鸿蒙化后,我增加了 ArkTS 侧的实现模板。这个适配的核心在于保持“通道名和方法签名”一致,因为 Flutter 侧的MethodChannel('com.example.channel')是跨平台统一的,原生侧无论 Kotlin、Swift 还是 ArkTS,都只需要处理同一个字符串通道名和方法参数。
生成出来的 ArkTS 通道实现大概是这样一个结构:
import { MethodCall, MethodChannelBase } from '@kit.ChannelKit'; export class ChannelPlugin extends MethodChannelBase { constructor() { super('com.example.channel'); } onMethodCall(method: string, call: MethodCall): void { switch (method) { case 'getDeviceInfo': { const result = new HashMap<string, string>(); result.set('platform', 'ohos'); call.reply(result); break; } default: call.replyError(`unknown method: ${method}`); } } }严格来说不同版本的鸿蒙 Kit 提供的 ChannelKit API 略有差异,但这个方向是对的。生成器不需要替开发者决定业务逻辑,只需要把通道注册、方法分发、错误返回这些样板代码生成出来,开发者在自己填业务实现就好。
EventChannel 也类似。原来生成的 Dart 侧是EventChannel('com.example.event'),我补了一个 ArkTS 侧的事件源实现模板,关键差别在于生命周期管理——鸿蒙侧的 EventChannel 要在 EntryAbility 的onCreate或页面aboutToAppear里初始化,在销毁时释放,不然会出现事件接收不到或者内存泄漏。
这部分改动是用户感知最明显的。团队里的 Android 开发切到鸿蒙工程时,发现通信层代码结构跟原来 Kotlin 版本几乎一一对应,学习成本低了很多。生成器的价值就在这种地方:不是帮你写业务,而是帮团队少写几百行重复的样板代码。
3.5 插件注册与资源声明改造
最后一个大头是插件注册和资源声明。Flutter 插件机制通过pubspec.yaml里的flutter.plugin.platforms声明各平台实现入口。原来只生成 android/ios,我改造后的模板在生成 ohos 平台时,会在 pubspec 里额外写:
flutter: plugin: platforms: android: package: com.example.myplugin pluginClass: MyPlugin ios: pluginClass: MyPlugin ohos: pluginClass: MyPlugin pluginImplementation: ohos/src/main/ets/MyPlugin.ets注意,鸿蒙插件声明里除了pluginClass还要指明pluginImplementation路径,指向 ArkTS 文件。这个字段写错,插件在运行时会被静默跳过,Flutter 侧调用方法时直接返回MissingPluginException,排查起来非常恶心。
同时还要生成oh-package.json5,把 Flutter 引擎的鸿蒙实现包和必要的鸿蒙 SDK 依赖写进去。我建议生成器在模板里把这些依赖的版本号做成变量,而不是硬编码,方便后续升级引擎版本时全局替换。
4. 实战:用 scaffoldio 生成鸿蒙 Flutter 模块
4.1 一次实际生成的命令与参数说明
环境准备好以后,我实际跑了一条生成命令,生成一个带“用户中心”模块的鸿蒙 Flutter 工程。命令如下:
scaffoldio create feature --name user_center --platforms ohos,android \ --pages profile,settings,security \ --channel com.example.user \ --with-network --with-router这条命令的参数含义我列一下,方便你对照自己的工具设计:
| 参数 | 说明 | 备注 |
|---|---|---|
create feature | 创建业务功能模块 | 与 create app 区分 |
--name | 模块名 | 会转成目录名、类名两个变量 |
--platforms | 目标平台 | ohos 与 android 同时生成 |
--pages | 页面清单 | 每个页面生成 Dart + ArkTS 两套文件 |
--channel | 通信通道名 | 生成 MethodChannel 注册代码 |
--with-network | 是否生成网络层 | 基于 dio 封装 |
--with-router | 是否生成路由 | Dart 路由 + ArkTS 路由 |
参数设计有一个原则:尽量让一个参数对应一个明确的模板分支,不要让参数之间产生联动副作用,否则后期维护模板的人会崩溃。
4.2 完整执行过程与输出目录
命令执行后,终端输出大致如下:
[scaffoldio] resolving templates ... done (42 templates) [scaffoldio] generating ohos module: user_center ✓ oh-package.json5 ✓ build-profile.json5 ✓ hvigorfile.ts ✓ entry/src/main/ets/entryability/EntryAbility.ets ✓ entry/src/main/ets/pages/ProfilePage.ets ✓ entry/src/main/ets/pages/SettingsPage.ets ✓ entry/src/main/ets/pages/SecurityPage.ets ✓ entry/src/main/ets/router/AppRouter.ets ✓ entry/src/main/ets/plugins/ChannelPlugin.ets [scaffoldio] generating android module: user_center ✓ android/src/main/kotlin/.../UserCenterPlugin.kt ✓ lib/src/user_center_api.dart [scaffoldio] verifying ohos build ... passed in 26.8s [scaffoldio] finished in 4.3s (cache warm) / 1.1s (cache hit)生成的目录结构里,ohos/entry/src/main/ets/pages/下面三个页面文件一一对应命令里的--pages参数。AppRouter.ets里自动注册了三个页面的路由表,ChannelPlugin.ets里自动实现了com.example.user通道。
这个输出结构是特意设计的:开发者在 DevEco Studio 里打开生成的ohos目录就能直接构建,不用再手动创建工程文件。
4.3 生成后如何验证跑通
生成只是第一步,关键是验证。我的验证流程分三步:
第一步,命令行进入生成目录,执行flutter pub get,然后执行hvigorw assembleHap做命令行构建。这一步能发现有类型错误、缺依赖、模板里写错路径之类的问题。命令行构建的好处是可以集成到 CI,不用每次都打开 IDE。
第二步,打开 DevEco Studio,跑一次真机或模拟器。重点检查三件事:应用能启动并进入首页;路由能完成页面跳转;通过 MethodChannel 能成功调用原生方法并拿到返回结果。
第三步,做一次“生成结果幂等性”验证。同一个命令跑两次,第二次应该不会改变任何生成文件。我的模板里所有时间戳都用了固定的模块创建时间,而不是当前时间,不然 git diff 每次都有一堆无意义变化,团队协作时会被烦死。
4.4 性能实测:到底快了多少
我专门做了一组对比,模拟“从零手动搭建”和“用 scaffoldio 自动生成”两种方式的耗时差异。手动过程包括建目录、写基础页面、配路由、配通道、写网络层封装,平时团队成员完成一个 3 页面的模块大约需要 10 到 15 分钟,还不包括出错排查时间。
| 模块规模 | 手动搭建耗时 | scaffoldio 生成耗时(首次/缓存命中) | 提升 |
|---|---|---|---|
| 3 页面模块 | 约 12 分钟 | 4.3s / 1.1s | 约 150 倍以上 |
| 10 页面模块 | 约 40 分钟 | 6.8s / 1.5s | 约 350 倍以上 |
| 已有模块新增 2 页面 | 约 8 分钟 | 2.1s / 0.7s | 约 200 倍以上 |
生成速度本身的优化反而不那么重要,更关键的是它让团队成员在页面多、路由多、通道多的情况下不会因为手写漏掉某个文件而陷入“编译报错-查文件-补文件”的循环。在鸿蒙这种新生态下,省掉的不仅是时间,还有大量心智负担。
5. 常见问题与排查技巧实录
5.1 问题速查表
适配和后续实际使用中,我整理了一批高频问题,列成速查表,方便直接对照排查。
| 症状 | 原因 | 解决办法 |
|---|---|---|
| 生成后 ohos 目录为空 | 模板根目录未按平台分发 | 检查 path resolver,确认platform == ohos时选择的是templates/ohos |
MissingPluginException | pubspec 里 ohos 插件声明缺失或 implementation 路径错误 | 核对pluginImplementation是否指向真实 ArkTS 文件 |
| 通道能调用但返回 null | ArkTS 侧返回类型与 Dart 侧不一致 | 在 ChannelKit 里显式将原始对象转成HashMap或Array,不要返回自定义对象 |
构建报ohpm install failed | 依赖版本冲突或仓库源不通 | 清理ohos/.ohpm缓存后重试,确认使用团队统一镜像源 |
| 文件生成了但 git diff 显示全部重写 | 模板内嵌了时间戳或随机数 | 模板中只使用与业务相关的变量,禁用DateTime.now()之类动态值 |
| 换 Flutter 分支后生成结果异常 | 模板缓存未失效 | 缓存 key 加入 Flutter 版本号,分支切换后自动重建缓存 |
| 打包到真机后首屏白屏 | 缺少EntryAbility生命周期初始化 | 检查onCreate里是否正确注册 Flutter 引擎与 ChannelPlugin |
| 路由跳转后页面状态丢失 | 固定路由栈导致页面重建 | 使用命名路由占位符加Navigator.push,不要直接替换根路由 |
5.2 我踩过坑之后沉淀的三个技巧
第一个技巧:模板里不要写绝对路径。听起来很基础,但我见过不止一次有人在模板里写上C://work/...或者/Users/xxx/...类似的路径,模板一旦提交到仓库,其他同事生成出来的代码就会带上你的个人路径。正确的做法是模板里只写相对路径,生成时的根目录统一由 CLI 传入,并且生成前做一层路径合法性校验。
第二个技巧:生成前先删掉旧目录,再生成新目录。scaffoldio 在最开始默认“增量生成”,也就是只覆盖变化的文件,旧文件会残留。但鸿蒙工程的目录结构如果发生调整(比如从单模块改成多模块),残留文件会导致构建时出现“重复类”或者“重复资源”的报错。我的处理方式是增加一个--clean参数,生成前把目标模块目录下标记为生成产物的文件全部删除,但保留开发者手写的文件。这个删除逻辑要非常谨慎,默认只删除文件名带特定前缀或者位于生成清单里的文件,防止误删。
第三个技巧:用 git diff 来做模板回归测试。每次改完模板,我会跑一个固定的生成命令,然后把生成结果 git diff 一下,人工看看这次改动影响了哪些文件、有没有意外改动其他模块。这一步看起来费时间,但能避免很多“模板升级引发老项目编译失败”的事故。我甚至把它写进了 CI:每次合并模板变更的 MR 时自动运行一次全量生成,再对比基线快照,有异常直接拦截合并。
5.3 我下一步打算继续做的事
这次适配解决了“生成鸿蒙工程骨架”的问题,但它还远远不是终点。我下一步想继续做三件事:
一是把 CI 集成做完整。现在生成命令可以在本地跑,但还没有统一沉淀到团队的流水线里。理想的状态是开发者在 MR 里提交配置文件,CI 自动生成代码并做一次 hvigor 构建,不合格直接打回。
二是做模板的多端一致性检查。同一个业务模块同时生成 Dart、Kotlin、ArkTS 三种产物,路由表、通道名、页面清单必须保持一致。我准备在 scaffoldio 里增加一个校验命令,扫描所有生成产物,对比三端的路由注册和通道声明,发现不一致直接报错。
三是补上从脚手架到运行时的最后一公里。生成器目前解决的是“代码结构”问题,但鸿蒙应用的签名、权限配置、App 图标这些资源和配置,还没有完全模板化。这部分如果也能自动化,新项目接入成本还能再降一档。
最后多说两句
这次鸿蒙化适配给我最大的感受是:代码生成器这种东西,真正值钱的不是“生成”这个动作本身,而是把团队约定固化下来的过程。在 Android/iOS 时代,这些约定散落在各个老项目的脚手架里,新项目接入靠老员工口口相传;到了鸿蒙这个新生态,反而给了我们一次重新梳理的机会——把路由怎么写、通道怎么开、模块怎么拆,一次性写进模板里,任何人都能一键复现。
如果你也正在做 Flutter 库或者工具链的鸿蒙化适配,我的建议是先别急着改业务代码,把“模板即约定”的思路立起来。工具慢一点没关系,但生成的产物一定要稳定、一致、可重复。等到团队里每个人生成出来的代码都长一个样,你才会真正感受到脚手架工具的价值。