最近搭建了一个 react-typescript
项目,在配置路由的过程中遇到一些问题,记录一下。
路由配置管理
安装 react-router-dom
依赖:
pnpm i react-router-dom
在 main.tsx
中,引入 BrowserRouter
并包裹在 App
组件外层:
// main.tsx
import { BrowserRouter } from "react-router-dom";
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
在 router
目录下新建 index.tsx
文件,引入所需依赖,并将组件映射到对应路由上,让 Router
知道在哪里渲染它们:
// router/index.tsx
import { lazy } from "react";
import { Outlet } from 'react-router-dom';
export interface RouteType {
path: string;
element: React.ReactNode;
children?: Array<RouteType>;
}
const Index = lazy(() => import('@/views/Index/index'));
const Information = lazy(() => import('@/views/personal/Information/index'));
const Contacts = lazy(() => import('@/views/personal/Contacts/index'));
const routes: Array<RouteType> = [
{
path: '/index',
element: <Index />,
}
{
path: '/personal',
element: <><Outlet></>,
children: [
{
path: 'information',
element: <Information />
},
{
path: 'contacts',
element: <Contacts />
}
]
}
];
export default routes;
在上述代码中,使用 React.lazy()
方法实现路由组件的懒加载。路由懒加载,就是把不同的路由组件分割成不同的代码块,只有在路由被访问的时候,才会加载对应的组件,本质上就是按需加载。使用方法如下:
lazy(() => import(`@/views/${component}`))
如果在某个路由下有子路由,通过 children
属性配置:
const routes: Array<RouteType> = [
{
path: '/personal',
element: <><Outlet></>,
children: [
{
path: 'information',
element: <Information />
},
{
path: 'contacts',
element: <Contacts />
}
]
}
];
由于我使用 Fragment
作为子组件的容器,就在渲染子组件的位置使用 Outlet
组件占位;当然也可以使用 useOutlet()
,它会返回当前路由组件的子路由元素。
最后,使用 useRoutes()
动态配置路由。useRoutes()
是 React Router V6
的一个钩子,它接收一个路由数组,根据匹配到的路由渲染相应的组件。我们可以将这个逻辑抽离出来,封装成一个方法。
import { useRoutes } from 'react-router-dom';
const routes = [...];
const WrappedRoutes = () => {
return useRoutes(routes);
};
export default WrappedRoutes;
最后,在 App.tsx
中引入 WrapperRoutes
组件,路由就配置完成了。相关代码如下:
// App.tsx
import WrappedRoutes from '@/router/index';
const App: React.FC = () => {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider>
<div style={{ height: 90 }} />
<Menu />
</Sider>
<Layout>
<Content style={{ margin: 16 }}>
<WrappedRoutes />
</Content>
</Layout>
</Layout>
)
};
// router/index.tsx
import { lazy } from "react";
import { Outlet, useRoutes } from 'react-router-dom';
export interface RouteType {
path: string;
element: React.ReactNode;
children?: Array<RouteType>;
}
const Index = lazy(() => import('@/views/Index/index'));
const Information = lazy(() => import('@/views/personal/Information/index'));
const Contacts = lazy(() => import('@/views/personal/Contacts/index'));
const routes: Array<RouteType> = [
{
path: '/index',
element: <Index />,
}
{
path: '/personal',
element: <><Outlet></>,
children: [
{
path: 'information',
element: <Information />
},
{
path: 'contacts',
element: <Contacts />
}
]
}
];
const WrappedRoutes = () => {
return useRoutes(routes);
};
export default WrappedRoutes;
如果不使用 useRoutes()
,可以使用 Routes
和 Route
实现路由结构:
// App.tsx
import { lazy } from "react";
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
const Index = lazy(() => import('@/views/Index/index'));
const Information = lazy(() => import('@/views/personal/Information/index'));
const Contacts = lazy(() => import('@/views/personal/Contacts/index'));
const App: React.FC = () => {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider>
<div style={{ height: 90 }} />
<Menu />
</Sider>
<Layout>
<Content style={{ margin: 16 }}>
<Routes>
<Route path="/" element={<Navigate to="/index" />} />
<Route path="index" element={<Index />} />
<Route path="personal" element={<><Outlet /></>} >
<Route path="information" element={<Information />} />
<Route path="contacts" element={<Contacts />} />
</Route>
</Routes>
</Content>
</Layout>
</Layout>
)
}
提示:React Router V6 中的嵌套路由可以只定义相对父路由的相对路径,内部会自动拼接全路径。
Menu 组件实现路由跳转
定义一个类型为 Array<MenuItem>
的 menuList
,保存菜单列表。
import WrappedRoutes from '@/router/index';
type MenuItem = {
label: string,
key: string,
icon?: React.ReactNode
children?: Array<MenuItem>
}
const menuList: Array<MenuItem> = [{
label: "首页",
key: "/index",
icon: <PieChartOutlined rev={undefined} />,
}, {
label: "个人办公",
key: "/personal",
icon: <PushpinOutlined rev={undefined} />,
children: [
{
label: "个人信息",
icon: <IdcardOutlined rev={undefined} />,
key: "/personal/information"
},
{
label: "通讯录",
icon: <ContactsOutlined rev={undefined} />,
key: "/personal/contacts"
}
]
}]
const App: React.FC = () => {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider>
<div style={{ height: 90 }} />
<Menu theme="dark" mode="inline" items={menuList} />
</Sider>
<Layout>
<Content style={{ margin: 16 }}>
<WrappedRoutes />
</Content>
</Layout>
</Layout>
)
};
export default App;
此时,发现点击菜单项不能跳转到对应的路由。为了解决问题,我阅读 Menu 组件官方文档,发现在 Menu
组件上可以通过 click
事件获取 key
、keyPath
等相关属性。
const App = () => {
const handleClick = (e: any) => {
console.log(e);
console.log('key', e.key);
}
return (
// ...
<Menu theme="dark" mode="inline" items={menuList} onClick={handleClick} />
)
}
点击【个人信息】菜单项,可以看到 key
属性就是希望跳转到的路由。
接下来,我们在 handleClick
函数中使用 useNavigate()
,将当前 event
对象的 key
属性传入,就可以实现点击菜单项跳转到对应路由的功能。
同时,为了在全局环境下保持当前选中项的激活状态,需要使用 useLocation()
实时获取并保存当前 pathname
,并交给 Menu
组件的 selectedKeys
属性。
// ...
import { useLocation, useNavigate } from 'react-router-dom';
const App = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const handleClick = (e: any) => {
navigate(e.key);
}
return (
// ...
<Menu theme="dark" selectedKeys={[pathname]} mode="inline" items={menuList} onClick={handleClick} />
)
}
忽略组件库的引入,这部分功能的完整代码如下:
// App.tsx
import { useLocation, useNavigate } from 'react-router-dom';
import WrappedRoutes from '@/router/index';
type MenuItem = {
label: string,
key: string,
icon?: React.ReactNode
children?: Array<MenuItem>
}
const menuList: Array<MenuItem> = [{
label: "首页",
key: "/index",
icon: <PieChartOutlined rev={undefined} />,
}, {
label: "个人办公",
key: "/personal",
icon: <PushpinOutlined rev={undefined} />,
children: [
{
label: "个人信息",
icon: <IdcardOutlined rev={undefined} />,
key: "/personal/information"
},
{
label: "通讯录",
icon: <ContactsOutlined rev={undefined} />,
key: "/personal/contacts"
}
]
}]
const App: React.FC = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const handleClick = (e: any) => {
navigate(e.key);
}
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider>
<div style={{ height: 90 }} />
<Menu theme="dark" selectedKeys={[pathname]} mode="inline" items={menuList} onClick={handleClick} />
</Sider>
<Layout>
<Content style={{ margin: 16 }}>
<WrappedRoutes />
</Content>
</Layout>
</Layout>
)
};
export default App;
为了方便管理,可以将 menuList
相关逻辑抽取成一个单独的配置文件;同时,也可以将 menuList
和 routes
整合成一套配置使用。本文仅提供一个简单示例作为参考。
Breadcrumb 组件动态展示
首先,实现 getBreadcrumbNameMap
函数,遍历生成 key
和 label
的映射关系。
export const breadcrumbMap: Map<string, string> = new Map();
// 因为本项目的子路由最多只有一层,这里使用双循环简单实现功能
export function getBreadcrumbNameMap(breadcrumbMap: Map<string, string>) {
for (let menuItem of menuList) {
breadcrumbMap.set(menuItem.key, menuItem.label);
if (menuItem.children) {
for (let child of menuItem.children) {
breadcrumbMap.set(child.key, child.label);
}
}
}
}
// 在需要使用的地方调用
getBreadcrumbNameMap(breadcrumbMap);
到此,我们已经使用 useLocation()
保存了当前路由对应的 pathname
。
将得到的 pathname
以 /
字符分割并逐节遍历,并在 breadcrumbMap
中寻找对应的 label
,找到了就拼接下一段,然后继续在 breadcrumbMap
中匹配 label
,以此类推,直到遍历完成。
const App = () => {
const { pathname } = useLocation();
const pathSnippets = pathname.split('/').filter((i) => i);
const breadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
console.log(url);
return {
key: url,
title: <Link to={url}>{breadcrumbMap.get(url)}</Link>,
};
});
return (
// ...
<Content style={{ margin: 16, padding: 12, minHeight: '95vh', backgroundColor: '#fff' }}>
<Breadcrumb style={{ marginBottom: 20 }} separator="/" items={breadcrumbItems} />
<WrappedRoutes />
</Content>
)
}
最后就可以成功获取当前路由对应的面包屑导航,效果如图。
最后
如果有什么不对的地方或者更好的建议,欢迎大家提出。