Skip to main content

React Router 路由及其原理

一、React Router 的定义

1、什么是路由

路由的本质就是页面的 URL 发生改变时,页面的显示结果可以根据 URL 的变化而变化,但页面不会刷新。

react-router 等前端路由可以实现无刷新的条件下切换显示不同的页面,因此可以通过前端路由可以实现单页面(SPA)应用。

2、什么是 React Router

React Router 是完整的 React 路由解决方案,拥有简单的 API 与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理。

3、如何安装

# npm
npm install react-router-dom@6
# yarn
yarn add react-router-dom@6

二、react-router 的使用

1、高阶 Router 连接 URL

使用 react-router 前,需要先在入口文件根据实际场景选择高阶 Router,连接 URL。常用的高阶 Router 有以下几种:

  • BrowserRouter:使用浏览器的 History API 来处理 URL,创建形如 example.com/some/path 的路由(推荐使用)
  • HashRouter:使用 URL 中的 hash(#)部分来处理 URL,创建形如 example.com/#/some/path 的路由,主要用于支持低版本的浏览器(不推荐使用)
  • NativeRouter:React Native 中 React Router 推荐的 history;
  • MemoryRouter:将历史记录保存在内存中,这个在测试和非浏览器环境中很有用。

使用方式一样,像这样连接 URL:

src/index.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { BrowserRouter } from 'react-router-dom'

ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)

上面也可以写成:

import { BrowserRouter as Router } from 'react-router-dom'
// ...
<Router>
{/*...*/}
</Router>

需要注意的是,HashRouter 是通过 hash 路径访问,在浏览器直接输入地址可以访问,经 <Link> 跳转后的页面可直接刷新。而 BrowserRouter 是通过 History Api 访问,输入地址时会向服务器查询,因为是单页面,没有历史所以查询不到,经 <Link> 跳转后的页面也无法直接刷新,需要在 webpack 中的 devserver 配置 historyApiFallback: true

webpack.config.js
const path = require('path')

module.exports = {
// ...
devServer: {
historyApiFallback: true,
},
// ...
}
import React from 'react'
import { Link } from 'react-router-dom'

const App = () => {
return <Link to="/about">Click To About Page</Link>
}

export default App

也可以通过 useNavigate 实现:

import React from 'react'
import { useNavigate } from 'react-router-dom'

const App = () => {
const navigate = useNavigate()
return <div onClick={() => navigate('/about')}>Click To About Page</div>
}

export default App

效果等效于 <a> 标签:

Link 相比,NavLink 可以判断链接是否在聚焦状态,从而设置相应的样式、类名等。使用如下:

import React from 'react'
import { NavLink } from 'react-router-dom'

const App = () => {
return (
<>
<NavLink
style={({ isActive }) => {
return {
color: isActive ? 'red' : ''
}
}}
to="/a"
>
Click To Page A
</NavLink>
<br />
<NavLink to="/b">
{({ isActive }) => (isActive ? 'Clicked' : 'Click') + 'To Page B'}
</NavLink>
</>
)
}

export default App

4、Routes + Route 配置跳转页

当页面路径等于 Routepath 属性值时,跳转到 Route 的 element 属性对应的组件,Route 由 Routes 包裹:

import React from 'react'
import { Routes, Route, Link } from 'react-router-dom'

const Home = () => {
return (
<>
<h1>Home Page</h1>
<Link to="/about">Click To About Page</Link>
</>
)
}

const About = () => {
return (
<>
<h1>About Page</h1>
<Link to="/">Click To Home Page</Link>
</>
)
}

// 上面组件也可分离到以下路径然后引用
// src/routes/Home.jsx
// src/routes/About.jsx

const App = () => {
return (
<>
<div>React Router Test</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</>
)
}

export default App

实现效果如下:

5、Outlet 嵌套路由局部跳转

嵌套路由可用于共享页面布局,实现局部跳转。嵌套路由需要在父级添加 Outlet 组件,作为子组件的占位符,它将根据不同的路由渲染子组件,类似于 vue-router 中的 router-view。

