🚀 20 行代码手写前端路由 + React Router 核心 API 一篇搞定!

🚀 20 行代码手写前端路由 + React Router 核心 API 一篇搞定!

摘要 :为什么 SPA 可以不刷新页面?Hash 路由的 # 有什么魔力?本文从原理出发,20 行代码手写路由器 ,再实战 React Router 核心 API,附上3 个踩坑记录


📌 前言

上周面试被问到:"前端路由的原理是什么?"

我脱口而出:"就是 React Router,用 <Route> 配置路径。"

面试官追问:"那为什么 URL 变了页面不刷新?Hash 路由和 History 路由有什么区别?"

我愣住了。

回来后我认真复盘,发现虽然天天在用路由框架,但对底层原理确实一知半解。于是我花了一周时间梳理了一遍,今天这篇文章就是我的学习笔记。


一、从 MPA 到 SPA:为什么需要前端路由?

路由的本质:URL 和资源的映射关系。

传统多页应用(MPA)中,每次点击链接都会:

  1. 重新发起 HTTP 请求
  2. 重新渲染整个页面
  3. 页面白一下(网速慢时尤其明显)

核心矛盾:页面中只有一小部分在变,导航栏、页脚完全一样,却要重新渲染整个页面。

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 是对应的组件
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 Router React Router v6 SPA 前端面试 路由守卫 代码分割 Hash路由 History API 前端基础

相关推荐
灏仟亿前端技术团队1 小时前
企业级 Agent 中台建设方案
前端
程序员黑豆1 小时前
鸿蒙应用开发:一次开发多端适配与自适应布局实战
前端·harmonyos
布兰妮甜1 小时前
Vue 状态管理选型:Pinia 完整实战,对比 Vuex,模块化持久化
前端·javascript·vue.js·pinia·vuex
神王宝宝 王者小学1 小时前
面向领域驱动架构的查询实现方式
前端·python·架构
小林ixn2 小时前
React Router 从入门到实战:一篇搞定路由配置、懒加载与嵌套路由
前端·react.js·前端框架
labixiong2 小时前
async/await 到底是不是 Generator 的语法糖?手写执行器,Babel 编译产物里藏着答案
前端·javascript·babel
windliang2 小时前
Claude Code 源码分析(六):上下文的发现、注入与压缩
前端·javascript·人工智能
张龙6872 小时前
10 万条数据不卡顿:不定高虚拟列表从原理到生产实现
前端·javascript·性能优化
玉鸯2 小时前
界面用完即消失:Agent 生成式 UI 的短暂性哲学与前端工程的未来
前端·llm·agent