feat(router): 添加场景和UI路由的Around中间件支持

- 在SceneRouterBase和UiRouterBase中集成管道执行Around处理器
- 实现场景过渡管道SceneTransitionPipeline的Around处理器注册和执行功能
- 实现UI过渡管道UiTransitionPipeline的Around处理器注册和执行功能
- 添加ISceneAroundTransitionHandler和IUiAroundTransitionHandler接口定义
- 扩展SceneTransitionPhases和UITransitionPhases枚举支持Around阶段
- 实现Around处理器的优先级排序和中间件链构建机制
- 添加Around处理器的超时控制和异常处理机制
This commit is contained in:
GeWuYou 2026-02-15 16:22:17 +08:00 committed by gewuyou
parent 6f61ff55aa
commit 53c2ee4ef3
8 changed files with 404 additions and 39 deletions

View File

@ -34,9 +34,16 @@ public enum SceneTransitionPhases
/// </summary>
AfterChange = 2,
/// <summary>
/// 中间件阶段(阻塞执行)。
/// 用于包裹整个场景切换过程的逻辑,如性能监控、事务管理、权限验证等。
/// Around 处理器在变更前后都会执行,可以控制是否继续执行变更。
/// </summary>
Around = 4,
/// <summary>
/// 所有阶段的组合标志。
/// 表示处理器适用于场景切换的所有阶段。
/// </summary>
All = BeforeChange | AfterChange
All = BeforeChange | AfterChange | Around
}

View File

@ -19,7 +19,14 @@ public enum UiTransitionPhases
AfterChange = 2,
/// <summary>
/// 所有阶段Handler将在BeforeChange和AfterChange阶段都执行
/// 中间件阶段,支持包裹整个变更过程的逻辑(阻塞执行)
/// 适用于:性能监控、事务管理、权限验证、日志记录开始/结束等需要控制流程的操作
/// Around 处理器在变更前后都会执行,可以决定是否继续执行后续逻辑
/// </summary>
All = BeforeChange | AfterChange
Around = 4,
/// <summary>
/// 所有阶段Handler将在BeforeChange、AfterChange和Around阶段都执行
/// </summary>
All = BeforeChange | AfterChange | Around
}

View File

@ -0,0 +1,48 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.scene;
/// <summary>
/// 场景切换中间件处理器接口,支持包裹整个变更过程的逻辑。
/// Around 处理器在变更前后都会执行,可以控制是否继续执行变更。
/// 适用于:性能监控、事务管理、权限验证、日志记录等横切关注点。
/// </summary>
public interface ISceneAroundTransitionHandler
{
/// <summary>
/// 获取处理器的执行优先级。
/// 数值越小优先级越高,越先执行(外层)。
/// 建议范围:-1000 到 1000。
/// </summary>
int Priority { get; }
/// <summary>
/// 判断处理器是否应该处理当前场景过渡事件。
/// </summary>
/// <param name="event">场景过渡事件。</param>
/// <returns>如果应该处理则返回 true否则返回 false。</returns>
bool ShouldHandle(SceneTransitionEvent @event);
/// <summary>
/// 执行中间件逻辑。
/// </summary>
/// <param name="event">场景过渡事件,包含切换的上下文信息。</param>
/// <param name="next">下一个中间件或实际操作的委托。调用此委托以继续执行流程。</param>
/// <param name="cancellationToken">取消令牌,用于支持操作取消。</param>
/// <returns>表示处理操作完成的异步任务。</returns>
Task HandleAsync(
SceneTransitionEvent @event,
Func<Task> next,
CancellationToken cancellationToken);
}

View File

@ -0,0 +1,34 @@
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI切换中间件处理器接口支持包裹整个变更过程的逻辑。
/// Around 处理器在变更前后都会执行,可以控制是否继续执行变更。
/// 适用于:性能监控、事务管理、权限验证、日志记录等横切关注点。
/// </summary>
public interface IUiAroundTransitionHandler
{
/// <summary>
/// 处理器优先级,数值越小越先执行(外层)。
/// 建议范围:-1000 到 1000。
/// </summary>
int Priority { get; }
/// <summary>
/// 判断是否应该处理当前事件。
/// </summary>
/// <param name="event">UI切换事件。</param>
/// <returns>如果应该处理则返回 true否则返回 false。</returns>
bool ShouldHandle(UiTransitionEvent @event);
/// <summary>
/// 执行中间件逻辑。
/// </summary>
/// <param name="event">UI切换事件。</param>
/// <param name="next">下一个中间件或实际操作的委托。调用此委托以继续执行流程。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步任务。</returns>
Task HandleAsync(
UiTransitionEvent @event,
Func<Task> next,
CancellationToken cancellationToken);
}

