react-router
数据模式, 声明模式
- 推荐使用数据模式
<Link />
,<NavLink />
类似 Vue 的<RouterLink />
tsx
import Home from "@/pages/Home";
import About from "@/pages/About";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
path: "/",
Component: Home, // Component
},
{
path: "/about",
element: <About />, // element
},
]);
tsx
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router";
import { router } from "@/router/index.tsx";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(<RouterProvider router={router} />);
tsx
import { BrowserRouter, Route, Routes } from "react-router";
import Home from "@/pages/Home";
import About from "@/pages/About";
import { createRoot } from "react-dom/client";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(
<BrowserRouter>
<Routes>
{/* Component */}
<Route path="/" Component={Home} />
{/* element */}
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>,
);
tsx
import { Link } from "react-router";
export default function Home() {
return (
<>
Home
<Link to="/about">about</Link>
</>
);
}
tsx
import { NavLink } from "react-router";
export default function About() {
return (
<>
About
<NavLink to="/">home</NavLink>
</>
);
}
路由模式
createBrowserRouter
: 使用 html5 的 history API (pushState, replaceState, popState), url 中没有 #, 需要服务器配置 fallback 路由, 以解决用户直接访问或刷新非 / 根路径的页面时, 返回 404 Not Found 问题createHashRouter
: 使用 url 的 hash 值, 改变 url 中的 hash 值不会导致页面的重新加载, 通常用于单页面内的导航, 不需要服务器配置, 不利于 SEOcreateMemoryRouter
: 适用于 node 环境和 SSR, url 不会改变createStaticRouter
: 适用于 SSR
nginx 配置 fallback 路由
解决用户直接访问或刷新非 / 根路径的页面时, 返回 404 Not Found 问题
bash
# 检查配置文件是否有语法错误
nginx -t
# 重新加载配置文件
nginx -s reload c
txt
// nginx.conf
http {
server {
listen 80;
server_name localhost;
location / {
root html
index index.html
// try_files $uri $uri.html $uri/ =404;
try_files $uri $uri.html $uri/ /index.html;
}
}
}
useNavigate 编程式导航
tsx
import App from "@/App";
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
path: "/",
Component: App,
},
{
path: "/home",
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/about",
Component: lazy(() => import("@/pages/About")),
},
]);
tsx
import { useNavigate } from "react-router";
export default function App() {
const navigate = useNavigate();
return (
<>
App
<button onClick={() => navigate("/home")}>home</button>
<button onClick={() => navigate("/about")}>about</button>
</>
);
}
嵌套路由、索引路由、布局路由、前缀路由
- 嵌套路由: 有 children 属性, 父组件默认不挂载子路由组件, 需要父组件使用
<Outlet />
组件, 作为子路由组件的容器, 以挂载子路由组件 - 索引路由:
index: true
, 即默认二级路由 - 布局路由: 没有 path 属性, 只提供统一的页面布局
- 前缀路由: 没有 Component 或 element 属性, 只提供统一的路由前缀
tsx
import Home from "@/pages/Home";
import Layout from "@/pages/Layout";
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
path: "/layout",
element: <Layout />,
// 嵌套路由: 有 children 属性
children: [
{
// 索引路由: index: true, 即默认二级路由
index: true,
Component: Home,
},
{
path: "home", // 等价于 path: "/layout/home"
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/layout/about", // 等价于 path: "about"
Component: lazy(() => import("@/pages/About")),
},
],
},
]);
tsx
import Layout from "@/pages/Layout";
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
// 布局路由: 没有 path 属性, 只提供统一的页面布局
element: <Layout />,
children: [
{
path: "/home",
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/about",
Component: lazy(() => import("@/pages/About")),
},
],
},
]);
tsx
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
// 前缀路由: 没有 Component 或 element 属性, 只提供统一的路由前缀
path: "/layout",
children: [
{
path: "home", // 等价于 path: "/layout/home"
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/layout/about", // 等价于 path: "about"
Component: lazy(() => import("@/pages/About")),
},
],
},
]);
tsx
import { Outlet } from "react-router";
export default function Layout() {
return (
<>
<header>header</header>
{/* 默认不挂载子路由组件 */}
{/* 需要父组件使用 <Outlet /> 组件, 作为子路由组件的容器, 以挂载子路由组件 */}
<Outlet />
<footer>footer</footer>
</>
);
}
fallback 路由
tsx
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
// ...
{
path: "*",
element: <>Not Found</>,
},
]);
路由传参
hook: useSearchParams (url 查询参数)
tsx
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
Component: lazy(() => import("@/pages/Layout")),
children: [
{
path: "/home",
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/about",
Component: lazy(() => import("@/pages/About")),
},
],
},
]);
tsx
import { NavLink, useNavigate } from "react-router";
export default function Home() {
const navigate = useNavigate();
return (
<>
Home
{/* useNavigate */}
<button onClick={() => navigate("/about?id=1&project=原神")}>原神</button>
{/* <NavLink /> */}
<NavLink to="/about?id=2&project=星穹铁道">星穹铁道</NavLink>
</>
);
}
tsx
import { useSearchParams, useLocation } from "react-router";
export default function About() {
const [searchParams, setSearchParams] = useSearchParams();
const id = searchParams.get("id");
const project = searchParams.get("project");
console.log(id, project);
const location = useLocation();
console.log(location.search);
// 如果 url 查询参数中有中文, 则需要手动解码
console.log(decodeURIComponent(location.search));
const handleClick = () =>
setSearchParams((params) => {
params.set("id", "1");
params.set("project", "Genshin Impact");
return params;
});
return (
<>
About
<button onClick={handleClick}>setSearchParams</button>
</>
);
}
hook: useParams (url 路径参数)
tsx
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
Component: lazy(() => import("@/pages/Layout")),
children: [
{
path: "/home",
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/about/:id/:project",
Component: lazy(() => import("@/pages/About")),
},
],
},
]);
tsx
// http://localhost:5173/about/1/原神
import { useParams } from "react-router";
export default function About() {
const params = useParams() as { id: string; project: string };
return (
<>
About
<div>id: {params.id}</div>
<div>project: {params.project}</div>
</>
);
}
state 传递参数
- 使用 state 传递的参数, url 中不显示
- 使用 state 传递参数时, 不方便通过 url 分享
tsx
import { lazy } from "react";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
Component: lazy(() => import("@/pages/Layout")),
children: [
{
path: "/home",
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/about",
Component: lazy(() => import("@/pages/About")),
},
],
},
]);
tsx
import { NavLink, useNavigate } from "react-router";
export default function Home() {
const navigate = useNavigate();
const handleClick = () =>
navigate("/about", { state: { id: 1, project: "原神" } });
return (
<>
{/* useNavigate */}
<button onClick={handleClick}>原神</button>
{/* <NavLink /> */}
<NavLink to="/about" state={{ id: 2, project: "星穹铁道" }}>
星穹铁道
</NavLink>
</>
);
}
tsx
import { useLocation } from "react-router";
export default function About() {
const location = useLocation();
const state = location.state as { id: number; project: string };
return (
<>
About
<div>id: {state.id}</div>
<div>project: {state.project}</div>
</>
);
}
懒加载
懒加载: 延迟加载路由组件、代码分包
- useNavigate: 获取路由器对象
- useLocation: 获取路由对象
- useNavigation: 获取导航状态
navigation.state
: idle 空闲、loading 加载、submitting 提交- 路由导航时, 导航状态 idle -> loading -> idle
tsx
import App from "@/App";
import { lazy, Suspense } from "react";
import { createBrowserRouter } from "react-router";
const Home = lazy(() =>
new Promise((resolve) => {
setTimeout(resolve, 5000);
}).then(() => import("@/pages/Home")),
);
export const router = createBrowserRouter([
{
path: "/",
Component: App,
children: [
{
path: "/home",
// react 提供的懒加载: 延迟加载路由组件、代码分包
// 需要配合 <Suspense /> 异步组件使用
// 路由导航时, 导航状态始终是 idle
element: (
<Suspense fallback="请等待 Home 加载...">
<Home />
</Suspense>
),
},
{
path: "/about",
// react-router 提供的懒加载: 延迟加载路由组件、代码分包
// 路由导航时, 导航状态 idle -> loading -> idle
lazy: async () => {
await new Promise((resolve) => {
setTimeout(resolve, 5000);
});
const About = await import("@/pages/About");
return {
Component: About.default,
};
},
},
],
},
]);
tsx
import { Outlet, useNavigate, useNavigation } from "react-router";
export default function App() {
const navigate = useNavigate();
const navigation = useNavigation();
// 路由导航时, 导航状态 idle -> loading -> idle
console.log("[App] navigation.state:", navigation.state);
return (
<>
App
<button onClick={() => navigate("/home")}>home</button>
<button onClick={() => navigate("/about")}>about</button>
{navigation.state === "loading" ? (
<div>请等待子路由组件加载...</div>
) : (
<Outlet />
)}
</>
);
}
tsx
import { NavLink } from "react-router";
export default function Home() {
return (
<>
Home
<NavLink to="/">app</NavLink>
<NavLink to="/about">about</NavLink>
</>
);
}
tsx
import { NavLink } from "react-router";
export default function About() {
return (
<>
About
<NavLink to="/">app</NavLink>
<NavLink to="/home">home</NavLink>
</>
);
}
路由操作: loader, action
- loader 用于查询, GET 请求会触发 loader
- loader 路由导航时, 导航状态 idle -> loading -> idle
- action 用于增删改, POST、DELETE、PATCH 请求会触发 action
- action 路由导航时, 导航状态 idle -> submitting -> loading -> idle
ErrorBoundary
loader 或 action 抛出错误时, fallback 到 ErrorBoundary
ts
import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
import { fileURLToPath, URL } from "node:url";
const vitePluginServer = (): Plugin => {
return {
name: "vite-plugin-server",
configureServer(server) {
const data = [
{ name: "foo", age: 22 },
{ name: "bar", age: 23 },
];
server.middlewares.use("/queryUsers", async (req, res) => {
await new Promise((resolve) => {
setTimeout(resolve, 5000);
});
res.setHeader("Content-Type", "application/json");
const resData = { data };
res.end(JSON.stringify(resData));
});
server.middlewares.use("/addUser", async (req, res) => {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
data.push(JSON.parse(body));
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ code: 0, echo: body }));
});
});
},
};
};
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), vitePluginServer()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
tsx
import App from "@/App";
import About from "@/pages/About";
import Fallback from "@/pages/Fallback";
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
path: "/",
Component: App,
children: [
{
path: "/about",
Component: About,
// loader 用于查询
loader: async () => {
const { data } = await fetch("/queryUsers").then((res) => res.json());
return { code: 200, data };
},
// action 用于增删改
action: async ({ request }) => {
const item = await request.json();
await new Promise((resolve) => {
setTimeout(resolve, 5000);
});
return await fetch("/addUser", {
method: "POST",
body: JSON.stringify(item),
}).then((res) => res.json());
},
// loader 或 action 抛出错误时, fallback 到 ErrorBoundary
ErrorBoundary: Fallback,
},
],
},
]);
tsx
import { Outlet, useNavigate, useNavigation } from "react-router";
export default function App() {
const navigate = useNavigate();
const navigation = useNavigation();
// loader 路由导航时, 导航状态 idle -> loading -> idle
// action 路由导航时, 导航状态 idle -> submitting -> loading -> idle
console.log("[App] navigation.state:", navigation.state);
return (
<>
App
<button onClick={() => navigate("/about")}>about</button>
{
(() => {
switch (navigation.state) {
case "idle":
return <Outlet />;
case "loading":
return <div>请等待子路由组件加载...</div>;
case "submitting":
return <div>请等待子路由组件提交...</div>;
}
})() /** IIFE */
}
</>
);
}
tsx
import { useState } from "react";
import { useActionData, useLoaderData, useSubmit } from "react-router";
export default function About() {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
// loader
const { code, data } = useLoaderData<{
code: 0 | 1;
data: { name: string; age: number }[];
}>();
// actionData
const actionData = useActionData();
console.log(actionData);
// action
const submit = useSubmit();
const handleClick = (data: { name: string; age: number }) => {
submit(data, { method: "POST", encType: "application/json" });
};
return (
<>
About
<input
placeholder="name"
value={name}
onChange={(ev) => setName(ev.target.value)}
/>
<input
placeholder="age"
value={age}
onChange={(ev) => setAge(Number.parseInt(ev.target.value))}
type="number"
/>
{/* action */}
<button onClick={() => handleClick({ name, age })}>submit</button>
{/* loader */}
<div>code: {code}</div>
<ul>
{data.map((item, idx) => (
<li key={idx}>
name: {item.name}, age: {item.age}
</li>
))}
</ul>
</>
);
}
tsx
import { useRouteError } from "react-router";
export default function Fallback() {
const routeErr = useRouteError();
return <div>routeErr: {JSON.stringify(routeErr)}</div>;
}
4 种导航方式
<Link />
:<Link />
组件会被渲染为<a>
标签, 并且阻止了<a>
标签点击事件的默认行为, 不会重新加载页面<NavLink />
:<NavLink />
属性和<Link />
属性相同- 编程式导航
useNavigate
- 重定向
redirect
<Link />
- to 导航的目的路径
- replace
replace={false}
默认, 不替换当前路径, 保留历史记录, 反映在浏览器的前进/后退按钮 (history.pushState)replace={true}
替换当前路径, 不保留历史记录, 反映在浏览器的前进/后退按钮 (history.replaceState)
- state 参考路由传参, state 传递参数
- relative
- 例如 3 条路径 /layout, /layout/home, /layout/about
relative="route"
默认, 必须使用绝对路径- 例如当前路径
/layout/home
- 目的路径
/layout
,/layout/about
- 例如当前路径
relative="path"
可以使用相对路径- 例如当前路径
/layout/home
- 目标路径
../
,../about
- 例如当前路径
- reloadDocument 页面跳转时, 是否重新加载页面
- preventScrollReset 是否阻止滚动位置重置
- viewTransition 页面跳转时, 是否开启 opacity 过渡
<NavLink />
<NavLink />
属性和 <Link />
属性相同
不同: 路由导航时, <NavLink />
会经过 3 个状态的转换, <Link />
不会
- active 激活状态, 当前路径和目的路径匹配
- pending 等待状态, 等待 loader 加载数据, 参考路由操作: loader
- transitioning 过渡状态, 需要使用 viewTransition 属性开启 opacity 过渡
css
/* 激活状态时, react-router 自动添加类名 active */
a.active {
}
/* 等待状态时, react-router 自动添加类名 pending */
a.pending {
}
/* 过渡状态时, react-router 自动添加类名 transitioning */
a.transitioning {
}
也可以使用 style 属性
jsx
<NavLink
to="/about"
viewTransition
style={({ isActive, isPending, isTransitioning }) => {
return {
color: (() => {
if (isActive) {
return "#ff0000";
}
if (isPending) {
return "#00ff00";
}
if (isTransitioning) {
return "#0000ff";
}
return "#000";
})(), // IIFE
};
}}
>
about
</NavLink>
useNavigate
ts
import { useNavigate } from "react-router";
const navigate = useNavigate();
navigate(
"/home",
{
replace: false, // 默认, 不替换当前路径, 保留历史记录
state: { love: "you" }, // 参考路由传参, state 传递参数
relative: "route", // 默认, 必须使用绝对路径
preventScrollReset: false, // 不阻止滚动位置重置
viewTransition: true, // 页面跳转时, 开启 opacity 过渡
} /** options */,
);
redirect
需要配合 loader 使用
tsx
import App from "@/App";
import { lazy } from "react";
import { createBrowserRouter, redirect } from "react-router";
const getToken = () => {
return new Promise((resolve) => {
setTimeout(
() => resolve(Math.random() * 10 > 5 ? "I love you" : null),
3000,
);
});
};
export const router = createBrowserRouter([
{
path: "/",
Component: App,
children: [
{
path: "/home",
Component: lazy(() => import("@/pages/Home")),
},
{
path: "/about",
Component: lazy(() => import("@/pages/About")),
loader: async () => {
const token = await getToken();
if (!token) {
return redirect("/home");
}
return { token };
},
},
],
},
]);
tsx
import { Outlet, useNavigate } from "react-router";
export default function App() {
const navigate = useNavigate();
return (
<>
App
<button onClick={() => navigate("/home")}>home</button>
<button onClick={() => navigate("/about")}>about</button>
<Outlet />
</>
);
}
tsx
import { NavLink } from "react-router";
export default function Home() {
return (
<>
Home
<NavLink to="/">app</NavLink>
<NavLink to="/about">about</NavLink>
</>
);
}
tsx
import { NavLink, useLoaderData } from "react-router";
export default function About() {
const { token } = useLoaderData<{ token: string }>();
return (
<>
About
<div>token: {token}</div>
<NavLink to="/">app</NavLink>
<NavLink to="/home">home</NavLink>
</>
);
}