跳到主要内容

客户端路由

evjs 路由基于 TanStack Router 构建。所有路由 API 从 @evjs/client 重新导出 —— 不要直接从 @tanstack/react-router 导入。

important

路由路径必须是字符串字面量。 path 属性只接受字符串字面量类型——传入 string 类型的变量或模板字符串会产生 TypeScript 编译错误。这是通过类型系统强制执行的,以确保路由可被静态分析。

// ✅ 正确 — 字符串字面量
createRoute({ path: "/users/$id", ... });

// ❌ 编译错误 — 宽泛的 `string` 类型
const p: string = "/users";
createRoute({ path: p, ... });

// ❌ 编译错误 — 模板字符串
createRoute({ path: `/users/${segment}`, ... });

入口配置

// src/main.tsx
import { createApp } from "@evjs/client";
import { rootRoute } from "./pages/__root";
import { homeRoute } from "./pages/home";
import { postsRoute, postsIndexRoute, postDetailRoute } from "./pages/posts";

const routeTree = rootRoute.addChildren([
homeRoute,
postsRoute.addChildren([postsIndexRoute, postDetailRoute]),
]);

const app = createApp({ routeTree });

declare module "@tanstack/react-router" {
interface Register {
router: typeof app.router;
}
}

app.render("#app");

根布局

每个应用都需要一个带 <Outlet /> 的根路由来渲染子路由:

import { createAppRootRoute, Link, Outlet } from "@evjs/client";

export const rootRoute = createAppRootRoute({
component: () => (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/posts">文章</Link>
</nav>
<Outlet />
</div>
),
});

动态路由($param

使用 $name 语法定义路径参数,通过 route.useParams() 进行类型安全访问:

export const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/users/$username",
loader: ({ params, context }) =>
context.queryClient.ensureQueryData(
getFnQueryOptions(getUser, params.username),
),
component: () => {
const { username } = userRoute.useParams();
return <h2>{username}</h2>;
},
});

嵌套路由

父路由通过 <Outlet /> 渲染子路由,在 main.tsx 中通过 addChildren() 组装:

export const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/posts",
component: () => (
<div style={{ display: "flex" }}>
<nav>侧边栏</nav>
<Outlet />
</div>
),
});

export const postDetailRoute = createRoute({
getParentRoute: () => postsRoute,
path: "$postId",
component: PostDetail,
});

无路径布局

使用 id 代替 path 创建不增加 URL 片段的共享 UI:

export const dashboardLayout = createRoute({
getParentRoute: () => rootRoute,
id: "dashboard-layout",
component: () => <div className="layout"><Outlet /></div>,
});

搜索参数

使用 validateSearch 定义带类型的查询字符串参数:

export const searchRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/search",
validateSearch: (search: Record<string, unknown>) => ({
q: (search.q as string) || "",
page: Number(search.page) || 1,
}),
component: () => {
const { q, page } = searchRoute.useSearch();
return <div>搜索: {q},第 {page}</div>;
},
});

路由加载器(预取)

使用 loader 在路由渲染前预取数据 —— 消除加载转圈:

export const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/users",
loader: ({ context }) =>
context.queryClient.ensureQueryData(getFnQueryOptions(getUsers)),
component: UsersPage,
});

重定向

beforeLoad 中抛出 redirect() 实现渲染前重定向:

import { createRoute, redirect } from "@evjs/client";

export const redirectRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/old-blog",
beforeLoad: () => {
throw redirect({ to: "/posts" });
},
});

404 兜底

使用 path: "*" 捕获所有未匹配的 URL:

export const notFoundRoute = createRoute({
getParentRoute: () => rootRoute,
path: "*",
component: () => <h1>404 —— 页面未找到</h1>,
});

导航

import { Link, useNavigate, Navigate } from "@evjs/client";

// 声明式
<Link to="/posts/$postId" params={{ postId: "1" }}>查看</Link>

// 命令式
const navigate = useNavigate();
navigate({ to: "/posts" });

// 重定向组件
<Navigate to="/login" />

可用的重新导出

全部从 @evjs/client 导入:

类别API
路由创建createAppRootRoute, createRoute, createRouter, createRootRouteWithContext, createRouteMask
组件Link, Outlet, Navigate, RouterProvider, ErrorComponent, CatchBoundary, CatchNotFound
HooksuseParams, useSearch, useNavigate, useLocation, useMatch, useMatchRoute, useRouter, useRouterState, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack
工具redirect, notFound, isRedirect, isNotFound, getRouteApi, linkOptions, lazyRouteComponent, createLink