In-App-Routing
Beschreiben Sie Seiten als Enum, stellen Sie die aktuelle Seite und ihre Parameter als zwei Signals bereit und erhalten Sie Zurück / Vor über einen Verlaufsstapel mit fester Kapazität.
Das Router-Modell
ui.fx.Router(Page) ist ein Enum-gesteuerter In-Memory-Router — kein Web-URL-Router. Er stellt zwei Signals bereit, current: *Signal(Page) und params: *Signal(RouteParams), und führt einen Verlaufsstapel mit bis zu 64 Einträgen.
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;
}Navigation und Parameter
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 kopiert die Key- und Value-Bytes in eigene Puffer, spätere Änderungen am Puffer des Aufrufers wirken sich also nicht aus; ein vorhandener Key wird überschrieben. Die Kapazität ist fest, und ein Überlauf liefert einen Fehler statt abzuschneiden:
max_params = 8error.TooManyParamsmax_key_len = 64error.KeyTooLongmax_value_len = 512error.ValueTooLongnavigate, back und forward schreiben jeweils beide Signals. canGoBack() / canGoForward() sind einfache Lesezugriffe, keine Signals. Um den Router aus einem Handler zu steuern, halten Sie den Zeiger in einem cx.bindState-Struct:
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);Seiten rendern
Die aktuelle Seite ist ein Signal(Page); wechseln Sie die Seiten-Teilbäume darauf mit ui.Match. Match gibt jeder Seite einen Kind-Scope; bei einem Wechsel wird zuerst der alte Scope per dispose freigegeben (seine Signals, Effects und Ressourcen gleich mit), dann werden die alten Nodes freigegeben und die neue Seite gemountet.
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 baut nur neu auf, wenn sich der Enum-Wert ändert: Eine Navigation von .editor zu .editor mit anderen Parametern lässt die Seite gemountet. Eine Seite, die von Parametern abhängt, sollte router.params selbst abonnieren:
// 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);Verlaufsregeln
- ✓
Neue Navigation kappt den Vorwärtszweig. Ein navigate nach back verwirft die alten forward-Einträge.
- ✓
Jedes navigate legt ab. Dieselbe Seite mit anderen Parametern — oder sogar ein identisches Ziel — wird ein neuer Eintrag.
- ✓
Bei 64 Einträgen fällt die ältere Hälfte weg. Vor dem 65. Ablegen bleiben die neuesten 32 erhalten, und der Cursor verschiebt sich mit.
- ✓
Keine URLs. Deep Links und System-URL-Schemes muss die App selbst auf
navigateabbilden.
Navigation protokollieren
Der Router loggt nicht von selbst. Um die Navigation zu debuggen, schreiben Sie die relevanten Übergänge unter einem router-Scope in die Console des aktuellen Cx; die DevTools Console kann dann nach Scope filtern und Level sowie Aufrufstelle anzeigen.
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});Die vollständige Console-API finden Sie unter DevTools.