React Router V6详解

Posted xiangzhihong8

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Router V6详解相关的知识,希望对你有一定的参考价值。

一、简介

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组件,用于渲染子路由;

使用之前,可以先使用下面的命令进行安装。

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路由模式时,需要先在应用的入口文件中进行路由的申明和配置,如下所示。

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()方法即可打开新页面。

history.push("teams")

2.1.2 Link

除了声明路由饿的方式外,我们还可以使用Link组件来打开一个新页面,Link组件最终会被渲染成a元素,最常见的场景就是打开一个网页页面。打开一个新页面时,需要添加to属性。

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,也能够实现路由操作。

import  useNavigate  from "react-router-dom";
function Invoices() 
  let navigate = useNavigate();
  return (
    <div>
      <NewInvoiceForm onSubmit=() => navigate(`/invoices/$newInvoice.id`) />
    </div>
  );

2.1.4 获取路由参数

在两个页面进行跳转的过程中,必然会涉及参数值传递的问题,那怎么拿到上一个页面的传递的参数值呢?此时需要用到useParams()。

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路径定义如下。

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。

function Invoices() 
  return (
    <div>
      <nav>
        <Link to="invoices">Invoices</Link>
        <Link to="dashboard">Dashboard</Link>
      </nav>
      <Outlet /> // 匹配对应的<Invoice /> 或者 <SentInvoices />
    </div>
  );

2.1.6 兜底路由

在React应用中,为了防止路由匹配失败的情况,我们还需要配置一个默认路由。

function App() 
  return (
    <Routes>
      <Route path="/" element=<Home /> />
      <Route path="dashboard" element=<Dashboard /> />
      <Route path="*" element=<NotFound /> />
    </Routes>
  );

2.1.7 多路由集成到一个组件

在很多时候,我们还会看到多路由集成到一个组件。

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的使用方法。

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语法,所以只需要可以将类组件转为函数组件即可。

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版本写法:

<Switch>
  <Route path="/users" component=Users />
</Switch>;


function Users() 
  return (
    <div>
      <h1>Users</h1>
      <Switch>
        <Route path="/users/account" component=Account />
      </Switch>
    </div>
  );

V6版本写法:

<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路径适配的时候可以直接使用正则,如下:

function App() 
  return (
    <Switch>
      <Route path=/(en|es|fr)/ component=Lang />
    </Switch>
  );

function Lang( params ) 
  let lang = params[0];
  let translations = I81n[lang];
  // ...

由于V6版本取消了正则路由,所以上面的代码需要改成如下方式:

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,而不需要浏览器向服务器发出请求的默认行为。

<a
  href="/contact"
  onClick=(event) => 
    // 阻止默认事件
    event.preventDefault();
    // push 并将 URL转想/contact
    window.history.pushState(, undefined, "/contact");
  />

以上代码会修改URL,但不会渲染任何UI的变化,如果我们需要修改页面UI,那么需要我们监听变化。

window.addEventListener("popstate", () =>  
 
);

但此类事件只在点击前进后退按钮才生效,对window.history.pushState 或者 window.history.replaceState无效。因此,React Router使用history对象来监听事件的变化,如POP、PUSH或者REPLACE。

let history = createBrowserHistory();
history.listen(( location, action ) => 
   
);

在开发环境中,我们不需要关系history object,这些在React Router底层实现了,React Router提供监听history stack的变化,最终在URL变化时更新其状态,并重新渲染。

4.3 location

React Router 的location模块申明如下:


  pathname: "/bbq/pig-pickins",
  search: "?campaign=instagram",
  hash: "#menu",
  state: null,
  key: "aefz24ie"

pathname、search、hash大致等同于window.location一致,三者拼接起来就是URL。我们可以使用urlSearchParams来获取对应的search内容。

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 会将位置与您的路由配置进行匹配,以提供一组要渲染的匹配项。比如,有下面一段

<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)进行获取。

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",
  ,
];

所以,我们在应用中申明的路由,可以匹配程如下的内容:

<Route path=":teamId" element=<Team/>/>
//匹配为  

  pathname: "/teams/firebirds",  
  params: 
    teamId: "firebirds"  
  ,
  route: 
    element: <Team />,
    path: ":teamId"
  

由于routes是树状结构,因此一个单一的URL可以匹配所有的树中的“分支”。

4.5 渲染

会将位置与路由配置相匹配,得到一组匹配的内容,然后呈现一个React元素树。比如,有下面一段路由申明:

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”路径,渲染的层级如下:

<App>
  <Teams>
    <Team />
  </Teams>
</App>

4.6 导航函数

在V6版本中,我们可以使用useNavigate钩子函数来导航到某个页面。

let navigate = useNavigate();
useEffect(() => 
  setTimeout(() => 
    navigate("/logout");
  , 30000);
, []);

不过,需要提醒的是,不要随意使用navigate,这样会增加程序的复杂性,推荐使用<Link>组件。

以上是关于React Router V6详解的主要内容,如果未能解决你的问题,请参考以下文章

React Router V6详解

React Router(react-router-dom V6 整理)

React Router v6路由组件传参params/search/state(router v6)

react-router v6对比react-router v5

react之react-router v6

react-router-dom v5和react-router-dom v6区别