举个例子,下面的页面 a、b、c 将在 <Outlet /> 的位置共享布局:

import React from 'react'
import { Routes, Route, Link, Outlet } from 'react-router-dom'

const Home = () => {
return (
<>
<h1>Home Page</h1>

<Link to="/a">Click To Page A</Link>
<br />
<Link to="/b">Click To Page B</Link>
<br />
<Link to="/c">Click To Page C</Link>

<Outlet />
{/* Outlet 将根据不同的路由渲染 "/a"、"/b"、"/c" 对应的组件 */}
</>
)
}

const PageA = () => {
return <h1>Page A</h1>
}
const PageB = () => {
return <h1>Page B</h1>
}
const PageC = () => {
return <h1>Page C</h1>
}

const App = () => {
return (
<>
<div>React Router Test</div>
<Routes>
<Route path="/" element={<Home />}>
<Route path="a" element={<PageA />} />
<Route path="b" element={<PageB />} />
<Route path="c" element={<PageC />} />
</Route>
</Routes>
</>
)
}

export default App

效果如下:

6、* 配置错误路由的空白页

"*" 表示路由都不匹配的情况,可以用于配置错误路由的空白页。举个例子:

import React from 'react'
import { Routes, Route, Link, Outlet } from 'react-router-dom'

const Home = () => {
return (
<>
<h1>Home Page</h1>

<Link to="/a">Click To Page A</Link>
<br />
<Link to="/b">Click To Page B</Link>
<br />
<Link to="/c">Click To Page C</Link>
<br />
<Link to="/xxx">Click To Error Page</Link>
<br />
<Link to="/123">Click To Error Page</Link>

<Outlet />
{/* Outlet 将根据不同路由渲染 "/a" 或 "/b" 对应的组件 */}
</>
)
}

const PageA = () => {
return <h1>Page A</h1>
}
const PageB = () => {
return <h1>Page B</h1>
}
const PageC = () => {
return <h1>Page C</h1>
}

const ErrorPage = () => {
return <h1>页面不存在</h1>
}

const App = () => {
return (
<>
<div>React Router Test</div>
<Routes>
<Route path="/" element={<Home />}>
<Route path="a" element={<PageA />} />
<Route path="b" element={<PageB />} />
<Route path="c" element={<PageC />} />
<Route path="*" element={<ErrorPage />} />
</Route>
</Routes>
</>
)
}

export default App

效果如下:

7、动态路由 与 useParams 读参

动态路由指的是路由中的路径不固定,例如将 path 在 Route 匹配时写成 /detail/:id,那么 /detail/abc/detail/123 都可以匹配到该 Route。

获取参数方式如下:

useParams 可用于读取路由的 params 参数:

import React from 'react'
import { Routes, Route, Link, Outlet } from 'react-router-dom'
import { Routes, Route, Link, Outlet, useParams } from 'react-router-dom'

const Home = () => {
return (
<>
<h1>Home Page</h1>

<Link to="/a">Click To Page A</Link>
<Link to="/a/123">Click To Page A</Link>
<br />
<Link to="/b">Click To Page B</Link>
<br />
<Link to="/c">Click To Page C</Link>

<Outlet />
{/* Outlet 将根据不同路由渲染 "/a" 或 "/b" 对应的组件 */}
</>
)
}

const PageA = () => {
const params = useParams()
console.log(params, 'PageA')
return <h1>Page A</h1>
}
const PageB = () => {
return <h1>Page B</h1>
}
const PageC = () => {
return <h1>Page C</h1>
}

const App = () => {
return (
<>
<div>React Router Test</div>
<Routes>
<Route path="/" element={<Home />}>
<Route path="a" element={<PageA />} />
<Route path="a/:userId" element={<PageA />} />
<Route path="b" element={<PageB />} />
<Route path="c" element={<PageC />} />
</Route>
</Routes>
</>
)
}

export default App

跳转到页面 A 后控制台输出:

8、useSearchParams ? 搜索参数

搜索参数可直接通过给 to 属性传对象来添加,例如:

