90%的SPA性能问题,可能都出在路由设计上!
你是不是以为 React Router 只会配置path
和element
?其实,路由的进阶用法 ------ 懒加载、权限守卫、状态码处理,才是决定单页应用(SPA)性能和安全性的关键。为什么别人的 React 应用首屏加载快到飞起?为什么未登录用户进不了支付页?
路由懒加载:让首屏加载快到 "没朋友"
单页应用(SPA)的痛点之一是 "首次加载慢"------ 如果把所有页面组件都打包到一个文件里,用户打开首页时要加载大量无关代码(比如 "关于页""详情页"),导致白屏时间变长。
路由懒加载就是为解决这个问题而生:只在访问某个路由时,才加载对应的组件代码,首页只加载当前需要的资源。
(1)为什么需要懒加载?看一个真实场景
假设你的项目有 10 个页面组件,不做懒加载时,初始打包会把这 10 个组件全部放入main.jsx
,导致文件体积达到 500KB;而用懒加载后,首页只需加载当前页面组件(约 50KB),其他组件在用户访问对应路径时才加载。这意味着:
- 首屏加载时间从 3 秒缩短到 500ms;
- 减少用户等待,降低跳出率。
(2)实现路由懒加载的 3 个关键步骤
react-router-dom
结合 React 的lazy
和Suspense
API,可轻松实现路由懒加载,步骤如下:
步骤 1:导入lazy
和Suspense
jsx
import { Suspense, lazy } from 'react';
lazy
:一个高阶函数,用于动态导入组件(返回一个懒加载组件);Suspense
:用于在懒加载组件加载完成前显示占位内容(如 "加载中...")。
步骤 2:用lazy
动态导入页面组件
替代传统的静态导入(import Home from './pages/Home'
),改用lazy
包裹动态import
:
jsx
// 懒加载首页组件
const Home = lazy(() => import('./pages/Home'));
// 懒加载关于页组件
const About = lazy(() => import('./pages/About'));
// 懒加载404页面组件
const NotFound = lazy(() => import('./pages/NotFound'));
- 动态
import('./pages/Home')
会返回一个 Promise,在组件被需要时才加载对应文件; lazy
会将这个 Promise 包装成一个 React 组件,供Route
的element
使用。
步骤 3:用Suspense
包裹路由,设置占位内容
在Routes
外层用Suspense
包裹,通过fallback
属性指定加载过程中显示的内容(通常是加载动画或文本):
jsx
function App() {
return (
<Router>
<Navigation />
{/* 懒加载组件必须被Suspense包裹,指定加载占位 */}
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> {/* 404路由 */}
</Routes>
</Suspense>
</Router>
);
}
工作原理:
- 首次访问
/
时,只加载Home
组件的代码,About
和NotFound
的代码不会加载。 - 当用户点击跳转到
/about
时,浏览器才会异步加载About
组件的代码,加载期间显示fallback
内容。
(3)懒加载的底层原理:ES 模块动态导入
路由懒加载的核心是 ES6 的import()
函数,它与静态import
的区别在于:
- 静态
import
:编译时执行,会将模块代码打包进当前文件,无法条件加载; - 动态
import()
:运行时执行,返回一个 Promise,当路径匹配时才加载模块,实现 "按需加载"。
在 React 中,lazy
函数将动态import()
返回的 Promise 转换为可被 React 渲染的组件,而Suspense
则监听这个 Promise 的状态,在pending
时显示fallback
,fulfilled
时显示组件内容。
避坑指南:懒加载的 2 个注意事项
Suspense
的位置 :必须包裹所有懒加载组件的父级,通常放在Routes
外层,确保所有懒加载路由都能被捕获;- 不要懒加载过小的组件:如果组件体积很小(如只有几行代码),懒加载带来的性能提升远小于网络请求开销,反而得不偿失;
- 404 页面建议不懒加载:404 页面通常简单且可能被频繁访问,提前加载能提升用户体验。
路由守卫:用鉴权控制谁能访问你的页面
在实际项目中,并非所有页面都对游客开放(如支付页、个人中心)。路由守卫(也叫 "路由鉴权")的作用是:在用户访问某个路径前,检查其权限(如是否登录),根据结果决定允许访问还是跳转到登录页。
实现路由守卫:封装ProtectRoute
组件
react-router-dom
没有内置的路由守卫组件,但我们可以通过自定义组件实现,核心思路是:在组件内部判断权限,有权限则渲染子组件,无权限则跳转。
步骤 1:创建ProtectRoute
鉴权组件
jsx
// src/pages/ProtectRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
const ProtectRoute = ({ children }) => {
// 从localStorage获取登录状态(实际项目可能从状态管理库获取)
const isLogin = localStorage.getItem('isLogin') === 'true';
// 获取当前访问的路径(用于登录后跳转回原页面)
const location = useLocation();
if (!isLogin) {
// 未登录:跳转到登录页,并携带当前路径作为参数
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
// 已登录:渲染受保护的子组件(如<Pay />)
return children;
};
export default ProtectRoute;
children
:受保护的子组件(如<Pay />
);Navigate
:react-router-dom
提供的跳转组件,类似window.location.href
但不刷新页面;state={{ from: pathname }}
:携带当前路径,方便登录后跳转回原页面;replace
:替换历史记录,避免用户点击 "回退" 按钮再次进入受保护页面。
步骤 2:在路由中使用ProtectRoute
将需要鉴权的路由用ProtectRoute
包裹:
jsx
// App.jsx中配置受保护的路由
import ProtectRoute from './pages/ProtectRoute';
import Pay from './pages/Pay';
<Routes>
{/* 其他路由... */}
{/* 支付页需要鉴权,用ProtectRoute包裹 */}
<Route
path="/pay"
element={
<ProtectRoute>
<Pay />
</ProtectRoute>
}
/>
</Routes>
步骤 3:优化登录页:登录后跳回原页面
登录成功后,自动跳转回用户原本想访问的页面(如用户访问/pay
被拦截,登录后应跳回/pay
):
jsx
// src/pages/Login.jsx
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate(); // 用于编程式跳转
const location = useLocation(); // 获取跳转来时携带的参数
const handleSubmit = (e) => {
e.preventDefault();
// 简单校验(实际项目需调用登录接口)
if (username === 'admin' && password === '123456') {
// 登录成功:保存登录状态
localStorage.setItem('isLogin', 'true');
// 跳回原页面(若没有则跳首页)
const from = location.state?.from || '/';
navigate(from, { replace: true });
} else {
alert('用户名或密码错误');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">登录</button>
</form>
);
};
export default Login;
useLocation().state.from
:获取ProtectRoute
传递的原路径;navigate(from)
:登录成功后跳回原路径,提升用户体验。
路由守卫的核心价值
- 权限控制:严格限制未授权用户访问敏感页面(支付、个人信息)。
- 用户体验:登录后自动跳回原页面,避免用户重复操作。
- 通用性 :一个
ProtectRoute
组件可保护多个页面,无需重复写校验逻辑。
HTTP 状态码与路由:前端如何处理 301/302/401?
路由不仅涉及前端页面跳转,还与 HTTP 状态码密切相关,尤其是涉及后端接口时,需要前端路由配合处理:
(1)401 Unauthorized(未授权)
-
场景:前端请求需要登录的接口(如
/api/pay
),后端返回 401; -
处理:在请求拦截器中捕获 401,通过路由跳转到登录页(类似路由守卫的逻辑):
jsx// axios请求拦截器示例 axios.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // 清除登录状态 localStorage.removeItem('isLogin'); // 跳转到登录页 navigate('/login', { replace: true }); } return Promise.reject(error); } );
(2)301 Moved Permanently(永久重定向)与 302 Found(临时重定向)
-
场景:后端返回 301/302 时,前端需要根据
Location
头跳转; -
处理:在路由中可通过
Navigate
组件实现前端重定向:jsx// 永久重定向:/old-page → /new-page <Route path="/old-page" element={<Navigate to="/new-page" replace />} />
replace
属性:对应 301 的 "永久" 特性,替换历史记录,避免回退到旧路径。
性能优化小结:路由层面的最佳实践
- 路由懒加载 :优先对体积大、非首屏的组件使用
lazy
+Suspense
,减少初始加载时间; - 合理设置
fallback
:懒加载时的fallback
应简单(如骨架屏),避免增加额外加载成本; - 路由守卫精准化:只对必要页面(如支付、个人中心)添加鉴权,减少不必要的判断开销;
- 结合代码分割 :现代构建工具(如 Vite、Webpack)会自动对懒加载组件进行代码分割,生成独立的
chunk
文件,进一步优化加载。