修复旅拍相册 API 未注入共享 APIClient 导致未登录错误。
在 RootView 注入 travelAlbumAPI,并将 travel_album 纳入首页常用功能;补充 App/Networking 依赖注入说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -23,6 +23,59 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
||||
- `AuthSessionCoordinator`:统一处理登录完成、退出登录、偏好读取和账号快照刷新。
|
||||
- `SessionBootstrapper`:冷启动时读取本地 token 和账号快照,并向服务端校验登录态。
|
||||
- `AccountContextLoader`:统一同步用户资料、角色权限、景区和门店。
|
||||
- `AppServiceEnvironment.swift`:为无 UI 状态的服务定义 SwiftUI `EnvironmentKey`,供 View 通过 `@Environment(\.xxxAPI)` 读取。
|
||||
|
||||
## 依赖注入:为何 API 走 Environment 而非单例
|
||||
|
||||
SwiftUI 的 `Environment` 在本项目中是**依赖注入(DI)通道**,不等于「UI 状态容器」。注入对象分两类:
|
||||
|
||||
| 类型 | 机制 | 代表 |
|
||||
|------|------|------|
|
||||
| 会驱动 UI 刷新的状态 | `environmentObject` + `ObservableObject` | `AppSession`、`ToastCenter` |
|
||||
| 无 UI 状态的服务 | 自定义 `EnvironmentKey` | `OrdersAPI`、`ProfileAPI` |
|
||||
|
||||
### 数据流
|
||||
|
||||
1. `RootView.init()` 创建**一个** `APIClient`,再传给所有 `*API(client:)`。
|
||||
2. `RootView.body` 通过 `.environment(\.ordersAPI, ordersAPI)` 等下发到整棵视图树。
|
||||
3. `task` 中 `apiClient.bindAuthTokenProvider { appSession.token }` 绑定 token。
|
||||
4. View 用 `@Environment(\.ordersAPI)` 读取,调用 ViewModel 时作为参数传入,例如 `await viewModel.reload(api: ordersAPI, ...)`。
|
||||
5. ViewModel 不持有 API,方法签名接收 `XxxServing` 协议,单元测试直接传 Mock。
|
||||
|
||||
网络层细节见 [Networking 模块](../Core/Networking/Networking.md)。
|
||||
|
||||
### 为何不用「每个 API 一个单例」
|
||||
|
||||
**核心原因:需要共享的是 `APIClient`,不是 API 类本身。**
|
||||
|
||||
`AuthAPI`、`OrdersAPI` 等只是 `APIClient` 上的薄封装,本身无状态。全 App 必须只有一个 `APIClient`,才能保证 token provider 只绑定一次、登录后 token 对所有请求一致、`tokenOverride`(如 `set-user`)走同一客户端。若每个 API 各自 `static let shared = XxxAPI(client: APIClient())`,容易在不经意间创建多个 client,导致 token 不同步。
|
||||
|
||||
`RootView` 作为**组合根**集中创建并装配依赖,例如:
|
||||
|
||||
- `OSSUploadService(configService: uploadAPI)` 依赖 `UploadAPI`
|
||||
- `SessionBootstrapper`、`AuthSessionCoordinator` 在 init 时注入具体 API
|
||||
- `PushNotificationManager.shared.configure(api: pushAPI, ...)` 需要与 RootView 相同的 `pushAPI` 实例
|
||||
|
||||
### Environment 的其他收益
|
||||
|
||||
- **Preview / 局部替换**:可在子树 `.environment(\.ordersAPI, mockAPI)` 替换,不影响全局;`AppServiceEnvironment` 的 `defaultValue` 供 Preview 兜底。
|
||||
- **测试**:ViewModel 测试靠 `XxxServing` 协议 + 方法参数注入 Mock,不依赖 Environment;Environment 主要给 View 层省掉层层传参。
|
||||
- **历史选择**:iOS 17 迁移到 iOS 16 时,UI 状态统一 `environmentObject`,服务统一自定义 `EnvironmentKey`(见根目录 `iOS16兼容迁移记录.md`)。
|
||||
|
||||
### 单例是否可行
|
||||
|
||||
可以,但应有纪律。项目里已有单例先例:`PushNotificationManager.shared`、`PaymentVoiceSpeaker.shared`、`CloudTransferStore.shared`——特点是跨模块、与 UI 树无关、需在 AppDelegate 或后台回调中访问。
|
||||
|
||||
若全面改单例,推荐收敛为一个 `AppServices` 容器共享同一 `client`,而不是 20 个 `OrdersAPI.shared`。运行时行为可与当前方案等价,差别在可维护性和团队习惯。
|
||||
|
||||
### 当前方案的代价
|
||||
|
||||
1. `AppServiceEnvironment.swift` 约 25 组 Key,`RootView` 大量 `@State` / `.environment` 行,样板代码多。
|
||||
2. `Environment` 命名易误导为 UI 绑定,实际是 service locator。
|
||||
3. Key 的 `defaultValue` 会 `new` 独立 `APIClient()`,仅未从 `RootView` 注入时使用;Preview 需注意是否拿到真实实例。
|
||||
4. View 用 `@Environment`,ViewModel 用方法参数,View 充当桥接层。
|
||||
|
||||
若将来觉得 Key 过多,可收敛为一个 `AppServices` 对象注入一次,属于简化写法,不是架构对错。
|
||||
|
||||
## 启动流程
|
||||
|
||||
|
||||
Reference in New Issue
Block a user