3. SusScreen — 屏幕、生命周期与接入
屏幕(SusScreen)是由 SusRouter 管理的全屏视图。 它继承自 SusComponent,因此拥有完整的核心响应式工具集 (Prop<T>、Watch、Build()、Mounted()),此外还具备路由器生命周期,并可访问 路由参数。
1. 最小屏幕示例
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)。此时 Router 和 Props尚未在 Build() 中设置——请在 OnBeforeEnter() / OnEntered() 中读取它们。
2. 生命周期——重写 On* 方法
⚠️ 重要:公开的
BeforeEnter/Entered/BeforeLeave/Left/BeforeRouteUpdate由路由器自身调用——不要重写它们。请重写protected virtual的On*钩子(模板方法模式)。
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 字典中。可以通过以下辅助方法读取:
// 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| 成员 | 类型 | 作用 |
|---|---|---|
Router | SusRouter | 屏幕所属的路由器(导航、模态框) |
Props | Dictionary<string,object> | 参数 + 查询 + 传入的 props(永不为 null) |
IsActive | bool | 屏幕当前是否处于激活状态 |
GetProp<T>(key, def) | T | 带类型转换的类型化读取 |
GetParam(key, def) / GetQuery(key, def) | string | 指向 Props 的别名(params/query 已合并) |
4. 接入屏幕的三种方式
4.1 命令式 —— router.Register(path, type, config?)
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
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)
SusApp.Create(uiDocument)
.UseRouter(new SusRouter(), r =>
{
r.Register("/", typeof(HomeScreen));
r.Register("/settings", typeof(SettingsScreen));
}, initialPath: "/")
.Run();UseRouter 会在 SusApp 正确的收尾阶段嵌入注册逻辑与 Mount (在 token 级联/自定义样式之后,主题应用之前)。
5. SusRouteConfig 的全部选项
| 选项 | 类型 | 作用 |
|---|---|---|
Name | string | 供 PushNamed/ReplaceNamed 使用的名称 |
KeepAlive | bool | 离开时不重新创建屏幕(实例缓存) |
Alias | List<string> | 额外解析到该路由的路径 |
Children | List<SusRouteRecord> | 嵌套路由(一层) |
Redirect | string | 进入时改为跳转到此处,而不是当前路由 |
DefaultProps | Dictionary<string,object> | 屏幕的默认 props |
PropsFn | Func<SusRoute, Dictionary<string,object>> | 根据路由生成 props(对应 Vue 的 props: route => ({...})) |
LazyFactory | Func<SusScreen> | 延迟创建屏幕(替代 Activator.CreateInstance) |
Guard | ISusRouteGuard | 单个路由的守卫 CanEnter/CanLeave |
BeforeEnter | SusRouterGuard | 函数式进入守卫(在 Guard.CanEnter 之后执行) |
Transition | SusRouteTransition | 过渡动画(Fade()、SlideLeft() 等) |
Meta | Dictionary<string,object> | 任意元数据(requiresAuth、title) |
CaseSensitive | bool | 路径匹配是否区分大小写(默认关闭) |
Strict | bool | 是否严格匹配结尾斜杠(/a ≠ /a/) |
各选项示例
// 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 (成功 / 被守卫中止 / 重定向)。
if (Router.CanGoBack) Router.Back();
Router.Push("/checkout", new() { ["cartId"] = cart.Id });
Router.Modal(typeof(InfoDialog), new() { ["text"] = "Done!" });7. 嵌套屏幕
父屏幕注册一个子级 SusRouteView,路由器会把 子屏幕挂载到这里:
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)
[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/Props从OnBeforeEnter()起才可用,在Created()/Build()中尚不可用。Unmounted()——从面板分离时调用(在OnLeft()之后);使用 KeepAlive 时 屏幕会被隐藏,Unmounted()不会被调用。
相关文档
- 02-router-api.md — 完整的
SusRouterAPI - 04-routeview.md —
SusRouteView与 KeepAlive - 05-modals.md — 模态框
- 06-guards-transitions.md — 守卫流水线与动画