View File

@ -92,10 +92,13 @@ public abstract class SceneRouterBase
var @event = CreateEvent(sceneKey, SceneTransitionType.Replace, param);
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await PushInternalAsync(sceneKey, param);
AfterChange(@event);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await PushInternalAsync(sceneKey, param);
AfterChange(@event);
});
}
finally
{
@ -209,9 +212,12 @@ public abstract class SceneRouterBase
var @event = CreateEvent(sceneKey, SceneTransitionType.Push, param);
await BeforeChangeAsync(@event);
await PushInternalAsync(sceneKey, param);
AfterChange(@event);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await PushInternalAsync(sceneKey, param);
AfterChange(@event);
});
}
finally
{
@ -287,9 +293,12 @@ public abstract class SceneRouterBase
var @event = CreateEvent(null, SceneTransitionType.Pop);
await BeforeChangeAsync(@event);
await PopInternalAsync();
AfterChange(@event);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await PopInternalAsync();
AfterChange(@event);
});
}
finally
{
@ -355,9 +364,12 @@ public abstract class SceneRouterBase
var @event = CreateEvent(null, SceneTransitionType.Clear);
await BeforeChangeAsync(@event);
await ClearInternalAsync();
AfterChange(@event);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
AfterChange(@event);
});
}
finally
{

View File

@ -25,6 +25,8 @@ namespace GFramework.Game.scene;
public class SceneTransitionPipeline
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneTransitionPipeline));
private readonly List<ISceneAroundTransitionHandler> _aroundHandlers = [];
private readonly Dictionary<ISceneAroundTransitionHandler, SceneTransitionHandlerOptions> _aroundOptions = new();
private readonly List<ISceneTransitionHandler> _handlers = [];
private readonly Dictionary<ISceneTransitionHandler, SceneTransitionHandlerOptions> _options = new();
@ -67,6 +69,44 @@ public class SceneTransitionPipeline
Log.Debug("Handler unregistered: {0}", handler.GetType().Name);
}
/// <summary>
/// 注册 Around 中间件处理器。
/// </summary>
/// <param name="handler">处理器实例。</param>
/// <param name="options">执行选项,如果为 null 则使用默认选项。</param>
public void RegisterAroundHandler(ISceneAroundTransitionHandler handler,
SceneTransitionHandlerOptions? options = null)
{
ArgumentNullException.ThrowIfNull(handler);
if (_aroundHandlers.Contains(handler))
{
Log.Debug("Around handler already registered: {0}", handler.GetType().Name);
return;
}
_aroundHandlers.Add(handler);
_aroundOptions[handler] = options ?? new SceneTransitionHandlerOptions();
Log.Debug(
"Around handler registered: {0}, Priority={1}",
handler.GetType().Name,
handler.Priority
);
}
/// <summary>
/// 注销 Around 中间件处理器。
/// </summary>
/// <param name="handler">处理器实例。</param>
public void UnregisterAroundHandler(ISceneAroundTransitionHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
if (!_aroundHandlers.Remove(handler)) return;
_aroundOptions.Remove(handler);
Log.Debug("Around handler unregistered: {0}", handler.GetType().Name);
}
/// <summary>
/// 执行指定阶段的所有处理器。
/// </summary>
@ -114,6 +154,53 @@ public class SceneTransitionPipeline
Log.Debug("Pipeline execution completed for phases: {0}", phases);
}
/// <summary>
/// 执行 Around 中间件处理器,包裹核心操作。
/// </summary>
/// <param name="event">场景过渡事件。</param>
/// <param name="coreAction">核心操作委托。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步任务。</returns>
public async Task ExecuteAroundAsync(
SceneTransitionEvent @event,
Func<Task> coreAction,
CancellationToken cancellationToken = default)
{
var handlers = _aroundHandlers
.Where(h => h.ShouldHandle(@event))
.OrderBy(h => h.Priority)
.ToList();
if (handlers.Count == 0)
{
await coreAction();
return;
}
Log.Debug(
"Executing {0} around handlers for event: {1}",
handlers.Count,
@event.TransitionType
);
// 构建中间件链
Func<Task> pipeline = coreAction;
for (int i = handlers.Count - 1; i >= 0; i--)
{
var handler = handlers[i];
var options = _aroundOptions[handler];
var next = pipeline;
pipeline = async () =>
{
await ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
};
}
await pipeline();
}
private List<ISceneTransitionHandler> FilterAndSortHandlers(
SceneTransitionEvent @event,
SceneTransitionPhases phases)
@ -179,4 +266,37 @@ public class SceneTransitionPipeline
throw;
}
}
private static async Task ExecuteSingleAroundHandlerAsync(
ISceneAroundTransitionHandler handler,
SceneTransitionHandlerOptions options,
SceneTransitionEvent @event,
Func<Task> next,
CancellationToken cancellationToken)
{
Log.Debug("Executing around handler: {0}", handler.GetType().Name);
try
{
using var timeoutCts = options.TimeoutMs > 0
? new CancellationTokenSource(options.TimeoutMs)
: null;
using var linkedCts = timeoutCts != null && cancellationToken.CanBeCanceled
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken);
Log.Debug("Around handler completed: {0}", handler.GetType().Name);
}
catch (Exception ex)
{
Log.Error("Around handler failed: {0}, Error: {1}",
handler.GetType().Name, ex.Message);
if (!options.ContinueOnError)
throw;
}
}
}

