---
title: "应用内路由 — zenit Zig UI 文档"
description: "用枚举描述页面、用两个 Signal 暴露当前页与参数、用固定容量的历史栈支持后退与前进。"
url: https://zenit.z.express/zh/docs/advanced/routing
language: zh-CN
alternate_en: https://zenit.z.express/docs/advanced/routing.md
alternate_es: https://zenit.z.express/es/docs/advanced/routing.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/routing.md
alternate_ko: https://zenit.z.express/ko/docs/advanced/routing.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/routing.md
alternate_de: https://zenit.z.express/de/docs/advanced/routing.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 应用内路由

用枚举描述页面、用两个 Signal 暴露当前页与参数、用固定容量的历史栈支持后退与前进。

## Router 模型

`ui.fx.Router(Page)` 是枚举驱动的内存路由器，不是 Web URL router。它暴露 `current: *Signal(Page)` 与 `params: *Signal(RouteParams)` 两个 Signal，并在内部维护最多 64 项的历史栈。

`app.zig`

```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;
}
```

> NOTE
> 
> **Router 是一个大值。** 历史栈是内联数组：64 个条目，每条带一份最多 8 个参数的 `RouteParams`（key 64 字节 + value 512 字节的内联缓冲），合计约 290 KB。把它放在长期存储里（如 `cx.bindState`），传指针，不要按值复制。

## 导航与参数

`navigate.zig`

```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` 会把 key / value 字节复制进自身缓冲，调用方的缓冲区之后变化也不影响参数；同名 key 会覆盖旧值。容量是固定的，溢出时返回错误而不是截断：

| 限制 | 值 | 超出时 |
| --- | --- | --- |
| 每个条目的参数个数 | `max_params = 8` | `error.TooManyParams` |
| key 长度 | `max_key_len = 64` | `error.KeyTooLong` |
| value 长度 | `max_value_len = 512` | `error.ValueTooLong` |

`navigate`、`back`、`forward` 都会同步写入两个 Signal。`canGoBack()` / `canGoForward()` 是普通读取，不是 Signal；在事件处理器里调用 Router 时，用 `cx.bindState` 保存指针：

`back_button.zig`

```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);
```

## 渲染页面

当前页是 `Signal(Page)`，用 `ui.Match` 按枚举值切换页面子树。Match 为每个页面创建子 Scope；切换时先 dispose 旧 Scope（其中的 Signal、Effect 与资源一起释放），再销毁旧节点、挂载新页面。

`pages.zig`

```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 只在枚举值变化时重建：从 `.editor` 导航到带不同参数的 `.editor` 不会重新挂载页面。需要响应参数的页面应自己订阅 `router.params`：

`editor_page.zig`

```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);
```

## 历史规则

HISTORY STACK

back / forward 只移动游标；back 之后的 navigate 先把 history\_len 截到游标处，再压入新条目。

-   **新导航截断前进分支。**从 back 之后的位置 navigate，原来的 forward 条目被丢弃。
    
-   **每次 navigate 都压栈。**同一页面的不同参数、甚至完全相同的目标，都会成为新的历史条目。
    
-   **满 64 项时丢弃较旧的一半。**第 65 次压栈前保留最新的 32 项，游标随之前移。
    
-   **没有 URL。**深链、系统 URL scheme 等需要应用层自己映射到 `navigate`。
    

CAPACITY 64

丢弃是批量的：不是每次挤掉一条，而是一次腾出一半容量。

## 记录导航

Router 本身不写日志。需要排查导航时，把关键跳转写到当前 Cx 的 Console，并用一个 `router` scope 标记，就可以在 DevTools Console 中按 scope 过滤，看到等级与调用位置。

`trace.zig`

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

Console 的完整用法见 [调试与检查](https://zenit.z.express/zh/docs/advanced/devtools)。

> WARNING
> 
> **区分两种路由。** 本手册网站用的是浏览器 path router；Zenit 桌面应用的 `ui.fx.Router` 使用枚举与 Signal，不依赖 URL。
