跳转到内容

3. SusScreen — 屏幕、生命周期与接入

屏幕(SusScreen)是由 SusRouter 管理的全屏视图。 它继承自 SusComponent,因此拥有完整的核心响应式工具集 (Prop<T>WatchBuild()Mounted()),此外还具备路由器生命周期,并可访问 路由参数。


1. 最小屏幕示例

csharp
using UnityEngine.UIElements;
using Sharq.Router;

public class HomeScreen : SusScreen
{
    protected override void Build()
    {
        style.flexGrow = 1f;
        Add(new Label("Home"));

        var btn = new Button { text = "Go to About" };
        btn.RegisterCallback<ClickEvent>(_ => Router.Push("/about"));
        Add(btn);
    }
}

Build() 用于构建树(来自 SusComponent)。此时 RouterProps尚未Build() 中设置——请在 OnBeforeEnter() / OnEntered() 中读取它们。


2. 生命周期——重写 On* 方法

⚠️ 重要:公开的 BeforeEnter/Entered/BeforeLeave/Left/BeforeRouteUpdate路由器自身调用——不要重写它们。请重写 protected virtualOn* 钩子(模板方法模式)。

csharp
public class BattleScreen : SusScreen
{
    // 1. Enter validation + reading Props. false = enter cancelled.
    protected override bool OnBeforeEnter(SusRoute fromRoute)
    {
        var matchId = GetParam("matchId");
        return !string.IsNullOrEmpty(matchId);
    }

    // 2. Screen is already in the DOM — start animations, load data.
    protected override void OnEntered() => StartBattle();

    // 3. Props changed on the SAME screen instance (e.g. /users/1 → /users/2).
    protected override bool OnBeforeRouteUpdate(SusRoute toRoute) => true;

    // 4. Leave guard. false = navigation blocked (e.g. dirty form).
    protected override bool OnBeforeLeave(SusRoute toRoute)
    {
        if (_isDirty)
        {
            Router.Modal(typeof(ConfirmLeaveDialog), new() { ["message"] = "Leave without saving?" });
            return false;
        }
        return true;
    }

    // 5. Screen is being removed — unsubscribe, clean up resources.
    protected override void OnLeft() => CleanupBattle();
}

完整流程

Push("/battle/42"):
  1. Activator.CreateInstance / LazyFactory()
       Created()              ← SusComponent; Router/Props NOT set yet
       Build()                ← tree is built
  2. screen.Router = router
  3. screen.Props = params + query + DefaultProps + PropsFn(...)
  4. OnBeforeEnter(from)      ← false = cancel entire navigation
  5. history updates, SusRouteView swaps screen + transition.PlayIn()
  6. OnEntered()             ← screen in DOM
  7. (next frame) Mounted()   ← deferred SusComponent hook

Leave:
  8. OnBeforeLeave(to)       ← guard, false = cancel
  9. OnLeft()
 10. SusRouteView removes screen (or hides on KeepAlive)
 11. Unmounted()            ← SusComponent; NOT called on KeepAlive

使用 KeepAlive 时屏幕不会被重新创建:OnLeft() 会执行,但实例 会被隐藏(而不是销毁);返回时会再次触发 OnBeforeEnter() / OnEntered(), 但不会调用新的 Build()


3. 访问路由数据

SusRouter 会把路径参数:id)和查询参数?tab=x)合并到同一个 Props 字典中。可以通过以下辅助方法读取:

csharp
// route "/users/:id",  URL "/users/42?tab=profile"
string id  = GetParam("id");          // "42"      (path param)
string tab = GetQuery("tab");         // "profile" (query)
int    page = GetProp("page", 1);     // typed, with default
object raw = GetProp("payload");      // untyped
成员类型作用
RouterSusRouter屏幕所属的路由器(导航、模态框)
PropsDictionary<string,object>参数 + 查询 + 传入的 props(永不为 null)
IsActivebool屏幕当前是否处于激活状态
GetProp<T>(key, def)T带类型转换的类型化读取
GetParam(key, def) / GetQuery(key, def)string指向 Props 的别名(params/query 已合并)

4. 接入屏幕的三种方式

4.1 命令式 —— router.Register(path, type, config?)

csharp
var router = new SusRouter();
router.Register("/", typeof(HomeScreen));
router.Register("/about", typeof(AboutScreen));
router.Register("/users/:id", typeof(UserScreen),
    new SusRouteConfig { Name = "user", KeepAlive = true });
router.Mount(root, "/");     // or Mount(uiDocument, "/")

Register 返回 SusRouteRecord——可以将其嵌套到父级的 Children 中(参见 §5,嵌套路由)。

4.2 声明式 —— 通过 SusApp.UseRouter 使用 SusRouteBuilder

csharp
SusApp.Create(uiDocument)
      .UseTheme(SusTheme.Dark)
      .UseRouter(new SusRouter(), routes => routes
          .Route("/", typeof(HomeScreen)).Name("home")
          .Route<LoginScreen>("/login").Alias("/signin")
          .Route("/users/:id", typeof(UserScreen))
              .Name("user").KeepAlive().Meta("requiresAuth", true)
              .Children(c => c
                  .Route("profile", typeof(ProfileScreen))
                  .Route("posts", typeof(PostsScreen))),
          initialPath: "/")
      .Run();

4.3 命令式 UseRouter(在 SusApp 内部 Register)

csharp
SusApp.Create(uiDocument)
      .UseRouter(new SusRouter(), r =>
      {
          r.Register("/", typeof(HomeScreen));
          r.Register("/settings", typeof(SettingsScreen));
      }, initialPath: "/")
      .Run();

