前端路由,其实就是三个对象在演戏
我第一次翻开 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 都不用背。
navigator ------ 导航器,管"往哪走"
<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 的文档,你是在理解浏览器怎么管理导航状态。这事儿想通了,路由在你眼里就再也没有秘密了。