跳到主要内容

React-Router

注意
  • 本篇内容都是基于 v6 版本,参考时请以最新官方文档为准!
  • 2024-11-22 更新为 v7,v6 的最后一个版本是 6.28.0

深入问题:

  • 如何实现一个自定义的 Link 组件?
  • Code Splitting 与路由的关系?
备注

React Router v6.4 引入了数据 API(如 loader、action 和 useFetcher 等),目的是提升路由在数据获取方面的能力。不过,我觉得它有些多余,将数据加载集成到路由中,这样提高了复杂度、缺少灵活性、不符合我的代码组织习惯,路由承担页面跳转的功能即可,数据加载和状态交给更专业的工具去管理更好。

如何组织路由

按页面模块拆分路由配置,每个模块对应一个路由文件,最后在主路由文件中集中导入。

有两种方式:

  1. Router Components
  2. 数据路由器对象的形式

使用 BrowserRouter

  • <Outlet /> 嵌套路由,用来渲染子路由
  • <Route index /> 上添加一个 index 路由,作为默认子路由
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';

const Layout = () => (
<div>
<header>Header</header>
<Outlet />
<footer>Footer</footer>
</div>
);

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
</Route>
</Routes>
</BrowserRouter>
);
}

export default App;

使用 createBrowserRouter

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> }
]
},
{ path: '*', element: <NoMatch /> }
]);

export default function App() {
return <RouterProvider router={router} />;
}

路由传参和获取的方式

使用 useParams 获取动态参数,使用 useSearchParams 获取查询参数

动态参数

  • 定义动态路径:path="/user/:id"
  • 传递方式:
    • <Link to="/user/123">detail</Link>
    • navigate("/user/123")
  • 获取方式:使用 useParams
import { useParams } from 'react-router-dom';

const UserPage = () => {
const { id } = useParams();
return <div>User ID: {id}</div>;
};

查询参数

  • 定义:使用 URL 查询字符串:?key=value
  • 传递方式:
    • <Link to="/search?keyword=react">search</Link>
    • navigate("/search?keyword=react")
  • 获取方式:使用 useSearchParams
import { useSearchParams } from 'react-router-dom';

const SearchPage = () => {
const [searchParams] = useSearchParams();
const keyword = searchParams.get('keyword');
return <div>Searching for: {keyword}</div>;
};

嵌套路由

重定向

在路由配置中使用 Navigate 组件

import { createBrowserRouter, Navigate } from 'react-router-dom';

const router = createBrowserRouter([
{ path: '/page1', element: Page1 },
{ path: '/', element: <Navigate to="/page1" /> }
]);

在组件里如何重定向?

import { redirect } from 'react-router-dom';

function action() {
redirect('/page1');
}

动态路由

动态路由是指根据匹配的 URL 参数动态地渲染不同的组件或内容。通常用于处理带有参数的 URL,例如详情页。

通过 path 定义占位符并动态匹配:

const App = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserDetail />} /> {/* 动态路由 */}
</Routes>
</Router>
);
};

自定义 404 页面

加一个通配符路由,放到最后

import NoMatch from '@/pages/404';

const router = createBrowserRouter([
{ path: '/', element: <Layout /> },
{ path: '*', element: <NoMatch /> }
]);

错误页面

通过 errorElement 配置错误页面。在根路由上配置的错误页面会在全屏显示,如果子路由配置有错误页面并且出现了错误,则会显示子路由的错误页面。子路由出现错误页面,不会影响根路由的页面。

示例:假设根路由的布局组件是左侧菜单、右侧是内容,如果子路由出现错误,在内容区域会显示子路由的错误页面,而菜单区域正常显示。

const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
path: 'news',
errorElement: <div>Error Page</div>
}
]
}
]);

Outlet

Outlet 用来渲染子路由,可以显示嵌套 UI。

import { Outlet } from 'react-router-dom';

const Layout = () => (
<div>
<header>Header</header>
<Outlet />
<footer>Footer</footer>
</div>
);

