应用内路由
用枚举描述页面、用两个 Signal 暴露当前页与参数、用固定容量的历史栈支持后退与前进。
Router 模型
ui.fx.Router(Page) 是枚举驱动的内存路由器,不是 Web URL router。它暴露 current: *Signal(Page) 与 params: *Signal(RouteParams) 两个 Signal,并在内部维护最多 64 项的历史栈。
const std = @import("std");
const ui = @import("ui");
const Page = enum { welcome, editor, settings };
const Router = ui.fx.Router(Page);
fn mountApp(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
// The router's two Signals are created in `scope`, so pass a scope that
// lives as long as the window. The router value itself is large (~290 KB of
// inline history), so keep it in Cx-owned state and pass the pointer around.
const router = try cx.bindState(Router, try Router.init(scope, .welcome));
const shell = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
.direction = .column,
}, .{});
// ...append a toolbar and the page host (see "Rendering pages")
_ = router;
return shell;
}导航与参数
var params: ui.fx.router.RouteParams = .{};
try params.set("file", "draft.md"); // copies the bytes; errors on overflow
router.navigate(.editor, params); // page + params
router.navigateTo(.settings); // page, empty params
_ = router.back(); // false when already at the oldest entry
_ = router.forward(); // false when there is no forward branch
if (router.canGoBack()) {
// e.g. enable a Back button
}RouteParams.set 会把 key / value 字节复制进自身缓冲,调用方的缓冲区之后变化也不影响参数;同名 key 会覆盖旧值。容量是固定的,溢出时返回错误而不是截断:
max_params = 8error.TooManyParamsmax_key_len = 64error.KeyTooLongmax_value_len = 512error.ValueTooLongnavigate、back、forward 都会同步写入两个 Signal。canGoBack() / canGoForward() 是普通读取,不是 Signal;在事件处理器里调用 Router 时,用 cx.bindState 保存指针:
const Nav = struct {
router: *Router,
fn goBack(self: *Nav) void {
_ = self.router.back();
}
};
const nav = try cx.bindState(Nav, .{ .router = router });
const back_button = try ui.widgets.Button(.{
.label = "Back",
.on_click = cx.on(Nav, nav, Nav.goBack),
}).mount(scope, cx);渲染页面
当前页是 Signal(Page),用 ui.Match 按枚举值切换页面子树。Match 为每个页面创建子 Scope;切换时先 dispose 旧 Scope(其中的 Signal、Effect 与资源一起释放),再销毁旧节点、挂载新页面。
const page_host = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
}, .{});
// Match appends the page to page_host and rebuilds it when router.current
// changes; each page gets a fresh child Scope that is disposed on leave.
_ = try ui.Match(Page, scope, page_host, router.current, cx, struct {
fn build(child_scope: *ui.Scope, c: *ui.Cx, page: Page) anyerror!*ui.Node {
return switch (page) {
.welcome => mountWelcome(c, child_scope),
.editor => mountEditor(c, child_scope),
.settings => mountSettings(c, child_scope),
};
}
}.build);Match 只在枚举值变化时重建:从 .editor 导航到带不同参数的 .editor 不会重新挂载页面。需要响应参数的页面应自己订阅 router.params:
// editor -> editor with other params does not rebuild the page,
// so subscribe to the params Signal inside the page.
try scope.createEffect(.{ .params = router.params }, struct {
fn run(ctx: anytype) void {
// get() returns a copy; keep it in a local so the slices stay valid.
const params = ctx.params.get();
const file = params.get("file") orelse "untitled.md";
std.log.info("editor shows {s}", .{file});
}
}.run);历史规则
- ✓
新导航截断前进分支。从 back 之后的位置 navigate,原来的 forward 条目被丢弃。
- ✓
每次 navigate 都压栈。同一页面的不同参数、甚至完全相同的目标,都会成为新的历史条目。
- ✓
满 64 项时丢弃较旧的一半。第 65 次压栈前保留最新的 32 项,游标随之前移。
- ✓
没有 URL。深链、系统 URL scheme 等需要应用层自己映射到
navigate。
记录导航
Router 本身不写日志。需要排查导航时,把关键跳转写到当前 Cx 的 Console,并用一个 router scope 标记,就可以在 DevTools Console 中按 scope 过滤,看到等级与调用位置。
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});Console 的完整用法见 调试与检查。