앱 내 라우팅
페이지를 enum으로 표현하고, 현재 페이지와 파라미터를 두 개의 Signal로 노출하며, 고정 용량 히스토리 스택으로 뒤로 / 앞으로 이동을 지원합니다.
Router 모델
ui.fx.Router(Page)는 enum 기반의 인메모리 라우터이며 웹 URL 라우터가 아닙니다. 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 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);페이지 렌더링
현재 페이지는 Signal(Page)이며, ui.Match로 enum 값에 따라 페이지 서브트리를 전환합니다. Match는 페이지마다 자식 Scope를 만들고, 전환 시 먼저 이전 Scope를 dispose한 뒤(그 안의 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는 enum 값이 바뀔 때만 다시 빌드합니다. .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 API 전체는 DevTools를 참고하십시오.