前端路由,其实就是三个对象在演戏

前端路由,其实就是三个对象在演戏

我第一次翻开 React Router 文档的时候,整个人是懵的。满脑子就一个问题:

"URL 不是后端管的吗?前端在这儿掺和啥呢?"

这个问题卡了我挺久。后来我想通了,回头再看,React Router 那些 API 压根不用背,顺理成章就记住了。今天我就把那个"想通"的瞬间分享给你。

咱得先把脑子里的这根筋转过来:URL 不等于后端接口

你打开浏览器,输入 https://taobao.com/shop/123,回车。

你下意识会觉得:浏览器发了个请求给后端,后端根据 /shop/123 把页面返回来。没错,在传统网站里,URL 每变一次,浏览器就老老实实发一次 HTTP 请求

但单页应用(SPA)不玩这套。

SPA 从头到尾只有一个 HTML 文件,就是 index.html。不管你是访问 //shop/123,还是 /user/profile,后端统统给你扔回同一个 index.html,里面藏着一大坨 JavaScript。然后这坨 JS 在浏览器里接管了一切。

那问题就来了:既然就一个页面,URL 变来变去还有什么意义?

答案是------URL 不再是用来问后端要资源的,而是用来告诉前端"用户想去哪个页面"的。

说白了,URL 从"资源定位符"变成了"前端状态小标签"。

这就是前端路由存在的全部理由。后面的所有 API,都是从这个根上长出来的。

说句扎心的大实话:前端路由从头到尾都是"骗"

浏览器有个死规矩:你不能随便用 JS 改地址栏 URL 还不让页面刷新。 这是为了保护你,不然恶意网站就能把你的地址栏当玩具耍。

可前端路由偏要干的就是"改 URL 但不刷新"。怎么办呢?开发者们找到了两条"后门":

后门一:Hash(#号)

URL 里 # 后面的东西叫 hash,也就是锚点。这玩意儿改了,浏览器默认不刷新页面。这是浏览器打娘胎里就带的设计,不是 bug。

所以你可以在 # 后面做文章://#/shop/#/user/123。浏览器眼皮都不抬一下,不刷新。前端只需要监听 hashchange 事件,发现 hash 变了,就把对应的组件渲染出来。

HashRouter 就是这么干的。它利用了浏览器的锚点机制。

后门二:History API

HTML5 给开发者开了另一个后门:history.pushState()history.replaceState()。这两个方法可以"偷偷"改掉地址栏的 URL,而浏览器完全不刷新

你调用 history.pushState(null, '', '/shop'),地址栏立刻变成 /shop,但浏览器不会向后端发请求。前端监听 popstate 事件,发现 URL 变了,就自己把组件换掉。

BrowserRouter 就是基于这个原理。

说到底,不管是 Hash 还是 History API,本质上都是在"忽悠"浏览器------让浏览器以为 URL 变了,实际上啥网络请求都没发生。前端路由根本算不上真正的页面跳转,它是一套精心设计的障眼法。

搞明白这个"骗局"之后,React Router 的 API 就很好懂了

React Router 说白了,就是把上面那套障眼法包装成了三个对象。你只要搞清楚这三个对象分别是干嘛的,所有 API 都不用背。

<Link>useNavigate<Navigate>,别看名字不一样,底层都在操作同一个东西:navigator。

jsx

ini 复制代码
// 声明式:放个链接,点一下就去 /about
<Link to="/about">关于</Link>

// 命令式:用户登录成功了,赶紧给他拽走
const navigate = useNavigate();
navigate('/dashboard', { replace: true });

// 条件式:没登录?直接渲染一个"跳转指令"
<Navigate to="/login" replace />

三种写法,核心动作就一个:往浏览器的历史栈里塞一条新记录(或者替换当前记录),然后通知 React Router "该换组件啦"。

你可能想问:为啥不用普通的 <a> 标签?

因为 <a> 是浏览器亲儿子,它的行为是硬编码的:发 HTTP 请求 → 收 HTML → 刷新整个页面。你 SPA 里的状态全没了,白忙活一场。

location ------ 位置信息,管"现在在哪"

它就是 URL 的"结构化翻译":

js

csharp 复制代码
{
  pathname: '/shop/123',      // 路径
  search: '?color=red',       // 查询参数
  hash: '#size',              // 锚点
  state: { from: '/checkout' } // 隐藏状态(URL 里看不到)
}

useLocation() 一调用,你就能在组件里知道"用户此时此刻在哪个页面"。

这里最容易被忽视的是 state 字段。它不在 URL 里露脸,但能悄咪咪地跨页面传数据。最经典的用法是记住"用户从哪来的":

jsx

csharp 复制代码
// 在 ProtectRoute 里
<Navigate to="/login" state={{ from: "/pay" }} />

// 在 Login 里
const location = useLocation();
const from = location.state?.from || "/";
// 登录成功就跳回 from

没有 state 的话,你只能把来源页塞到 URL 的 query 参数里,又长又丑,敏感信息还全暴露了。

history ------ 历史记录栈,管"从哪来、能不能回去"

浏览器的历史记录就是个栈结构。你每点开一个新页面,就往栈顶压一条;点"后退",就从栈顶弹出一条。

React Router 的 useNavigate 操作的就是这个栈:

jsx

php 复制代码
navigate('/shop');      // push ------ 压一条新记录,用户可以点后退回来
navigate('/shop', { replace: true }); // replace ------ 替换当前记录,后退就回不来了

什么时候用 replace?登录页跳转的时候。用户都登录成功了,你让他点"后退"又回到登录表单,这不是折磨人吗?正常人不需要"回到登录页"这个操作。

鉴权路由,其实就是把这三个对象串起来用

任何登录拦截的逻辑,都可以浓缩成一句话:

如果用户没登录,导航到 登录页,同时记住 当前页面地址,登录成功了再根据地址导航回去

翻译成代码就是:

jsx

ini 复制代码
// ProtectRoute ------ 用 navigator + location
const ProtectRoute = ({ children }) => {
  const isLogin = localStorage.getItem('isLogin') === 'true';
  const location = useLocation(); // 看一下现在在哪

  if (!isLogin) {
    // 导航到登录页,顺手把当前位置带上
    return <Navigate to="/login" replace state={{ from: location.pathname }} />;
  }

  return children;
};

// Login ------ 用 navigator + location.state
const Login = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from || '/'; // 从哪来的?

  const handleLogin = () => {
    // ...校验账号密码
    localStorage.setItem('isLogin', 'true');
    navigate(from, { replace: true }); // 导航回去,replace 防止回退到登录页
  };
};

