15_react-router@v6

100 阅读7分钟

路由的概念

web的发展主要经历了三个阶段:后端路由阶段、前后端分离阶段、单页面富应用(SPA)

后端路由阶段

早期的网站开发整个HTML页面都是由服务器直接返回渲染好的页面给客户端进行展示。
这种情况下渲染好的页面,不再需要单独的加载任何的JS和CSS,有利于SEO优化。
缺点:整个页面可能由后端人员编写和维护,如果是前端人员开发页面还需要对Java等服务端语言有一定的了解。
在这开发模式下HTML代码、数据以及对应的逻辑是混在一起的,编写和维护都是非常糟糕的。

前后端分离阶段

AJAX的出现,有了前后端分离这种开发模式;
后端只提供返回数据的API,前端通过AJAX获取数据,通过JavaScripty将数据渲染到页面
优点:前后端责任划分更加清晰,后端专注于数据,前端专注于交互和可视化

单页面富应用

只有一个html文件,在前后端分离的基础上增加了前端路由,通过路由进行页面切换。
优点:避免页面整体刷新、用户体验变好

URL的hash

URL的 hash 也就是锚点(#),本质上是改变window.location.href。可以直接通过修改location.hash修改href,页面不会整体刷新
优点:页面不会整体刷新,兼容性好(老板IE也支持)
缺点:url中有一个 #,看起来不那么好看

<div>
	<a href="#/index">首页</a>
	<a href="#/my">我的</a>
</div>
<div id="router-view"></div>

<script>
	const routerViewEl = document.getElementById('router-view')
	window.addEventListener('hashchange', () => {
		console.log(location.hash)
		switch (location.hash) {
			case '#/index':
				routerViewEl.innerHTML = '这是首页'
				break
			case '#/my':
				routerViewEl.innerHTML = '我的'
				break
			default:
				routerViewEl.innerHTML = '这是首页'
		}
	})
</script>

HTML5的History API

History API的HTML5中新增的,它有六种方式改变URL且不会刷新页面,具体用法可查看MDN

  • replaceState:替换原来的路径
  • pushState:使用新的路径
  • popState:路径的后退
  • go:先前或向后改变路径
  • forward:向前改变路径
  • back:向后改变路径
<div>
	<a href="/index">首页</a>
	<a href="/my">我的</a>
</div>
<div id="router-view"></div>

<script>
	const routerViewEl = document.getElementById('router-view')
	const aEls = document.getElementsByTagName('a')
	
	for (let aEl of aEls) {
		aEl.addEventListener('click', (e) => {
			e.preventDefault()
			
			const href = aEl.getAttribute('href')
			history.pushState({}, '', href)
			historyChange()
		})
	}

	window.addEventListener('popstate', historyChange)

	function historyChange() {
		console.log(location.pathname)
		switch (location.pathname) {
			case '/index':
				routerViewEl.innerHTML = '这是首页'
				break
			case '/my':
				routerViewEl.innerHTML = '我的'
				break
			default:
				routerViewEl.innerHTML = '这是首页'
		}
	}
</script>

react-router介绍

react-router是React中的前端路由工具,通过react-router可以将URL地址和React组件进行映射,当URL地址发生变化时,与其对应的组件会自动的挂载。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过前端路由进行切换。
安装时需要在项目中安装react-router-dom,因为react-router包会包含一些react-native的内容,web开发并不需要。
官网:reactrouter.com/en/main
安装:npm install react-router-dom

基础使用

// 引入必要的内置组件
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'

// 路由组件
const Home = () => <div>this is home</div>
const My = () => <div>this is my</div>
const NotFound = () => <div>Not Found Page</div>

function App() {
	return (
		<div className="App">
			{/* 使用history模式 */}
			<BrowserRouter>
				<Link to="/">首页</Link>
				<Link to="/my">我的</Link>

				{/* Routes组件包裹所有的route,在其中匹配一个路由,
						在Router5.x中使用的是Switch组件 */}
				<Routes>
					{/* 
              Route组件用于路径的匹配 
		 					path:设置匹配到的路径
							element:设置匹配到路径后,需要渲染的组件,在Router5.x中使用的是component属性
          */}
					<Route path="/" element={<Home />}></Route>
					<Route path="/my" element={<My />}></Route>
					<Route path="*" element={<NotFound />}></Route>
				</Routes>
			</BrowserRouter>
		</div>
	)
}

export default App

内置组件

BrowerRouter

BrowerRouter由H5的 history API实现,并使用浏览器内置的历史堆栈进行导航。
baseName属性(可选):设置基础url,设置前 /home、设置后 /app/home

import ReactDOM from "react-dom/client"
import { BrowerRouter } from "react-router-dom"
import App from "./App"

const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(
	<BrowerRouter baseName="/app">
		<App/>
	</BrowerRouter>
)

注意事项:
当我们在生产环境使用history模式时,我们通过点击Link构建的链接进行跳转时,跳转并没有经过服务器,所以没有问题,
但是当我们刷新页面,或通过普通链接进行跳转时,会向服务器发送请加载数据,
这时的请求并没有经过react-router 所以会返回404
解决方案:
1.使用HashRouter,服务器不会去判断hash值,所以使用HashRouter后请求将会由React Router处理
2.修改nginx的配置,将所有请求都转发到index.html

location / {
	root   html;
	#index  index.html index.htm;
	try_files $uri /index.html;
}

HashRouter

通过监听URL的hash值实现,官方建议强烈建议不要使用HashRouter,除非万不得已。
baseName属性(可选):设置基础url,设置前 #/home、设置后 #/app/home

import ReactDOM from "react-dom/client"
import { HashRouter } from "react-router-dom"
import App from "./App"

const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(
	<HashRouter baseName="/app">
		<App/>
	</HashRouter>
)

Link

用于指定导航链接,完成声明式的路由跳转,类似于。最终会被渲染为a元素。
to:设置跳转到的路径,最重要的属性;
replace:可选,使用hisrory.replaceState跳转,替换当前路径(默认使用的是history.pushState
state:可选,为新的路径设置设置state。该值随后可以通过useLocation()访问const {state} = useLocation()
reloadDocument:可选,跳过客户端路由,从服务器重新加载当前路径对应的组件(类似于)。

import * as React from "react";
import { Link } from "react-router-dom";

function NavBar() {
	return (
		<div>
			<Link to="/">首页</Link>
			<Link to="/new">新品</Link>
			<Link to="/cart">购物车</Link>
			<Link to="/my">我的</Link>
		</div>
	);
}

export default NavBar

NavLink

和Link组件一样都是用于指定导航链接,NavLink是在Link基础上增加了一些样式属性。
默认匹配的NavLink组件会有一个class="active",可以使用该class设置样式。

<NavLink 
	to="/home" 
	className={({ isActive, isPending }) =>
    isPending ? "link-pending" : isActive ? "link-active" : ""
  }
>
首页
</NavLink>

<NavLink to="/home" style={({ isActive, isPending }) => ({ color: isActive ? "red" : "" })}>首页</NavLink>

{/* 通过children属性自定义渲染的内容 */}
<NavLink to="/detail">
  {({ isActive, isPending }) => (
    <span className={isActive ? "active" : ""}>详情</span>
  )}
</NavLink>

Routes和Route

Routes组件包裹一组route组件,url发生变化时会查找所有的子路由,在其中找到最佳匹配并渲染对应的组件。
Route组件用于定义路由路径和渲染组件的对应关系, (element:因为react体系内 把组件叫做react element)

  • path:要匹配的路径
  • element:路径匹配后挂载的组件,直接传JSX
  • index:布尔值,路由是否作为默认组件显示
<Routes>
	<Route path="/" element={<Home />}></Route>
	<Route path="/my" element={<My />}></Route>
	<Route path="*" element={<NotFound />}></Route>
</Routes>

Oulet

Outlet组件用于在父路由元素中作为子路由的占位元素。

function Layout() {
    render() {
        return (
            <div>
                <div className='nav'>
                    <Link to="/">首页</Link>
                    <Link to="/community">社区</Link>
                    <Link to="/my">我的</Link>
                </div>

                {/* 占位的组件 */}
                <Outlet/>
            </div>
        )
    }
}

function App() {
	return (
    <div className="app">
        <Routes>
           <Route path='/' element={<Layout/>}>
              <Route index element={<Home />} />
                <Route path='/community' element={<Community/>}/>
                <Route path='/my' element={<my/>}/>
            </Route>
        </Routes>
    </div>
	)
}

export default App

Navigate

Navigate用于路由的重定向,它是一个围绕useNavigate的组件包装器,并接受所有相同的参数作为props。

function App() {
	return (
    <div className="app">
        <Routes>
            <Routes>
                <Route path='/' element={<Layout/>}>
                    <Route path='/' element={<Navigate to="/home"/>}/>
                    <Route path='/home' element={<Home/>}/>
                    <Route path='/community' element={<Community/>}/>
                    <Route path='/my' element={<my/>}/>
                </Route>
            </Routes>
        </Routes>
    </div>
	)
}

Hooks

useNavigate

返回一个函数,使用该函数进行编程式导航。

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

const Home = () => {
  const navigate = useNavigate()
  return (
    <div>
      <h1>Home</h1>>
      <button onClick={ ()=> navigate('/about') }> 跳转到About页面 </button>
    </div>
  )
}

export default Home

注: 如果在跳转时不想添加历史记录,可以添加额外参数replace 为true 替换原本的历史记录

navigate('/about', { replace: true } )

useLocation

获取当前的地址的location对象,包含以下属性

  • hash:hash字符串
  • pathname:请求的路径
  • search:查询字符串
  • state:历史记录中的状态对象,可以在跳转时传递数据
import * as React from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  React.useEffect(() => {
    // 在当前位置发生更改时执行某些副作用,则这将非常有用。
  }, [location]);

  return (
    <div>App</div>
  )
}

useParams

动态路由<Route path="/about/:id" element={<About/>}/>,获取动态路由参数:

const params = useParams();
const id = params.id;

useSearchParams

返回一个由两个值组成的数组:当前url的查询参数和一个可用于更新它们的函数。
传参:navigate('/about?id=1234')

const [searchParams, setSearchParams] = useSearchParams()
const id = searchParams.get('id') // 1234
const query = Object.fromEntries(searchParams)
console.log(query) //{id: '1234'}

setSearchParams({name: 'jay'})

useMatch

检查当前url是否匹配某个路由,如果路径匹配,则返回一个对象,不匹配则返回null

const match = useMatch("/student/:id");

NotFound路由配置

当用户输入的url路径在整个路由配置中都找不到对应的path,使用404兜底组件进行渲染。
在最后添加一个Route组件, 将path设置为 *,element使用NotFound组件即可。

<BrowserRouter>
	<Routes>
		<Route path='/' element={<Home/>}>
			<Route path='/' element={<Navigate to="/recommend"/>}/>
			{/* <Route index element={<HomeRecommend />} /> */}
			<Route path='/recommend' element={<Recommend/>}/>
			<Route path='/ranking' element={<Ranking/>}/>
			<Route path='/songmenu' element={<HomeSongMenu/>}/>
		</Route>
		<Route path='*' element={<NotFound/>}/>
	</Routes>
</BrowserRouter>

集中式路由配置

集中式路由配置就是用一个数组统一把所有的路由对应关系写好替换本来的Routes组件。
在早期时react-router并没有提供相关的API,需要使用react-router-config库来完成。
Router6.x中提供了useRoutes方法来实现集中式路由配置。

import { BrowserRouter, Routes, Route, useRoutes } from 'react-router-dom'
// 异步加载路由组件
const Recommend = React.lazy(() => import("../pages/Recommend"))
const Ranking = React.lazy(() => import("../pages/ranking"))
const LoSongMenugin = React.lazy(() => import("../pages/SongMenu"))
const NotFound = React.lazy(() => import("../pages/NotFound"))

// 路由数组,数组中定义所有的路由对应关系
const routesList = [
	{
		path: "/",
		element: <Navigate to="/home"/>
	},
	{
		path: "/home",
		element: <Home/>,
		children: [
			{
				index: true,
				element: <Navigate to="/home/recommend"/>
			},
			{
				path: "/home/recommend",
				element: <Recommend/>
			},
			{
				path: "/home/ranking",
				element: <Ranking/>
			},
			{
				path: "/home/songmenu",
				element: <SongMenu/>
			}
		]
	},
	{
		path: '*',
		element: <NotFound />,
	},
]

function App() {
	return (
		<div className="App">
			{/* 如果对某些组件使用的异步加载(懒加载),那么需要使用Suspense进行包裹 */}
			<Suspense fallback={<h3>Loading...</h3>}>
				{useRoutes(routes)}
			</Suspense>
		</div>  
	)
}

export default App