React-Router-dom的二次封装,增加路由守卫等功能

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. 使用发布订阅模式处理路由守卫函数
  1. 新增发布订阅模式
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';
  1. 简单提供一个发布订阅模式的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;
  1. 使用发布订阅模式处理路由守卫函数
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;
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax