React Router V6详解

2023-01-06 09:13:43 浏览数 (1)

一、简介

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>组件。

0 人点赞