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:
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
:
const path = require('path')
module.exports = {
// ...
devServer: {
historyApiFallback: true,
},
// ...
}
2、<Link>
实现链接跳转
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>
标签:
3、<NavLink>
设置链接聚焦态
与 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 配置跳转页
当页面路径等于 Route 的 path
属性值时,跳转到 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
分别切换至页面 a、b、c,打印结果如下:
三、React Router 原理解析
React Router 的原理是通过 JS 监听 URL 的变化,对页面进行处理。其中:
默认的 hash 模式通过 onhashchange 事件监听 url 的 hash 值来实现路由跳转。
而 history 模式则利用 pushState
和 replaceState
来修改浏览器的历史记录栈,并通过 onpopState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以还需要后台配置。
具体实现可参考「SPA 的原理及实现」