1. 安装依赖
bash
复制代码
pnpm i react-router-dom -S
2. 简单使用
1. 新增router/index.ts
tsx
复制代码
import { BrowserRouter, Routes, Route } from 'react-router-dom'
// 在使用前可以新增views/home.tsx
import Home from '@/views/home';
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</BrowserRouter>
)
}
export default Router;
2. 在App.tsx中
使用
tsx
复制代码
import Router from '@/router';
function App() {
return (
<>
<Router />
</>
);
}
export default App;
3. 使用Lazy懒加载路由
tsx
复制代码
// router/index.ts
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy } from 'react';
// 通过lazy包裹组件,在使用时才会加载组件
const Home = lazy(() => import('@/views/home'));
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
4. 使用suspend添加路由前的loading效果
tsx
复制代码
// 不建议封装Suspense, 结合动态导入会莫名其妙出现路由跳转了,界面未刷新问题
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('@/views/home'));
const Router = () => {
return (
<BrowserRouter>
<!-- 使用Suspense包裹组件,在加载时显示loading效果 -->
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
export default Router;
5. 使用高阶函数实现路由守卫
1. 新增高阶函数路由守卫组件router/RouteGuard.tsx
tsx
复制代码
import { useCallback, useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
/**
* 路由守卫参数
* @param children 子组件
* @param beforeEach 路由变化前执行
* @param afterEach 路由变化后执行
* @param error 路由错误执行
*/
interface RouteGuardProps {
children: React.ReactNode;
beforeEach?: (to: string, from: string) => boolean | Promise<boolean>;
afterEach?: (to: string, from: string) => void;
error?: (error: Error) => void;
}
/**
* 路由守卫
* @param RouteGuardProps
* children: React.ReactNode;
* beforeEach?: (to: string, from: string) => boolean;
* afterEach?: (to: string, from: string) => void;
* error?: (to: string, from: string) => void;
* @returns
*/
const RouteGuard = ({ children, beforeEach, afterEach, error }: RouteGuardProps) => {
const navigate = useNavigate();
const location = useLocation();
// 初始化的时候默认前者为空
const previousPath = useRef<string>('');
const memoryBeforeEach = useCallback(
async (to: string, from: string) => {
if (beforeEach) {
return await beforeEach(to, from);
}
return true;
},
[beforeEach]
);
const memoryAfterEach = useCallback(
async (to: string, from: string) => {
afterEach && afterEach(to, from);
},
[afterEach]
);
const memoryError = useCallback(
async (e: Error) => {
error && error(e);
},
[error]
);
useEffect(() => {
// 处理路由变化
const handleRouteChange = async () => {
try {
const to = location.pathname;
const from = previousPath.current;
// 如果to和from一致,则不处理
if (to === from) {
return;
}
if (beforeEach) {
// 校验beforeEach,如果校验不通过,则回跳之前的界面
const result = await beforeEach(to, from);
if (!result) {
navigate(from);
return;
}
}
previousPath.current = to;
afterEach && afterEach(to, from);
} catch (e) {
error && error(e as Error);
return;
}
};
handleRouteChange();
}, [location.pathname, memoryBeforeEach, memoryAfterEach, memoryError, navigate, previousPath]);
return <>{children}</>;
};
export default RouteGuard;
2. 在router/index.tsx
中使用
tsx
复制代码
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import RouteGuard from './RouteGuard';
const Home = lazy(() => import('@/views/home'));
const About = lazy(() => import('@/views/about'));
const beforeEach = (to: string, from: string) => {
console.log('beforeEach', to, from);
return true;
};
const Router = () => {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/home" element={
<RouteGuard beforeEach={beforeEach} afterEach={() => { console.log("after route each");}}>
<Home />
</RouteGuard>
} />
<Route path="/about" element={
<RouteGuard beforeEach={beforeEach} afterEach={() => { console.log("after route each");}}>
<About />
</RouteGuard>
} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
export default Router;
3. 优化下,使用routes数组来循环处理
tsx
复制代码
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import RouteGuard from './RouteGuard';
const beforeEach = (to: string, from: string) => {
console.log('beforeEach', to, from);
return true;
};
const routes = [
{
path: '/home',
element: lazy(() => import('@/views/home')),
},
{
path: '/about',
element: lazy(() => import('@/views/about')),
},
]
const Router = () => {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{
routes.map((route, index) => {
return (
<Route key={`${index}_${route.path}`} path={route.path} element={
<RouteGuard beforeEach={beforeEach} afterEach={() => { console.log("after route each");}}>
<route.element />
</RouteGuard>
} />
)
})
}
</Routes>
</Suspense>
</BrowserRouter>
);
};
export default Router;
4. 使用发布订阅模式处理路由守卫函数
- 新增发布订阅模式
ts
复制代码
// src/utils/pubsub.ts
/**
* 发布订阅模式监听注入
* listeners: {
* // 路由 router事件
* router: beforeEach: [],
* router: afterEach: [],
* router: error: []
* }
*/
class PubSub {
// 定义listeners对象,用于存储事件监听器
private listeners: Record<string, ((...args: any[]) => any)[]> = {};
constructor() {
this.listeners = {};
}
/**
* 订阅事件
* @param eventName 事件名称,默认为空字符串
* @param listener 事件监听器函数
*/
on(eventName: string = '', listener: (...args: any[]) => any): void {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(listener);
}
/**
* 取消订阅事件
* @param eventName 事件名称,默认为空字符串
* @param listener 事件监听器函数
*/
off(eventName: string = '', listener: ((...args: any[]) => any) | null): void {
if (!listener || !this.listeners[eventName]) {
console.log('not on event ', eventName, 'or listener must be a function');
return;
}
this.listeners[eventName].some((item, idx) => {
if (item.name === listener.name) {
this.listeners[eventName].splice(idx, 1);
return true;
}
return false;
});
}
/**
* 发布事件
* @param eventName 事件名称,默认为空字符串
* @param args 传递给事件监听器的参数
* @returns 事件监听器的返回值数组
*/
async emit(eventName: string = '', ...args: any[]): Promise<any[]> {
if (!this.listeners[eventName]) {
console.log('not on event ', eventName);
return [];
}
const results: any[] = [];
for (const listener of this.listeners[eventName]) {
const res = await listener.apply(this, args);
if (res!== undefined) {
results.push(res);
}
}
return results;
}
}
const pubsub = new PubSub();
export default pubsub;
// 导出事件名称常量
export const ROUTER_BEFOREEACH: string = 'router:beforeEach';
export const ROUTER_AFTEREACH: string = 'router:afterEach';
export const ROUTER_ERROR: string = 'router:error';
- 简单提供一个发布订阅模式的hooks
tsx
复制代码
import pubsub from '@/utils/pubsub';
/**
* 发布订阅事件的hooks
* @returns addListener: 添加监听,deleteListener:删除监听,emitListener:执行监听事件
*/
const usePubsub = () => {
const addListener = (eventName: string = '', listener: (...args: any[]) => any) => {
pubsub.on(eventName, listener);
}
const deleteListener = (eventName: string = '', listener: ((...args: any[]) => any) | null) => {
pubsub.off(eventName, listener);
}
const emitListener = (eventName: string = '', ...args: any[]) => {
pubsub.emit(eventName, ...args);
}
return { addListener, deleteListener, emitListener };
}
export default usePubsub;
- 使用发布订阅模式处理路由守卫函数
tsx
复制代码
import { useCallback, useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import usePubsub from '@/hooks/common/usePubsub';
import { ROUTER_BEFOREEACH, ROUTER_AFTEREACH, ROUTER_ERROR } from '@/utils/pubsub';
/**
* 路由守卫参数
* @param children 子组件
* @param beforeEach 路由变化前执行
* @param afterEach 路由变化后执行
* @param error 路由错误执行
*/
interface RouteGuardProps {
children: React.ReactNode;
beforeEach?: (to: string, from: string) => boolean | Promise<boolean>;
afterEach?: (to: string, from: string) => void;
error?: (error: Error) => void;
}
/**
* 路由守卫
* @param RouteGuardProps
* children: React.ReactNode;
* beforeEach?: (to: string, from: string) => boolean;
* afterEach?: (to: string, from: string) => void;
* error?: (to: string, from: string) => void;
* @returns
*/
const RouteGuard = ({ children, beforeEach, afterEach, error }: RouteGuardProps) => {
const navigate = useNavigate();
const location = useLocation();
// 初始化的时候默认前者为空
const previousPath = useRef<string>('');
const { emitListener } = usePubsub();
const memoryBeforeEach = useCallback(
async (to: string, from: string) => {
const res = await emitListener(ROUTER_BEFOREEACH, to, from);
if (!res || res.length === 0) {
return true;
}
// 如果有一个返回false,则返回false, 如果是返回undefined,默认为true
let flag = true;
res.some((item) => {
if (item === false) {
flag = false;
return true;
}
});
return flag;
},
[beforeEach]
);
const memoryAfterEach = useCallback(
async (to: string, from: string) => {
emitListener(ROUTER_AFTEREACH, to, from);
},
[afterEach]
);
const memoryError = useCallback(
async (e: Error) => {
emitListener(ROUTER_ERROR, e);
},
[error]
);
useEffect(() => {
// 处理路由变化
const handleRouteChange = async () => {
try {
const to = location.pathname;
const from = previousPath.current;
// 如果to和from一致,则不处理
if (to === from) {
return;
}
// 校验beforeEach,如果校验不通过,则回跳之前的界面
const result = await memoryBeforeEach(to, from);
if (!result) {
navigate(from);
return;
}
previousPath.current = to;
memoryAfterEach(to, from);
} catch (e) {
memoryError(e as Error);
return;
}
};
handleRouteChange();
}, [location.pathname, memoryBeforeEach, memoryAfterEach, memoryError, navigate, previousPath]);
return <>{children}</>;
};
export default RouteGuard;
// 将routes添加路由守卫组件
export const resolveRoutes = (routes: ReactRouterProps[]) => {
if (!routes) {
return [];
}
for (const route of routes) {
if (route.children) {
route.children = resolveRoutes(route.children);
}
if (route.redirect) {
route.element = <Navigate to={route.redirect} />;
} else if (route.element || route.Component) {
route.element = <RouteGuard>{route.Component ? <route.Component /> : route.element}</RouteGuard>;
}
}
return routes;
};
5. 在router/index.tsx中使用
tsx
复制代码
import usePubsub from '@/hooks/common/usePubsub';
import { ROUTER_AFTEREACH, ROUTER_BEFOREEACH } from '@/utils/pubsub';
const Router = () => {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{routes.map((route, index) => {
return (
<Route
key={`${index}_${route.path}`}
path={route.path}
element={
<RouteGuard>
<route.element />
</RouteGuard>
}
/>
);
})}
</Routes>
</Suspense>
</BrowserRouter>
);
};
const { addListener } = usePubsub();
addListener(ROUTER_BEFOREEACH, (to: string, from: string) => {
console.log('beforeEach', to, from);
return true;
});
addListener(ROUTER_AFTEREACH, (to: string, from: string) => {
console.log('afterEach', to, from);
});
6. 封装useRouter
,构建路由
1. 新增src/router/useRouter.tsx
tsx
复制代码
import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom';
import { ReactRouterProps } from './model';
import { Suspense } from 'react';
import { resolveRoutes } from './RouteGuard';
const useRouter = (routes: ReactRouterProps[], mode: string, baseName?: string, guardFlag: boolean = true) => {
if (guardFlag) {
// 路由守卫
routes = resolveRoutes(routes);
}
if (mode === ROUTE_MODE_HASH) {
return (
<HashRouter basename={baseName}>
<Suspense fallback={<div>Loading....</div>}>
<Routes>
{routes.map((route, index) => {
return <Route key={`${index}_${route.path}`} path={route.path} element={route.element} />;
})}
</Routes>
</Suspense>
</HashRouter>
);
}
return (
<BrowserRouter basename={baseName}>
<Suspense fallback={<div>Loading....</div>}>
<Routes>
{routes.map((route, index) => {
return <Route key={`${index}_${route.path}`} path={route.path} element={route.element} />;
})}
</Routes>
</Suspense>
</BrowserRouter>
);
};
export default useRouter;
export const ROUTE_MODE_HASH = 'HASH';
export const ROUTE_MODE_HISTORY = 'HISTORY';
2. 修改src/router/index.tsx
tsx
复制代码
import { Navigate } from 'react-router-dom';
import usePubsub from '@/hooks/common/usePubsub';
import { ROUTER_AFTEREACH, ROUTER_BEFOREEACH } from '@/utils/pubsub';
import useRouter, { ROUTE_MODE_HISTORY } from './useRouter';
const { addListener } = usePubsub();
addListener(ROUTER_BEFOREEACH, (to: string, from: string) => {
console.log('beforeEach', to, from);
return true;
});
addListener(ROUTER_AFTEREACH, (to: string, from: string) => {
console.log('afterEach', to, from);
});
const routes = [
{
path: '/',
element: <Navigate to={'/home'} />,
meta: {},
}
];
export default () => {
return useRouter(routes, ROUTE_MODE_HISTORY);
}
6. 读取目录结构生成路由
1. 读取目录结构生成对应的路由数组
ts
复制代码
// src/router/useDir2Routes.ts
import { lazy } from 'react';
import { ReactRouterProps } from './model';
import routeConfig from './routeConfig';
const getRoutesByViews = (): ReactRouterProps[] => {
// 获取所有依照规范建的页面
const routes: ReactRouterProps[] = [];
const modules = import.meta.glob<{ default: React.ComponentType<any> }>('@/views/**/*.tsx');
for (const [pathKey, moduleImport] of Object.entries(modules)) {
if (pathKey.includes('components') || pathKey.includes('layouts')) {
continue;
}
let matchResult;
// 如果包含index.tsx, 则目录结构可能为views/about/index.tsx;
if (pathKey.indexOf('index.tsx') > 0) {
matchResult = pathKey.match(/\/views\/(.*)\//);
} else {
matchResult = pathKey.match(/\/views\/(.*?)\.tsx$/);
}
if (!matchResult) {
continue;
}
const pageName = matchResult[1];
let idArr = pageName.split('/'),
id = idArr[idArr.length - 1];
const { name = id, meta = {}, noRoute } = routeConfig[pageName] || {};
// 没有配置路由的页面不加入路由
if (noRoute) {
continue;
}
// 显式指定moduleImport的类型
routes.push({
id,
name,
path: '/' + pageName,
Component: lazy(moduleImport),
meta,
});
}
return routes;
};
export default getRoutesByViews;
2. 修改src/router/index.tsx
tsx
复制代码
import { Navigate } from 'react-router-dom';
import usePubsub from '@/hooks/common/usePubsub';
import { ROUTER_AFTEREACH, ROUTER_BEFOREEACH } from '@/utils/pubsub';
import useDir2Routes from './useDir2Routes';
import useRouter, { ROUTE_MODE_HISTORY } from './useRouter';
const { addListener } = usePubsub();
addListener(ROUTER_BEFOREEACH, (to: string, from: string) => {
console.log('beforeEach', to, from);
return true;
});
addListener(ROUTER_AFTEREACH, (to: string, from: string) => {
console.log('afterEach', to, from);
});
const routes = [
{
path: '/',
element: <Navigate to={'/home'} />,
meta: {},
},
...useDir2Routes(),
];
export default () => {
return useRouter(routes, ROUTE_MODE_HISTORY);
}
7.相关模型
1. src/router/model.ts
ts
复制代码
import { RouteObject } from "react-router-dom";
export interface RouterBeforeEachProps {
route: ReactRouterProps;
children?: React.ReactNode;
}
export type ReactRouterProps = RouteObject & {
id?: string;
name?: string,
path: string;
meta?: Record<string, any>;
children?: ReactRouterProps[];
redirect?: string;
}
2. src/router/routeConfig.ts
ts
复制代码
/**
* 存放路由meta信息
*/
const routeConfig: Record<string, { name: string; meta?: Record<string, any>, noRoute?: boolean }> = {
home: {
name: 'Home',
},
child: {
name: 'Child',
},
about: {
name: 'About',
},
login: {
name: 'Login',
},
}
export default routeConfig;