Add architecture documentation and retail system workshop PPT
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# 08. 多环境构建
|
||||
|
||||
## 决策
|
||||
|
||||
App 侧维护 **3 个 flavor:`dev` / `uat` / `prod`**,环境划分与现有后端 CI/CD(见 `gitlab-cicd-azure-deployment-diagram.drawio` 里的 Dev/UAT/Prod Azure 环境)保持一致命名,三个 flavor 各自对应不同的 API 地址、应用图标/名称、包名后缀,可在同一台设备上同时安装、互不覆盖。CI 沿用现有 GitLab CI(Runner 与后端共用),但产物是 App 二进制(apk/ipa),分发渠道与后端的 ACR/Azure App Hosting 不同。
|
||||
|
||||
## Flavor 划分规则
|
||||
|
||||
| Flavor | Application ID / Bundle ID | API 目标 | 分发渠道 |
|
||||
|---|---|---|---|
|
||||
| `dev` | `com.conti.retail.dev` | Dev 环境(对应后端 Dev) | Firebase App Distribution(内部测试) |
|
||||
| `uat` | `com.conti.retail.uat` | UAT 环境(对应后端 UAT) | Firebase App Distribution(验收测试) |
|
||||
| `prod` | `com.conti.retail` | Prod 环境(对应后端 Prod) | App Store Connect / Google Play(生产发布) |
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每个 flavor 对应一个独立的 Dart 入口文件(`main_dev.dart`/`main_uat.dart`/`main_prod.dart`),三者都只是设置好环境标识后调用同一个共享的 `bootstrap()` 启动函数,不允许在入口文件里写业务逻辑分支。
|
||||
- 环境相关的可变配置(API base URL、是否开启日志等,见 [05-networking.md](./05-networking.md) 的 `appEnvProvider`)通过 `--dart-define-from-file=env/{flavor}.json` 注入,不写死在代码里、也不用 `if (flavor == 'dev')` 这种运行时字符串判断来分支配置。
|
||||
- `env/*.json` 只包含非敏感配置(API 地址等);密钥类配置(如第三方 SDK App Key)通过 CI 变量在构建时注入,不提交进仓库。
|
||||
- Android 侧用 Gradle `productFlavors` 区分 `applicationId`/图标/`versionNameSuffix`;iOS 侧用对应的 xcconfig + Scheme 区分 `Bundle Identifier`/图标;两端 flavor 名称必须完全一致(`dev`/`uat`/`prod`),不允许两端用不同命名。
|
||||
- CI 流水线阶段固定为:`melos run analyze` → `melos run test` → 按 flavor `flutter build apk/ipa --flavor {flavor} --dart-define-from-file=env/{flavor}.json` → 上传对应分发渠道。`prod` flavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。
|
||||
|
||||
## 参考链接
|
||||
|
||||
- [Flutter 官方 Flavors 文档](https://docs.flutter.dev/deployment/flavors)
|
||||
- [--dart-define-from-file 官方说明](https://docs.flutter.dev/deployment/flavors#configuration-approaches)
|
||||
- [Firebase App Distribution](https://firebase.google.com/docs/app-distribution)
|
||||
|
||||
## 附录:Flavor 是什么,日常怎么用
|
||||
|
||||
给还没接触过多环境构建方式的同学看的入门说明。
|
||||
|
||||
### 要解决的问题
|
||||
|
||||
一个 App 通常需要同时存在"开发中还没上线的版本"和"已经上线的正式版本",测试期间还需要一个给验收测试用的版本——这三个版本理想情况下要能**同时装在同一台测试手机上**,方便对比测试,而不是每次切换环境都要卸载重装。同时它们各自要打到不同的后端地址(Dev/UAT/Prod API),不能一个 apk 通过运行时开关切换后端就完事——因为如果只用运行时环境变量,三个环境包的 `applicationId`/`Bundle ID` 完全一样,装第二个会直接覆盖第一个。
|
||||
|
||||
**Flavor** 是 Android(Gradle `productFlavors`,历史悠久的原生概念)和 iOS(Xcode Build Configuration/Scheme)本来就有的机制:在同一份代码基础上,用不同的编译配置产出`applicationId`/图标/名称都不同的多个安装包。Flutter 从工具链层面(`flutter build --flavor xxx`)把两端的 flavor 机制包装成统一的命令行接口。
|
||||
|
||||
### 核心概念
|
||||
|
||||
1. **Android `productFlavors`**:在 `android/app/build.gradle` 里声明多套 `applicationId`/`versionNameSuffix`/资源目录,编译时用 `--flavor` 选择其中一套。
|
||||
2. **iOS Scheme + xcconfig**:iOS 没有 Gradle 那样的单文件配置,而是通过 Xcode 里多个 Build Configuration(对应不同的 `.xcconfig` 文件设置 Bundle ID 等)+ 多个 Scheme 组合实现同样的效果,`flutter build ipa --flavor xxx` 背后就是选中同名 Scheme。
|
||||
3. **Dart 入口文件(`main_xxx.dart`)**:flavor 决定的是"编译出什么样的原生外壳",Dart 代码本身默认只有一个 `main.dart` 入口——项目约定用多个入口文件对应各 flavor,让每个 flavor 能设置不同的启动参数(比如传给 `bootstrap()` 一个环境枚举)。
|
||||
4. **`--dart-define-from-file`**:flavor 解决的是原生层面的差异(图标、包名),但 API 地址这类 Dart 侧读取的配置,用编译期注入的 JSON 文件解决,避免打包进一个写死 `http://dev-api...` 的字符串常量。
|
||||
|
||||
### 使用示例
|
||||
|
||||
Android 侧 flavor 声明(`android/app/build.gradle`):
|
||||
|
||||
```gradle
|
||||
android {
|
||||
flavorDimensions "env"
|
||||
productFlavors {
|
||||
dev {
|
||||
dimension "env"
|
||||
applicationId "com.conti.retail.dev"
|
||||
versionNameSuffix "-dev"
|
||||
}
|
||||
uat {
|
||||
dimension "env"
|
||||
applicationId "com.conti.retail.uat"
|
||||
versionNameSuffix "-uat"
|
||||
}
|
||||
prod {
|
||||
dimension "env"
|
||||
applicationId "com.conti.retail"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
环境配置文件(`env/dev.json`,非敏感部分):
|
||||
|
||||
```json
|
||||
{
|
||||
"API_BASE_URL": "https://dev-api.conti-retail.com",
|
||||
"ENABLE_LOG": true
|
||||
}
|
||||
```
|
||||
|
||||
共享启动入口 + 各 flavor 的 Dart 入口文件:
|
||||
|
||||
```dart
|
||||
// lib/bootstrap.dart —— 三个 flavor 共用的启动逻辑
|
||||
Future<void> bootstrap(AppEnv env) async {
|
||||
runApp(ProviderScope(
|
||||
overrides: [appEnvProvider.overrideWithValue(env)],
|
||||
child: const App(),
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// lib/main_dev.dart
|
||||
void main() => bootstrap(AppEnv.fromDartDefine(name: 'dev'));
|
||||
```
|
||||
|
||||
构建命令:
|
||||
|
||||
```bash
|
||||
flutter build apk \
|
||||
--flavor dev \
|
||||
--target lib/main_dev.dart \
|
||||
--dart-define-from-file=env/dev.json
|
||||
```
|
||||
|
||||
GitLab CI 片段(衔接现有 Runner,产物走 Firebase App Distribution 而非后端用的 ACR):
|
||||
|
||||
```yaml
|
||||
build_dev:
|
||||
stage: build
|
||||
script:
|
||||
- melos run analyze
|
||||
- melos run test
|
||||
- flutter build apk --flavor dev --target lib/main_dev.dart --dart-define-from-file=env/dev.json
|
||||
- firebase appdistribution:distribute build/app/outputs/.../dev/release/app-dev-release.apk
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "develop"'
|
||||
|
||||
build_prod:
|
||||
stage: build
|
||||
script:
|
||||
- flutter build appbundle --flavor prod --target lib/main_prod.dart --dart-define-from-file=env/prod.json
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG' # 只在打 tag 时触发,避免误发生产包
|
||||
```
|
||||
Reference in New Issue
Block a user