<Link
to={{
pathname: '/a',
search: '?name=Tim&skill=sleep'
}}
>
Click To Page A
</Link>

React Router 提供 useSearchParams hooks 来操作搜索参数。使用如下:

import React from 'react'
import { useSearchParams } from 'react-router-dom'

const App = () => {
const [searchParams, setSearchParams] = useSearchParams()

// 执行完之后 url 变为 xxx?name=Tim&skill=sleep
const changeSearchParams = () => {
setSearchParams({
name: 'Tim',
skill: 'sleep'
})
}

return (
<>
<div>
姓名 :{searchParams.get('name')}
技能 :{searchParams.get('skill')}
</div>
<button onClick={changeSearchParams}>设置 searchParams</button>
</>
)
}

export default App

useSearchParams 的原理是基于 URLSearchParams 接口,先监听 location.search 变化,当触发 setSearchParams 时根据入参创建新的 URLSearchParams 对象,再执行改变路由的方法,进而修改 location 对象。源码如下:

type ParamKeyValuePair = [string, string]

type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams
type navigateOptions = { replace?: boolean; state?: State }

function useSearchParams(defaultInit?: URLSearchParamsInit) {
// 根据传参创建默认的 URLSearchParams 类型的对象
let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit))
// 获取当前 location 对象
let location = useLocation()
// 以 location.search 为依赖值
// 创建一个类型为 URLSearchParams 的 memoized 值
let searchParams = React.useMemo(() => {
let searchParams = createSearchParams(location.search)
// 循环默认 URLSearchParams 类型的对象的 key 值数组
// 判断数组每一项是否出现在 searchParams 对象中
// 如果没有出现则将该 key 所有的键值对添加到 searchParams 中
for (let key of defaultSearchParamsRef.current.keys()) {
if (!searchParams.has(key)) {
defaultSearchParamsRef.current.getAll(key).forEach((value) => {
searchParams.append(key, value)
})
}
}

return searchParams
}, [location.search])
// 获取 navigate 方法
let navigate = useNavigate()

let setSearchParams = React.useCallback(
(
nextInit: URLSearchParamsInit,
navigateOptions?: { replace?: boolean; state?: State }
) => {
// 路由跳转修改 location.search
// location.search 的改变会重新触发对应 useMemo 中参数的函数,返回新的 searchParams 值
navigate('?' + createSearchParams(nextInit), navigateOptions)
},
[navigate]
)

return [searchParams, setSearchParams] as const
}

9、useLocation 获取 location

useLocation 会返回当前 URL 的 location 对象。使用如下:

import React from 'react'
import { Routes, Route, Link, Outlet, useLocation } from 'react-router-dom'

const Home = () => {
return (
<>
<h1>Home Page</h1>

<Link to="/a">Click To Page A</Link>
<br />
<Link to="/b">Click To Page B</Link>
<br />
<Link to="/c">Click To Page C</Link>

<Outlet />
{/* Outlet 将根据不同路由渲染 "/a" 或 "/b" 对应的组件 */}
</>
)
}

const PageA = () => {
const location = useLocation()
console.log(location)
return <h1>Page A</h1>
}
const PageB = () => {
const location = useLocation()
console.log(location)
return <h1>Page B</h1>
}
const PageC = () => {
const location = useLocation()
console.log(location)
return <h1>Page C</h1>
}

const App = () => {
return (
<>
<div>React Router Test</div>
<Routes>
<Route path="/" element={<Home />}>
<Route path="a" element={<PageA />} />
<Route path="b" element={<PageB />} />
<Route path="c" element={<PageC />} />
</Route>
</Routes>
</>
)
}

export default App

分别切换至页面 abc,打印结果如下:

点击查看更多 API

三、React Router 原理解析

React Router 的原理是通过 JS 监听 URL 的变化,对页面进行处理。其中:

默认的 hash 模式通过 onhashchange 事件监听 url 的 hash 值来实现路由跳转。

而 history 模式则利用 pushStatereplaceState 来修改浏览器的历史记录栈,并通过 onpopState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以还需要后台配置。

具体实现可参考「SPA 的原理及实现」