🚀 20 行代码手写前端路由 + React Router 核心 API 一篇搞定!
摘要 :为什么 SPA 可以不刷新页面?Hash 路由的
#有什么魔力?本文从原理出发,20 行代码手写路由器 ,再实战 React Router 核心 API,附上3 个踩坑记录。
📌 前言
上周面试被问到:"前端路由的原理是什么?"
我脱口而出:"就是 React Router,用 <Route> 配置路径。"
面试官追问:"那为什么 URL 变了页面不刷新?Hash 路由和 History 路由有什么区别?"
我愣住了。
回来后我认真复盘,发现虽然天天在用路由框架,但对底层原理确实一知半解。于是我花了一周时间梳理了一遍,今天这篇文章就是我的学习笔记。
一、从 MPA 到 SPA:为什么需要前端路由?
路由的本质:URL 和资源的映射关系。
传统多页应用(MPA)中,每次点击链接都会:
- 重新发起 HTTP 请求
- 重新渲染整个页面
- 页面白一下(网速慢时尤其明显)
核心矛盾:页面中只有一小部分在变,导航栏、页脚完全一样,却要重新渲染整个页面。
SPA 的思路:只加载一个 HTML,后续"页面切换"通过 JavaScript 动态替换 DOM。
但问题来了------URL 怎么办? URL 不变,用户就没法分享、收藏、用浏览器前进/后退。
我们需要:URL 要变,但页面不能刷新。 前端路由就是为了解决这个问题而生的。
二、Hash 路由的原理
先看 URL 结构:
ini
https://www.example.com/path?query=1#/page1
│ │ │ │
协议 主机 路径 Hash(# 开头的部分)
关键发现 :# 后面的 Hash 改变时,浏览器不会发起新的 HTTP 请求,页面不会刷新!
浏览器提供了 hashchange 事件来监听 Hash 变化:
javascript
window.addEventListener('hashchange', function (event) {
console.log('旧 URL:', event.oldURL);
console.log('新 URL:', event.newURL);
});
Hash 路由的原理:
bash
用户点击链接(href="#/xxx")
↓
URL 的 hash 部分改变
↓
触发 hashchange 事件
↓
JavaScript 根据 hash 值渲染对应的 DOM 内容
↓
URL 变了,页面没刷新 ✅
三、20 行代码手写 Hash 路由器
理解了原理,我动手实现了一个迷你 HashRouter:
html
<header>
<nav>
<ul>
<li><a href="#/page1">页面一</a></li>
<li><a href="#/page2">页面二</a></li>
<li><a href="#/page3">页面三</a></li>
</ul>
</nav>
</header>
<div id="container"></div>
<script>
class HashRouter {
constructor() {
this.router = {};
window.addEventListener('hashchange', this.load.bind(this));
}
register(hash, callback) {
this.router[hash] = callback;
}
load() {
let hash = location.hash.slice(1);
let handler = this.router[hash];
if (handler) handler.call(this);
}
}
let router = new HashRouter();
let container = document.getElementById('container');
router.register('/page1', () => container.innerHTML = '<h1>页面一</h1>');
router.register('/page2', () => container.innerHTML = '<h1>页面二</h1>');
router.register('/page3', () => container.innerHTML = '<h1>页面三</h1>');
</script>
💡 这 20 行代码就是 React Router 的"老祖宗"。理解了它,你就理解了前端路由的本质。
四、Hash 路由 vs History 路由
| 对比项 | Hash 路由 | History 路由 |
|---|---|---|
| URL 美观度 | ❌ 带 # |
✅ 干净的 URL |
| 兼容性 | ✅ IE8 就支持 | ❌ 需要 IE10+ |
| 服务端配置 | ✅ 无需配置 | ❌ 需要配置 fallback |
| SEO | ❌ 不太友好 | ✅ 更友好 |
怎么选?
- 内部系统、兼容性要求高 →
HashRouter - 面向用户的项目、SEO 有要求 →
BrowserRouter
⚠️ 使用
BrowserRouter时,服务器需要配置所有路径都返回index.html,否则刷新页面会 404。
五、React Router 核心 API
5.1 路由配置三件套
jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
}
- Router:提供路由上下文,整个应用只需要一个
- Routes:路由配置的容器
- Route :单条路由规则,
path是 URL,element是对应的组件
5.2 导航:Link 组件
jsx
// ❌ 用 a 标签会导致整页刷新!
<a href="/about">About</a>
// ✅ 用 Link 组件,SPA 内部跳转
<Link to="/about">About</Link>
<Link> 底层调用 history.pushState() 或修改 location.hash,URL 变了但不刷新页面。
5.3 动态路由:useParams
jsx
// 路由配置:用 :id 表示动态参数
<Route path="user/:id" element={<UserProfile />} />
// 组件中获取参数
import { useParams } from 'react-router-dom';
function UserProfile() {
let { id } = useParams();
return <h2>用户ID: {id}</h2>;
}
⚠️
useParams()返回的都是字符串,如果需要数字记得做类型转换。
5.4 嵌套路由:Outlet
jsx
// 路由配置
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
// 父组件中使用 Outlet
import { Outlet } from 'react-router-dom';
const Products = () => {
return (
<>
<h1>产品列表</h1>
<Outlet /> {/* 子路由内容渲染在这里 */}
</>
);
};
Outlet 就像一个"占位符",子路由的组件会显示在 Outlet 的位置。
5.5 路由懒加载
jsx
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Suspense>
);
}
lazy() 实现代码分割,Suspense 处理加载状态。这是面试高频考点!
5.6 重定向与 404
jsx
// 重定向
<Route path="/old-path" element={<Navigate replace to="/new-path" />} />
// 404 兜底路由(必须放最后)
<Route path="*" element={<NotFound />} />
六、实战:后台管理系统路由配置
jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
const Login = lazy(() => import('./pages/Login'));
const Layout = lazy(() => import('./pages/Layout'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserList = lazy(() => import('./pages/UserList'));
const UserDetail = lazy(() => import('./pages/UserDetail'));
const NotFound = lazy(() => import('./pages/NotFound'));
// 路由守卫:检查登录状态
function PrivateRoute() {
const token = localStorage.getItem('token');
if (!token) return <Navigate replace to="/login" />;
return <Outlet />;
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<PrivateRoute />}>
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="users" element={<UserList />} />
<Route path="users/:id" element={<UserDetail />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
5 个核心知识点:懒加载、路由守卫、嵌套路由、默认路由(index)、404 兜底。
七、3 个踩坑记录
坑 1:BrowserRouter 刷新 404
现象:本地开发没问题,部署后刷新页面 404。
解决 :服务器配置所有路径返回 index.html。
nginx
# Nginx
location / {
try_files $uri $uri/ /index.html;
}
坑 2:404 路由放错位置
现象:配置了 404 页面,但访问不存在的路径显示空白。
原因 :path="*" 放在了其他路由前面,会"贪婪匹配"所有路径。
解决 :404 路由必须放在最后面。
坑 3:嵌套路由忘记 Outlet
现象:配置了子路由,但只显示父组件,子组件不显示。
解决 :在父组件中添加 <Outlet /> 作为子路由的渲染位置。
💡 总结
前端路由的核心就一句话:
URL 变了 → 匹配路由 → 渲染组件
记住这三个关键词:
- Route:配置路径和组件的映射
- Link:跳转时不刷新页面
- Outlet:嵌套路由的"插槽"
觉得有用?点个赞👍收藏⭐关注👆,后续更新更多前端原理深度解析!
📌 标签 :
前端路由React RouterReact Router v6SPA前端面试路由守卫代码分割Hash路由History API前端基础