1. 安装依赖
pnpm i react-router-dom -S
2. 简单使用
1. 新增router/index.ts
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from '@/views/home';
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</BrowserRouter>
)
}
export default Router;
2. 在App.tsx中
使用
import Router from '@/router';
function App() {
return (
<>
<Router />
</>
);
}
export default App;
3. 使用Lazy懒加载路由
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy } from 'react';
const Home = lazy(() => import('@/views/home'));
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
4. 使用suspend添加路由前的loading效果
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('@/views/home'));
const Router = () => {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
export default Router;
5. 使用高阶函数实现路由守卫
1. 新增高阶函数路由守卫组件router/RouteGuard.tsx
import { useCallback, useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
interface RouteGuardProps {
children: React.ReactNode;
beforeEach?: (to: string, from: string) => boolean | Promise<boolean>;
afterEach?: (to: string, from: string) => void;
error?: (error: Error) => void;
}
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;
if (to === from) {
return;
}
if (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
中使用
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数组来循环处理
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. 使用发布订阅模式处理路由守卫函数
- 新增发布订阅模式
class PubSub {
private listeners: Record<string, ((...args: any[]) => any)[]> = {};
constructor() {
this.listeners = {};
}
on(eventName: string = '', listener: (...args: any[]) => any): void {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(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;
});
}
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
import pubsub from '@/utils/pubsub';
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;
- 使用发布订阅模式处理路由守卫函数
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';
interface RouteGuardProps {
children: React.ReactNode;
beforeEach?: (to: string, from: string) => boolean | Promise<boolean>;
afterEach?: (to: string, from: string) => void;
error?: (error: Error) => void;
}
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;
}
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;
if (to === from) {
return;
}
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;
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中使用
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
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
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. 读取目录结构生成对应的路由数组
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;
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;
}
routes.push({
id,
name,
path: '/' + pageName,
Component: lazy(moduleImport),
meta,
});
}
return routes;
};
export default getRoutesByViews;
2. 修改src/router/index.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
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
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;