继上一篇react+typescript+wbpack5项目搭建完成基本搭建后,路由补充
React Router Dom安装
npm install react-router-dom
官方推荐使用
BrowserRouter
1,基本使用:
(1)BrowserRouter
要想在 React 应用中使用 React Router,就需要在 React 项目的根文件(index.tsx)中导入 Router 组件:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import APP from './APP'
const container = document.getElementById('root') as HTMLElement
const root = ReactDOM.createRoot(container)
root.render(
<BrowserRouter>
<APP />
</BrowserRouter>
)
在这个文件中,我们导入了 BrowserRouter 组件,然后使用该组件包裹了 App 组件。现在,在这个 BrowserRouter 组件中,来自 react-router-dom 的其他组件和 hooks 就可以正常工作了。
BrowserRouter 是最常用的路由方式,即浏览器路由。官方文档也建议将 BrowserRouter 组件用于 Web 应用程序。除了这种方式,React Router 还支持其他几种路由方式:
HashRouter:在路径前加入#成为一个哈希值,Hash 模式的好处是不会因为刷新页面而找不到对应路径;
MemoryRouter:不存储 history,路由过程保存在内存中,适用于 React Native 这种非浏览器环境;
NativeRouter:配合 React Native 使用,多用于移动端;
StaticRouter:主要用于服务端渲染时。
(2)NavLink
在App.tsx 创建三个导航链接,这些链接会指向App.tsx 中的一些路由。这时就需要导入 NavLink 组件,它是一个导航链接组件,类似于 HTML 中的标签。NavLink 组件使用 to 来指定需要跳转的链接:
import React from "react"
import { NavLink } from "react-router-dom"
import styles from './APP.scss'
const APP = () => {
return (
<div>
<header>
<h1>APP</h1>
</header>
<NavLink to="">首页</NavLink>
<NavLink to="product">产品</NavLink>
<NavLink to="contact">联系我们</NavLink>
</div>
)
}
export default APP
当点击这些导航链接时,网页的 URL 就会改变,跳转到对应的路由。
NavLink 是存在 active 状态的,所以可以为active 状态和非active 状态的导航链接添加样式:
.nav-active {
color: red;
font-weight: bold;
}
(3)Link
在 react-router-dom 中,可以使用 Link 组件来创建常规链接。Link 组件与 NavLink 组件非常相似,唯一的区别就是 NavLink 存在 active 状态,而 Link 没有。
(4)Routes
下面来看看如何将路由映射为对应的页面(组件)。首先需要从 react-router-dom 中导入一个名为 Routes 的组件,它将包含可以在页面特定部分显示的所有不同的路由。
我们需要在 Routes 组件中使用 Route 组件来定义所有路由。该组件接受两个 props:
path:页面 URL 应导航到的路径,类似于 NavLink 组件的 to;
element:页面导航到该路由时加载的元素。
Route 组件用于将应用的位置映射到不同的 React 组件。例如,当用户导航到 /product 路径时呈现 Product 组件,可以这样来写:
import React from "react"
import { NavLink, Routes, Route } from "react-router-dom"
import styles from './APP.scss'
import Home from './component/Home/Home'
import Product from "./component/Product/Product"
import Contact from "./component/Contact/Contact"
const APP = () => {
return (
<div>
<header>
<h1>APP</h1>
</header>
<nav>
<NavLink to="">首页</NavLink>
<NavLink to="product">产品</NavLink>
<NavLink to="contact">联系我们</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product" element={<Product />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
)
}
export default APP
(5)路由顺序
在 React Router v6 以前,我们必须按照一定的顺序来定义路由,以获得准确的渲染。在 v6 及之后的版本中,路由定义的顺序无关紧要。
v6 中,将
2,编程式导航:
React Router 提供了两种不同的编程式导航方式:
声明式导航组件: 组件
命令式导航方法:useNavigate Hook
我们可以使用这两种编程的方式来跳转到指定的路由,也可以实现路由的重定向,比如在浏览器的地址栏输入一个 URL 并进行跳转时,如果应用中没有定义该路由,就跳转到应用的首页。
(1)Navigate
此组件是一种声明式的导航方式,组件中通过 to props 来指定要跳转的路径。
import React from "react"
import { NavLink, Routes, Route, Navigate } from "react-router-dom"
import styles from './APP.scss'
import Home from './component/Home/Home'
import Product from "./component/Product/Product"
import Contact from "./component/Contact/Contact"
const APP = () => {
return (
<div>
<header>
<h1>APP</h1>
</header>
<nav>
<NavLink to="">首页</NavLink>
<NavLink to="product">产品</NavLink>
<NavLink to="contact">联系我们</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product" element={<Product />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</div>
)
}
export default APP
(2)useNavigate
useNavigate Hook是一种命令式导航方式,使用方法如下
import React from "react"
import styles from './Home.scss'
import { useNavigate } from "react-router-dom"
const Home = () => {
const navgite = useNavigate()
return (
<div className={styles.title}>
主页面
<button onClick={() => { navgite('/contact') }}>跳转到联系页面</button>
</div>
)
}
export default Home
3,通过路由传递状态
在 react-router-dom 中可以通过以下三种方式来传递状态:
使用Link 组件
使用Navigate 组件
使用useNavigate 钩子
(1)Link
通过state传递
import React from "react"
import { Link } from "react-router-dom"
import styles from './Product.scss'
const Product = () => {
return (
<div className={styles.title}>
产品页面
<Link to='/' state={"产品页面的返回"}>返回主页面</Link>
</div>
)
}
export default Product
通过useLocation接收
import React from "react"
import styles from './Home.scss'
import { useNavigate, useLocation } from "react-router-dom"
const Home = () => {
const navgite = useNavigate()
const location = useLocation()
return (
<div className={styles.title}>
主页面
<button onClick={() => { navgite('/contact') }}>跳转到联系页面</button>
<p>{location.state}</p>
</div>
)
}
export default Home
(2)Navigate
使用方式和 Link 组件类似state传递useLocation接收,多用于重定向
(3)useNavigate
上面讲过使用方法,一样state传递useLocation接收
4,动态路由
<Route path="/product/:keyword" element={<Product />} />
keyword为不固定参数,比如是你的搜索内容,只需要声明一个带有 keyword 占位符的路由,就会加载一个同一个组件
import React from "react"
import { NavLink, Routes, Route, Navigate } from "react-router-dom"
import styles from './APP.scss'
import Home from './component/Home/Home'
import Product from "./component/Product/Product"
import Contact from "./component/Contact/Contact"
const APP = () => {
return (
<div>
<header>
<h1>APP</h1>
</header>
<nav>
<NavLink to="">首页</NavLink>
<NavLink to="product/123">产品</NavLink>
<NavLink to="contact">联系我们</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:keyword" element={<Product />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</div>
)
}
export default APP
React Router 就提供了一个 useParams Hook,它返回一个对象,该对象具有 URL 参数及其值之间的映射。使用方式如下:
import React from "react"
import { Link, useParams } from "react-router-dom"
import styles from './Product.scss'
const Product = () => {
const { keyword } = useParams()
return (
<div className={styles.title}>
产品页面
<Link to='/' state={"产品页面的返回"}>返回主页面</Link>
<p>{keyword}</p>
</div>
)
}
export default Product
5,嵌套路由
嵌套路由允许父路由充当包装器并控制子路由的渲染。比如,在应用中点击消息时,会跳转到 /messages 路由,并显示所有的通知列表。当点击某一条消息时,就会跳转到 /messages/:id 路由,这时就能看到指定 id 的消息详情,同时消息列表是显示在左侧的。这个场景就要依赖嵌套路由来实现。下面来看看如何使用 React Router 实现这种嵌套路由模式。
import React from "react"
import { NavLink, Routes, Route, Navigate } from "react-router-dom"
import styles from './APP.scss'
import Home from './component/Home/Home'
import Product from "./component/Product/Product"
import Contact from "./component/Contact/Contact"
import Messages from "./component/Messages/Messages"
import MessagesDetails from "./component/MessagesDetails/MessagesDetails"
const APP = () => {
return (
<div>
<header>
<h1>APP</h1>
</header>
<nav>
<NavLink to="">首页</NavLink>
<NavLink to="product/123">产品</NavLink>
<NavLink to="contact">联系我们</NavLink>
<NavLink to="messages/1">messages</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:keyword" element={<Product />} />
<Route path="/contact" element={<Contact />} />
<Route path="/messages" element={<Messages />}>
<Route path=":id" element={<MessagesDetails />} />
</Route>
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</div>
)
}
export default APP
然后告诉 React Router 应该在父路由(Messges)中的哪个位置渲染子路由(MessagesDetails)。这就就需要使用 React Router 的 Outlet 组件
import React from "react"
import { Outlet } from "react-router-dom"
// import MessagesDetails from "../MessagesDetails/MessagesDetails"
import Conversations from "../Conversations/Conversations"
import styles from './Messages.scss'
const Messages = () => {
return (
<div className={styles.title}>
<Conversations />
<Outlet />
</div>
)
}
export default Messages
6,查询参数
在 React Router 中,如何从 URL 中获取参数呢?例如以下 URL:
/search?q=react&src=typed_query&f=live
从 v6 开始,React Router 使用 URLSearchParams API 来处理查询字符串,URLSearchParams 内置于所有浏览器(IE 除外)中,并提供了处理查询字符串的实用方法。当创建 URLSearchParams 实例时,需要向它传递一个查询字符串:
const queryString = "?q=react&src=typed_query&f=live";
const sp = new URLSearchParams(queryString);
sp.has("q"); // true
sp.get("q"); // react
sp.getAll("src"); // ["typed_query"]
sp.get("nope"); // null
sp.append("sort", "ascending");
sp.toString(); // "?q=react&src=typed_query&f=live&sort=ascending"
sp.set("q", "bytes.dev");
sp.toString(); // "?q=bytes.dev&src=typed_query&f=live&sort=ascending"
sp.delete("sort");
sp.toString(); // "?q=bytes.dev&src=typed_query&f=live"
React Router 提供了一个自定义的 useSearchParams Hook,它是基于 URLSearchParams 进行的封装。useSearchParams 返回一个数组,该数组第一个元素是 URLSearchParams 的实例,第二个元素是更新查询参数的一个方法。
import { useSearchParams } from 'react-router-dom'
const Results = () => {
const [searchParams, setSearchParams] = useSearchParams();
const q = searchParams.get('q')
const src = searchParams.get('src')
const f = searchParams.get('f')
return (
// ...
)
}
如果需要更新查询字符串,可以使用 setSearchParams,向它传递一个对象,该对象的key/value 对将作为 &key=value 添加到 url:
const Results = () => {
const [searchParams, setSearchParams] = useSearchParams();
const q = searchParams.get('q')
const src = searchParams.get('src')
const f = searchParams.get('f')
const updateOrder = (sort) => {
setSearchParams({ sort })
}
return (
...
)
}
7,Route 配置
React Router v6 内置了一个 useRoutes Hook,它在功能上等同于 ,但它是使用 JavaScript 对象而不是 元素来定义路由。这个对象具有与普通 元素相同的属性,但它们不需要使用 JSX 来编写。
useRoutes 的返回值要么是一个有效的 React 元素(可以使用它来渲染路由树),如果没有匹配项,则返回 null。
import { useRoutes } from "react-router-dom";
const routes = useRoutes([
{ path: "/", element: <Home /> },
{
path: "/messages",
element: <Messages />,
children: [
{ path: ":id", element: <MessagesDetails /> },
],
},
]);
export default function App() {
return (
<div>
<Navbar />
{routes}
</div>
);
}
注意:使用BrowserRouter刷新当前页会出问题,需要配置historyApiFallback和publicPath,用来解决此问题
// webpack.dev.js
const { merge } = require('webpack-merge')
const path = require('path')
const webpack = require('webpack')
const common = require('./webpack.common')
const { PROJECT_PATH, SERVER_HOST, SERVER_PORT } = require('../constant')
module.exports = merge(common, {
target: 'web', // 防止配置browserslist 热更新失效
mode: 'development',
devtool: 'cheap-module-source-map',
output: {
publicPath: '/', // 本地开发
filename: 'js/[name].js',
path: path.resolve(PROJECT_PATH, './dist')
},
devServer: {
host: SERVER_HOST, // 服务ip
port: SERVER_PORT, // 服务端口
static: {
directory: path.join(__dirname, 'public'),
},
// contentBase: path.join(__dirname, 'build'),已经被废弃,请使用static
// stats: 'errors-only', // 设为errors-only表示终端只打印错误类型的日志,不会打印warning以及其他信息影响阅读
// clientLogLevel: 'none', // 设为none表示去除多余网页console信息
compress: true, // 设为true表示启用gzip压缩,加快网站打开速度
open: true, // 设为true表示第一次启动项目时自动打开默认浏览器
hot: true, // 设为true表示启用服务热替换配置
// noInfo: true, // 设为true表示去除启动项目时显示的打包信息
https: true, // 启动项目的时候,默认是https协议
historyApiFallback: true, // 解决BrowserRouter路由跳转之后刷新浏览器按钮报404的情况
},
plugins: [
// 实际上只开启 hot:true 就会自动识别有无声明该插件,没有则自动引入,但是怕有隐藏问题这里还是手动加上了
new webpack.HotModuleReplacementPlugin()
]
})
注意:contentBase已经废弃,请使用static
HashRouter
使用 URL 的 hash 部分(即 window.location.hash )的 使您的 UI 与 URL 保持同步。
注意:HashRouter不支持 location.key 或 location.state
HashRouter 是通过 hash 值来对路由进行控制。使用 HashRouter,路由会默认有个#
举例来说,用户访问http://www.example.com/,实际会看到的是http://www.example.com/#/
1,basename: string
这个代表所有位置的基本url,格式正确的基本名称基本都有一个前导斜杠"/",但没有尾部斜杠。
<HashRouter basename="/calendar"/>
2,getUserConfirmation: func
用于确认导航的功能。默认使用 window.confirm。
const getConfirmation = (message, callback) => {
const allowTransition = window.confirm(message)
callback(allowTransition)
}
<HashRouter getUserConfirmation={getConfirmation}/>
3,hashType: string
用于window.location.hash的编码类型,取值范围为
slash: #/
noslash: #
hashbang: #!(已经被Google弃用)
默认为slash。
4,children: node
一个用于渲染的 single child element
MemoryRouter
能在内存中保存你的 “URL” 的历史记录(并不会对地址栏进行读写)。很适合在测试环境和非浏览器环境中使用,例如 React Native
1,initialEntries: array
history 栈中的一个 location 数组。这些可能是具有 { pathname, search, hash, state } 或简单的 URL 字符串的完整地址对象
<MemoryRouter
initialEntries={[ '/one', '/two', { pathname: '/three' } ]}
initialIndex={1}
>
<App/>
</MemoryRouter>
2,initialIndex: number
在 initialEntries 数组中的初始化地址索引
3,getUserConfirmation: func
用于确认导航的函数
4,keyLength: number
location.key 的长度。默认为 6
5,children: node
渲染一个 单独子元素
Redirect
渲染 将使导航到一个新的地址。这个新的地址会覆盖 history 栈中的当前地址,类似服务器端(HTTP 3xx)的重定向
import { Route, Redirect } from 'react-router'
<Route exact path="/" render={() => (
loggedIn ? (
<Redirect to="/dashboard"/>
) : (
<PublicHomePage/>
)
)}/>
HashRouter和BrowserRouter的区别
1,原理上
HashRouter:在路径中包含了#,相当于HTML的锚点定位。(# 符号的英文叫hash,所以叫HashRouter)
BrowserRouter:使用的是HTML5的新特性History,没有HashRouter(锚点定位)那样通用,低版本浏览器可能不支持。
2,用法上
BrowserRouter进行组件跳转时可以传递任意参数实现组件间的通信
HashRouter不能(除非手动拼接URL字符串),因此一般配合Redux使用,实现组件间的数据通信。
3.生产实践上
HashRouter:
HashRouter相当于锚点定位,因此不论#后面的路径怎么变化,请求的都相当于是#之前的那个页面。可以很容易的进行前后端不分离的部署(也就是把前端打包后的文件放到服务器端的public或static里),因为请求的链接都是ip地址:端口/#/xxxx,因此请求的资源路径永远为/,相当于index.html,而其他的后端API接口都可以正常请求,不会和/冲突,由于前后端不分离也不会产生跨域问题。
缺点就是丑,路径里总有个#
BrowserRouter:
因为BrowserRouter模式下请求的链接都是ip地址:端口/xxxx/xxxx,因此相当于每个URL都会访问一个不同的后端地址,如果后端没有覆盖到路由就会产生404错误。
可以通过加入中间件解决,放在服务器端路由匹配的最后,如果前面的API接口都不匹配,则返回index.html页面。但这样也有一些小问题,因为要求前端路由和后端路由的URL不能重复。
比如商品列表组件叫/product/list,而请求商品列表的API也是/product/list,那么就会访问不到页面,而是被API接口匹配到。
解决方法:
进行前后端分离的部署,比如前端地址ip1:端口1,后端接口地址ip2:端口2,使用Nginx反向代理服务器进行请求分发。前端向后端发起请求的URL为nginx所在的服务器+/api/xxx,通过NGINX的配置文件判断,如果URL以api开头则转发至后端接口,否则转发至前端的地址,访问项目只需访问Nginx服务器即可。