跳转到内容

4. SusRouteView — 可视化容器

API

SusRouteView 是核心基础组件 SusScreenOutlet<SusScreen> 的子类——一个与导航无关的 屏幕层容器。它只添加了路由相关的逻辑;挂载 + KeepAlive(LRU)的底层机制都在 core 中实现。

csharp
public class SusRouteView : SusScreenOutlet<SusScreen>
{
    public SusRouter Router { get; set; }

    // Inherited from SusScreenOutlet<SusScreen>:
    public int MaxKeepAlive { get; set; } = 10;
    public SusScreen CurrentScreen { get; protected set; }

    public void OnRouteChanged(SusRoute fromRoute, SusRoute toRoute);
    public void RenderNestedChain(List<SusScreen> chainScreens);
}

流程

SusRouter.Navigate(from, to)
      |
SusRouteView.OnRouteChanged(from, to):
    1. if !KeepAlive → from.Screen.parent.Remove(from.Screen)   ← detach + drop
       if  KeepAlive → Remove(from.Screen) + CacheKeepAliveScreen(key, screen)  ← detach + cache
    2. Add(to.Screen)                                           ← add new/cached screen
    3. to.Screen.style.flexGrow = 1; CurrentScreen = to.Screen

过渡动画(PlayIn/PlayOut)由 SusRouter 围绕这次替换来驱动。

KeepAlive —— off-DOM LRU 缓存

csharp
router.Register("/battle/:id", typeof(BattleScreen), new SusRouteConfig
{
    KeepAlive = true,
    Transition = SusRouteTransition.Fade(0.2f)
});

工作原理

KeepAlive 是一个 off-DOM 缓存,而不是一个包装元素——屏幕会从 outlet 中分离parent.Remove(screen)),不会display:none 隐藏,也不会被包装进 core 的 SusKeepAlive 组件中。

  1. 离开 KeepAlive 屏幕时:OnRouteChanged 会把该屏幕从 outlet 中移除,并调用 CacheKeepAliveScreen(key, screen)。被分离的 VisualElement 子树及其 Prop<T> 状态都会保留在内存中。fromRoute.Screen 会被清空,IsActive = false
  2. 返回时: router 会调用 TryGetKeepAliveScreen(key, out screen);命中时, 缓存中的实例会被重新添加回 outlet ——不会创建新实例,也不会调用 Left()。 键为 SusRouter.KeepAliveKey(route)(回退为 route.FullPath,含 query 参数)。
  3. 在 router 中: 如果目标 KeepAlive 路由已经有一个存活的缓存屏幕,router 会复用它, 而不是重新构造一个。

LRU 淘汰

缓存和顺序都保存在 SusScreenOutlet<TScreen> 中:

csharp
private readonly Dictionary<string, TScreen> _keepAliveCache;
private readonly List<string> _keepAliveOrder; // LRU: front = oldest
public int MaxKeepAlive { get; set; } = 10;

当添加一个新键会超出 MaxKeepAlive 时,最老的条目会被优先淘汰:

  1. _keepAliveOrder[0](最老的)从顺序列表中移除;
  2. 执行 OnScreenEvicted(oldScreen)——SusRouteView 重写了它,调用 screen.Left()(清理);
  3. 该条目从 _keepAliveCache 中删除。

ClearKeepAliveCache() 会淘汰所有条目(对每一个都运行清理钩子)。

在 App.sharq 中的用法

xml
<template>
<ui:VisualElement $MainElement class="app">
    <sus:SusRouteView ref="RouteView" />
</ui:VisualElement>
</template>

<script>
public SusRouter Router = new();
public SusRouteView RouteView;

public override void Mounted()
{
    Router.SetRouteView(RouteView);
    RouteView.Router = Router;

    Router.Register("/main-menu", typeof(MainMenuScreen));
    Router.Register("/battle", typeof(BattleHudScreen),
        new SusRouteConfig { KeepAlive = true });
    Router.Register("/loading", typeof(LoadingScreen));

    Router.Replace("/loading");
}
</script>

嵌套路由 —— 嵌套渲染

每个父级屏幕都可以注册一个嵌套的 SusRouteView

csharp
public class SettingsScreen : SusScreen
{
    public SusRouteView ChildRouterView;

    protected override void Build()
    {
        ChildRouterView = new SusRouteView();
        RegisterChildView(ChildRouterView);
        Add(ChildRouterView);
    }
}

导航到 /settings/profile 时:

  1. ResolveChain() 构建出链:SettingsRecord → ProfileRecord
  2. SusRouteView 渲染 SettingsScreen
  3. SettingsScreen.ChildRouterView 渲染 ProfileTabScreen

限制

当子路由发生变化(/settings/profile/settings/account)时,父级会被重建。链级别的 diffing 计划在未来实现。