View File

@ -89,9 +89,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
BeforeChange(@event);
DoPushPageInternal(uiKey, param, policy);
AfterChange(@event);
_pipeline.ExecuteAroundAsync(@event, async () =>
{
BeforeChange(@event);
DoPushPageInternal(uiKey, param, policy);
AfterChange(@event);
}).GetAwaiter().GetResult();
}
/// <summary>
@ -111,9 +114,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
BeforeChange(@event);
DoPushPageInternal(page, param, policy);
AfterChange(@event);
_pipeline.ExecuteAroundAsync(@event, async () =>
{
BeforeChange(@event);
DoPushPageInternal(page, param, policy);
AfterChange(@event);
}).GetAwaiter().GetResult();
}
/// <summary>
@ -138,9 +144,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
var nextUiKey = _stack.Count > 1 ? _stack.ElementAt(1).Key : null;
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
BeforeChange(@event);
DoPopInternal(policy);
AfterChange(@event);
_pipeline.ExecuteAroundAsync(@event, async () =>
{
BeforeChange(@event);
DoPopInternal(policy);
AfterChange(@event);
}).GetAwaiter().GetResult();
}
/// <summary>
@ -153,14 +162,17 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param);
Log.Debug("Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}", uiKey, popPolicy, pushPolicy);
BeforeChange(@event);
DoClearInternal(popPolicy);
_pipeline.ExecuteAroundAsync(@event, async () =>
{
BeforeChange(@event);
DoClearInternal(popPolicy);
var page = _factory.Create(uiKey);
Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name);
var page = _factory.Create(uiKey);
Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
AfterChange(@event);
DoPushPageInternal(page, param, pushPolicy);
AfterChange(@event);
}).GetAwaiter().GetResult();
}
/// <summary>
@ -175,11 +187,14 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
Log.Debug("Replace UI Stack with existing page: key={0}, popPolicy={1}, pushPolicy={2}",
uiKey, popPolicy, pushPolicy);
BeforeChange(@event);
DoClearInternal(popPolicy);
Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
AfterChange(@event);
_pipeline.ExecuteAroundAsync(@event, async () =>
{
BeforeChange(@event);
DoClearInternal(popPolicy);
Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
AfterChange(@event);
}).GetAwaiter().GetResult();
}
/// <summary>
@ -190,9 +205,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
var @event = CreateEvent(string.Empty, UiTransitionType.Clear);
Log.Debug("Clear UI Stack, stackCount={0}", _stack.Count);
BeforeChange(@event);
DoClearInternal(UiPopPolicy.Destroy);
AfterChange(@event);
_pipeline.ExecuteAroundAsync(@event, async () =>
{
BeforeChange(@event);
DoClearInternal(UiPopPolicy.Destroy);
AfterChange(@event);
}).GetAwaiter().GetResult();
}
/// <summary>

