一、简介
1.1 SAP
SAP全称是【single-page application】,中文译为单页面应用。它是网站应用的一种模型,可以动态重写当前的页面来与用户交互,而不需要重新加载整个页面。相对于传统的 Web 应用程序,单页应用做到了前后端分离,即后端只负责处理数据提供接口,而页面逻辑和页面渲染都交由前端处理。前端发展到现在,单页应用的使用已经很广泛,目前时兴的 React、Vue、Angular 等前端框架均采用了 SPA 原则。
相比于传统的Web应用,SPA一个最重要的特性就是改变路由时不会触发整个页面的刷新,只会刷新需要刷新的模块或组件。要实现这种效果,通常有两种方式,分别似乎window.history和 location.hash。其中,window.history包含了浏览器的历史信息,主要的方法有history.back()、history.forward()和history.go(n)等。hash是location 对象的属性,它指的是当前链接的锚,也就是从【#】号开始的部分。
不过,虽然SPA有它的优点,也得到了主流框架的支持,但它也存在一定的局限性。比如,对 SEO不太优好;易出错,需要使用程序管理前进、后退、地址栏等操作。基于此,在一些中大型项目中,我们更推荐使用路由的概念来管理应用的页面。
1.2 路由
在前端应用中,路由可以理解为是一种映射关系,即路径与组件/函数的对应关系,比如,当用户访问’/dashboard’时,页面将呈现各种仪表板组件,如图表和表格;当用户访问’/user’时,页面将列出各种用户属性。
在基于React的前端架构中,React是不附带路由库的,所以要管理多个路由页面就需要使用到第三方库,比如React Router。事实上,react-router并不是一个库,塔包含3个库:react-router、react-router-dom和react-router-native,分别用来适配浏览器环境和手机原生环境。并且,react-router-dom和 react-router-native都需要依赖react-router,所以在安装时会自动安装react-router。
目前,React Router已经发布了V6版本,用法和组件相比之前的版本也有一些变化,总结如下:
- 重命名为;
- 的新特性变更,如component/render被element替代、routeProps可以在element中直接获取等;
- 标签支持嵌套,可以在一个文件内配置嵌套路由;
- 新钩子useRoutes代替react-router-config;
- useNavigate代替useHistory;
- Link不再支持component属性;
- NavLink 的exact属性替换为end;
- 添加Outlet组件,用于渲染子路由;
使用之前,可以先使用下面的命令进行安装。
代码语言:javascript复制npm:npm install react-router-dom@6
//或者
yarn:yarn add react-router-dom@6
1.3 路由模式
在单页面应用中,为了实现切换页面不刷新浏览器的功能在React Router提供了两种,有两种路由模式,分别是hash路由模式和history路由模式。
HashRouter
HashRouter基于Hash模式,页面跳转基于location.hash和location.replace实现;基于Hash模式的路由,在域名后通常以【#】号开头,再拼接路径,格式为:http://www.abc.com/#/xx。
History
History基于history模式,页面跳转使用的是HTML5为浏览器全局的history对象来实现的,即 history.pushState和history.replaceState。History相比HashRouter更加优雅,比如:http://www.abc.com/xx。
二、基本使用
2.1 基础API
2.1.1 配置路由
使用BrowserRouter路由模式时,需要先在应用的入口文件中进行路由的申明和配置,如下所示。
代码语言:javascript复制import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const root = ReactDOM.createRoot( document.getElementById("root"));
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}> //默认页面
<Route element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
);
完成路由的定义之后,接下来,只需要在使用的地方使用history.push()方法即可打开新页面。
代码语言:javascript复制history.push("teams")
2.1.2 Link
除了声明路由饿的方式外,我们还可以使用Link组件来打开一个新页面,Link组件最终会被渲染成a元素,最常见的场景就是打开一个网页页面。打开一个新页面时,需要添加to属性。
代码语言:javascript复制import { Link } from "react-router-dom";
function Home() {
return (
<div>
<h1>Home</h1>
<nav>
<Link to="/">Home</Link> |{" "}
<Link to="about">About</Link>
</nav>
</div>
);
}
2.1.3 Navigation
为了React Hook,react-router-dom还提供了useNavigate,也能够实现路由操作。
代码语言:javascript复制import { useNavigate } from "react-router-dom";
function Invoices() {
let navigate = useNavigate();
return (
<div>
<NewInvoiceForm onSubmit={() => navigate(`/invoices/${newInvoice.id}`)} />
</div>
);
}
2.1.4 获取路由参数
在两个页面进行跳转的过程中,必然会涉及参数值传递的问题,那怎么拿到上一个页面的传递的参数值呢?此时需要用到useParams()。
代码语言:javascript复制import { Routes, Route, useParams } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="invoices/:invoiceId" element={<Invoice />}/>
</Routes>
);
}
function Invoice() {
let params = useParams(); // 第一种
let { invoiceId } = useParams(); // 第二种
return <h1>Invoice {params.invoiceId}</h1>;
}
2.1.5 嵌套路由
如果项目中涉及到嵌套路由,路由路径匹配url路径定义如下。
代码语言:javascript复制function App() {
return (
<Routes>
<Route path="invoices" element={<Invoices />}> // /invoices
<Route path=":invoiceId" element={<Invoice />} /> // /invoices/:invoiceId
<Route path="sent" element={<SentInvoices />} /> // /invoices/sent
</Route>
</Routes>
);
}
而父router中子router可以用组件表示,然后Link修改url。
代码语言:javascript复制function Invoices() {
return (
<div>
<nav>
<Link to="invoices">Invoices</Link>
<Link to="dashboard">Dashboard</Link>
</nav>
<Outlet /> // 匹配对应的<Invoice /> 或者 <SentInvoices />
</div>
);
}
2.1.6 兜底路由
在React应用中,为了防止路由匹配失败的情况,我们还需要配置一个默认路由。
代码语言:javascript复制function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
2.1.7 多路由集成到一个组件
在很多时候,我们还会看到多路由集成到一个组件。
代码语言:javascript复制function App() {
return (
<div>
<Sidebar>
<Routes>
<Route path="/" element={<MainNav />} />
<Route path="dashboard" element={<DashboardNav />} />
</Routes>
</Sidebar>
<MainContent>
<Routes>
<Route path="/" element={<Home />}>
<Route path="about" element={<About />} />
<Route path="support" element={<Support />} />
</Route>
<Route path="dashboard" element={<Dashboard />}>
<Route path="invoices" element={<Invoices />} />
<Route path="team" element={<Team />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</MainContent>
</div>
);
}
2.2 API
除了上面的一些基本的使用方法外,React Router还提供了非常丰富的API,下面列举一些常见的:
2.2.1 Routers
- BrowserRouter:浏览器router,web开发首选;
- HashRouter:在不能使用browserRouter时使用,常见SPA的B端项目
- HistoryRouter:使用history库作为入参,允许开发者在非 React context中使用history实例作为全局变量,标记为unstable_HistoryRouter,后续可能会被修改,不建议直接引用;
- MemoryRouter:不依赖于外界(如 browserRouter的 history 堆栈),常用于测试用例;
- NativeRouter:RN环境下使用的router,不作过多介绍;
- Router:可以视为所有其他router的基类;
- StaticRouter:Node环境下使用的router;
2.2.2 Components
- Link:在react-router-dom中,Link被渲染为有真实href的
<a/>
标签,同时,Link to 支持相对路径路由; - NavLink:有“active”标的Link,尝被用于导航栏等场景;
- Navigate:可以理解为被useNavigate包裹的组件,作用通Link类似;
- Outlet:类似slot,向下传递route;
- Routes & Route:URL变化时,Routes匹配出最符合要求的Routes渲染;
2.2.3 Hooks
- useHref:用于返回Link to 指定的URL;
- useInRouterContext :返回是否在的context中;
- useLinkClickHandler:在使用自定义后返回点击事件;
- useLinkPressHandler:类似useLinkClickHandler,用于RN;
- useLocation:返回当前的location对象;
- useMatch:返回当前path匹配到的route;
- useNavigate:类似于Navigate,显示声明使用;
- useNavigationType:pop、push、replace;
- useOutlet;获取此route层级的子router元素;
- useOutletContext:用于向子route传递context;
- useParams:匹配当前路由path;
- useResolvedPath:返回当前路径的完整路径名,主要用于相对子route中;
- useRoutes:等同于,但要接收object形式;
- useSearchParams:用于查询和修改location 中query字段;
- useSearchParams(RN):RN中使用;
2.2.4 Utilities
- createRoutesFromChildren :将转为route object形式;
- createSearchParams:类似useSearchParams;
- generatePath:将通配符和动态路由和参数转为真实path;
- Location:用于hostory router,声明Location的interface;
- matchPath:类似useMatch,返回匹配到的route path;
- matchRoutes:返回匹配到的route 对象;
- renderMatches:返回matchRoutes的react元素;
- resolvePath:将Link to的值转为带有绝对路径的真实的path对象;
参考链接:https://reactrouter.com/en/6.6.1/docs/en/v6/routers/browser-router
三、 适配V6
3.1.1 去掉withRouter
withRouter的用处是将一个组件包裹进Route里面, 然后react-router的三个对象history,、location、match就会被放进这个组件的props属性中,可以实现对应的功能。下面是V5版本withRouter的使用方法。
代码语言:javascript复制import React from 'react'
import './nav.css'
import { NavLink, withRouter } from "react-router-dom"
class Nav extends React.Component{
handleClick = () => {
console.log(this.props);
}
render() {
return (
<div className={'nav'}>
<span className={'logo'} onClick={this.handleClick}>xx电商</span>
<li><NavLink to="/" exact>首页</NavLink></li>
<li><NavLink to="/activities">动态</NavLink></li>
<li><NavLink to="/topic">话题</NavLink></li>
<li><NavLink to="/login">登录</NavLink></li>
</div>
);
}
}
export default withRouter(Nav)
React Router的V6中,更多使用的是Hooks语法,所以只需要可以将类组件转为函数组件即可。
代码语言:javascript复制import { useLocation, useNavigate, useParams } from "react-router-dom";
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return (
<Component {...props} router={{ location, navigate, params }} />
);
}
return ComponentWithRouterProp;
}
3.1.2 树形结构里嵌套路由
由于和在V6版本中被移除,所以在V6版本的树形结构里嵌套路由需要做如下的修改。
V5版本写法:
代码语言:javascript复制<Switch>
<Route path="/users" component={Users} />
</Switch>;
function Users() {
return (
<div>
<h1>Users</h1>
<Switch>
<Route path="/users/account" component={Account} />
</Switch>
</div>
);
}
V6版本写法:
代码语言:javascript复制<Routes>
<Route path="/users/*" element={<Users />} />
</Routes>;
function Users() {
return (
<div>
<h1>Users</h1>
<Routes>
<Route path="account" element={<Account />} />
</Routes>
</div>
);
}
3.1.3 取消正则路由
V6版本有一个重要的细节是:取消正则路由。之所以取消正则路由,是因为如下几点原因:
- 正则路由为V6版本的路由排序带来很多问题,比如,如果定义一个正则的优先级;
- 正则路由占据了React Router近1/3的体积;
- 正则路由能表达的,V6版本都支持;
例如,我们在V5版本中,在进行Route路径适配的时候可以直接使用正则,如下:
代码语言:javascript复制function App() {
return (
<Switch>
<Route path={/(en|es|fr)/} component={Lang} />
</Switch>
);
}
function Lang({ params }) {
let lang = params[0];
let translations = I81n[lang];
// ...
}
由于V6版本取消了正则路由,所以上面的代码需要改成如下方式:
代码语言:javascript复制function App() {
return (
<Routes>
<Route path="en" element={<Lang lang="en" />} />
<Route path="es" element={<Lang lang="es" />} />
<Route path="fr" element={<Lang lang="fr" />} />
</Routes>
);
}
function Lang({ lang }) {
let translations = I81n[lang];
// ...
}
四、React Router原理
与后端路由不同,前端网站都是单页面应用,要实现路由切换时不触发整个页面的刷新,就需要前端路由框架满足两个关键点。
- 改变路径url时不触发页面刷新
- 当url发生改变时会重新渲染url对应的界面
所以,我们谈React Router的原理,其实就是分析订阅和操作history堆栈、URL 与router匹配以及渲染router相匹配的UI的问题。
4.1 基本概念
在正式讲解之前,我们先看一下路由中的一些概念:
- URL:地址栏中的URL;
- Location:由React Router基于浏览器内置的window.location对象封装而成的特定对象;
- Location State:代表Location的状态;
- History Stack:浏览器保留的location堆栈数据,可以使用它进行返回操作;
- History:一个object,它允许 React Router 订阅 URL 中的更改,并提供 API 以编程方式操作浏览器历史堆栈;
- History Action :路由操作,包括POP、PUSH或者 REPLACE。
- Segment :【/】字符之间的URL或 path pattern部分。例如“/users/123”有两个segment;
- Path Pattern:用于URL与路由匹配的特殊字符。
- Dynamic Segment:动态路径匹配;
- URL Params: 动态段匹配的URL的解析值;
- Router :使所有其他组件和hooks工作的有状态的最高层的组件;
- Route Config:将当前路径进行匹配,通过排序和匹配创建一个树状的routes对象;
- Route:具有 { path, element } 或 的路由元素;
- Route Element: 也就是 , 读取该元素的 props 以创建路由;
- Nested Routes: 由于路由可以有子路由,且每个路由通过segment来定义URL 的一部分,所以单个 URL 可以匹配树的嵌套“分支”中的多个路由。并且还可以通过outlet、relative links等实现自动布局嵌套;
- Relative links:不以 / 开头的链接,继承渲染它们的最近路径。在无需知道和构建整个路径的情况下,就可以实现更深层的url macth;
- Match:路由匹配 URL 时保存信息的对象;
- Matches:与当前位置匹配的路由数组,此结构用于nested routes;
- Parent Route:带有子路由的父路由节点;
- Outlet: 匹配match中的下一个匹配项的组件;
- Index Route :当没有path时,在父路由的outlet中匹配;
- Layout Route: 专门用于在特定布局内对子路由进行分组;
4.2 history
React Router工作的前提是,它必须能够订阅浏览器history stack中的数据,并进行push、pop和replace操作。通过客户端路由(CSR),我们可以通过代码操纵浏览器历史记录栈。例如,我们可以编写代码来改变URL,而不需要浏览器向服务器发出请求的默认行为。
代码语言:javascript复制<a
href="/contact"
onClick={(event) => {
// 阻止默认事件
event.preventDefault();
// push 并将 URL转想/contact
window.history.pushState({}, undefined, "/contact");
}}/>
以上代码会修改URL,但不会渲染任何UI的变化,如果我们需要修改页面UI,那么需要我们监听变化。
代码语言:javascript复制window.addEventListener("popstate", () => {
});
但此类事件只在点击前进后退按钮才生效,对window.history.pushState 或者 window.history.replaceState无效。因此,React Router使用history对象来监听事件的变化,如POP、PUSH或者REPLACE。
代码语言:javascript复制let history = createBrowserHistory();
history.listen(({ location, action }) => {
});
在开发环境中,我们不需要关系history object,这些在React Router底层实现了,React Router提供监听history stack的变化,最终在URL变化时更新其状态,并重新渲染。
4.3 location
React Router 的location模块申明如下:
代码语言:javascript复制{
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram",
hash: "#menu",
state: null,
key: "aefz24ie"
}
pathname、search、hash大致等同于window.location一致,三者拼接起来就是URL。我们可以使用urlSearchParams来获取对应的search内容。
代码语言:javascript复制let location = {
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram&popular=true",
hash: "",
state: null,
key: "aefz24ie",
};
let params = new URLSearchParams(location.search);
params.get("campaign"); // "instagram"
params.get("popular"); // "true"
params.toString(); // "campaign=instagram&popular=true",
4.4 路由匹配
在初始渲染时,当历史堆栈发生变化时,React Router 会将位置与您的路由配置进行匹配,以提供一组要渲染的匹配项。比如,有下面一段
代码语言:javascript复制<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path=":teamId/edit" element={<EditTeam />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
<Route element={<PageLayout />}>
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<Tos />} />
</Route>
<Route path="contact-us" element={<Contact />} />
</Routes>
那么它对应的routes如下,可以使用 useRoutes(routesGoHere)进行获取。
代码语言:javascript复制let routes = [
{
element: <App />,
path: "/",
children: [
{
index: true,
element: <Home />,
},
{
path: "teams",
element: <Teams />,
children: [
{
index: true,
element: <LeagueStandings />,
},
{
path: ":teamId",
element: <Team />,
},
{
path: ":teamId/edit",
element: <EditTeam />,
},
{
path: "new",
element: <NewTeamForm />,
},
],
},
],
},
{
element: <PageLayout />,
children: [
{
element: <Privacy />,
path: "/privacy",
},
{
element: <Tos />,
path: "/tos",
},
],
},
{
element: <Contact />,
path: "/contact-us",
},
];
所以,我们在应用中申明的路由,可以匹配程如下的内容:
代码语言:javascript复制<Route path=":teamId" element={<Team/>}/>
//匹配为
{
pathname: "/teams/firebirds",
params: {
teamId: "firebirds"
},
route: {
element: <Team />,
path: ":teamId"
}
}
由于routes是树状结构,因此一个单一的URL可以匹配所有的树中的“分支”。
4.5 渲染
会将位置与路由配置相匹配,得到一组匹配的内容,然后呈现一个React元素树。比如,有下面一段路由申明:
代码语言:javascript复制const root = ReactDOM.createRoot( document.getElementById("root"));
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
</Route>
</Route>
<Route element={<PageLayout />}>
<Route path="/privacy" element={<Privacy />} />
</Route>
<Route path="contact-us" element={<Contact />} />
</Routes>
</BrowserRouter>
);
如果要匹配“/teams/firebirds”路径,渲染的层级如下:
代码语言:javascript复制<App>
<Teams>
<Team />
</Teams>
</App>
4.6 导航函数
在V6版本中,我们可以使用useNavigate钩子函数来导航到某个页面。
代码语言:javascript复制let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate("/logout");
}, 30000);
}, []);
不过,需要提醒的是,不要随意使用navigate,这样会增加程序的复杂性,推荐使用<Link>
组件。