app scaffold

This commit is contained in:
Guangfei.Zhao
2026-08-17 15:29:55 +08:00
commit 681688dfae
301 changed files with 18414 additions and 0 deletions
+289
View File
@@ -0,0 +1,289 @@
# 14. 工程规范与 CI 门禁
## 为什么单独一篇
这一篇是**建项目当天就要用上**的东西:lint 配置、格式化、生成产物是否入库、分支和提交规范、CI 卡什么。这些规则本身不难,难的是"没有在第一天定下来"——等到有 10 个人各写各的风格再统一,成本是第一天的几十倍。
## 一、SDK 版本锁定
```
# .fvmrc(仓库根目录,入库)
{ "flutter": "3.44.9" }
```
所有人用 [FVM](https://fvm.app/) 装同一个版本,命令统一走 `fvm flutter ...`。理由见 [01-project-structure.md](./01-project-structure.md)monorepo 里 SDK 版本不一致会导致 `.dart_tool` 反复重建、生成代码差异、以及"我这跑得好好的"这类无法复现的问题。CI 也用 FVM 装同一版本,保证本地和 CI 完全一致。
`flutter --version` 的实测 Dart 版本要写进 README,因为 `environment.sdk` 的约束以它为准。
## 二、静态分析
### 选 `flutter_lints`,不选 `very_good_analysis`
| | `flutter_lints` 6.0.0 | `very_good_analysis` 10.3.0 |
|---|---|---|
| 维护方 | **Flutter 官方** | Very Good Ventures |
| 规则数量 | 适中,只收官方认为普遍适用的 | 非常多,包含大量风格约束 |
| 跟随 SDK | 随 Flutter 版本同步更新 | 独立节奏 |
`very_good_analysis` 更严格,但它在一个新项目上开箱会产生**成百上千条 warning**,其中很大一部分是纯风格问题(比如强制所有 public API 写文档注释、强制 `final` 局部变量)。团队的第一反应必然是批量 `// ignore:` 或者在 `analysis_options.yaml` 里关掉一半规则——最后既没享受到严格的好处,还多了一层配置负担。
**结论:以 `flutter_lints` 为底,手动加一小组"能抓真 bug"的规则,而不是"管风格"的规则。**
### 根级共享配置
```yaml
# analysis_options.yaml(仓库根目录)
include: package:flutter_lints/flutter.yaml
analyzer:
language:
strict-casts: true # 禁止 dynamic 隐式转型——最容易藏 bug 的一条
strict-raw-types: true # 禁止裸 List/Map,逼着写类型参数
strict-inference: true
errors:
invalid_annotation_target: ignore # json_serializable + 注解组合会误报
# 下面几条从 warning 提到 error,即 CI 直接失败
unused_import: error
dead_code: error
unawaited_futures: error
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/generated/**" # pigeon 生成产物,见 07
plugins:
- custom_lint # riverpod_lint,见 03
formatter:
page_width: 100
linter:
rules:
# —— 能抓真 bug 的 ——
- always_declare_return_types
- avoid_dynamic_calls
- avoid_slow_async_io
- cancel_subscriptions # StreamSubscription 忘了 cancel 是常见内存泄漏
- close_sinks
- discarded_futures # 忘了 await 的异步调用
- unawaited_futures
- no_adjacent_strings_in_list # 少写一个逗号导致字符串被拼接
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
# —— 团队约定 ——
- prefer_single_quotes
- require_trailing_commas # 配合 formatterdiff 更干净
- directives_ordering
- sort_pub_dependencies
```
各包的 `analysis_options.yaml` 只写一行继承,不允许在包级关规则(要关就在根上关,让所有人都看得见):
```yaml
# packages/feature_xxx/analysis_options.yaml
include: ../../analysis_options.yaml
```
### `strict-casts` 值得单独说
它是这份配置里**唯一一条会真的挡住线上 bug** 的开关。没有它,`jsonDecode(...)` 返回的 `dynamic` 可以隐式赋给任何类型,类型错误要到运行时才炸;开了之后必须显式 `as Map<String, dynamic>`,写的人会被迫想一下"这里到底是什么类型"。
代价是接手 JSON 解析时要多写一些 `as`。这个代价值得付。
### `custom_lint` 在 workspace 下的接法
`riverpod_lint`(见 [03-state-management.md](./03-state-management.md))通过 `custom_lint` 插件运行。在 pub workspace 下:
- `custom_lint``riverpod_lint` 加在**根 `pubspec.yaml``dev_dependencies`**workspace 共享)。
- 检查命令是 `dart run custom_lint`**它不包含在 `flutter analyze` 里**——两条命令都要跑,CI 里是两个独立步骤。这一点很多人不知道,结果 riverpod_lint 装了但从来没生效过。
## 三、格式化
```bash
dart format --set-exit-if-changed --line-length 100 .
```
- **行宽 100,不是默认的 80。** Dart 3.9 起可以写在 `analysis_options.yaml``formatter: page_width:` 里(上面已配),命令行参数是给 CI 用的双保险。80 在 Flutter 的 widget 嵌套下换行过于频繁,一个三层嵌套的 `Column` 就能占满整屏。100 是一个在宽屏和可读性之间比较平衡的值。
- **不允许手动排版**。`dart format` 的结果就是唯一正确的结果,不接受"我觉得这样更好看"。省下的是每次 review 里关于换行的争论。
- CI 用 `--set-exit-if-changed` 卡死。
## 四、生成产物是否入库
**这是一个必须明确的二选一,模糊处理会导致仓库里一半入库一半不入库。**
| 类型 | 结论 | 理由 |
|---|---|---|
| `*.g.dart`riverpod / json_serializable / drift | **不入库** | 这类文件改动频繁且巨大,几乎每个 PR 都会产生冲突,而冲突的正确解法永远是"重新生成"——那入库就没有意义。加进 `.gitignore` |
| pigeon 生成产物(Dart + Kotlin + Swift | **入库** | 见 [07-native-integration.md](./07-native-integration.md)。原生侧的 Kotlin/Swift 文件要被 Gradle/Xcode 编译,而**这两条工具链不会跑 `build_runner`**。不入库的话原生构建直接失败 |
| `pubspec.lock` | 根目录**入库**,各 package 的**不入库** | workspace 模式下只有根 lock 生效 |
不入库 `.g.dart` 的代价是:**新克隆仓库后必须先跑一次生成,否则 IDE 满屏报错**。所以:
```yaml
# 根 pubspec.yaml 的 melos scripts
gen:
run: melos exec --depends-on=build_runner -- dart run build_runner build --delete-conflicting-outputs
gen:watch:
run: melos exec --depends-on=build_runner -- dart run build_runner watch --delete-conflicting-outputs
```
README 的"第一次跑起来"步骤必须是:`fvm flutter pub get``melos run gen``fvm flutter run`。**少写这一步,每个新人入职第一天都会卡住。**
CI 在 analyze 之前必须先 `melos run gen`
**pigeon 产物入库需要一道防腐**CI 里重新生成后 `git diff --exit-code`,确保有人改了 schema 但忘了提交生成结果时流水线会红(见 07)。
## 五、分支与提交
### 分支
```
main ← 生产,只接受来自 release/* 和 hotfix/* 的合并,打 tag 出包
develop ← 集成,日常合并目标
feature/<jira-id>-<短描述>
fix/<jira-id>-<短描述>
release/<version>
hotfix/<version>
```
`main`/`develop` **保护分支,禁止直接 push**,只能通过 MR 合入。
### 提交信息
用 [Conventional Commits](https://www.conventionalcommits.org/)
```
feat(feature_purchase): 支持采购单批量提交
fix(core_network): 修复 401 并发刷新导致全端登出
docs(05): 补充上传失败重传约定
chore(deps): 升级 drift 到 2.34.5
```
`scope` 用**包名**`feature_purchase``core_network`)或文档编号。monorepo 里没有 scope 的提交信息基本等于没有信息——`fix: 修复崩溃` 在半年后完全无法定位。
不引入自动化的 changelog 生成(首版没这个需求),但格式先立住,将来要加成本为零。
### MR 规范
- MR 标题同 commit 规范。
- 描述里必须有:**改了什么**、**为什么**、**怎么验证的**。
- **一个 MR 只做一件事。** 顺手格式化半个仓库的 MR 直接打回——它会让 review 变成不可能。
- 至少 1 人 approve。涉及 `core_*` 的改动需要 2 人(这些包被所有 feature 依赖,改错影响面最大)。
## 六、CI 门禁
```yaml
# .gitlab-ci.ymlApp 部分,与 08-build-flavors.md 的构建 job 拼在一起)
stages: [setup, verify, test, build]
.flutter_base: &flutter_base
image: <内部 flutter 镜像,预装 FVM 3.44.9>
before_script:
- fvm flutter --version
- dart pub global activate melos 8.2.2
- melos bootstrap
- melos run gen # 生成产物不入库,必须先生成
cache:
key: "$CI_COMMIT_REF_SLUG"
paths: [.dart_tool/, .pub-cache/]
format:
<<: *flutter_base
stage: verify
script: dart format --set-exit-if-changed --line-length 100 .
analyze:
<<: *flutter_base
stage: verify
script:
- melos exec -- fvm flutter analyze --fatal-infos
- dart run custom_lint # riverpod_lintanalyze 不含它
pigeon_check:
<<: *flutter_base
stage: verify
script:
- melos run gen:pigeon
- git diff --exit-code || (echo "pigeon 生成产物未提交" && exit 1)
test:
<<: *flutter_base
stage: test
script:
- melos run test
- melos run coverage # 阈值 60%,见 09
coverage: '/lines\.*: \d+\.\d+\%/'
artifacts:
paths: [coverage/]
reports: { coverage_report: { coverage_format: cobertura, path: coverage/cobertura.xml } }
```
### 门禁清单
| 检查 | 卡点 | 说明 |
|---|---|---|
| `dart format` | **阻断** | |
| `flutter analyze --fatal-infos` | **阻断** | `--fatal-infos` 让 info 级别也算失败,否则 lint 规则形同虚设 |
| `dart run custom_lint` | **阻断** | |
| pigeon 产物一致性 | **阻断** | |
| 单元测试 + Widget 测试 | **阻断** | |
| 覆盖率 ≥ 60% | **阻断** | 见 [09-testing.md](./09-testing.md) |
| 集成测试 | **不卡 MR**,只在合入 develop/main 时跑 | 慢,见 09 |
| Android release 构建 | 只在 tag 上跑 | 见 [08-build-flavors.md](./08-build-flavors.md) |
`--fatal-infos` 值得强调:不加这个参数,`flutter analyze` 对 info 级别的问题只是打印一下就返回 0,CI 永远绿。半年后仓库里会积累几百条 info,然后没人再看 analyze 的输出。
### 关于 `melos bootstrap` 的缓存
`.pub-cache` 必须缓存,否则每次 CI 都要重新下载所有依赖,一个 monorepo 下来是几分钟。缓存 key 用 `$CI_COMMIT_REF_SLUG`(按分支),并配一个按 `pubspec.yaml` 哈希的 fallback key。
## 七、本地钩子(可选但推荐)
```yaml
# lefthook.yml
pre-commit:
parallel: true
commands:
format:
glob: "*.dart"
run: dart format --line-length 100 {staged_files} && git add {staged_files}
analyze:
glob: "*.dart"
run: fvm flutter analyze --fatal-infos {staged_files}
```
**只跑 format 和 analyze,不跑测试。** pre-commit 跑测试会让每次提交等几十秒,人的第一反应是 `--no-verify`,钩子就废了。测试留给 CI。
钩子是**建议不是强制**——CI 才是真正的门禁。钩子的价值只是让人少推一次红色流水线。
## 八、目录与命名速查
| 项 | 约定 |
|---|---|
| 包名 / 目录 / 文件 | `snake_case` |
| 类 / enum | `UpperCamelCase` |
| 变量 / 方法 | `lowerCamelCase`,私有加 `_` |
| 常量 | `lowerCamelCase`Dart 惯例,不是 `SCREAMING_CASE` |
| 文件名 | 与主类名对应:`OrderListPage``order_list_page.dart` |
| provider | `xxxProvider`,由 `@riverpod` 生成,不手写 |
| 测试文件 | `<被测文件>_test.dart`,目录镜像 `lib/src/` |
| 包的公共 API | 只从 `lib/<package>.dart` 导出,`lib/src/` 下的一律视为私有(见 01 |
import 顺序由 `directives_ordering` 强制:`dart:``package:`(外部)→ `package:`(本仓库)→ 相对路径。
**包内用相对路径 import,跨包用 `package:`。** 混用会导致同一个类被 Dart 认为是两个不同的类型(典型症状:`type 'X' is not a subtype of type 'X'`),这个错误看起来完全不可理喻,实际就是 import 路径不一致。
## 待确认项
- GitLab Runner 上是否已有可用的 Flutter 镜像,还是需要自建(与 [08-build-flavors.md](./08-build-flavors.md) 的 runner 问题一起解决)。
- JIRA(或其他)issue key 的格式,用于分支和提交信息里的 `<jira-id>`
- 是否引入 lefthook(需要每个人本地 `lefthook install` 一次)。
## 参考链接
- [flutter_lints | Dart package](https://pub.dev/packages/flutter_lints)
- [Dart: Customizing static analysis](https://dart.dev/tools/analysis)
- [Dart linter rules 全量列表](https://dart.dev/tools/linter-rules)
- [FVM 官方文档](https://fvm.app/)
- [Conventional Commits](https://www.conventionalcommits.org/)
- [Melos 官方文档](https://melos.invertase.dev/)