React-Router
- 本篇内容都是基于 v6 版本,参考时请以最新官方文档为准!
- 2024-11-22 更新为 v7,v6 的最后一个版本是 6.28.0
深入问题:
- 如何实现一个自定义的 Link 组件?
- Code Splitting 与路由的关系?
React Router v6.4 引入了数据 API(如 loader、action 和 useFetcher 等),目的是提升路由在数据获取方面的能力。不过,我觉得它有些多余,将数据加载集成到路由中,这样提高了复杂度、缺少灵活性、不符合我的代码组织习惯,路由承担页面跳转的功能即可,数据加载和状态交给更专业的工具去管理更好。
如何组织路由
按页面模块拆分路由配置,每个模块对应一个路由文件,最后在主路由文件中集中导入。
有两种方式:
- Router Components
- 数据路由器对象的形式
使用 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
Link 组件实现路由跳转
import { Link } from 'react-router-dom';
<Link to="/user">Link to User</Link>;
如果将 Link 组件替换为 a 标签,会直接跳转页面,在控制台的 network 中会看到请求。
NavLink
高亮当前激活的路由
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
获取当前导航状态。状态有:idle
、submitting
、loading
例如:有一个左侧是菜单、右侧是内容的页面,点击菜单,右侧内容会先显示一个加载动画。此时可以通过 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;
路由守卫
需要路由守卫的目的:
- 判断是否登录
- 判断用户是否有权限访问该页面
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>
}
/>;
菜单动态渲染
懒加载
懒加载是指在需要时才加载组件,而不是一次性加载所有组件。
优势:
- 提升首屏加载性能:只加载首屏必要的资源,缩短首屏加载时间
- 按需加载:用户访问某个页面时才加载对应的代码,减少不必要的资源浪费
- 优化带宽利用:对网络条件较差的用户友好,未访问的页面不会下载对应资源
- 支持代码分割:懒加载自动生成多个代码块(chunks),更方便浏览器缓存,提升页面加载效率
通过 React 的 lazy
和 Suspense
实现:
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 的形式:
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> }
]
}
]);
export default router;
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 文件,则说明自动进行了代码分割。