当路由有子路由,并且当前位于父路由的路径时,Outlet 不会呈现任何内容,因为没有子路由匹配。例如处在 / 时,Outlet 占位的页面是空白的,可以将索引路由作为默认子路由。

const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Index /> },
{
path: 'news',
element: <News />
}
]
}
]);

Link 组件实现路由跳转

import { Link } from 'react-router-dom';

<Link to="/user">Link to User</Link>;

如果将 Link 组件替换为 a 标签,会直接跳转页面,在控制台的 network 中会看到请求。

高亮当前激活的路由

export default function Home() {
return (
<ul>
{menus.map(item => (
<li key={item.id}>
<NavLink
to={item.url}
className={({ isActive, isPending }) => (isActive ? 'active' : isPending ? 'pending' : '')}
>
{item.name}
</NavLink>
</li>
))}
</ul>
);
}

useNavigation

获取当前导航状态。状态有:idlesubmittingloading

例如:有一个左侧是菜单、右侧是内容的页面,点击菜单,右侧内容会先显示一个加载动画。此时可以通过 useNavigation 获取当前导航状态,然后根据状态来显示不同的内容。

import { useNavigation } from 'react-router-dom';

export default function Layout() {
const navigation = useNavigation();

return (
<>
<div id="sidebar"></div>
<div id="content" className={navigation.state === 'loading' ? 'loading' : ''}>
<Outlet />
</div>
</>
);
}

loading 的样式:

#content.loading {
opacity: 0.25;
transition: opacity 200ms;
transition-delay: 200ms;
}

useNavigate

路由跳转

import { useNavigate } from 'react-router-dom';

const App = () => {
const navigate = useNavigate();

const handleClick = () => navigate('/home');

return <button onClick={handleClick}>home</button>;
};

export default App;

navigate(-1); 与浏览器后退按钮的作用相同

useLocation

import { useLocation } from 'react-router-dom';

const Header = () => {
const location = useLocation();

useEffect(() => {
const route = location.pathname.substring(1);
}, [location]);
};
export default Header;

路由守卫

需要路由守卫的目的:

  1. 判断是否登录
  2. 判断用户是否有权限访问该页面

React Router 没有内置的路由守卫机制,可以通过以下方式实现:

高阶组件 (HOC)

import { Navigate } from 'react-router-dom';

const AuthGuard = ({ children }) => {
const isAuthenticated = useAuth(); // 自定义逻辑
return isAuthenticated ? children : <Navigate to="/login" />;
};

<Route
path="/dashboard"
element={
<AuthGuard>
<Dashboard />
</AuthGuard>
}
/>;

菜单动态渲染

懒加载

懒加载是指在需要时才加载组件,而不是一次性加载所有组件。

优势:

  1. 提升首屏加载性能:只加载首屏必要的资源,缩短首屏加载时间
  2. 按需加载:用户访问某个页面时才加载对应的代码,减少不必要的资源浪费
  3. 优化带宽利用:对网络条件较差的用户友好,未访问的页面不会下载对应资源
  4. 支持代码分割:懒加载自动生成多个代码块(chunks),更方便浏览器缓存,提升页面加载效率

通过 React 的 lazySuspense 实现:

1、路由组件形式:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import Layout from './layout';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

const App = () => (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
</Route>
</Routes>
</Suspense>
</BrowserRouter>
);

export default App;

2、使用 createBrowserRouter 的形式:

routes/index.tsx
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> }
]
}
]);

export default router;
App.tsx
import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import router from './routes';

function App() {
return (
<Suspense fallback={<div>loading...</div>}>
<RouterProvider router={router} />
</Suspense>
);
}

export default App;

代码分割:懒加载组件会自动进行代码分割,每个组件会被打包成单独的 chunk,只有在需要时才会加载。

怎么验证?

方式一:打开浏览器开发者工具的 Network 标签页,点击页面上的路由链接,看是否有新的 JS 请求。如果有,则说明懒加载生效。

方式二:对比打包后的产物,如果打包后多了一些 js 文件,则说明自动进行了代码分割。