React.js 路由
用了几年 react-router,从 v4 的动态路由到 v6 的嵌套路由,API 变了好几轮。但翻开源码会发现,不管外层怎么变,核心机制始终是三件事:监听 URL 变化、匹配路由表、渲染对应组件。
这篇文章从 v6 的用法讲起,然后拆解 BrowserRouter、Routes、history 库的源码链路,最后对比 react-router 和 vue-router 在实现上的关键差异——如果你需要在一个项目里同时用这两个框架,这些差异值得提前知道。
react-router 基本使用
路由配置
JSX 用法:
1 | import { render } from "react-dom"; |
config + hooks 用法:
1 | import { render } from 'react-dom' |
页面跳转
JSX 用法:
1 | <Link to="/">To</Link> |
hooks 用法:
1 | const location = useLocation() |
源码:从 BrowserRouter 到 history
react-router 的代码分在三个包里:react-router(核心,跨平台)、react-router-dom(Web 端实现)、history(浏览器 history API 的封装层)。
从一个最简单的例子开始追调用链:
1 | ReactDOM.render( |
BrowserRouter 实现:
1 | export function BrowserRouter({ |
BrowserRouter 做了三件事:创建 history 对象、监听 history 变化后触发 setState 让组件重新渲染、把 history 传给 <Router>。
<Router> 的核心逻辑:
1 | export function Router({ |
<Router> 用 NavigationContext 包裹 history 对象,让所有子组件都能通过 context 访问到它。
<Routes> 的实现:
1 | export function Routes({ |
Routes 通过 Route ⼦组件⽣成路由列表,通过 location 中的 pathname 匹配组件并渲染。
通过以上代码,我们基本理解了 react-router 如何感知 history 中的 pathname 变化,并渲染对应组件。
但我们具体是如何操作 history 变化的呢?
我们在回到最上⾯的 createBrowserHistory 和 history.listen ⽅法,看看 history 对象是怎么被创建和改变的:
1 | export function createBrowserHistory( |
createBrowserHistory 创建了⼀个标准的 history 对象,以及对 history 对象操作的各⽅法,且操作变更后,通过 listen ⽅法将变更结果回调给外部。
getIndexAndLocation实现:
1 | function getIndexAndLocation(): [number, Location] { |
push 实现:
1 | function push(to: To, state?: any) { |
你可能注意到,代码里我们写的是 const location = useLocation(); location.push("/"),这个 push 是怎么跟上面 createBrowserHistory 里的 push 关联的?
回顾 <Router> 中的这行:const LocationContext = React.createContext<LocationContextObject>(null!)——history 对象被注入到 LocationContext 中。useLocation 实际上就是从 context 里取出 location:
1 | export function useLocation(): Location { |
BrowserRouter 核心链路总结:
createBrowserHistory创建 history 对象,封装对window.history的操作<Router>将 history 注入全局 context<Routes>遍历子组件生成路由表,根据location.pathname匹配组件并渲染
HashRouter 原理一致,只是监听的 URL 部分从 pathname 变为 hash。
react-router vs vue-router:实现差异
路由类型
React:
- browserRouter
- hashRouter
- memoryRouter
Vue: - history
- hash
- abstract
memoryRouter 和 abstract 作用类似,都在不支持浏览器的环境(如 SSR、测试)中充当 fallback。
路由拦截:vue-router 提供了全局守卫、路由守卫、组件守卫三套拦截机制。react-router 没有内置守卫,需要在渲染过程中自己处理——类组件在 componentWillMount 或 getDerivedStateFromProps 中通过 props.history 拦截,函数组件用 useHistory 或 useNavigate。
Hash 模式(新版本已趋同):react-router 的 hash 模式基于 window.location.hash + hashchange;vue-router 优先使用 pushState/replaceState + popstate,只在浏览器不支持 pushState 时回退到 hash 方案。
History 模式 fallback:react-router 不支持 pushState 时直接 window.location.href 重新加载。vue-router 可以通过 fallback 配置决定是回退到 hash 模式还是重新加载。
懒加载:vue-router 在懒加载页面时,先等 JS 文件加载并执行完毕,再渲染页面。react-router 则先展示 loading 状态(Suspense fallback),JS 加载完成后触发更新渲染。
实战:几个常见需求
登录校验
1 | import { createContext, ReactNode, useContext, useEffect, useState } from 'react' |
路由懒加载
React.lazy()- dynamic
import() React.Suspense
1 | const Account = React.lazy(() => import("./pages/Account")); |
离开页面前确认
基于 history 的 block 实现
1 | import { useCallback, useContext, useEffect, useState } from 'react'; |
服务端渲染
在服务端使用 StaticRouter 替代 BrowserRouter:
1 | <StaticRouter location={req.url}> |
为什么要拆成三个包?
react-router(核心、跨平台)、react-router-dom(Web 端)、react-router-native(React Native 端)。核心与平台解耦——这和 React 的 reconciler 分层是同一个设计思路。
react-router v6 相比 v5 最大的变化是用 <Routes> + element 替代了 <Switch> + component,嵌套路由的写法更直观。但源码层面,history 监听 → context 注入 → 路由匹配 → 组件渲染这条链路从 v4 开始就没怎么变过。理解了这条链路,不管 API 怎么迭代,核心思路都在那里。