docs/advanced/routing
Funktionen · Navigation

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.

8 Min. Lesezeit

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.

app.zig
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;
}
navigate.zig
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:

GrenzeWertBei Überlauf
Parameter pro Eintragmax_params = 8error.TooManyParams
Key-Längemax_key_len = 64error.KeyTooLong
Value-Längemax_value_len = 512error.ValueTooLong

navigate, 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:

back_button.zig
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.

pages.zig
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_page.zig
// 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

HISTORY STACK
back / forward verschieben nur den Cursor; ein navigate nach back kürzt zuerst history_len auf den Cursor und legt dann den neuen Eintrag ab.
  • ✓

    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 navigate abbilden.

CAPACITY 64
Das Verwerfen erfolgt gebündelt: Statt pro Ablegen einen Eintrag zu verdrängen, wird auf einmal die halbe Kapazität frei.

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.

trace.zig
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});

Die vollständige Console-API finden Sie unter DevTools.

zenit · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 5f9add5+wip 2026-09-30