每一行拆开看都不难。但如果你心里没装着 navigator、location、history 这三个角色,它们搅在一起的时候就特别容易晕。

动态路由和嵌套路由:URL 是怎么变成组件树的

React Router 里最让我开窍的一句话是:路由配置就是拿 URL 的模式去匹配一棵组件树。

jsx

xml 复制代码
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/user/:id" element={<UserProfile />} />
  <Route path="/products" element={<Products />}>
    <Route path=":productId" element={<ProductDetail />} />
    <Route path="new" element={<NewProduct />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

/user/123 → 匹配 /user/:id → 渲染 <UserProfile />,参数 id = "123" 通过 useParams() 就能拿到。

嵌套路由稍微绕一点:/products/123 先匹配父路由 /products,把 <Products /> 渲染出来;再匹配子路由 :productId,把 <ProductDetail /> 塞到父组件里的 <Outlet /> 位置。

<Outlet /> 就是个"子路由插槽"。跟 Vue 的 <router-view> 一个意思,换个马甲而已。

动态路由有一个特别容易踩的坑:匹配顺序是按书写顺序来的,不是按谁更精确。 /products/new/products/:productId,谁写在前面谁先匹配。如果你把 :productId 写在前面,那 "new" 就会被当成参数值,而不是路径。所以具体路径一定要写在动态路径前面。React Router v6+ 说它会自动排序,但我还是信不过------每次都手动把具体的放前面,心里踏实。

最后说个事儿:HashRouter 的 URL 是真的很丑

HashRouter 出来的链接长这样:https://xx.com/#/user/123

BrowserRouter 是这样的:https://xx.com/user/123

"丑"其实不是关键。关键是语义

URL 是你系统跟用户交流的界面之一。/user/123 明明白白地告诉用户:"你在看用户 123 的页面"。/#/user/123 看起来就像:"你在一个叫 user/123 的 app 内部状态里"。

当你把链接复制给同事排查问题,或者用户把它存到书签里的时候,这两种语义的差别就特别明显了。

所以,除非你的部署环境特殊到没法配 nginx(比如某些奇葩的静态托管平台),否则无脑选 BrowserRouter 。那个 # 咱能不要就不要。


好了,收个尾:

navigator 管"怎么走",location 管"现在在哪",history 管"能不能回去"。

React Router 的所有 API,说白了就是这三个概念的语法糖。你不是在背 React Router 的文档,你是在理解浏览器怎么管理导航状态。这事儿想通了,路由在你眼里就再也没有秘密了。

相关推荐
用户2930750976691 小时前
React 自定义 Hooks 实战:用 useMouse 理解响应式封装
前端
烬羽1 小时前
组件拆了,逻辑没拆——自定义 Hook 才是 React 业务逻辑的正当归属
react.js·架构·typescript
小月土星1 小时前
自定义业务 Hook:从零解剖一个 React + TypeScript Todo 应用:用金字塔思维看透前端架构
react.js·架构·前端框架
Goodbye1 小时前
React 核心概念实战:从组件化思维到本地存储持久化
react.js
触底反弹1 小时前
🔥 别再只用 useRef 拿 DOM 了!这 5 个实战场景让你彻底理解它(附源码解析 + 面试题)
前端·javascript·react.js
玉宇夕落1 小时前
useRef + Web Worker —— 从零到一理解React多线程协作原理
前端
用户6919026813391 小时前
React useRef、useEffect、useState 与 Web Worker 多线程实践
前端
BreezeJiang1 小时前
Web Worker 不负责渲染:React 中的线程分工与消息闭环
javascript·react.js
烬羽1 小时前
useContext 用是用了,但你真的用对了吗?——把 Context 封装进自定义 Hook
前端·react.js·全栈