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 分钟前
前端程序员集体破防!AI工具same.dev像素级抄袭你的代码,你还能高傲多久?
前端·javascript·后端
2301_764441338 分钟前
小说文本分析工具:基于streamlit实现的文本分析
前端·python·信息可视化·数据分析·nlp
jackl的科研日常23 分钟前
“个人陈述“的“十要“和“十不要“
前端
一个处女座的程序猿O(∩_∩)O27 分钟前
Vue 中 this 使用指南与注意事项
前端·javascript·vue.js
大有数据可视化1 小时前
数字孪生像魔镜,映照出无限可能的未来
前端·html·webgl
一个处女座的程序猿O(∩_∩)O1 小时前
使用 Docker 部署前端项目全攻略
前端·docker·容器
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14_10空状态的固定表头表格
前端·javascript·vue.js·ecmascript·deepseek
天马37981 小时前
Vue 概念、历史、发展和Vue简介
前端·javascript·vue.js
小小鸭程序员2 小时前
NPM版本管理终极指南:掌握依赖控制与最佳实践
java·前端·spring·npm·node.js
KL's pig/猪头/爱心/猪头2 小时前
lws-minimal-ws-server前端分析
前端