186 lines
9.5 KiB
Markdown
186 lines
9.5 KiB
Markdown
# 01. 工程结构 / 分包策略
|
||||
|
|
|
|||
|
|
## 决策
|
|||
|
|
|
|||
|
|
使用 **[Melos](https://melos.invertase.dev/) monorepo**,按 **feature** 拆分成独立 Dart package,而不是单一 Flutter package 内部用文件夹分层。
|
|||
|
|
|
|||
|
|
## 包结构总览
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
conti-app/
|
|||
|
|
melos.yaml
|
|||
|
|
app/ # 壳工程:唯一的 Flutter application,负责路由汇总、DI 装配、编译出 ipa/apk
|
|||
|
|
packages/
|
|||
|
|
core_ui/ # 通用组件、主题、设计 token
|
|||
|
|
core_network/ # dio 封装、拦截器、统一异常
|
|||
|
|
core_storage/ # 本地存储抽象(Drift/secure storage 封装)
|
|||
|
|
core_auth/ # 登录态、token 管理
|
|||
|
|
core_router/ # 路由聚合、公共 route guard
|
|||
|
|
feature_scan/ # 原「扫码」小程序
|
|||
|
|
feature_payment/ # 原「支付」小程序
|
|||
|
|
feature_store/ # 原「门店」小程序
|
|||
|
|
feature_.../
|
|||
|
|
native_scan/ # 原生插件包:扫码(android/ios/ohos 三端实现)
|
|||
|
|
native_payment/
|
|||
|
|
native_bluetooth/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 依赖规则(编译期强制边界,是这套结构的核心价值)
|
|||
|
|
|
|||
|
|
- `app` 可以依赖所有 `core_*` 和 `feature_*`。
|
|||
|
|
- `feature_*` **只能**依赖 `core_*`,**不能**相互依赖(`feature_payment` 的 `pubspec.yaml` 里不允许出现 `feature_store` 的 path dependency)。
|
|||
|
|
- `core_*` 之间尽量不互相依赖;唯一允许的例外是 `core_network` 依赖 `core_auth`(取 token 做请求签名/刷新)。
|
|||
|
|
- `feature_*` 可以依赖对应的 `native_*` 包(如 `feature_scan` 依赖 `native_scan`)。
|
|||
|
|
- `native_*` 只依赖 Flutter SDK 和 [Pigeon](https://pub.dev/packages/pigeon) 生成的代码,不依赖任何 `core_*` / `feature_*`——保证原生插件包可以脱离业务单独编译、单独测试(详见 [07-native-integration.md](./07-native-integration.md))。
|
|||
|
|
|
|||
|
|
这些规则由 Dart 的包依赖机制**物理强制**:`feature_a` 根本 import 不到 `feature_b` 的任何符号,不是靠代码规范或 review 口头约束。
|
|||
|
|
|
|||
|
|
## Feature 间通信怎么处理
|
|||
|
|
|
|||
|
|
这是最容易被绕开、也是这套边界能否守住的关键点,必须写清楚合法方式:
|
|||
|
|
|
|||
|
|
1. **路由跳转 + 可序列化参数**(多数场景)——比如从 `feature_store` 跳到 `feature_payment`,通过 `core_router` 声明的路径 + query/extra 参数传递,不直接引用对方的 Dart 类型。
|
|||
|
|
2. **通过 `core_*` 定义的抽象接口 + DI 注册实现**——真正需要跨 feature 拿数据或发通知的场景(比如支付完成后要清空购物车),在某个 `core_*` 包里定义接口,各 feature 各自实现并在 `app` 层注册,调用方只依赖 `core_*` 里的抽象类型。
|
|||
|
|
|
|||
|
|
**不允许**的做法:任何 `feature_*` 在 `pubspec.yaml` 里直接 path dependency 另一个 `feature_*`,哪怕只是想复用一个 widget——这种情况应该把这个 widget 提到 `core_ui`。
|
|||
|
|
|
|||
|
|
## 命名规范
|
|||
|
|
|
|||
|
|
- `core_xxx`:基础设施层,不含具体业务逻辑。
|
|||
|
|
- `feature_xxx`:对应一个原小程序/业务域。
|
|||
|
|
- `native_xxx`:原生能力插件包,内部含 `android/`、`ios/`、`ohos/` 三套原生实现目录。
|
|||
|
|
|
|||
|
|
## Melos 配置示例(8.x,基于 Dart Pub Workspaces)
|
|||
|
|
|
|||
|
|
Melos 7.0 起改用 Dart 官方原生的 **[Pub Workspaces](https://dart.dev/tools/pub/workspaces)** 机制,不再有独立的 `melos.yaml` 文件,配置写进根目录 `pubspec.yaml`;每个子包的 `pubspec.yaml` 需要加 `resolution: workspace`。要求 **Dart SDK ≥ 3.6.0**(我们的基线 Flutter 3.44.8 对应 Dart 3.12.2,满足要求)。
|
|||
|
|
|
|||
|
|
根目录 `pubspec.yaml`:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
name: conti_app
|
|||
|
|
publish_to: none
|
|||
|
|
environment:
|
|||
|
|
sdk: ^3.12.0
|
|||
|
|
|
|||
|
|
workspace:
|
|||
|
|
- app
|
|||
|
|
- packages/core_ui
|
|||
|
|
- packages/core_network
|
|||
|
|
- packages/core_storage
|
|||
|
|
- packages/core_auth
|
|||
|
|
- packages/core_router
|
|||
|
|
- packages/feature_scan
|
|||
|
|
- packages/feature_payment
|
|||
|
|
- packages/feature_store
|
|||
|
|
- packages/native_scan
|
|||
|
|
- packages/native_payment
|
|||
|
|
- packages/native_bluetooth
|
|||
|
|
|
|||
|
|
dev_dependencies:
|
|||
|
|
melos: ^8.2.2
|
|||
|
|
|
|||
|
|
melos:
|
|||
|
|
scripts:
|
|||
|
|
analyze:
|
|||
|
|
run: melos exec -- flutter analyze
|
|||
|
|
test:
|
|||
|
|
run: melos exec -- flutter test
|
|||
|
|
format:
|
|||
|
|
run: melos exec -- dart format --set-exit-if-changed .
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
每个子包(比如 `packages/feature_scan/pubspec.yaml`):
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
name: feature_scan
|
|||
|
|
resolution: workspace
|
|||
|
|
|
|||
|
|
dependencies:
|
|||
|
|
core_ui:
|
|||
|
|
path: ../core_ui
|
|||
|
|
core_network:
|
|||
|
|
path: ../core_network
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 新增 feature 包的标准脚手架
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
feature_xxx/
|
|||
|
|
pubspec.yaml # resolution: workspace + 依赖 core_ui / core_network / core_router 等,不依赖其他 feature
|
|||
|
|
lib/
|
|||
|
|
feature_xxx.dart # 唯一对外导出文件(barrel file):只暴露路由注册函数和必要的 public widget
|
|||
|
|
src/
|
|||
|
|
presentation/
|
|||
|
|
domain/ # 可选,见下方分层规范文档
|
|||
|
|
data/
|
|||
|
|
test/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`src/` 目录下的内容视为包内私有实现,只有 `feature_xxx.dart` 这一个文件是对外契约——这条靠 code review 检查,Dart 语言本身没有强制的 package-private 关键字。`domain/` 目录的取舍规则详见 [02-layering.md](./02-layering.md)(待写)。
|
|||
|
|
|
|||
|
|
## 版本管理
|
|||
|
|
|
|||
|
|
不发布到 pub.dev,全部用 melos 的 path dependency,包版本号跟随 `app` 的整体版本号统一管理(fixed versioning),不做 melos 的 independent versioning——没有对外发布需求,独立版本号只会增加维护负担。
|
|||
|
|
|
|||
|
|
## 附录:Melos 是什么,日常怎么用
|
|||
|
|
|
|||
|
|
给没接触过 Dart 多包仓库工具的同学看的入门说明。
|
|||
|
|
|
|||
|
|
### 要解决的问题
|
|||
|
|
|
|||
|
|
Dart 官方的包管理工具 `pub` 天生只认"一个 `pubspec.yaml` = 一个包"。如果要在同一个 git 仓库里维护多个互相依赖的私有包(比如 `app` 依赖 `feature_scan`,`feature_scan` 依赖 `core_network`),原生 pub 只支持手动在每个包的 `pubspec.yaml` 里写 `path: ../../packages/core_network` 这种相对路径依赖——能跑,但没有任何批量操作能力:想给所有包统一跑一次 `flutter analyze`、`flutter test`,或者统一升级某个第三方库版本,都得一个包一个包手动进去执行。
|
|||
|
|
|
|||
|
|
**Melos 就是给这种多包仓库提供批量管理能力的工具**,类似 JS 生态里的 [Lerna](https://lerna.js.org/)/Nx,只不过是 Dart/Flutter 版本。它不改变 Dart 语言或 pub 本身的机制,只是在多个包外面包一层"批处理脚本 + 配置"。
|
|||
|
|
|
|||
|
|
### 核心概念
|
|||
|
|
|
|||
|
|
1. **根目录 `pubspec.yaml` 里的 `workspace:` 字段 + `melos:` 配置块**:8.x 版本不再有独立的 `melos.yaml` 文件(7.0 之前是独立文件,现已合并进 Dart 官方原生的 Pub Workspaces 机制)。`workspace:` 列出所有子包路径,`melos:` 块下的 `scripts:` 定义可复用脚本(见上文示例)。要求 Dart SDK ≥ 3.6.0。
|
|||
|
|
2. **`melos bootstrap`**(简写 `melos bs`):一键解析 workspace 内所有包之间的依赖关系——自动生成 `pubspec_overrides.yaml`,把 `feature_scan` 依赖 `core_network` 这种关系用本地路径链接起来,不用手写相对路径,也不需要真的发布到 pub.dev 才能互相依赖。**新人拉下代码后第一步永远是跑这个命令**。
|
|||
|
|
3. **`melos exec`**:在每一个包目录下依次/并行执行同一条命令,比如 `melos exec -- flutter test` 就是把所有包都跑一遍测试,替代手动 `cd packages/feature_scan && flutter test && cd ../feature_payment && ...`。
|
|||
|
|
4. **`melos run <script-name>`**:调用 `melos.yaml` 里预定义的脚本别名(比如上文的 `melos run test`),团队里统一敲固定命令,不用记 `exec` 的完整写法。
|
|||
|
|
|
|||
|
|
### 日常开发流程(拿本仓库举例)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. 第一次拉代码,或者别人加了新包/新依赖之后
|
|||
|
|
melos bootstrap
|
|||
|
|
|
|||
|
|
# 2. 正常改代码,比如在 feature_scan 里改一个页面
|
|||
|
|
cd packages/feature_scan
|
|||
|
|
flutter run # 这一步跟平时写单个 Flutter 项目完全一样,感觉不到 melos 的存在
|
|||
|
|
|
|||
|
|
# 3. 提交前,跑一遍全仓库检查
|
|||
|
|
melos run analyze
|
|||
|
|
melos run test
|
|||
|
|
|
|||
|
|
# 4. 新增了 feature 包,或者某个包新增了对另一个包的依赖之后
|
|||
|
|
melos bootstrap # 重新解析依赖关系
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**关键体感**:平时在某一个包里写代码、`flutter run`、热重载,跟没有 melos 时完全一样——melos 只在"跨包操作"(装依赖、批量测试、批量分析)时才会用到,不侵入日常单包开发的手感。
|
|||
|
|
|
|||
|
|
### 常见的坑
|
|||
|
|
|
|||
|
|
- 加了新包,或改了某个包的依赖之后忘记跑 `melos bootstrap`,会出现"明明加了依赖但 import 不到"的报错——看到这个报错先跑一遍 bootstrap 再排查。
|
|||
|
|
- 8.x 基于 Pub Workspaces 后,正常的包间链接**不再**依赖 `pubspec_overrides.yaml`(这是 7.0 之前版本的机制);只有配置了额外的 `dependencyOverridePaths`(用于覆盖外部第三方依赖,不是本仓库包之间的常规场景)时才会生成这个文件。如果看到这个文件出现却不记得配置过覆盖路径,说明配置可能有误,需要检查。
|
|||
|
|
|
|||
|
|
### 安装
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
dart pub global activate melos
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
全局命令,装一次即可,不需要每个项目单独安装。
|
|||
|
|
|
|||
|
|
## 参考链接
|
|||
|
|
|
|||
|
|
- [Melos 官方文档](https://melos.invertase.dev/)
|
|||
|
|
- [melos | Dart package (pub.dev)](https://pub.dev/packages/melos)
|
|||
|
|
- [Melos changelog](https://pub.dev/packages/melos/changelog)
|
|||
|
|
- [Melos Configuration overview](https://melos.invertase.dev/configuration/overview)
|
|||
|
|
- [Dart Pub Workspaces 官方文档](https://dart.dev/tools/pub/workspaces)
|
|||
|
|
- [Pigeon | Dart package](https://pub.dev/packages/pigeon)
|
|||
|
|
- [Drift | Dart package](https://pub.dev/packages/drift)
|
|||
|
|
- [Lerna(JS 生态对标工具)](https://lerna.js.org/)
|
|||
|
|
|