在React中,路由拦截器是一种机制,用于在导航到特定路由之前执行一些逻辑,比如权限校验、用户认证或动态路由控制。通常,React使用react-router-dom
库来管理路由,通过<Routes>
和<Route>
定义路由规则。
实现路由拦截的常见方式包括以下几种:
1. 使用<Navigate>
实现重定向拦截
通过react-router-dom
的<Navigate>
组件,可以在用户未通过权限校验时将其重定向到指定页面。
示例代码
import React from "react";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";// 模拟认证状态
const isAuthenticated = false;// 路由守卫组件
const ProtectedRoute = ({ children }) => {return isAuthenticated ? children : <Navigate to="/login" />;
};const App = () => {return (<Router><Routes><Route path="/login" element={<Login />} /><Routepath="/dashboard"element={<ProtectedRoute><Dashboard /></ProtectedRoute>}/></Routes></Router>);
};const Login = () => <h2>登录页面</h2>;
const Dashboard = () => <h2>仪表板页面</h2>;export default App;
2. 使用useEffect
在组件中拦截导航
可以在页面组件中使用useEffect
处理路由进入的逻辑,比如权限检查或数据初始化。
示例代码
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";const Dashboard = () => {const navigate = useNavigate();useEffect(() => {// 模拟权限检查const hasAccess = false;if (!hasAccess) {navigate("/login");}}, [navigate]);return <h2>仪表板页面</h2>;
};export default Dashboard;
3. 使用react-router
的嵌套路由和Layout组件
通过在布局组件中统一处理路由拦截,可以实现更简洁的权限管理。
示例代码
import React from "react";
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from "react-router-dom";// 模拟认证状态
const isAuthenticated = false;// 布局组件
const ProtectedLayout = () => {return isAuthenticated ? <Outlet /> : <Navigate to="/login" />;
};const App = () => {return (<Router><Routes><Route path="/login" element={<Login />} /><Route element={<ProtectedLayout />}><Route path="/dashboard" element={<Dashboard />} /><Route path="/settings" element={<Settings />} /></Route></Routes></Router>);
};const Login = () => <h2>登录页面</h2>;
const Dashboard = () => <h2>仪表板页面</h2>;
const Settings = () => <h2>设置页面</h2>;export default App;
4. 中间件逻辑封装(高阶组件)
如果需要复用逻辑,可以封装为高阶组件(HOC)。
示例代码
import React from "react";
import { Navigate } from "react-router-dom";const withAuth = (Component) => {return (props) => {const isAuthenticated = false; // 模拟认证状态return isAuthenticated ? <Component {...props} /> : <Navigate to="/login" />;};
};const Dashboard = () => <h2>仪表板页面</h2>;export default withAuth(Dashboard);
总结
- 小型项目:可以直接在组件内使用
useNavigate
或<Navigate>
处理路由跳转。 - 中型项目:推荐使用布局组件(如
<Outlet>
)统一管理路由拦截。 - 大型项目:封装高阶组件(HOC)或自定义Hook,结合
redux
或context
进行权限状态管理。
根据具体需求选择合适的实现方式!
案例:
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { getMenuParentKey } from "@/utils";
import { useDidRecover } from "react-router-cache-route"
import Error from "@pages/err";
import { Spin } from "antd";
import { useLocation } from "react-router-dom";
import { useDispatchLayout, useDispatchMenu, } from "@/store/hooks";const scrollPage = () => {window.scrollTo({top: 0,left: 0,behavior: "smooth",});
}const fallback = <Spin style={{display: "flex",alignItems: "center",justifyContent: "center",minHeight: 500,fontSize: 24,
}} tip="页面加载中...." />function Intercept({ menuList, components: Components, [MENU_TITLE]: title, [MENU_PATH]: pagePath, [MENU_KEEPALIVE]: isKeep, pageKey, [MENU_LAYOUT]: layout, ...itemProps }) {const location = useLocation()const { stateAddOpenedMenu: addOpenedMenuFn, stateSetSelectMenuKey: setSelectedKeys, stateSetOpenMenuKey: setOpenKeys, stateSetCurrentPath: setPath } = useDispatchMenu()const { stateChangeLayout } = useDispatchLayout()const [pageInit, setPageInit] = useState(false)const currentPath = useMemo(() => {const { pathname, search } = locationreturn pathname + search}, [location])// 监听 location 改变const onPathChange = useCallback(() => {if (isKeep !== "true") {addOpenedMenuFn({ key: currentPath, path: currentPath, title: title || "未设置标题信息" });}}, [currentPath, title, isKeep, addOpenedMenuFn])const setCurrentPageInfo = useCallback(() => {if (!title) {return;}document.title = title;setSelectedKeys([String(pageKey)]);let openkey = getMenuParentKey(menuList, pageKey);setOpenKeys(openkey);addOpenedMenuFn({ key: currentPath, path: currentPath, title });}, [currentPath, menuList, title, pageKey, setOpenKeys, setSelectedKeys, addOpenedMenuFn])const init = useCallback(() => {setCurrentPageInfo()scrollPage()}, [setCurrentPageInfo])useEffect(() => {if (!pageInit) {init()setPageInit(true)}}, [init, pageInit])useEffect(() => {if (pageInit) {onPathChange()}}, [onPathChange, pageInit])// 切换布局useEffect(() => {layout && stateChangeLayout("push", layout)}, [layout, stateChangeLayout])// 路由改变useEffect(() => {setPath(currentPath)}, [currentPath, setPath])useDidRecover(() => {setPath(currentPath)init()}, [init, currentPath, setPath])const hasPath = !menuList.find((m) => (m[MENU_PARENTPATH] || "") + m[MENU_PATH] === pagePath);if (hasPath && pagePath !== "/" && pagePath !== "*") {return (<Error{...itemProps}status="403"errTitle="权限不够"subTitle="Sorry, you are not authorized to access this page."/>);}return (<Components{...itemProps}fallback={fallback}/>);
}
export default Intercept
这段代码定义了一个名为Intercept
的React组件,核心作用是作为一个路由拦截器,完成以下任务:
-
初始化页面状态和路径管理:
- 设置页面标题、打开菜单、选中菜单、当前路径等信息。
- 通过
useEffect
和useCallback
处理组件的生命周期及路径变化。
-
权限检查:
- 检查当前路径是否有对应的菜单项。如果没有权限访问当前路径,返回一个错误组件,显示403页面。
-
页面布局切换:
- 动态切换页面布局(
layout
)。
- 动态切换页面布局(
-
路由恢复逻辑:
- 监听路由的切换和恢复事件(
useDidRecover
),以确保页面状态在路径切换后能够正确恢复。
- 监听路由的切换和恢复事件(
核心功能分析
1. 页面初始化逻辑
-
变量初始化
const currentPath = useMemo(() => {const { pathname, search } = location;return pathname + search; }, [location]);
- 使用
useMemo
监听location
变化,动态计算当前完整路径。
- 使用
-
初次加载初始化
useEffect(() => {if (!pageInit) {init();setPageInit(true);} }, [init, pageInit]);
- 在组件首次渲染时,调用
init
函数完成页面初始化工作(如设置标题、菜单状态等)。
- 在组件首次渲染时,调用
-
路径变化监听
useEffect(() => {if (pageInit) {onPathChange();} }, [onPathChange, pageInit]);
- 当路径发生变化时,执行
onPathChange
更新已打开的菜单列表。
- 当路径发生变化时,执行
2. 权限检查
- 核心逻辑
const hasPath = !menuList.find((m) => (m[MENU_PARENTPATH] || "") + m[MENU_PATH] === pagePath );if (hasPath && pagePath !== "/" && pagePath !== "*") {return (<Error{...itemProps}status="403"errTitle="权限不够"subTitle="Sorry, you are not authorized to access this page."/>); }
- 检查
menuList
中是否有匹配的路径:- 如果路径不合法(即未在
menuList
中找到对应项),则渲染403错误页面。 - 排除根路径
"/"
和通配路径"*"
。
- 如果路径不合法(即未在
- 检查
3. 动态布局切换
- 布局切换逻辑
useEffect(() => {layout && stateChangeLayout("push", layout); }, [layout, stateChangeLayout]);
- 如果传入了
layout
属性,则调用stateChangeLayout
方法切换布局模式。
- 如果传入了
4. 路由恢复支持
- 恢复逻辑
useDidRecover(() => {setPath(currentPath);init(); }, [init, currentPath, setPath]);
- 当路由恢复时,重新初始化页面状态并设置当前路径。
代码结构总结
1. 核心流程
- 初始化阶段:通过
init
设置页面标题、菜单状态、路径信息,并首次渲染组件内容。 - 路径变化监听:在
useEffect
中根据location
变化更新状态。 - 权限检查:验证路径是否存在于
menuList
,否则渲染403页面。 - 布局切换:根据
layout
动态改变布局。
2. 关键点拆解
- 性能优化
- 使用
useMemo
、useCallback
优化重复计算和函数重建。
- 使用
- 状态管理
- 集成了多个状态管理方法(如
useDispatchMenu
、useDispatchLayout
)。
- 集成了多个状态管理方法(如
- 用户体验
- 自动恢复页面状态,确保路径变化后体验一致。
3. 代码模块化
- 权限校验:独立实现逻辑,返回错误页面。
- 状态管理:通过
useDispatch
和setState
组合处理菜单、路径和布局等全局状态。
改进建议
-
增强代码可读性
- 把复杂逻辑(如路径变化监听、权限检查)拆分成独立的工具函数,提高代码复用性和可维护性。
- 添加注释解释每个核心逻辑。
-
减少依赖传递
- 使用Context或Redux集中管理状态,减少从
props
中传递过多参数。
- 使用Context或Redux集中管理状态,减少从
-
错误处理和提示优化
- 对
menuList
为空或useDispatchMenu
未初始化的情况进行额外的边界处理。
- 对
-
性能监控
- 在路径变化频繁时,注意
useEffect
中逻辑是否存在性能瓶颈(如大量DOM操作或API调用)。
- 在路径变化频繁时,注意