View File

@ -11,6 +11,8 @@ namespace GFramework.Game.ui;
public class UiTransitionPipeline
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("UiTransitionPipeline");
private readonly List<IUiAroundTransitionHandler> _aroundHandlers = [];
private readonly Dictionary<IUiAroundTransitionHandler, UiTransitionHandlerOptions> _aroundOptions = new();
private readonly List<IUiTransitionHandler> _handlers = [];
private readonly Dictionary<IUiTransitionHandler, UiTransitionHandlerOptions> _options = new();
@ -53,6 +55,43 @@ public class UiTransitionPipeline
Log.Debug("Handler unregistered: {0}", handler.GetType().Name);
}
/// <summary>
/// 注册 Around 中间件处理器
/// </summary>
/// <param name="handler">处理器实例</param>
/// <param name="options">执行选项</param>
public void RegisterAroundHandler(IUiAroundTransitionHandler handler, UiTransitionHandlerOptions? options = null)
{
ArgumentNullException.ThrowIfNull(handler);
if (_aroundHandlers.Contains(handler))
{
Log.Debug("Around handler already registered: {0}", handler.GetType().Name);
return;
}
_aroundHandlers.Add(handler);
_aroundOptions[handler] = options ?? new UiTransitionHandlerOptions();
Log.Debug(
"Around handler registered: {0}, Priority={1}",
handler.GetType().Name,
handler.Priority
);
}
/// <summary>
/// 注销 Around 中间件处理器
/// </summary>
/// <param name="handler">处理器实例</param>
public void UnregisterAroundHandler(IUiAroundTransitionHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
if (!_aroundHandlers.Remove(handler)) return;
_aroundOptions.Remove(handler);
Log.Debug("Around handler unregistered: {0}", handler.GetType().Name);
}
/// <summary>
/// 执行指定阶段的所有Handler
/// </summary>
@ -100,6 +139,53 @@ public class UiTransitionPipeline
Log.Debug("Pipeline execution completed for phases: {0}", phases);
}
/// <summary>
/// 执行 Around 中间件处理器,包裹核心操作
/// </summary>
/// <param name="event">UI切换事件</param>
/// <param name="coreAction">核心操作委托</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>异步任务</returns>
public async Task ExecuteAroundAsync(
UiTransitionEvent @event,
Func<Task> coreAction,
CancellationToken cancellationToken = default)
{
var handlers = _aroundHandlers
.Where(h => h.ShouldHandle(@event))
.OrderBy(h => h.Priority)
.ToList();
if (handlers.Count == 0)
{
await coreAction();
return;
}
Log.Debug(
"Executing {0} around handlers for event: {1}",
handlers.Count,
@event.TransitionType
);
// 构建中间件链
Func<Task> pipeline = coreAction;
for (int i = handlers.Count - 1; i >= 0; i--)
{
var handler = handlers[i];
var options = _aroundOptions[handler];
var next = pipeline;
pipeline = async () =>
{
await ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
};
}
await pipeline();
}
private List<IUiTransitionHandler> FilterAndSortHandlers(
UiTransitionEvent @event,
UiTransitionPhases phases)
@ -165,4 +251,37 @@ public class UiTransitionPipeline
throw;
}
}
private static async Task ExecuteSingleAroundHandlerAsync(
IUiAroundTransitionHandler handler,
UiTransitionHandlerOptions options,
UiTransitionEvent @event,
Func<Task> next,
CancellationToken cancellationToken)
{
Log.Debug("Executing around handler: {0}", handler.GetType().Name);
try
{
using var timeoutCts = options.TimeoutMs > 0
? new CancellationTokenSource(options.TimeoutMs)
: null;
using var linkedCts = timeoutCts != null && cancellationToken.CanBeCanceled
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken);
Log.Debug("Around handler completed: {0}", handler.GetType().Name);
}
catch (Exception ex)
{
Log.Error("Around handler failed: {0}, Error: {1}",
handler.GetType().Name, ex.Message);
if (!options.ContinueOnError)
throw;
}
}
}