我是小ao,大二在读,正在用
React + TypeScript做一个名为面试克星的 AI 面试准备平台。 完成用户系统闭环后,我遇到一个新问题: 如何保护首页、个人主页、练习页,确保只有登录用户才能访问?如果未登录,就自动跳转到欢迎页。 在之前的 Vue 论坛项目里,我用 Vue Router 的beforeEach全局钩子轻松搞定了这件事。到了 React,我发现 Router 根本没有beforeEach。这意味着,我得换个思路。
1. 后端鉴权 vs 前端路由守卫
在做前端守卫之前,我已经用 JWT 中间件实现了后端鉴权。逻辑很清晰:
- 从请求头
Authorization中取出 Token。 - 用
jwt.verify验证 Token 有效性。 - 验证通过后,从数据库查出用户信息,挂载到
req.user上。 - 后续路由通过
req.user判断用户身份。
前端的路由守卫,本质上做的是同一件事,只是换了一个环境:
- 从 Zustand Store 中读取
isLogin状态。 - 如果
isLogin为false,跳转到欢迎页。 - 如果
isLogin为true,正常渲染页面。
后端是"拦截请求",前端是"拦截页面渲染"。理解了这一点,剩下的就只是写代码了。
2. 用 ProtectedRoute 组件替代 beforeEach
React Router 没有 beforeEach,但它有一个更灵活的东西:组件。
我写了一个叫 ProtectedRoute 的组件,逻辑很简单:
tsx
import React from 'react'
import { Navigate } from 'react-router-dom'
import useUserStore from '../store/useUserStore'
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const isLogin = useUserStore((state) => state.isLogin)
if (!isLogin) {
return <Navigate to="/welcome" replace />
}
return <>{children}</>
}
export default ProtectedRoute
这个组件做的事本质上就是一个 if 判断:
- 没登录 → 渲染
<Navigate>组件,自动跳转到/welcome。 - 已登录 → 渲染
children,也就是正常的页面内容。
在 App.tsx 里使用时,只需要用它包裹需要保护的路由:
tsx
<Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/practice" element={<ProtectedRoute><Practice /></ProtectedRoute>} />
不需要保护的页面(欢迎页、登录页、注册页)保持原样即可。
为什么 React 没有 beforeEach?
后来我琢磨,这其实反映了 Vue 和 React 一个核心的思维差异:
- Vue Router 把路由守卫当成一个独立的功能模块,用
beforeEach这种全局钩子来处理。它像一道安检门,所有路由跳转都要先经过它。你只需要配置一次逻辑,它就自动生效,和你的页面组件是分离的。 - React Router 认为"一切都是组件"。路由守卫也不例外。
ProtectedRoute就是一个普通的 React 组件,它接收children,检查状态,然后决定渲染什么。
Vue 是配置式的全局守卫,React 是组件式的条件渲染。
两者没有优劣之分,只是设计哲学不同。理解了这个差异后,我突然觉得 React 的写法也挺优雅的------它把"要不要展示这个页面"这件事,变成了一个可复用的组件逻辑。
3. 导航栏改造:根据登录状态切换内容
做完路由守卫后,导航栏也需要跟着改造。之前的导航栏把所有链接都平铺展示出来了,不管用户有没有登录。现在需要根据 isLogin 状态,显示不同的导航项:
- 已登录:首页、练习、个人主页
- 未登录:欢迎页、登录、注册
改造的逻辑和 ProtectedRoute 一样,只是换成了条件渲染:
tsx
const isLogin = useUserStore((state) => state.isLogin)
{isLogin ? (
<>
<NavLink to="/">首页</NavLink>
<NavLink to="/practice">练习</NavLink>
<NavLink to="/profile">个人主页</NavLink>
</>
) : (
<>
<NavLink to="/welcome">欢迎页</NavLink>
<NavLink to="/login">登录</NavLink>
<NavLink to="/register">注册</NavLink>
</>
)}
同时加上了 Logo,用绝对定位把导航链接居中,背景改成了渐变色。这些样式细节就不展开了,重点是条件渲染的逻辑。
4. 滚动条抖动:一个诡异的 Bug
导航栏改造完成后,我发现一个诡异的现象:点击"练习"时,导航栏会轻微抖动一下,但点击"首页"和"个人主页"不会。
我开始逐个排查:
- 检查了所有 CSS 过渡效果,去掉了
transition-all、hover:scale-105、font-bold切换。 - 把所有可能引起布局变化的属性都移除了。
- 依然抖动。
然后我注意到了一个规律:只有"练习"页面会抖,其他页面都不抖。
这让我开始怀疑是页面内容长度的问题。检查了一下,练习页的内容确实比首页和个人主页更长。
根因找到了:浏览器的垂直滚动条。
当页面内容较短时(如首页),浏览器不显示滚动条。当页面内容较长时(如练习页),浏览器会在右侧自动显示一个滚动条。这个滚动条会占用大约 15-17 像素的宽度,把整个页面内容往左边挤一下。居中的导航栏也跟着往左移,肉眼看到的就是"抖了一下"。
解决方案 :在 index.css 中添加一行代码,强制滚动条始终存在:
css
html {
overflow-y: scroll;
}
这行代码的意思是:不管网页内容够不够长,都强制在浏览器窗口右侧显示垂直滚动条。它始终占着位,就不会因为突然出现或消失而引发抖动。
改完之后,导航栏彻底安静了。
5. 总结
路由守卫和导航栏改造,看起来是独立的功能,但本质上都在做同一件事:根据用户状态驱动视图变化。
- 路由守卫:根据
isLogin决定是否渲染页面。 - 导航栏:根据
isLogin决定显示哪些链接。
这是 React 的核心思维------一切都是组件,一切都可以通过条件渲染来实现。Vue 用全局钩子做的事,React 用组件来做。没有优劣之分,只有不同的设计哲学。
那个滚动条的 Bug,也让我意识到:前端开发中,很多诡异的问题,根因往往不是什么复杂的框架问题,而是一些基础 CSS 细节在捣鬼。耐心对比、逐个排查,就是最好的调试方法。