Compare commits
1 Commits
codex/new-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
491eb27cd2 |
1091
docs/guides/core-contracts-five-rules-preview.html
Normal file
1091
docs/guides/core-contracts-five-rules-preview.html
Normal file
File diff suppressed because it is too large
Load Diff
1348
docs/guides/current-project-core-evolution-summary.html
Normal file
1348
docs/guides/current-project-core-evolution-summary.html
Normal file
File diff suppressed because it is too large
Load Diff
350
docs/guides/loxodon-framework-core-analysis.md
Normal file
350
docs/guides/loxodon-framework-core-analysis.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Loxodon Framework Core 分析
|
||||
|
||||
> 分析对象:<https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework>
|
||||
>
|
||||
> 分析日期:2026-06-12
|
||||
|
||||
## 结论
|
||||
|
||||
Loxodon Framework 的 Core 本质上是一个 **Unity MVVM + DataBinding 运行时核心**,而不是覆盖完整游戏生命周期的游戏框架内核。
|
||||
|
||||
它的核心价值集中在:
|
||||
|
||||
- 用 `Context` 和 `ServiceContainer` 提供全局/局部上下文与轻量服务注册。
|
||||
- 用 `BindingServiceBundle` 装配 DataBinding 所需的 Parser、Converter、SourceProxy、TargetProxy、Binder。
|
||||
- 用 `ViewModelBase`、`ObservableObject`、Observable Collection 支撑 ViewModel 状态变化。
|
||||
- 用 `UIView`、`Window`、`WindowManager` 提供 UI 视图、窗口状态和转场管理。
|
||||
- 用 `Messenger`、`Command`、`Async`、`Prefs`、`Localization` 等模块补齐 MVVM UI 开发所需的基础设施。
|
||||
|
||||
因此,Loxodon 更适合作为 FlowScope 的 **UI / Binding / ServiceBundle 外围模块参考**,不适合作为 FlowScope Kernel 的边界模板。
|
||||
|
||||
## Core 模块拆解
|
||||
|
||||
### Context
|
||||
|
||||
源码位置:
|
||||
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Contexts/Context.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Contexts/ApplicationContext.cs>
|
||||
|
||||
`Context` 维护静态 `ApplicationContext` 和按 key 注册的多个 `Context`,每个 Context 内部有:
|
||||
|
||||
- `IServiceContainer`
|
||||
- 属性字典
|
||||
- 可级联查询的 parent context
|
||||
|
||||
`ApplicationContext` 在 `Context` 基础上补了:
|
||||
|
||||
- `IMainLoopExecutor`
|
||||
- Global Preferences
|
||||
- User Preferences
|
||||
|
||||
这个设计方便应用层快速拿到全局能力,但也说明 Loxodon 的 Core 并不是纯机制内核。它把主线程执行器和 Prefs 这类应用级能力直接放进了全局上下文。
|
||||
|
||||
### ServiceContainer
|
||||
|
||||
源码位置:
|
||||
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Services/ServiceContainer.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Services/AbstractServiceBundle.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Services/IServiceBundle.cs>
|
||||
|
||||
`ServiceContainer` 是轻量服务定位器,支持:
|
||||
|
||||
- 按 `Type` 注册对象或 factory。
|
||||
- 按 `string name` 注册对象或 factory。
|
||||
- `Resolve<T>()` / `Resolve(string)` 解析服务。
|
||||
- `Unregister<T>()` / `Unregister(string)` 注销服务。
|
||||
|
||||
`ServiceBundle` 是模块装配入口:
|
||||
|
||||
```csharp
|
||||
public interface IServiceBundle
|
||||
{
|
||||
void Start();
|
||||
void Stop();
|
||||
}
|
||||
```
|
||||
|
||||
`AbstractServiceBundle` 把容器传给 `OnStart` / `OnStop`,由具体 bundle 负责注册和注销一组服务。
|
||||
|
||||
这个模式对 FlowScope 有参考价值:模块可以用显式 `Start/Stop` 完成能力注册,避免散落的静态初始化。
|
||||
|
||||
### Binding
|
||||
|
||||
源码位置:
|
||||
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Binding/BindingServiceBundle.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Binding>
|
||||
|
||||
Binding 是 Loxodon Core 最厚、最核心的部分。
|
||||
|
||||
`BindingServiceBundle` 会装配:
|
||||
|
||||
- `PathParser`
|
||||
- `ExpressionPathFinder`
|
||||
- `ConverterRegistry`
|
||||
- `ObjectSourceProxyFactory`
|
||||
- `SourceProxyFactory`
|
||||
- `TargetProxyFactory`
|
||||
- `BindingFactory`
|
||||
- `StandardBinder`
|
||||
|
||||
然后注册:
|
||||
|
||||
- `IBinder`
|
||||
- `IBindingFactory`
|
||||
- `IConverterRegistry`
|
||||
- `IExpressionPathFinder`
|
||||
- `IPathParser`
|
||||
- `INodeProxyFactory`
|
||||
- `ISourceProxyFactory`
|
||||
- `ITargetProxyFactory`
|
||||
|
||||
这说明 Loxodon 的 Binding 不是简单的反射赋值,而是一条可扩展管线:
|
||||
|
||||
```text
|
||||
BindingDescription
|
||||
-> Path / Expression
|
||||
-> SourceProxy
|
||||
-> Converter
|
||||
-> TargetProxy
|
||||
-> Binding
|
||||
```
|
||||
|
||||
README 也强调该框架围绕 DataBinding 做了性能优化:减少装箱、降低 GC、通过动态委托或静态织入降低反射成本。
|
||||
|
||||
### ViewModel / Observable
|
||||
|
||||
源码位置:
|
||||
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/ViewModels/ViewModelBase.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Observables/ObservableObject.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Observables>
|
||||
|
||||
`ViewModelBase` 继承 `ObservableObject`,核心职责是:
|
||||
|
||||
- 封装属性变更。
|
||||
- 触发 `PropertyChanged`。
|
||||
- 可选通过 `IMessenger` 广播 `PropertyChangedMessage<T>`。
|
||||
|
||||
典型路径是:
|
||||
|
||||
```text
|
||||
ViewModel property set
|
||||
-> Set<T>
|
||||
-> field updated
|
||||
-> RaisePropertyChanged
|
||||
-> optional Messenger.Publish(PropertyChangedMessage<T>)
|
||||
-> Binding updates target UI
|
||||
```
|
||||
|
||||
这是标准 MVVM 的数据驱动 UI 模型。
|
||||
|
||||
### View / Window
|
||||
|
||||
源码位置:
|
||||
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Views/UIView.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Views/Window.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Views/WindowManager.cs>
|
||||
|
||||
`UIView` 负责 Unity UI 基础行为:
|
||||
|
||||
- `RectTransform`
|
||||
- `CanvasGroup`
|
||||
- `Visibility`
|
||||
- Enter / Exit Animation
|
||||
- Enable / Disable event
|
||||
|
||||
`Window` 在 `UIView` 基础上增加:
|
||||
|
||||
- `WindowType`
|
||||
- `windowPriority`
|
||||
- `WindowState`
|
||||
- activated / dismissed / visibility changed event
|
||||
- state broadcast
|
||||
|
||||
`WindowManager` 管理窗口集合、当前窗口、可见窗口和转场执行。
|
||||
|
||||
这部分是 UI 框架能力,不是游戏 Feature 生命周期管理。
|
||||
|
||||
### Messaging / Command / Async / Prefs
|
||||
|
||||
源码位置:
|
||||
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Messaging/Messenger.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Commands/RelayCommand.cs>
|
||||
- <https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Asynchronous>
|
||||
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Prefs/Preferences.cs>
|
||||
|
||||
这些模块服务于 MVVM/UI 工程化:
|
||||
|
||||
- `Messenger`:按消息类型和 channel 做发布订阅。
|
||||
- `RelayCommand`:给 UI 事件绑定命令。
|
||||
- `AsyncResult` / `ProgressResult` / `CoroutineTask`:把异步、协程、进度结果统一成可等待对象。
|
||||
- `Preferences`:默认基于 `PlayerPrefs`,支持全局和用户偏好。
|
||||
|
||||
这些能力有实用价值,但它们放在 Core 中会让 Core 变厚。对 FlowScope 来说,应当拆成独立模块或适配层,而不是进入 Kernel。
|
||||
|
||||
## 架构优点
|
||||
|
||||
### 1. MVVM UI 工程化成熟
|
||||
|
||||
Binding、ViewModel、Command、Window、Messenger 形成了完整闭环。对于 UI 重、状态同步复杂、需要数据绑定的 Unity 项目,Loxodon 能显著减少手写 UI 刷新代码。
|
||||
|
||||
### 2. Binding 管线拆得足够细
|
||||
|
||||
`SourceProxy`、`TargetProxy`、`Converter`、`PathParser`、`Binder` 的拆分使绑定系统具备较强扩展性。不同 UI 系统或不同数据源可以通过 proxy/factory 接入,而不是改主流程。
|
||||
|
||||
### 3. ServiceBundle 模式简单可控
|
||||
|
||||
`Start/Stop` 注册/注销一组服务,适合插件化装配。相比全局静态初始化,这种方式更容易表达模块边界。
|
||||
|
||||
### 4. 有性能意识
|
||||
|
||||
README 中明确强调减少 value type 装箱、减少 GC、优化绑定性能。这说明 Loxodon 的设计目标不是单纯追求抽象,而是面向 Unity UI 高频更新场景。
|
||||
|
||||
## 架构局限
|
||||
|
||||
### 1. Core 边界偏厚
|
||||
|
||||
`ApplicationContext` 直接提供主线程执行器和 Preferences,`Preferences` 默认落到 `PlayerPrefs`,`Messenger.Default` 也带有全局静态倾向。
|
||||
|
||||
这些设计让使用更方便,但会让 Core 逐步吸收平台、存储、消息、UI 等应用级能力。
|
||||
|
||||
### 2. 不是完整游戏框架内核
|
||||
|
||||
Loxodon Core 没有把以下能力作为主轴:
|
||||
|
||||
- 游戏 Feature 生命周期
|
||||
- 模块启动顺序
|
||||
- 资源后端边界
|
||||
- 配置表加载边界
|
||||
- 存档版本迁移
|
||||
- 热更新策略
|
||||
- 场景/流程切换策略
|
||||
|
||||
它的插件生态里有 Addressable、Data、XLua、ILRuntime 等扩展,但 Core 本体的主轴仍然是 MVVM/DataBinding。
|
||||
|
||||
### 3. 对小团队很省事,对强边界内核有侵蚀风险
|
||||
|
||||
如果项目目标是快速做 UI 驱动业务,Loxodon 的全局上下文和内置 Prefs 会很方便。
|
||||
|
||||
但如果目标是 FlowScope 这种机制型内核,直接照搬会让 Kernel 同时承担 UI、Prefs、Messaging、Async、Binding 等职责,边界会变得越来越难控。
|
||||
|
||||
## 对 FlowScope 的借鉴建议
|
||||
|
||||
### 可以借鉴
|
||||
|
||||
1. **ServiceBundle 的模块装配方式**
|
||||
|
||||
FlowScope 的 P1/P2 模块可以采用类似显式装配模式:
|
||||
|
||||
```text
|
||||
ModuleBundle.Start(container)
|
||||
-> register contracts
|
||||
-> register default implementations
|
||||
-> register adapters
|
||||
|
||||
ModuleBundle.Stop(container)
|
||||
-> unregister contracts
|
||||
-> release module-owned state
|
||||
```
|
||||
|
||||
2. **Binding 管线的分层思想**
|
||||
|
||||
如果 FlowScope 后续需要 UI Binding 或 Editor Binding,可以参考 Loxodon 的拆分:
|
||||
|
||||
```text
|
||||
Source
|
||||
Target
|
||||
Path
|
||||
Converter
|
||||
Binder
|
||||
BindingContext
|
||||
```
|
||||
|
||||
但这应该作为 UI 扩展模块,而不是 Kernel 机制。
|
||||
|
||||
3. **ViewModelBase 的属性变更封装**
|
||||
|
||||
`Set<T>` 模式可以降低 ViewModel 代码重复,也能统一 PropertyChanged 和消息广播策略。
|
||||
|
||||
4. **Window 状态模型**
|
||||
|
||||
`Window` / `WindowManager` 对弹窗、页面、转场、可见性和激活状态有参考价值,可用于 FlowScope UI 模块设计。
|
||||
|
||||
### 不建议照搬
|
||||
|
||||
1. **不要把 Prefs 放进 Kernel**
|
||||
|
||||
FlowScope 的 Save / Prefs / PlayerPrefs 应保持在 Save 模块或 Unity Adapter 中。
|
||||
|
||||
2. **不要把 Messenger 作为 Kernel 默认通信机制**
|
||||
|
||||
消息总线容易变成隐式耦合通道。Kernel 应保持 Feature 生命周期和 Slot/Policy 机制清晰,事件系统应作为 P1/P2 模块单独设计。
|
||||
|
||||
3. **不要把 UI Window 放进 Kernel**
|
||||
|
||||
UI 是 Feature/Module 的外围能力。Kernel 只需要管理 Feature 的启动、替换、停止策略,不应知道 Window 栈。
|
||||
|
||||
4. **不要用全局 ApplicationContext 承载所有能力**
|
||||
|
||||
FlowScope 更适合显式 Bootstrap + Container + FeatureContext。全局上下文可以作为上层便利入口,但不应成为 Kernel 的事实中心。
|
||||
|
||||
## 与 FlowScope Kernel 边界的关系
|
||||
|
||||
FlowScope 当前更适合保持:
|
||||
|
||||
```text
|
||||
Kernel
|
||||
-> Container
|
||||
-> AppKernel
|
||||
-> StartupPipeline
|
||||
-> FeatureHost
|
||||
-> FeatureInstance
|
||||
-> FeatureContext
|
||||
-> FeatureLifetime
|
||||
-> FeatureSlot
|
||||
-> FeaturePolicy
|
||||
```
|
||||
|
||||
Loxodon 的能力更适合放在:
|
||||
|
||||
```text
|
||||
FlowScope.UI
|
||||
-> ViewModel
|
||||
-> Binding
|
||||
-> Window / Panel
|
||||
-> Command
|
||||
|
||||
FlowScope.Events
|
||||
-> Messenger / EventBus
|
||||
|
||||
FlowScope.Save.Unity
|
||||
-> PlayerPrefs adapter
|
||||
|
||||
FlowScope.Runtime.Extensions
|
||||
-> ServiceBundle helpers
|
||||
```
|
||||
|
||||
换句话说,Loxodon 对 FlowScope 的启发是:**外围模块可以做厚,Kernel 必须保持薄。**
|
||||
|
||||
## 最终判断
|
||||
|
||||
Loxodon Framework 的 Core 是一个成熟的 Unity MVVM/UI 运行时。它适合解决 UI 数据绑定、ViewModel 状态传播、窗口管理和 UI 事件命令化问题。
|
||||
|
||||
但它不是 FlowScope Kernel 的直接模板。对 FlowScope 来说,最值得吸收的是:
|
||||
|
||||
- `ServiceBundle` 式模块装配。
|
||||
- Binding 管线拆分方式。
|
||||
- ViewModel 属性变更封装。
|
||||
- Window 状态管理经验。
|
||||
|
||||
最需要避免的是:
|
||||
|
||||
- 把 Prefs、UI、Messaging、Binding 一起塞进 Core。
|
||||
- 用全局上下文替代显式生命周期。
|
||||
- 让 Kernel 从机制层滑向应用便利层。
|
||||
|
||||
501
docs/guides/qframework-core-analysis.md
Normal file
501
docs/guides/qframework-core-analysis.md
Normal file
@@ -0,0 +1,501 @@
|
||||
# QFramework Core 细读与 FlowScope 借鉴边界
|
||||
|
||||
更新时间:2026-06-12
|
||||
|
||||
参考源码:<https://github.com/liangxiegame/QFramework/blob/master/QFramework.cs>
|
||||
|
||||
本文分析 QFramework 单文件 Core 的架构设计、适用范围、优缺点,以及它对 FlowScope Kernel / CoreModules 的可借鉴部分。本文不是选型结论,也不是实现计划;它用于后续讨论 FlowScope 业务架构层、事件层、状态层时作为参考。
|
||||
|
||||
## 1. 总结
|
||||
|
||||
QFramework Core 的价值不在于提供完整商业游戏运行时,而在于用很少的类型固定 Unity 业务代码的依赖方向。
|
||||
|
||||
它的核心模型可以概括为:
|
||||
|
||||
```text
|
||||
Architecture<T>
|
||||
-> IOCContainer
|
||||
-> Model / System / Utility
|
||||
-> Command / Query
|
||||
-> Event / BindableProperty
|
||||
|
||||
Controller / MonoBehaviour
|
||||
-> SendCommand / SendQuery
|
||||
-> GetModel / GetSystem
|
||||
-> RegisterEvent
|
||||
```
|
||||
|
||||
QFramework Core 解决的是 Unity 项目里常见的“表现层乱改状态、MonoBehaviour 互相引用、业务逻辑没有入口、通知链路散落”的问题。它不是资源框架、UI 框架、热更框架、构建框架,也不负责 Feature、场景、流程或模块生命周期编排。
|
||||
|
||||
对 FlowScope 来说,QFramework Core 最值得借鉴的是:
|
||||
|
||||
- 状态变更必须有入口。
|
||||
- 上层可以调用下层,下层不直接引用上层。
|
||||
- 下层通过事件或可观察状态通知上层。
|
||||
- Command / Query 是短生命周期行为对象,不承载长期状态。
|
||||
- 用能力接口限制对象可访问的架构能力。
|
||||
|
||||
不适合直接照搬的是:
|
||||
|
||||
- 静态全局 `Architecture<T>.Interface` 入口。
|
||||
- 强制 `Model / System / Utility / Command` 成为 Kernel 业务分层。
|
||||
- 没有 RootScope / FeatureScope 的单容器模型。
|
||||
- 没有 Feature 级生命周期和释放边界。
|
||||
- 把 Event / BindableProperty 作为所有项目默认基础设施。
|
||||
|
||||
## 2. Core 范围
|
||||
|
||||
QFramework.cs 里的核心组成大致分为以下几组。
|
||||
|
||||
| 组 | 类型 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| 架构根 | `IArchitecture`, `Architecture<T>` | 注册和获取 Model / System / Utility,执行 Command / Query,发送和注册事件 |
|
||||
| 表现层协议 | `IController` | 通常由 MonoBehaviour 实现,负责接入架构,不直接持有业务状态 |
|
||||
| 业务逻辑层 | `ISystem`, `AbstractSystem` | 放跨表现层共享的业务逻辑 |
|
||||
| 数据层 | `IModel`, `AbstractModel` | 放状态和数据操作 |
|
||||
| 工具层 | `IUtility` | 放外部能力适配,例如存储、SDK、序列化 |
|
||||
| 行为对象 | `ICommand`, `ICommand<TResult>`, `IQuery<TResult>` | 封装写操作和读操作 |
|
||||
| 能力接口 | `ICanGetModel`, `ICanSendCommand` 等 | 用接口组合限制对象能做什么 |
|
||||
| 事件系统 | `TypeEventSystem`, `EasyEvent` | 按类型注册、发送、反注册事件 |
|
||||
| 可绑定状态 | `BindableProperty<T>` | 值变化时通知订阅者 |
|
||||
| 容器 | `IOCContainer` | 按类型注册和解析实例 |
|
||||
|
||||
这个范围说明 QFramework Core 更接近“业务代码架构层”,而不是“应用 Kernel”。它把代码写法约束住,但不管理完整应用运行期。
|
||||
|
||||
## 3. Architecture<T>
|
||||
|
||||
`Architecture<T>` 是 QFramework 的架构根。它使用静态字段保存当前架构实例,`Interface` 首次访问时触发初始化。
|
||||
|
||||
典型启动过程:
|
||||
|
||||
1. 创建 `T : Architecture<T>` 实例。
|
||||
2. 调用子类实现的 `Init()`。
|
||||
3. 在 `Init()` 中注册 Model、System、Utility。
|
||||
4. 执行 `OnRegisterPatch`。
|
||||
5. 初始化所有未初始化的 Model。
|
||||
6. 初始化所有未初始化的 System。
|
||||
7. 将架构标记为已初始化。
|
||||
|
||||
反初始化过程:
|
||||
|
||||
1. 调用 `OnDeinit()`。
|
||||
2. 对已初始化的 System 调用 `Deinit()`。
|
||||
3. 对已初始化的 Model 调用 `Deinit()`。
|
||||
4. 清空容器。
|
||||
5. 清空静态架构实例。
|
||||
|
||||
这个设计很适合中小项目:入口少、初始化简单、读源码很快。它的问题也同样明确:全局静态入口会自然演化成 Service Locator,业务对象很容易绕开组合根直接拿依赖。
|
||||
|
||||
FlowScope 不应该照搬这个入口。FlowScope 的 Kernel 需要显式 `AppKernel`、`RootScope`、`StartupPipeline` 和 `FeatureHost`,让启动顺序、依赖边界和关闭顺序都可测试、可替换、可释放。
|
||||
|
||||
## 4. Model / System / Utility
|
||||
|
||||
QFramework 的分层规则可以简化理解为:
|
||||
|
||||
```text
|
||||
Controller -> Command / Query -> System / Model -> Utility
|
||||
Model / System -> Event / BindableProperty -> Controller
|
||||
```
|
||||
|
||||
### Model
|
||||
|
||||
`IModel` 表示数据层。它可以:
|
||||
|
||||
- 持有状态。
|
||||
- 调用 Utility。
|
||||
- 发送事件。
|
||||
- 初始化和反初始化。
|
||||
|
||||
它不应该直接引用 Controller,也不应该直接操作 UI。
|
||||
|
||||
### System
|
||||
|
||||
`ISystem` 表示共享业务逻辑层。它可以:
|
||||
|
||||
- 获取 Model。
|
||||
- 获取 Utility。
|
||||
- 获取其他 System。
|
||||
- 注册事件。
|
||||
- 发送事件。
|
||||
- 初始化和反初始化。
|
||||
|
||||
System 适合放跨 UI、跨场景、跨表现层复用的业务逻辑。它比 Model 更偏行为,比 Controller 更偏领域。
|
||||
|
||||
### Utility
|
||||
|
||||
`IUtility` 是最薄的一层,只是一个标记接口。它通常承载外部能力,例如:
|
||||
|
||||
- 本地存储。
|
||||
- 网络 SDK。
|
||||
- 平台 SDK。
|
||||
- JSON 序列化。
|
||||
- 时间服务。
|
||||
|
||||
Utility 本身不被强制注入 Architecture,这也意味着它更像纯工具或外部适配器。
|
||||
|
||||
### 对 FlowScope 的启发
|
||||
|
||||
FlowScope Kernel 不应该规定 `Model / System / Utility`。这是项目业务架构选择,不是 Kernel 机制。
|
||||
|
||||
但 FlowScope 可以在 CoreModules 或推荐模板里提供类似规则:
|
||||
|
||||
- Feature 内部状态归 Feature 自己或 Feature-scoped state service。
|
||||
- 跨 Feature 长期状态归 Root-scoped service。
|
||||
- 外部能力通过接口注册到 RootScope。
|
||||
- UI 和 MonoBehaviour 不直接写长期状态,而是调用 UseCase、Command 或 Service 方法。
|
||||
|
||||
## 5. Command / Query
|
||||
|
||||
QFramework 用 Command 作为状态变更入口,用 Query 作为读操作入口。
|
||||
|
||||
Command 的特征:
|
||||
|
||||
- 短生命周期。
|
||||
- 执行前被注入 Architecture。
|
||||
- 可获取 Model / System / Utility。
|
||||
- 可发送事件。
|
||||
- 可继续发送其他 Command 或 Query。
|
||||
- 不保存长期状态。
|
||||
|
||||
Query 的特征:
|
||||
|
||||
- 短生命周期。
|
||||
- 执行前被注入 Architecture。
|
||||
- 主要读取 Model / System。
|
||||
- 返回查询结果。
|
||||
|
||||
这个设计的好处是把“写操作”从 Controller 中拿出来。Controller 不再到处写:
|
||||
|
||||
```csharp
|
||||
model.Coins.Value += 10;
|
||||
```
|
||||
|
||||
而是写:
|
||||
|
||||
```csharp
|
||||
this.SendCommand(new AddCoinCommand(10));
|
||||
```
|
||||
|
||||
这样状态变化可以被搜索、测试和复用。
|
||||
|
||||
对 FlowScope 来说,这个思想很值得吸收,但名称和形态不应进 Kernel。Kernel 不应该强制所有状态变更都叫 Command。项目可以选择:
|
||||
|
||||
- `Command`
|
||||
- `UseCase`
|
||||
- `Action`
|
||||
- `Service Method`
|
||||
- `Application Service`
|
||||
|
||||
Kernel 只需要保证依赖边界和生命周期,不需要规定业务行为对象的命名体系。
|
||||
|
||||
## 6. 能力接口组合
|
||||
|
||||
QFramework 一个很好的设计点是能力接口组合。
|
||||
|
||||
例如:
|
||||
|
||||
- `ICanGetModel`
|
||||
- `ICanGetSystem`
|
||||
- `ICanGetUtility`
|
||||
- `ICanSendCommand`
|
||||
- `ICanSendEvent`
|
||||
- `ICanRegisterEvent`
|
||||
- `ICanSendQuery`
|
||||
|
||||
这些接口本身几乎没有实现,实际能力通过扩展方法提供。对象只有实现了对应接口,才获得对应扩展方法。
|
||||
|
||||
这带来的好处是:
|
||||
|
||||
- Controller 能做的事和 Model 能做的事不同。
|
||||
- Model 可以发事件,但不能随意发 Command。
|
||||
- Command 可以访问 Model/System/Utility,但不变成长期服务。
|
||||
- 架构规则体现在类型系统里,而不是只写在文档里。
|
||||
|
||||
这个方向对 FlowScope 有参考价值。FlowScope 如果后续做业务架构层,可以避免提供一个万能 `FeatureContext`。更好的方式是拆小能力:
|
||||
|
||||
```text
|
||||
ICanResolveService
|
||||
ICanPublishEvent
|
||||
ICanReadState
|
||||
ICanExecuteUseCase
|
||||
ICanRegisterLifetime
|
||||
```
|
||||
|
||||
但这些应属于 P1/P2 业务层或扩展层。Kernel 的 `FeatureContext` 仍应保持最小,只暴露 Scope、Lifetime、CancellationToken、Handle 等机制概念。
|
||||
|
||||
## 7. TypeEventSystem
|
||||
|
||||
QFramework 的 `TypeEventSystem` 是一个按事件类型分发的轻量事件系统。
|
||||
|
||||
它的核心机制:
|
||||
|
||||
```text
|
||||
TypeEventSystem
|
||||
-> EasyEvents
|
||||
-> Dictionary<Type, IEasyEvent>
|
||||
-> EasyEvent<T>
|
||||
```
|
||||
|
||||
发送事件时按 `EasyEvent<T>` 类型取出事件对象并触发。注册事件时如果没有对应事件对象,就创建一个。
|
||||
|
||||
优点:
|
||||
|
||||
- 使用简单。
|
||||
- 类型安全。
|
||||
- 适合 UI 刷新和轻量业务通知。
|
||||
- 反注册模型清晰。
|
||||
|
||||
风险:
|
||||
|
||||
- 没有作用域隔离。
|
||||
- 没有事件优先级。
|
||||
- 没有异步派发协议。
|
||||
- 没有错误隔离。
|
||||
- 没有事件链路追踪。
|
||||
- 大型项目里容易变成全局黑箱。
|
||||
|
||||
FlowScope 可以借鉴“下层发事件,上层订阅”的方向,但 EventBus 不应进入 Kernel 主依赖。更合理的位置是 `FlowScope.Events` 或 P1 Events 模块:
|
||||
|
||||
```text
|
||||
Kernel
|
||||
不知道 EventBus
|
||||
|
||||
P1 Events
|
||||
注册 IEventBus 到 RootScope
|
||||
提供 FeatureLifetime 可登记的订阅句柄
|
||||
提供同步或异步事件策略
|
||||
```
|
||||
|
||||
这样 Feature 可以使用事件,但 Kernel 不被事件模型绑死。
|
||||
|
||||
## 8. BindableProperty<T>
|
||||
|
||||
`BindableProperty<T>` 是一个可观察值对象。它在 `Value` 变化时触发 `EasyEvent<T>`,并提供:
|
||||
|
||||
- `Register`
|
||||
- `RegisterWithInitValue`
|
||||
- `UnRegister`
|
||||
- `SetValueWithoutEvent`
|
||||
- 自定义比较器
|
||||
|
||||
它适合表达 UI 绑定和简单状态监听,例如金币、生命值、进度条、开关状态。
|
||||
|
||||
优点:
|
||||
|
||||
- 简单直接。
|
||||
- UI 订阅体验好。
|
||||
- `RegisterWithInitValue` 避免 UI 初始刷新遗漏。
|
||||
- 对中小项目足够实用。
|
||||
|
||||
风险:
|
||||
|
||||
- 比较器是静态泛型级别配置,使用时要小心全局影响。
|
||||
- 复杂状态容易被拆成大量可绑定字段,导致状态事务边界不清。
|
||||
- 不适合表达复杂异步流、错误流、完成流。
|
||||
- 如果到处暴露可写 `BindableProperty<T>`,仍然会出现状态乱改。
|
||||
|
||||
FlowScope 可以把类似能力放到状态或 UI 扩展模块,但不应放进 Kernel。Kernel 只负责 Feature 生命周期;状态响应式属于业务层或表现层。
|
||||
|
||||
## 9. IOCContainer
|
||||
|
||||
QFramework 的 `IOCContainer` 很小:
|
||||
|
||||
- `Register<T>(T instance)` 按 `typeof(T)` 注册。
|
||||
- `Get<T>()` 按 `typeof(T)` 获取。
|
||||
- `GetInstancesByType<T>()` 枚举所有兼容类型实例。
|
||||
- `Clear()` 清空。
|
||||
|
||||
优点:
|
||||
|
||||
- 极易理解。
|
||||
- 调试成本低。
|
||||
- 足够支撑 QFramework 的 Model/System/Utility 注册。
|
||||
|
||||
不足:
|
||||
|
||||
- 没有构造函数注入。
|
||||
- 没有 Scope。
|
||||
- 没有生命周期释放。
|
||||
- 没有 `IDisposable` / `IAsyncDisposable` 管理。
|
||||
- 没有多绑定。
|
||||
- 没有 named/keyed service。
|
||||
- 没有循环依赖检测。
|
||||
- 没有线程安全。
|
||||
|
||||
FlowScope 可以借鉴“按类型注册和解析”的最小体验,但实现必须比 QFramework 的容器多一层生命周期能力:
|
||||
|
||||
```text
|
||||
RootScope
|
||||
长生命周期服务
|
||||
|
||||
FeatureScope
|
||||
Feature 实例和本次启动参数
|
||||
先查本地,再回退 RootScope
|
||||
|
||||
FeatureLifetime
|
||||
释放运行期句柄、订阅、资源组、临时对象
|
||||
```
|
||||
|
||||
这也是 FlowScope 不能直接采用 QFramework IOC 的核心原因。
|
||||
|
||||
## 10. 生命周期对比
|
||||
|
||||
| 维度 | QFramework Core | FlowScope Kernel 目标 |
|
||||
| --- | --- | --- |
|
||||
| 应用入口 | 静态 `Architecture<T>.Interface` | 显式 `AppKernel` |
|
||||
| 启动流程 | 首次访问时初始化 | `StartupPipeline` 顺序执行 |
|
||||
| 依赖容器 | 单个 `IOCContainer` | `RootScope + FeatureScope` |
|
||||
| 业务运行单元 | Model/System 长驻 | Feature 实例 |
|
||||
| Feature 生命周期 | 不负责 | `LoadAsync / EnterAsync / ExitAsync` |
|
||||
| 释放边界 | Model/System Deinit,容器 Clear | `FeatureLifetime + FeatureScope + RootScope` |
|
||||
| 异步关闭 | 不突出 | Kernel 必须处理 async stop/shutdown |
|
||||
| 运行位置 | 不负责 | `FeatureSlot + FeaturePolicy` |
|
||||
|
||||
QFramework 的生命周期适合“一个游戏一个 Architecture,若干长驻 Model/System”。FlowScope 的目标是“一个应用 Kernel 管多个 Feature 实例,每个 Feature 有独立依赖边界和释放边界”。
|
||||
|
||||
这两个方向不冲突,但层级不同。QFramework 更像可选的业务组织方式,FlowScope Kernel 更像运行机制。
|
||||
|
||||
## 11. 与完整框架的横向比较
|
||||
|
||||
| 维度 | QFramework Core | UnityGameFramework | ET Framework | TinaX | FlowScope 应吸收 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 核心定位 | 轻量业务分层 | 客户端模块全家桶 | 双端大型联网运行时 | 服务化 Package 框架 | 小 Kernel + 可选模块 |
|
||||
| 启动入口 | Architecture | GameEntry / Procedure | Scene / Fiber / Actor | Core service bootstrap | AppKernel / StartupPipeline |
|
||||
| 资源系统 | Core 不负责 | 内置 Resource | 可接入包和热更链路 | VFS | P1 Resources 模块 |
|
||||
| UI | Core 不负责,UIKit 在工具生态 | 内置 UI 模块 | 通常通过包扩展 | UIKit | P1 UI 模块 |
|
||||
| 事件 | TypeEventSystem | Event 模块 | 消息/事件体系 | Event/System service | P1 Events 模块 |
|
||||
| 状态变更 | Command | 模块 API / Procedure | Entity/System/消息 | Service API | 可选 Command/UseCase |
|
||||
| 工程流水线 | 不负责 | 部分覆盖 | 强工具链 | Package 化 | Tools/Package 后续模块 |
|
||||
|
||||
QFramework 在这张表里的优势是轻量和清晰。它不应该被拿来和 UGF 的资源/UI/Procedure 覆盖度硬比,也不应该被要求承担 BDFramework 的热更构建流水线职责。
|
||||
|
||||
## 12. 对 FlowScope 的建议
|
||||
|
||||
### 12.1 Kernel 不吸收 QFramework 分层
|
||||
|
||||
FlowScope Kernel 继续保持:
|
||||
|
||||
```text
|
||||
AppKernel
|
||||
StartupPipeline
|
||||
FeatureHost
|
||||
Container
|
||||
FeatureLifetime
|
||||
FeatureSlot
|
||||
FeaturePolicy
|
||||
IFeature
|
||||
FeatureContext
|
||||
FeatureHandle
|
||||
```
|
||||
|
||||
Kernel 不加入:
|
||||
|
||||
- `IModel`
|
||||
- `ISystem`
|
||||
- `IUtility`
|
||||
- `ICommand`
|
||||
- `IQuery`
|
||||
- `BindableProperty`
|
||||
- `TypeEventSystem`
|
||||
- `IController`
|
||||
|
||||
原因是这些属于业务架构风格,不是 Feature 编排机制。
|
||||
|
||||
### 12.2 CoreModules 可吸收状态变更规则
|
||||
|
||||
FlowScope 可以在后续 CoreModules 或 template 中提供推荐规则:
|
||||
|
||||
```text
|
||||
View / MonoBehaviour
|
||||
-> Command / UseCase / Service Method
|
||||
-> State Service / Feature Local State
|
||||
-> Event / Observable
|
||||
-> View / Presenter / Controller
|
||||
```
|
||||
|
||||
这能吸收 QFramework 的优点,同时避免 Kernel 被业务范式锁死。
|
||||
|
||||
### 12.3 Events 模块可参考 TypeEventSystem,但要补足边界
|
||||
|
||||
如果 FlowScope 做 P1 Events,不建议只复制 `TypeEventSystem`。至少要明确:
|
||||
|
||||
- 事件作用域:Root-scoped 还是 Feature-scoped。
|
||||
- 订阅句柄如何进入 FeatureLifetime。
|
||||
- 事件处理异常是否聚合、记录或继续派发。
|
||||
- 是否支持异步事件。
|
||||
- 是否允许跨 Feature 事件。
|
||||
- 是否提供调试追踪。
|
||||
|
||||
QFramework 的 TypeEventSystem 可作为最小同步事件模型参考,但不是最终模块规格。
|
||||
|
||||
### 12.4 State 模块可参考 BindableProperty,但不要裸露可写状态
|
||||
|
||||
如果 FlowScope 提供类似 BindableProperty 的能力,建议区分:
|
||||
|
||||
```text
|
||||
IReadonlyState<T>
|
||||
IMutableState<T>
|
||||
StateWriter<T>
|
||||
StateChanged<T>
|
||||
```
|
||||
|
||||
UI 层优先拿只读接口,写入通过 UseCase/Command/Service。这样比直接把 `BindableProperty<T>` 暴露给所有人更稳。
|
||||
|
||||
### 12.5 业务模板可以提供 QFramework 风格
|
||||
|
||||
可以考虑后续提供一个可选模板:
|
||||
|
||||
```text
|
||||
FlowScope.BusinessArchitecture
|
||||
- ICommand
|
||||
- ICommand<TResult>
|
||||
- IQuery<TResult>
|
||||
- IUseCase
|
||||
- IStateService
|
||||
- IEventPublisher
|
||||
```
|
||||
|
||||
但这应是项目选择,不是 Kernel 强制。
|
||||
|
||||
## 13. 风险清单
|
||||
|
||||
如果直接照搬 QFramework Core 到 FlowScope,主要风险是:
|
||||
|
||||
1. Kernel 变成业务架构框架,失去机制最小化。
|
||||
2. `Architecture<T>.Interface` 和 `RootScope` 形成双全局入口。
|
||||
3. `IOCContainer` 无法表达 FeatureScope 生命周期。
|
||||
4. Command/Event 进入 Kernel 后,P1/P2 模块会被迫统一业务风格。
|
||||
5. EventBus 变成隐式跨 Feature 通道,削弱 FeatureHost 的显式编排边界。
|
||||
6. BindableProperty 进入底层后,UI/状态响应式会反向污染 Kernel。
|
||||
|
||||
规避方式:
|
||||
|
||||
- Kernel 只保留生命周期、依赖边界、Feature 编排。
|
||||
- QFramework 的分层规则只进入文档、模板或可选模块。
|
||||
- Event/State/Command 都作为 CoreModules 或扩展层设计。
|
||||
- FeatureContext 不提供全能业务能力。
|
||||
|
||||
## 14. 结论
|
||||
|
||||
QFramework Core 是一个优秀的轻量 Unity 业务架构样本。它用很少代码表达了三条关键规则:
|
||||
|
||||
- 上层驱动下层。
|
||||
- 状态变更有入口。
|
||||
- 下层通过通知影响上层。
|
||||
|
||||
FlowScope 应吸收这些规则,但不能把 QFramework 的 Core 形态直接变成自己的 Kernel。FlowScope Kernel 的目标更底层:它负责应用启动、依赖 Scope、Feature 生命周期、运行槽和释放链。QFramework 风格更适合作为 FlowScope 的可选业务层、示例模板或 P1/P2 模块设计参考。
|
||||
|
||||
最终边界建议:
|
||||
|
||||
```text
|
||||
FlowScope.Kernel
|
||||
只负责机制
|
||||
|
||||
FlowScope.CoreModules
|
||||
提供 Events / State / Config / Save / Resources / UI 等模块
|
||||
|
||||
FlowScope.BusinessTemplate.QFrameworkLike
|
||||
可选提供 Command / Query / State / Event 的业务写法参考
|
||||
```
|
||||
|
||||
这样既能保留 QFramework 的轻量分层优点,又不会牺牲 FlowScope 当前“Kernel tiny, modules optional”的主方向。
|
||||
473
docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md
Normal file
473
docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# uFrame MVVM Core 分析
|
||||
|
||||
日期:2026-06-12
|
||||
|
||||
## 结论
|
||||
|
||||
uFrame MVVM 的核心价值不在它的 Kernel、IOC 启动或代码生成器,而在运行时 MVVM 的几个机制:
|
||||
|
||||
- `ViewModel` 持有可观察状态。
|
||||
- `P<T>` 把 ViewModel 属性包装成可订阅的数据流。
|
||||
- `Signal<TCommand>` 把 UI 操作表达成命令流。
|
||||
- `ViewBase` 通过 `Bind / Unbind` 管理订阅生命周期。
|
||||
- `ViewService` 负责把 View 与 ViewModel 对接。
|
||||
|
||||
但这套 MVVM 被 uFrame 自身的全局 Kernel、全局容器、命名约定、`Resources.Load` 和设计器生成代码强绑定。它可以作为反面边界样本和局部机制参考,不适合作为 FlowScope Kernel 或 UI 模块的直接原型。
|
||||
|
||||
FlowScope 可以吸收“可观察状态 + 命令流 + 绑定生命周期作用域”,但不应吸收它的全局 IOC 查找、ViewModel 自动解析、Controller 命名约定和资源自动定位。
|
||||
|
||||
## 源码范围
|
||||
|
||||
本次分析基于 `uFrame/uFrame.MVVM` 当前公开仓库源码,重点阅读:
|
||||
|
||||
- [`ViewModel.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ViewModel.cs)
|
||||
- [`ModelPropertyBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ModelPropertyBase.cs)
|
||||
- [`Signal.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/Signal.cs)
|
||||
- [`ViewBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewBase.cs)
|
||||
- [`View.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/View.cs)
|
||||
- [`Controller.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Controllers/Controller.cs)
|
||||
- [`ViewService.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Services/ViewService.cs)
|
||||
- [`MVVMKernelExtensions.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/MVVMKernelExtensions.cs)
|
||||
- [`ViewBindings.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Bindings/ViewBindings.cs)
|
||||
- [`ViewResolver.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewResolver.cs)
|
||||
|
||||
## 运行时结构
|
||||
|
||||
uFrame MVVM 的运行时可以拆成五组对象:
|
||||
|
||||
```text
|
||||
ViewModel 层
|
||||
ViewModel
|
||||
P<T>
|
||||
ModelCollection<T>
|
||||
Signal<TCommand>
|
||||
ViewModelCommand
|
||||
|
||||
View 层
|
||||
ViewBase
|
||||
View<TModel>
|
||||
ViewComponent
|
||||
ViewBindings
|
||||
|
||||
创建与连接层
|
||||
Controller
|
||||
ViewService
|
||||
ViewResolver
|
||||
|
||||
绑定层
|
||||
Binding
|
||||
ModelPropertyBinding
|
||||
ModelViewPropertyBinding
|
||||
ModelViewModelCollectionBinding
|
||||
|
||||
全局基础设施
|
||||
uFrameKernel.Container
|
||||
IEventAggregator
|
||||
SystemService / SystemServiceMonoBehavior
|
||||
```
|
||||
|
||||
它不是一个独立、纯粹的 MVVM runtime。`ViewModel`、`View`、`Controller` 和 `ViewService` 都会触碰 uFrame Kernel 或全局容器。
|
||||
|
||||
## ViewModel
|
||||
|
||||
`ViewModel` 是所有生成 ViewModel 的基类。它承担了状态、绑定、序列化、事件发布、销毁通知等多种职责。
|
||||
|
||||
关键职责:
|
||||
|
||||
- 通过 `FillProperties` 暴露属性元信息。
|
||||
- 通过 `FillCommands` 暴露命令元信息。
|
||||
- 持有 `Bindings`,用于释放订阅。
|
||||
- 持有 `Identifier`,用于全局容器按 id 查找。
|
||||
- 通过 `PropertyChanged` 通知属性变化。
|
||||
- `Dispose` 时发布 `ViewModelDestroyedEvent`。
|
||||
|
||||
问题是它太重。一个干净的 ViewModel 应该主要表达 UI 状态和 UI 意图,但这里的 `ViewModel` 混入了:
|
||||
|
||||
- 序列化协议。
|
||||
- `IBindable`。
|
||||
- `IDisposable`。
|
||||
- 全局事件聚合器。
|
||||
- binding 容器。
|
||||
- 引用计数。
|
||||
- controller wiring hook。
|
||||
|
||||
这会让 ViewModel 从“状态对象”变成“框架对象”。长期项目里,这种设计会让测试、复用和生命周期判断都变复杂。
|
||||
|
||||
## P<T>
|
||||
|
||||
`P<T>` 是 uFrame MVVM 最值得看的部分。它把单个属性封装成 observable property:
|
||||
|
||||
```csharp
|
||||
public class P<T> : ISubject<T>, IObservableProperty, ISimpleNotifyPropertyChanged
|
||||
```
|
||||
|
||||
它提供:
|
||||
|
||||
- `Value`:当前值。
|
||||
- `LastValue`:上一次值。
|
||||
- `ChangedObservable`:只在值变化时通知。
|
||||
- `ObjectValue`:非泛型访问入口。
|
||||
- `Owner`:所属 ViewModel。
|
||||
- `PropertyName`:属性名。
|
||||
- `ToComputed`:根据其他 observable property 计算派生值。
|
||||
|
||||
View 侧可以订阅 `P<T>`,属性变化时刷新 UI。这个机制适合 Unity UI,因为它避免了大量手动 `Refresh()` 和每帧轮询。
|
||||
|
||||
可取点:
|
||||
|
||||
- 属性变化可以显式订阅。
|
||||
- 派生属性可以声明依赖。
|
||||
- View 绑定时可以立即应用当前值。
|
||||
- IDisposable 可以挂到绑定作用域统一释放。
|
||||
|
||||
风险点:
|
||||
|
||||
- `Value` setter 没有先比较新旧值,任何赋值都会触发通知。
|
||||
- `Owner` 让 property 反向知道 ViewModel。
|
||||
- `P<T>` 同时实现 subject、property、notify,职责偏多。
|
||||
- `ChangedObservable` 依赖 `Value` 和 `LastValue` 的外部状态,语义不够直观。
|
||||
|
||||
FlowScope 如果吸收这个思路,应改成更小的 `ObservableProperty<T>`:
|
||||
|
||||
```text
|
||||
只负责保存值、比较值、发布变化、释放订阅。
|
||||
不要知道全局 Kernel。
|
||||
不要知道 View。
|
||||
不要知道资源或 Controller。
|
||||
```
|
||||
|
||||
## Signal<TCommand>
|
||||
|
||||
`Signal<TCommand>` 用来表达 ViewModel 命令:
|
||||
|
||||
```csharp
|
||||
public class Signal<TClass> : ISubject<TClass>, ISignal<TClass>
|
||||
where TClass : IViewModelCommand, new()
|
||||
```
|
||||
|
||||
它的行为是:
|
||||
|
||||
1. View 或绑定层构造 command。
|
||||
2. 调用 `Signal.OnNext(command)`。
|
||||
3. `Signal` 把 `Sender` 设置为当前 ViewModel。
|
||||
4. 执行本地 `Action`。
|
||||
5. 推送给订阅者。
|
||||
|
||||
这个方向是好的:UI 操作不应该直接调用业务系统,而应该变成可观察的 command 或 intent。
|
||||
|
||||
可取点:
|
||||
|
||||
- UI 输入被建模为显式命令。
|
||||
- 命令可以订阅、组合、测试。
|
||||
- View 与业务处理之间不需要直接互相引用。
|
||||
|
||||
风险点:
|
||||
|
||||
- command 的 sender 被 `Signal` 自动写入,有隐式副作用。
|
||||
- 命令流最终仍然容易接到全局 EventAggregator。
|
||||
- 生成器会为每个命令生成强约定代码,手写维护成本高。
|
||||
|
||||
FlowScope 可以吸收 command stream,但建议命名为更中性的 `UIIntent` / `ViewAction` / `FeatureCommand`,并放在 feature scope 内,不进入 Kernel core。
|
||||
|
||||
## ViewBase / View<TModel>
|
||||
|
||||
`ViewBase` 是 View 与 ViewModel 的连接点。它提供:
|
||||
|
||||
- `ViewModelObject`:当前绑定的 ViewModel。
|
||||
- `Bind()`:用户编写订阅逻辑。
|
||||
- `PreBind()`:生成器插入绑定逻辑。
|
||||
- `AfterBind()`:绑定完成后的 hook。
|
||||
- `Unbind()`:释放绑定。
|
||||
- `AddBinding()`:把 disposable 放到当前 View 的绑定列表。
|
||||
- `BindOnStart`:是否启动时自动绑定。
|
||||
- `DisposeViewModelOnDestroy`:View 销毁时是否销毁 ViewModel。
|
||||
|
||||
`View<TModel>` 只是给 `ViewBase` 加了泛型 Model 访问:
|
||||
|
||||
```csharp
|
||||
public abstract class View<TModel> : ViewBase where TModel : ViewModel, new()
|
||||
```
|
||||
|
||||
可取点:
|
||||
|
||||
- View 的绑定逻辑有明确生命周期。
|
||||
- 订阅统一挂到 binding scope。
|
||||
- View 只响应 ViewModel 的状态,不需要每帧刷 UI。
|
||||
- View 销毁时可以集中释放订阅。
|
||||
|
||||
问题点:
|
||||
|
||||
- `ViewBase.KernelLoaded()` 会调用 `uFrameKernel.Container.Inject(this)`。
|
||||
- `OnDestroy()` 依赖 `uFrameKernel.IsKernelLoaded` 和全局事件。
|
||||
- View 的 `Bindings` 实际挂在 `ViewModelObject.Bindings[ViewId]` 上,View 和 ViewModel 的生命周期互相缠住。
|
||||
- `References` 引用计数用于判断 ViewModel 是否销毁,复杂且脆弱。
|
||||
|
||||
FlowScope 可以借鉴 `Bind / Unbind / AddBinding`,但绑定容器应属于 View 或 feature-local binding scope,而不是塞进 ViewModel。
|
||||
|
||||
## Controller
|
||||
|
||||
uFrame 的 `Controller` 是特殊的 `SystemService`,主要负责创建和初始化 ViewModel。
|
||||
|
||||
流程大致是:
|
||||
|
||||
```text
|
||||
Controller.Create(identifier)
|
||||
-> CreateEmpty(identifier)
|
||||
-> RegisterViewModel(vm, identifier)
|
||||
-> Initialize(vm)
|
||||
-> Publish ViewModelCreatedEvent
|
||||
```
|
||||
|
||||
问题在于 `CreateEmpty` 内部直接注册到全局容器:
|
||||
|
||||
```text
|
||||
uFrameKernel.Container.RegisterViewModel(vm, identifier)
|
||||
```
|
||||
|
||||
`MVVMKernelExtensions.CreateViewModel(type)` 又会通过 ViewModel 类型名去全局容器找 Controller:
|
||||
|
||||
```text
|
||||
Resolve<Controller>(type.Name)
|
||||
```
|
||||
|
||||
这是强命名约定 + 服务定位器。它表面上把 ViewModel 创建集中到了 Controller,实际上把依赖关系藏进全局容器和类型名字符串。
|
||||
|
||||
FlowScope 不应该吸收这套 Controller 模式。更合适的是:
|
||||
|
||||
```text
|
||||
Feature 明确创建 ViewModel。
|
||||
Feature 明确创建/绑定 View。
|
||||
FeatureContext 提供本 feature 需要的服务。
|
||||
UI module 不通过全局容器按名字找 Controller。
|
||||
```
|
||||
|
||||
## ViewService / ViewResolver
|
||||
|
||||
`ViewService` 负责:
|
||||
|
||||
- 监听 `InstantiateViewCommand`。
|
||||
- 根据 ViewModel 找 View prefab。
|
||||
- 实例化 View。
|
||||
- 在 View 创建时补齐 ViewModel。
|
||||
- View 销毁时移除记录。
|
||||
- ViewModel 销毁时从全局容器移除实例。
|
||||
|
||||
`ViewResolver` 默认根据 ViewModel 类型名找 prefab:
|
||||
|
||||
```text
|
||||
PlayerViewModel -> Player -> Resources.Load("Player")
|
||||
```
|
||||
|
||||
这是老式 Unity 框架常见做法,但不适合 FlowScope:
|
||||
|
||||
- 资源路径是隐式命名约定。
|
||||
- 默认绑定到 `Resources.Load`。
|
||||
- 不支持现代资源后端的显式 handle、group、release。
|
||||
- 不利于 Addressables / YooAsset / AssetBundle / 自定义资源系统适配。
|
||||
- View 的创建和 ViewModel 的创建混在同一个服务里。
|
||||
|
||||
FlowScope 如果需要 UI 模块,可以把它拆成:
|
||||
|
||||
```text
|
||||
IViewFactory
|
||||
负责创建 View,不负责创建 ViewModel。
|
||||
|
||||
IViewBinder<TView, TViewModel>
|
||||
负责绑定和解绑。
|
||||
|
||||
IUIViewRegistry 或 IViewCatalog
|
||||
负责显式注册 view key 到 prefab/address。
|
||||
|
||||
IResourceService adapter
|
||||
负责真实资源加载和释放。
|
||||
```
|
||||
|
||||
## Binding
|
||||
|
||||
uFrame 的 binding 层包括:
|
||||
|
||||
- `Binding`
|
||||
- `ModelPropertyBinding`
|
||||
- `ModelViewPropertyBinding`
|
||||
- `ModelViewModelCollectionBinding`
|
||||
- `ViewBindings` 扩展方法
|
||||
|
||||
最实用的是 `ViewBindings.BindProperty`:
|
||||
|
||||
```text
|
||||
立即用当前值刷新一次 UI。
|
||||
订阅后续变化。
|
||||
把 IDisposable 加入 bindable 的绑定列表。
|
||||
```
|
||||
|
||||
这是 FlowScope 可以吸收的重点:
|
||||
|
||||
```csharp
|
||||
bindingScope.Bind(vm.Title, value => titleText.text = value);
|
||||
```
|
||||
|
||||
但不要吸收它的反射绑定和生成器绑定作为 core。反射/生成器可以是工具层能力,不应该成为 UI runtime 的基本前提。
|
||||
|
||||
## 与 FlowScope Kernel 的关系
|
||||
|
||||
uFrame MVVM 不应该进入 FlowScope Kernel core。
|
||||
|
||||
FlowScope Kernel 已经明确应保持机制小核:
|
||||
|
||||
```text
|
||||
Container
|
||||
AppKernel
|
||||
StartupPipeline
|
||||
FeatureHost
|
||||
FeatureInstance
|
||||
FeatureContext
|
||||
FeatureLifetime
|
||||
FeatureSlot
|
||||
FeaturePolicy
|
||||
```
|
||||
|
||||
MVVM 属于更高层的 UI module 或 feature-local module。Kernel 最多提供 feature 生命周期和上下文,不提供:
|
||||
|
||||
- ViewModel 基类。
|
||||
- View 自动绑定。
|
||||
- UI prefab 查找。
|
||||
- 命令总线。
|
||||
- 全局 ViewService。
|
||||
- 全局事件聚合器。
|
||||
- 全局 IOC 注入 View。
|
||||
|
||||
推荐边界:
|
||||
|
||||
```text
|
||||
FlowScope.Kernel
|
||||
只负责 Feature 生命周期和 Slot/Policy 机制。
|
||||
|
||||
FlowScope.UI
|
||||
提供 ViewModel、ObservableProperty、BindingScope、ViewBinder。
|
||||
|
||||
FlowScope.Resources
|
||||
提供资源加载接口和后端适配。
|
||||
|
||||
Game Feature
|
||||
自己声明 ViewModel、View、Binder、Command handler。
|
||||
```
|
||||
|
||||
## 可吸收清单
|
||||
|
||||
可以吸收:
|
||||
|
||||
- `ObservableProperty<T>` 思路。
|
||||
- `ObservableCollection<T>` 思路。
|
||||
- UI command / intent stream。
|
||||
- `Bind / Unbind` 生命周期。
|
||||
- `IDisposable` binding scope。
|
||||
- View 订阅 ViewModel,ViewModel 不直接操作 View。
|
||||
- View 创建和 ViewModel 绑定分离。
|
||||
- 集合绑定生成子 View 的思路,但要显式管理资源释放。
|
||||
|
||||
不应吸收:
|
||||
|
||||
- `uFrameKernel.Container` 全局创建 ViewModel。
|
||||
- `Container.Inject(view)`。
|
||||
- 按 `ViewModel` 类型名查找 `Controller`。
|
||||
- 按 `ViewModel` 类型名 `Resources.Load` prefab。
|
||||
- ViewModel 持有 View 绑定列表。
|
||||
- ViewModel 引用计数决定销毁。
|
||||
- 全局 EventAggregator 作为 UI command 默认通道。
|
||||
- 代码生成器作为 runtime 必需条件。
|
||||
- ViewModel 同时承担序列化、绑定、事件、销毁、controller hook。
|
||||
|
||||
## FlowScope 可采用的最小 UI MVVM 形态
|
||||
|
||||
建议的最小形态:
|
||||
|
||||
```text
|
||||
ObservableProperty<T>
|
||||
保存值并发布变化。
|
||||
|
||||
BindingScope
|
||||
持有 IDisposable 列表,Dispose 时统一释放。
|
||||
|
||||
IViewModel
|
||||
标记 UI 状态对象,不依赖 Kernel。
|
||||
|
||||
IViewBinder<TView, TViewModel>
|
||||
Bind(view, vm, scope)
|
||||
Unbind 由 scope.Dispose 承担。
|
||||
|
||||
UIView<TViewModel>
|
||||
MonoBehaviour,只持有 Unity UI 引用和当前 ViewModel。
|
||||
|
||||
UICommand<T>
|
||||
feature-local command,不走全局事件总线。
|
||||
```
|
||||
|
||||
示意:
|
||||
|
||||
```csharp
|
||||
public sealed class BindingScope : IDisposable
|
||||
{
|
||||
private readonly List<IDisposable> _items = new();
|
||||
|
||||
public T Add<T>(T item) where T : IDisposable
|
||||
{
|
||||
_items.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var item in _items)
|
||||
item.Dispose();
|
||||
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public sealed class MainMenuViewModel
|
||||
{
|
||||
public ObservableProperty<string> Title { get; } = new("Main Menu");
|
||||
public Subject<StartGameIntent> StartGame { get; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public sealed class MainMenuBinder
|
||||
{
|
||||
public void Bind(MainMenuView view, MainMenuViewModel vm, BindingScope scope)
|
||||
{
|
||||
scope.Bind(vm.Title, value => view.TitleText.text = value);
|
||||
scope.Add(view.StartButton.OnClickAsObservable()
|
||||
.Subscribe(_ => vm.StartGame.OnNext(new StartGameIntent())));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键点是:这里没有全局容器查找,没有 `Resources.Load` 约定,没有 Kernel 注入 View,也没有 ViewModel 引用计数。
|
||||
|
||||
## 最终判断
|
||||
|
||||
uFrame MVVM 的局部机制比它的 Kernel 更有参考价值,但必须拆开看。
|
||||
|
||||
真正值得保留的是:
|
||||
|
||||
```text
|
||||
状态可观察
|
||||
操作命令化
|
||||
绑定有生命周期
|
||||
View 不直接写业务
|
||||
```
|
||||
|
||||
必须避免的是:
|
||||
|
||||
```text
|
||||
全局容器创建一切
|
||||
命名约定连接一切
|
||||
ViewModel 变成框架对象
|
||||
资源加载隐藏在 ViewResolver
|
||||
UI 模块污染 Kernel core
|
||||
```
|
||||
|
||||
对 FlowScope 来说,uFrame MVVM 最好的用途不是成为模板,而是提醒我们:UI MVVM 应该是 Kernel 之上的可选模块,且必须保持 feature-local、显式绑定、显式资源、显式生命周期。
|
||||
Reference in New Issue
Block a user