UseRouter 会在 SusApp 正确的收尾阶段嵌入注册逻辑与 Mount (在 token 级联/自定义样式之后,主题应用之前)。


5. SusRouteConfig 的全部选项

选项类型作用
NamestringPushNamed/ReplaceNamed 使用的名称
KeepAlivebool离开时不重新创建屏幕(实例缓存)
AliasList<string>额外解析到该路由的路径
ChildrenList<SusRouteRecord>嵌套路由(一层)
Redirectstring进入时改为跳转到此处,而不是当前路由
DefaultPropsDictionary<string,object>屏幕的默认 props
PropsFnFunc<SusRoute, Dictionary<string,object>>根据路由生成 props(对应 Vue 的 props: route => ({...})
LazyFactoryFunc<SusScreen>延迟创建屏幕(替代 Activator.CreateInstance
GuardISusRouteGuard单个路由的守卫 CanEnter/CanLeave
BeforeEnterSusRouterGuard函数式进入守卫(在 Guard.CanEnter 之后执行)
TransitionSusRouteTransition过渡动画(Fade()SlideLeft() 等)
MetaDictionary<string,object>任意元数据(requiresAuthtitle
CaseSensitivebool路径匹配是否区分大小写(默认关闭)
Strictbool是否严格匹配结尾斜杠(/a/a/

各选项示例

csharp
// KeepAlive + Transition
router.Register("/feed", typeof(FeedScreen),
    new SusRouteConfig { KeepAlive = true, Transition = SusRouteTransition.Fade() });

// Named + params → PushNamed
router.Register("/users/:id", typeof(UserScreen), new SusRouteConfig { Name = "user" });
router.PushNamed("user", new() { ["id"] = "42" });

// Alias + Redirect
router.Register("/home", typeof(HomeScreen), new SusRouteConfig { Alias = new() { "/start" } });
router.Register("/old", typeof(HomeScreen), new SusRouteConfig { Redirect = "/home" });

// DefaultProps + PropsFn
router.Register("/report", typeof(ReportScreen), new SusRouteConfig
{
    DefaultProps = new() { ["format"] = "pdf" },
    PropsFn = route => new() { ["id"] = route.Params.GetValueOrDefault("id") },
});

// Lazy + Meta + Guard
router.Register("/admin", typeof(AdminScreen), new SusRouteConfig
{
    LazyFactory = () => new AdminScreen(),
    Meta = new() { ["requiresAuth"] = true },
    BeforeEnter = (from, to) => Session.IsAdmin,
});

// Nested (Children) via Register records
var users = router.Register("/users", typeof(UsersScreen), new SusRouteConfig
{
    Children = new()
    {
        router.Register("/users/:id", typeof(UserDetailScreen)),
        router.Register("/users/search", typeof(UserSearchScreen)),
    }
});

6. 导航(SusRouter 的方法)

方法作用
Push(path, props?)压入历史记录并导航
Replace(path, props?)替换当前记录
PushNamed(name, pathParams?, props?)按路由名称导航
ReplaceNamed(name, pathParams?, props?)按名称替换
Back() / Forward()历史导航(基于游标)
Go(n)n 步偏移
NavigateWithTransition(path, …)使用显式动画导航
CanGoBack / CanGoForward历史记录可用性(用于按钮状态)
Modal(type, props?) / CloseModal()通过 ModalService 显示/关闭模态框

所有 Push*/Replace*/Back/Forward/Go 方法都会返回 NavigationResult (成功 / 被守卫中止 / 重定向)。

csharp
if (Router.CanGoBack) Router.Back();
Router.Push("/checkout", new() { ["cartId"] = cart.Id });
Router.Modal(typeof(InfoDialog), new() { ["text"] = "Done!" });

7. 嵌套屏幕

父屏幕注册一个子级 SusRouteView,路由器会把 子屏幕挂载到这里:

csharp
public class UsersScreen : SusScreen
{
    protected override void Build()
    {
        Add(new Label("Users"));

        var childView = new SusRouteView();
        RegisterChildView(childView);   // router mounts /users/:id etc. here
        Add(childView);
    }
}

代码中可以访问 ChildView(第一个)和 ChildViews(全部)。链式解析 会挑选出从根到叶的记录 MatchedChain


8. 入口点(MonoBehaviour)

csharp
[RequireComponent(typeof(UIDocument))]
public class AppEntry : MonoBehaviour
{
    [SerializeField] private UIDocument _uiDocument;

    private void OnEnable()
    {
        // Create(UIDocument) applies SusDefault.tss to the panel; prefer it over
        // Create(rootVisualElement), which skips TSS.
        SusApp.Create(_uiDocument)
              .UseTheme(SusTheme.Dark)
              .UseRouter(new SusRouter(), routes => routes
                  .Route("/main-menu", typeof(MainMenuScreen)).Name("menu")
                  .Route("/battle/:matchId", typeof(BattleScreen))
                      .KeepAlive().Transition(SusRouteTransition.Fade()),
                  initialPath: "/main-menu")
              .Run();
    }
}

与 SusComponent 的集成

  • OnEntered() 会在 Mounted() 之前调用。需要访问尚未在 Build() 阶段存在的子节点的逻辑,请放到 Mounted() 中。
  • Router/PropsOnBeforeEnter() 起才可用,在 Created()/Build()尚不可用
  • Unmounted()——从面板分离时调用(在 OnLeft() 之后);使用 KeepAlive 时 屏幕会被隐藏,Unmounted() 不会被调用。

相关文档