先说结论:Flutter 项目做鸿蒙化适配,UI 层的坑其实不算多,真正让人反复折腾的是环境配置这件事。开发环境、测试环境、生产环境的 API 地址、应用标识、密钥全都不一致,Flutter 生态里 flavors_chef 是处理这类多环境配置比较顺手的库,但它在鸿蒙平台上几乎是空白状态。我花了两周时间把一个中型 Flutter 项目往鸿蒙迁移,期间对 flavors_chef 做了一轮完整的鸿蒙化适配。这套思路跑通之后,多环境切换从“每次构建前手改配置”变成了“一个命令跑天下”,跟它宣传语里说的“如烹小鲜”确实有几分对应——备菜、调味、下锅全是标准化流程,你要做的只是选今天做哪道菜。
这篇文章把整个适配过程拆开讲清楚,包括 flavors_chef 的工作原理、鸿蒙侧不同于 Android/iOS 的配置注入机制、三个关键映射点的处理方案、完整的实操步骤和踩坑记录。适合正在做 Flutter 鸿蒙化改造的团队,也适合想理解“Flutter 三方库如何迁移到鸿蒙体系”的开发者。
1. 项目认识与适配思路拆解
1.1 flavors_chef 到底解决了什么问题
老 Flutter 开发者对多环境配置应该都不陌生。早期项目一般这么搞:在constants.dart里写一堆 const 字符串,每次切换环境就手动改一遍,改完再全局搜一遍有没有漏改的。等到了提测阶段,测试报了个 bug,你一看日志发现 API 地址是生产环境的,那一刻真的很想把手提电脑扔出去。
flavors_chef 做的事情,是把“环境配置”这件事从代码层面抽离出来。它通过命令行交互的方式,让你定义多个 flavor(风味),比如 dev、sit、production,每个 flavor 里可以配置应用显示名称、Bundle ID、API 地址、以及任意自定义参数。配好之后,它帮你生成对应的入口文件(通常是main_dev.dart、main_sit.dart、main_production.dart),应用运行时会通过FlavorConfig.instance这样的全局单例拿到当前环境的配置。
用生活化的类比理解:多环境配置就像做一桌菜。没有 flavors_chef 的时候,你做每道菜都要自己洗菜、切菜、调酱汁;有了 flavors_chef,相当于把配菜和调料都按菜谱预装成“半成品料理包”,下锅前只需要选“今天做哪道菜”。
1.2 鸿蒙化适配为什么不能直接“照搬”
在 Android 和 iOS 上,flavors_chef 能做到“构建期注入配置”,底层依赖的是各自原生构建体系的机制:Android 有 Gradle 的productFlavors和BuildConfig字段,iOS 有 Info.plist 的变量替换和 xcconfig 文件。Flavor 配置在编译阶段就已经被写进原生的构建产物里,Dart 层运行时读取的是已经被“定死”的值。
到了鸿蒙这里情况就变了。鸿蒙的 Flutter 引擎是 OpenHarmony 团队维护的 OHOS 分支,Dart 层 API 基本兼容,但原生侧的构建体系和配置注入机制跟 Android/iOS 完全不同。鸿蒙用的是自己的 hvigor 构建系统,配置集中在build-profile.json5和module.json5,没有BuildConfig这种编译期自动生成的类,也没有 Xcode 那套 Info.plist 预处理机制。
所以 flavors_chef 的鸿蒙化适配,核心难点不在 Dart 层,而在于回答三个问题:
- flavors_chef 生成的 Dart 入口文件能否被鸿蒙 Flutter 引擎正常识别?—— 可以,因为引擎入口本身就是 Dart
main(),平台差异不影响。 - 鸿蒙原生侧如何在应用启动阶段拿到当前 flavor 的配置?—— 需要桥接,在 Native 和 Dart 之间打通一条配置读取通道。
- 构建期如何按环境区分应用名、图标这类原生资源?—— 用 hvigor 的 product 机制,在构建侧做资源区分。
1.3 适配方案选型:改造库还是绕开库
动手之前我评估过三个方向。第一个是 fork flavors_chef 源码,给鸿蒙平台增加原生侧的文件生成逻辑,工作量大但一劳永逸。第二个是在 Dart 层用--dart-define配合String.fromEnvironment做环境区分,完全绕开 flavors_chef,但项目里大量代码已经依赖FlavorConfig.instance的 API,改造成本也不低。第三个是在不改 flavors_chef 源码的前提下,通过“Dart 层照常生成配置 + 鸿蒙侧桥接读取 + 构建侧 product 区分资源”三层组合拳完成适配。
最终选了第三个方案。理由很实际:flavors_chef 的核心价值是“配置管理 + 入口生成”,这两件事在鸿蒙上依然成立;鸿蒙平台缺失的只是“原生侧注入”,而这一块可以通过桥接层补齐,不一定要动库本身。第三方库的鸿蒙化适配,尤其是开源库,优先考虑“不改源码”的适配方案,后续上游版本更新时能直接升级,维护成本低很多。
2. 核心机制解析与适配点识别
2.1 flavors_chef 的工作机制拆解
要适配一个库,先得把它的工作流程吃透。flavors_chef 的核心逻辑其实不复杂,我拆成三个阶段:
配置定义阶段。通过命令行交互(dart run flavors_chef),开发者创建 flavor 并把配置项写进去。配置项分为两类,一类是通用配置,比如环境名称、API 地址、参数键值对;另一类是平台相关配置,比如 Android 和 iOS 的应用 ID、显示名称、图标资源路径。
代码生成阶段。配置保存后,flavors_chef 会在项目里生成两个东西:main_<flavor>.dart入口文件和flavors_config相关代码。生成的入口文件里会调用类似ConfigFlavor.setup()的方法完成环境初始化,然后才执行runApp()。
运行时读取阶段。业务代码通过FlavorConfig.instance读取当前环境参数。这个单例里存的值,本质上来源于生成入口文件时通过编译常量或配置文件注入的数据。
理解了这三个阶段,鸿蒙化适配的切入点也就清晰了。前两个阶段在 Dart 层,天然跨平台,不用动;第三个阶段里“业务代码读取配置”也还是 Dart 层,也不用动。真正需要补的是:鸿蒙原生侧如何感知当前 flavor,以及如何在构建期按 flavor 区分原生资源。
2.2 鸿蒙侧三大差异化机制
先说 hvigor 的 product 机制。鸿蒙的build-profile.json5里有一个products数组,可以定义多个产品变体。每个 product 可以有自己的名称、签名配置、以及自定义的构建参数。这一点上它和 Android 的productFlavors非常像。你可以为 dev、sit、production 各定义一个 product,然后在构建命令里指定用哪个 product 出包。鸿蒙侧的差异就在于 flavors_chef 并不知道这个机制,它生成的配置文件里没有鸿蒙 product 的映射,所以这部分要我们手动补上。
再说module.json5的资源区分。鸿蒙应用的显示名称、图标等元数据,是配置在 module 级别的module.json5里的,引用的是resources目录下的资源文件。和 Android 的 resource 叠加机制不同,鸿蒙的资源配置在构建时是按 product 维度区分的。也就是说,你可以为不同 product 准备不同的应用名和图标资源,构建时 hvigor 会挑选对应的一份。这一点利用好了,鸿蒙侧的“环境差异化 UI”就能实现。
最后是 ArkTS 和 Flutter 引擎的桥接通道。鸿蒙 Flutter 引擎支持和 Flutter 一样的MethodChannel/EventChannel机制,只是桥接的另一端不是 Java/Kotlin,而是 ArkTS 或 C++。Dart 层发起的 MethodChannel 调用,鸿蒙侧通过registerMethodChannel这样的 API 注册处理方法即可响应。整个配置读取通道,就是基于这套机制搭起来的。
2.3 三个适配点的优先级排序
我把整个适配过程分成三个递进层次,对应三个不同目标:
第一层:Dart 层入口跑通。目标是让 flavors_chef 生成的主入口在鸿蒙 Flutter 引擎上能正常启动,并且在业务代码里能读到正确的 flavor 名称和参数。这是基础,不解决这个就没法往下走。
第二层:Native 层读取环境标识。目标是让鸿蒙原生侧(Ability、Service、原生插件)能拿到当前 flavor 的名称和关键参数。典型场景是原生侧的日志上报、统计分析、推送通道需要知道当前环境。这层通过 MethodChannel 解决。
第三层:构建与分发层面的环境隔离。目标是让不同环境的包在应用名、图标、签名上彻底隔离,避免分发时拿错包。这层靠 hvigor product 和 module.json5 资源配置解决。
三个层次是递进关系,也是依赖关系。我建议按层次逐步验证,不要跳级。第一层没过就去做第三层的构建配置,最后排查问题的时候会非常痛苦。
3. 实操过程与核心环节实现
3.1 环境准备:鸿蒙 Flutter 开发前置条件
开始实操前,先把环境搭好。这里说的不是 Flutter 常规环境,而是鸿蒙侧那一套。
我用的组合是:Flutter 3.7.x 以上的稳定版 + OpenHarmony SDK(API 10 及以上)+ DevEco Studio 4.x + 鸿蒙 Flutter 引擎二进制包。鸿蒙 Flutter 引擎不是 Flutter 官方发布的,而是 OpenHarmony 团队维护的 flutter_flutter 仓库的 ohos 分支编译产物。这个东西无法通过flutter pub get自动获取,需要手动下载后放到指定目录,并在local.properties或者环境变量里配置路径。
配置路径这件事踩过坑。不同版本的鸿蒙 Flutter 引擎对 Flutter SDK 版本有要求,如果版本不匹配,构建时会直接报类似“unsupported engine version”的错误。我的建议是先锁定一个组合版本,比如 OpenHarmony 4.1 SDK 配某个已知可用的 Flutter 引擎 commit,不要两个都取最新。网上很多组合冲突的帖子,本质上都是版本矩阵没对齐。
还需要确认 DevEco Studio 的 hvigor 版本和命令行工具链可用。鸿蒙的构建体系独立于 Flutter 的构建体系,最终产物需要 DevEco 侧的编译打包能力。
3.2 依赖引入与项目初始化
项目里引入 flavors_chef 的方式很常规,在pubspec.yaml的dev_dependencies里加依赖,然后执行flutter pub get:
dev_dependencies: flavors_chef: ^1.0.0执行完后,在项目根目录运行:
dart run flavors_chef它会启动一个交互式命令行界面,引导你创建 flavor。这里注意一下:flavors_chef 的命令在不同版本里略有差异,有的版本是dart run flavors_chef,有的版本可能是flutter pub run flavors_chef,以实际安装版本的 README 为准。
创建完 flavor 后,项目里会出现类似这样的结构:
lib/ main.dart main_dev.dart main_sit.dart main_production.dart flavors/ flavors_config.dartmain_dev.dart内容大致是:
import 'package:flutter/material.dart'; import 'package:flavors_chef/flavors_chef.dart'; import 'app.dart'; void main() { ConfigFlavor.setup(); runApp(const App()); }这个入口文件就是鸿蒙 Flutter 引擎要找的 Dart 入口。开发调试时,在 DevEco 的 Flutter 相关配置里把入口指向lib/main_dev.dart,构建时指定对应的 flavor 即可。
3.3 配置项设计:以一个真实项目的多环境配置为例
我以我这边的项目为例,说明 flavor 里应该配置哪些东西。我们定义了 dev、sit、production 三个环境,配置项分这么几块:
通用参数:环境名称flavorName、API 基础地址apiBaseUrl、日志级别logLevel、数据上报开关analyticsEnabled。
平台参数:应用显示名称、应用 ID。在 flavors_chef 的配置界面里,这部分是为 Android 和 iOS 准备的。鸿蒙侧暂时识别不到这些配置,需要我们在后续步骤里手动映射到 hvigor 的 product 配置中。
自定义键值对:比如地图密钥、推送通道 ID、客服系统地址。这类参数的特点是不影响构建,运行时由 Dart 层读取后分发给业务代码或原生插件。
以 dev 环境为例,配置大概是:
flavorName: dev apiBaseUrl: https://dev-api.example.com logLevel: debug analyticsEnabled: false appDisplayName: 示例应用-开发版生产环境则是:
flavorName: production apiBaseUrl: https://api.example.com logLevel: error analyticsEnabled: true appDisplayName: 示例应用注意生产环境的显示名和开发环境不一样,这一步如果构建期没处理好,分发时很容易出“把开发版包当生产版发出去”的事故。
3.4 鸿蒙构建侧:用 hvigor product 实现环境隔离
这一步是整个适配里最关键的工程环节。先说思路:flavors_chef 的 flavor 配置停留在 Dart 层,鸿蒙原生侧要区分环境,就得在鸿蒙自己的构建体系里也建一套“产品变体”。建好之后,两个体系的对应关系是:flavors_chef 的dev对应 hvigor 的product_dev,以此类推。
打开鸿蒙应用模块根目录下的build-profile.json5,在products数组里增加配置:
{ "app": { "products": [ { "name": "product_dev", "signingConfigs": [], "compileSdkVersion": 12, "compatibleSdkVersion": 12, "runtimeOS": "HarmonyOS", "buildOption": { "debug": { "arkOptions": { "buildFlags": ["--define=flavor=dev"] } } } }, { "name": "product_production", "signingConfigs": ["production_signing"], "compileSdkVersion": 12, "compatibleSdkVersion": 12, "runtimeOS": "HarmonyOS" } ] } }这里用buildFlags传了一个flavor的编译期变量。这个变量可以在 ArkTS 代码里通过类似BuildProfile的方式读取,也可以配合 hvigor 脚本做资源选择。不同版本鸿蒙 SDK 对 buildOption 的支持细节有差异,如果你的 SDK 版本不支持arkOptions.buildFlags,可以用 hvigor 的自定义 task 或者环境变量方式传递,原理是一样的:在编译期把一个“当前构建环境”的标记传入原生构建链路。
签名配置的隔离是另一个重点。开发环境通常用自动签名或 debug 证书,生产环境必须用正式发布证书。把 dev 的签名配置留空,production 配正式签名,可以从源头避免“拿开发证书打生产包”的低级错误。我当时在鸿蒙开发者后台重新生成了两个证书项目,dev 用一个独立的测试证书,生产用发布证书,两边彻底分离开。
接下来是module.json5里的应用名和图标区分。鸿蒙的显示名配置在module.json5里通常是:
{ "module": { "name": "entry", "displayName": "$string:app_name", "icon": "$media:app_icon" } }$string:app_name指向resources/base/element/string.json里的资源。要实现按 product 区分,做法是在resources目录下建立 product 维度的资源分目录。鸿蒙的资源限定目录机制支持用产品名做一级区分,结构类似:
resources/ base/ element/ string.json media/ app_icon.png product_dev/ element/ string.json media/ app_icon.png product_production/ element/ string.json media/ app_icon.pngproduct_dev/string.json里把app_name写成“示例应用-开发版”,product_production/string.json里写成“示例应用”。构建时 hvigor 会自动选择 product 对应的资源目录。这个机制和 Android 的 source set 叠加机制很像,理解起来不费劲。
这里有一个之前提到的坑点:hvigor 的 product 机制和 flavors_chef 的 flavor 机制是两套独立体系,两者之间没有自动联动。我在项目里用一个 shell 脚本做了统一调度,构建命令大致是:
# 构建 dev 环境鸿蒙包 flutter build hap --flavor dev --target-platform ohos脚本内部先调 flavors_chef 生成对应的 Dart 入口,再解析 flavor 名拼接 hvigor 的 product 名,最后调用 hvigor 构建对应产品变体。脚本相当于一个“翻译层”,把两者对应关系固化下来,避免每次手工来回切。
3.5 Native-Dart 桥接:让鸿蒙原生侧读到环境配置
构建期的隔离解决的是“包层面”的问题。还有一类需求是运行期的——鸿蒙原生侧的能力,比如推送 SDK、统计 SDK、崩溃上报,它们也需要知道当前是哪个环境。这些 SDK 通常在 Ability 的onCreate阶段初始化,如果初始化时机早于 Flutter 引擎的 Dart 侧启动,那通过 MethodChannel 从 Dart 拿配置就来不及了。
我的处理方案分两步。第一步,在 ArkTS 侧维护一份环境变量的映射表,来源就是 hvigor 构建时传入的 flavor 标记。这个映射表在 Ability 创建时就能读到,保证原生 SDK 初始化不阻塞。第二步,在 Flutter 引擎就绪后,通过 MethodChannel 把 Dart 侧的FlavorConfig全量参数同步给原生侧,保证两边数据一致。
ArkTS 侧读取构建参数的示意代码(方法和具体 API 因 SDK 版本而异,这里给出思路):
import { BuilderProfile } from '@ohos/app/ability/BuilderProfile'; // 在 EntryAbility 的 onCreate 中读取 const flavor = BuilderProfile.getBuildProfileValueSync('flavor'); let configMap = new Map<string, string>(); configMap.set('apiBaseUrl', flavor === 'dev' ? 'https://dev-api.example.com' : 'https://api.example.com');同步数据用的 MethodChannel,在 ArkTS 侧注册:
// EntryAbility.ets 中,在 Flutter 引擎创建后 let methodChannel = new MethodChannel('flavor_sync'); methodChannel.setMethodCallHandler((call) => { if (call.method === 'syncFlavorConfig') { // 接收 Dart 侧传来的完整配置 let args = call.arguments as Record<string, string>; globalThis.flavorConfig = args; } return Promise.resolve(true); });Dart 侧在main_dev.dart的ConfigFlavor.setup()之后调用同步:
import 'package:flutter/services.dart'; Future<void> syncFlavorToNative() async { const channel = MethodChannel('flavor_sync'); final config = { 'flavorName': FlavorConfig.instance.name, 'apiBaseUrl': FlavorConfig.instance.apiBaseUrl, }; await channel.invokeMethod('syncFlavorConfig', config); } void main() { ConfigFlavor.setup(); syncFlavorToNative(); runApp(const App()); }这个设计里有个值得注意的细节:同步动作放在ConfigFlavor.setup()之后、runApp()之前。因为这个阶段 Flutter 引擎已经和 Native 建立了基础通信,而业务代码还没开始跑,传递配置不会被业务层的并发调用干扰。实际测试下来,这种初始化顺序非常稳。
3.6 验证与分发:一包一环境,杜绝串包
整条链路搭完后,验证环节要覆盖三个层面。
第一是 Dart 层验证。在应用首页放一个调试面板,显示当前FlavorConfig.instance.name和 API 地址,启动后肉眼确认环境正确。这个面板只保留在 dev/sit 环境,通过kDebugMode或 flavor 名控制显隐。
第二是原生层验证。打开日志过滤,确认原生侧初始化的 SDK 日志里带的环境标识正确。比如推送 SDK 的初始化日志里会出现环境名,崩溃上报的 URL 是测试地址还是生产地址等。我当时排查出一个线上事故隐患就是靠这一步——某个原生插件无论如何都默认读生产环境的密钥,桥接数据到了但插件内部的配置项优先级更高,导致测试环境偷偷用着生产密钥在跑。
第三是产物验证。构建出的 HAP 包,解包后检查module.json5中的显示名、图标 hash 是否正确对应当前 product。用 hvigor 的产物检查或者直接解压 HAP 都可以。这一步看起来多余,但真正到了要分发的时候,它就是避免“发错包”的最后一道保险。我习惯在 CI 脚本里加一道产物校验,shell 脚本读 HAP 里的配置和构建参数做比对,不一致直接 fail 构建。
到这里,一个完整的“多环境鸿蒙包”构建链条就闭环了。开发环境出一版骚粉图标的应用,测试环境出蓝色图标的包,生产环境出正式图标正式名的包,三个包变现方式互不干扰,分发时按环境直接推送,不再需要人工反复确认“这个包是不是生产包”。
4. 常见问题与排查技巧实录
适配过程中踩了不少坑,有些坑属于“新手必经”,有些属于“文档里根本不会写”。整理成速查表格,方便后来者直接对照。
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 鸿蒙 Flutter 引擎找不到 Dart 入口 | DevEco 里配置的入口文件不是 flavors_chef 生成的main_<flavor>.dart | 检查 DevEco 的 Flutter 模块配置,把默认主入口改为对应 flavor 生成的入口文件 |
| 构建报错“flavor not found” | hvigor product 名和 flavors_chef flavor 名不一致 | 建立统一的命名映射,dev 对应 product_dev,不要出现大小写或连字符差异 |
| 原生侧拿到的配置和 Dart 侧不一致 | 桥接数据同步时机晚于原生 SDK 初始化 | 原生 SDK 初始化用构建期的flavor标记做兜底,等桥接同步后再覆盖刷新 |
| 更换 flavor 后热重载没有生效 | flavors_chef 生成的入口文件有缓存,或者 dev 工具仍指向旧入口 | 完全停掉应用,重新执行对应 flavor 入口的启动命令,不要依赖热重载切换环境 |
| HAP 内应用名始终是默认值 | module.json5的资源引用没有走 product 限定目录 | 检查resources/product_<name>目录是否被 hvigor 正确识别,必要时清理构建缓存重试 |
| 构建期传入的 buildFlags 读取不到 | hvigor 版本对buildOption的支持层级不一致 | downgrade 或改用环境变量方式传参,ArkTS 侧通过进程环境获取 |
| 反复出现“版本不匹配”引擎报错 | Flutter SDK 和鸿蒙 Flutter 引擎是随意组合的最新版 | 锁定一组经过验证的版本组合,把版本号写进项目 README,全员统一 |
4.1 环境切换的“用户态”陷阱
还有一个很典型的业务侧问题:开发同学把 dev 环境的包装到手机上,过了两天测 bug,发现测试环境的数据没了。查了半天,结果是开发同学手机上装了两个不同环境的包,系统按包名覆盖安装时把 dev 包覆盖成了 sit 包。这个问题的根源是三个环境的包名(Bundle ID)没有区分开,应用层也无法分辨当前装的是哪个环境。
在鸿蒙上,这个问题同样存在。如果 dev/sit/production 共用同一个包名,只能等用户手动卸载重装,体验极差。我的经验是:至少在 dev 环境的 App 内做环境标识一体化,包括桌面图标、应用名、应用内主题色、显式的 Debug 模式角标。比如 dev 版图标右下角加个“DEV”角标,进入应用后首页状态栏旁边显示环境名。这个习惯养成了,后面排查问题会省非常多时间。
4.2 桥接信令设计:配置同步不要“悄无声息”
我在设计flavor_sync这个 MethodChannel 时,一开始只做了一次单向同步,结果测试中发现原生侧和 Dart 侧的配置偶尔对不上。后来加了“回执确认”机制:Dart 侧同步完配置后,原生侧必须回传一个sync_ack,确认参数确实写入成功。如果 3 秒内没收到回执,Dart 侧就把当前 flavor 名打印在日志里,方便定位是桥接失败还是初始化顺序问题。
这个改动看着不大,但对排查跨端配置问题非常有效。多环境配置的问题,最怕的不是配置错,而是两边配置不一致还互相不知道。给桥接加一个显式的握手确认,等于给配置同步加上了“已读回执”,两边配置最终必然收敛到同一状态。
4.3 关于 flutter 中 part 和 eventchannel 的连带思考
热搜词里有几个 Flutter 相关的关键词,正好在鸿蒙化过程中也遇到过。比如flutter 中 part,Dart 的part机制在生成 flavors_chef 的配置类时会被用到,鸿蒙引擎的 Dart 侧对 part 的支持和标准 Flutter 一致,这一点不需要做额外适配。再比如flutter eventchannel,我用 MethodChannel 做配置同步,其实用 EventChannel 也可以,差别在于 MethodChannel 是请求-响应模式,EventChannel 是持续推送模式。配置同步场景更适合 MethodChannel,因为是一次性握手;而如果之后要做“运行时动态切换环境”这种能力,EventChannel 反而更适合,它能主动把新配置推给所有监听方。这两种通道在鸿蒙 Flutter 引擎里都有对应实现,不存在平台缺失的问题。
5. 写在最后的几句经验谈
这套鸿蒙化适配方案跑通之后,我最大的体会是:所谓“鸿蒙化”,大多数情况下并不是真的要把现有生态的库重写一遍,而是要理解鸿蒙自己的平台机制,然后在“第三方库的能力边界”和“鸿蒙平台的能力边界”之间找到一个翻译层。flavors_chef 管的是 Dart 层的配置生成和入口管理,鸿蒙管的是构建期的产品变体和资源区分,两者本就不冲突,缺的只是中间那层“翻译官”。
另外,分发这件事值得多说一句。多环境配置做得再好,如果分发环节靠人工识别包,迟早会出事。我目前的方案是把构建产物和 flavor 配置的校验写进了 CI 流水线,任何环境下发的包都要经过自动校验才会进入待发布状态。这个流程一开始搭建需要一点投入,但换来的是一劳永逸的分发安全感。最后再分享一个小技巧:鸿蒙侧的 product 命名和 flavors_chef 的 flavor 命名尽量保持完全一致,连大小写、连字符都不要有差异,这样脚本映射逻辑会简单非常多,排查问题时也少一层误解。