背景
上一篇(前端路由(二):React Router组件化路由实战.md)用 React Router 搭起了路由的组件化骨架:<Routes> 声明路径到组件的映射,<Link> 处理导航,useParams 提取动态参数,<Outlet> 承载嵌套渲染,lazy() 做代码分割。到这一步,页面之间的切换已经可以正常工作。
但「能切换页面」和「能管理访问」之间还有一段距离。一个面向用户的 Web 应用通常会需要:
- 某些页面只有登录后才能访问(如支付页、个人中心)
- 未登录用户访问受限页面时,自动跳转到登录页
- 登录成功后,回到用户原本想去的那一页
- 登录页在浏览历史中不留痕迹------用户按「后退」不应该再看到登录表单
这些需求不再属于「路由怎么配」,而是「路由在运行时怎么决策」。本文基于 react-router-demo 中新增的鉴权相关代码,拆解路由守卫、登录重定向、状态传递这一组模式的实现方式。
路由守卫:用 children 模式做条件渲染
路由守卫的职责是在进入目标页面之前做一个判定:当前用户是否有权访问?如果有,正常渲染;如果没有,拦截并跳转。
ProtectRoute.jsx 的实现:
javascript
import { Navigate } from 'react-router-dom'
const ProtectRoute = ({ children }) => {
const isLogin = localStorage.getItem('isLogin') === 'true'
if (!isLogin) {
return (
<Navigate to="/login" replace state={{ from: location.pathname }} />
)
}
return <>{children}</>
}
export default ProtectRoute
这个组件的结构值得拆开来看。
children 是路由守卫的通用模式。 ProtectRoute 不关心自己包裹的是什么页面------它只做判定。通过 props 接收 children,调用方决定被保护的内容:
xml
<ProtectRoute>
<Pay />
</ProtectRoute>
这种写法让守卫逻辑和页面逻辑彻底解耦。ProtectRoute 可以包任何组件------支付页、个人中心、后台管理------不需要为每个受保护页面单独写一个守卫。
判定逻辑依赖 localStorage。 HTTP 是无状态协议,登录状态需要客户端自己维护。常见方案有三种:请求头携带 Token、Cookie、localStorage。demo 选择了 localStorage------登录成功时写入 isLogin = 'true',ProtectRoute 读取这个标记做判定。选择 localStorage 的好处是读写都在前端完成,不依赖服务端 Session,适合 demo 级别的快速验证。
未登录时返回 <Navigate> 组件。 <Navigate> 是 React Router 提供的声明式重定向组件------渲染它,就会触发路由跳转。这里的 replace 和 state 两个属性各有用途:
replace:用新 URL 替换当前历史记录,用户无法通过「后退」回到被拦截前的页面state:携带location.pathname------用户原本想去哪里。这个值将在登录成功后用于跳回
登录与回跳:useNavigate、location.state 和 replace
Login/index.jsx 是鉴权流程的另一半:
javascript
import { useNavigate, useLocation } from 'react-router-dom'
const Login = () => {
const navigate = useNavigate()
const location = useLocation()
const from = location.state?.from || '/'
function handleSubmit(e) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const username = formData.get('username')
const password = formData.get('password')
if (username === 'admin' && password === '123456') {
localStorage.setItem('isLogin', 'true')
navigate(from, { replace: true })
}
}
return (
<form onSubmit={handleSubmit}>
<h1>登录</h1>
<input name="username" placeholder="请输入用户名" required />
<input name="password" placeholder="请输入密码" required />
<button type="submit">登录</button>
</form>
)
}
这里有几个设计选择值得展开。
const from = location.state?.from || '/'------可选链 + 默认值。
location.state 是路由跳转时携带的附属数据,不会出现在 URL 里。用户直接访问 /login(而非被拦截后跳转过来)时,location.state 为 undefined,可选链运算符 ?. 防止访问 undefined.from 抛错,|| '/' 提供默认跳转目标。
从支付页被拦截的用户,from 的值是 /pay,登录后回到支付页。直接访问登录页的用户,from 为 /,登录后回到首页。一条表达式覆盖两种场景。
navigate(from, { replace: true })------登录成功后替换历史记录。
这里用了 replace: true,和 ProtectRoute 中 <Navigate replace> 形成呼应。如果不加 replace,登录成功后的跳转会在历史栈中新增一条记录。用户按「后退」按钮时,浏览器会回到登录页------此时用户已经登录,登录页再读 localStorage 判定已登录,又自动跳走。这个循环不会报错,但会让导航行为变得不可预测。
replace: true 用新页面的历史记录覆盖掉登录页的记录,用户在登录成功后的页面按「后退」,会直接跳过登录页,回到进入登录页之前的页面。对用户来说,登录是一个「透明的中间步骤」------来过,但历史里找不到痕迹。
useNavigate 与 <Navigate> 的对应关系。 <Navigate> 是声明式写法,适合放在 JSX 中做条件渲染;useNavigate() 是编程式写法,适合放在事件处理、异步回调中触发跳转。ProtectRoute 用前者(判定未登录时直接返回 <Navigate>),Login 用后者(表单提交成功后才调用 navigate())。同一套路由跳转能力,两种使用入口。
串联:一次完整的鉴权流程
把上面的组件放回路由配置中,看一次完整请求的走向。
App.jsx 中的相关路由:
xml
<Route path="/login" element={<Login />} />
<Route path="/pay" element={
<ProtectRoute>
<Pay />
</ProtectRoute>
} />
用户点击导航栏中的「支付」链接时:
- 路由匹配到
/pay,渲染<ProtectRoute><Pay /></ProtectRoute> ProtectRoute读取localStorage,发现isLogin !== 'true'- 返回
<Navigate to="/login" replace state={{ from: '/pay' }} />,触发重定向 - URL 变为
/login,渲染<Login />,location.state.from为/pay - 用户输入用户名密码,点击登录
handleSubmit写入localStorage,调用navigate('/pay', { replace: true })- URL 变为
/pay,ProtectRoute再次判定------这次isLogin === 'true',渲染<Pay /> - 用户按「后退」,浏览器回到
/login之前的页面------登录页已从历史栈中消失
整个流程中,路由守卫、登录表单、状态传递、历史记录管理各司其职,组合在一起形成了一条完整的访问控制链路。
三篇回顾:一条从原理到实践的路线
系列三篇文章覆盖了前端路由的三个层次:
- 原理层 (其一):浏览器如何管理 URL------Hash 的
hashchange事件、History API 的pushState和popstate。手写一个 HashRouter 理解路由的本质是「键值对映射 + 事件监听」。 - 配置层 (其二):React Router 如何把路由表组织成组件树------
<Routes>声明式配置、<Link>导航、useParams参数提取、<Outlet>嵌套渲染、lazy()按需加载。组件化路由解决的核心问题是:当路由规模从三个页面扩展到三十个页面时,怎样用一致的 API 管理复杂性。 - 控制层 (其三,本文):路由在运行时如何做访问控制------
children模式做守卫组件、<Navigate>声明式重定向、useNavigate编程式跳转、location.state跨页面传递状态、replace管理历史记录。这些模式让路由从「能切换」进到「能决策」。
三层之间的关系是递进而不是替代:原理层解释「为什么能工作」,配置层提供「怎么组织」,控制层补充「怎么决策」。实际项目中,三层同时存在------底层是浏览器的 URL API,中间是框架的路由配置,上层是业务方的守卫和跳转逻辑。
小结
鉴权路由这一组模式,本质上是在路由的匹配-渲染流程中插入了一个判定节点。ProtectRoute 用 children 接收被保护的内容,用 localStorage 做状态判定,用 <Navigate> 触发拦截跳转,用 location.state 记录来源路径。Login 用 useNavigate 做编程式回跳,用 replace: true 清理历史记录。
这些模式不限于鉴权场景。任何需要在进入页面前做前置检查的逻辑------角色权限、功能开关、A/B 实验分流------都可以复用同一套「守卫组件 + children + 条件重定向」的结构。理解了这一层,前端路由就不再只是「切换页面的工具」,而是一个可以承载业务决策的运行时框架。