Skip to content

react-router

  • 推荐使用数据模式
  • <Link />, <NavLink /> 类似 Vue 的 <RouterLink />
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
},
]);
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>,
);
  1. createBrowserRouter: 使用 html5 的 history API (pushState, replaceState, popState), url 中没有 #, 需要服务器配置 fallback 路由, 以解决用户直接访问或刷新非 / 根路径的页面时, 返回 404 Not Found 问题
  2. createHashRouter: 使用 url 的 hash 值, 改变 url 中的 hash 值不会导致页面的重新加载, 通常用于单页面内的导航, 不需要服务器配置, 不利于 SEO
  3. createMemoryRouter: 适用于 node 环境和 SSR, url 不会改变
  4. createStaticRouter: 适用于 SSR

解决用户直接访问或刷新非 / 根路径的页面时, 返回 404 Not Found 问题

Terminal window
# 检查配置文件是否有语法错误
nginx -t
# 重新加载配置文件
nginx -s reload c
nginx.conf
http {
server {
listen 80;
server_name localhost;
location / {
root html
index index.html
// try_files $uri $uri.html $uri/ =404; // [!code --]
try_files $uri $uri.html $uri/ /index.html; // [!code ++]
}
}
}
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")),
},
]);

父组件使用 <Outlet /> 组件, 作为子路由组件的容器, 类似 Vue 的 <RouterView />

嵌套路由, 索引路由, 布局路由, 前缀路由

Section titled “嵌套路由, 索引路由, 布局路由, 前缀路由”
  • 嵌套路由: 有 children 属性, 需要使用 <Outlet /> 组件
  • 索引路由: index: true, 即默认二级路由
  • 布局路由: 没有 path 属性, 只提供统一的页面布局
  • 前缀路由: 没有 Component 或 element 属性, 只提供统一的路由前缀
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")),
},
],
},
]);
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
// ...
{
path: "*",
element: <>Not Found</>,
},
]);
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")),
},
],
},
]);
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")),
},
],
},
]);

使用 state 传递的参数, url 中不显示, 不方便通过 url 分享

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")),
},
],
},
]);

hooks: useNavigate, useLocation, useNavigation

Section titled “hooks: useNavigate, useLocation, useNavigation”

懒加载: 延迟加载路由组件, 代码分包

  • useNavigate: 获取路由器对象
  • useLocation: 获取路由对象
  • useNavigation: 获取导航状态
    • navigation.state: idle 空闲, loading 加载, submitting 提交
    • 路由导航时, 导航状态 idle -> loading -> idle
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,
};
},
},
],
},
]);
  • loader 用于查询, GET 请求会触发 loader
  • loader 路由导航时, 导航状态 idle -> loading -> idle
  • action 用于增删改, POST, DELETE, PATCH 请求会触发 action
  • action 路由导航时, 导航状态 idle -> submitting -> loading -> idle

loader 或 action 抛出错误时, fallback 到 ErrorBoundary

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)),
},
},
});
  1. <Link />: <Link /> 组件会被渲染为 <a> 标签, 并且阻止了 <a> 标签点击事件的默认行为, 不会重新加载页面
  2. <NavLink />: <NavLink /> 属性和 <Link /> 属性相同
  3. 编程式导航 useNavigate
  4. 重定向 redirect
  • 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 /> 属性和 <Link /> 属性相同

不同: 路由导航时, <NavLink /> 会经过 3 个状态的转换, <Link /> 不会

  • active 激活状态, 当前路径和目的路径匹配
  • pending 等待状态, 等待 loader 加载数据, 参考路由操作: loader
  • transitioning 过渡状态, 需要使用 viewTransition 属性开启 opacity 过渡
/* 激活状态时, react-router 自动添加类名 active */
a.active {
}
/* 等待状态时, react-router 自动添加类名 pending */
a.pending {
}
/* 过渡状态时, react-router 自动添加类名 transitioning */
a.transitioning {
}

也可以使用 style 属性

<NavLink
to="/about"
viewTransition
style={({ isActive, isPending, isTransitioning }) => {
return {
color: (() => {
if (isActive) {
return "#ff000088";
}
if (isPending) {
return "#00ff0088";
}
if (isTransitioning) {
return "#0000ff88";
}
return "#000";
})(), // IIFE
};
}}
>
about
</NavLink>
import { useNavigate } from "react-router";
const navigate = useNavigate();
navigate(
"/home",
{
replace: false, // 默认, 不替换当前路径, 保留历史记录
state: { love: "you" }, // 参考路由传参, state 传递参数
relative: "route", // 默认, 必须使用绝对路径
preventScrollReset: false, // 不阻止滚动位置重置
viewTransition: true, // 页面跳转时, 开启 opacity 过渡
} /** options */,
);

需要配合 loader 使用

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 };
},
},
],
},
]);