likes
comments
collection
share

一小时快速回顾 React Router

作者站长头像
站长
· 阅读数 19

React 回顾

一小时快速回顾 React Router

React 官网是这样描述 React 的:

The library for web and native user interfaces

用于构建 Web 和 原生 交互界面的库

意思就是 React 是用来构建和操作视图的。

其具有以下三个最主要的特点:

  1. 声明式:声明式编程区别于命令式编程,对于命令式编程来讲,我们要操作视图上的一系列 DOM 元素,需要我们进行包括创建容器、获取 DOM、更新数据等一系列繁琐的事情来告诉机器要怎么做。而声明式编程,我们只需要通过 setState 来修改状态,React 就能完成对应视图的更新,只需要告诉机器我们需要什么样的结果,而不需要关心内部 DOM 究竟是如何被修改的。
  2. 组件化React 的图标是物理化学中的原子的概念,通过 React 我们可以将 UI 拆分成多个独立单元,这些独立单元就是组件,也就是“原子”。而 React 中编写组件的方式是 JSX,也就是在 JavaScript 中编写 HTML,可以将结构和交互逻辑组合在一起,通过 原子化 CSSCSS in JS 等方案还可以将结构、样式、交互逻辑组合在一起,也就真正做到一个 JSX 文件对应一个组件。
  3. 跨平台编写React 是通过先构建虚拟 DOM 再来操作真实 DOM 的。虚拟 DOM 的本质就是 JavaScript 对象,我们只需要不同的渲染器就可以实现一次编写,跨平台使用。

但以上仅仅只是 React 在构建和操作视图方面的功能,只使用这些特性是很难开发大型应用的,对此我们需要格外学习 React 生态的其他工具帮助我们开发,比如路由工具状态管理工具等。

今天我们要分享的就是对 React Router 的一些理解和应用。

一小时快速回顾 React Router

本篇文章主要是针对 React Router v6.10 的相关介绍和分析

为什么不是 React Router v5

一小时快速回顾 React Router

虽然 npm 的官网显示 v5.3.4 的周下载量甚至还要高于 v6.10.0,但是 Remix 团队在 v6.4 后的版本中增添了许多新特性,其中包括读取写入和路由相关的数据等方案。此外,新版本的写法也有很大的改变,这些改变肯定会延续到后续版本的变更中。尽管 v6.4 后的版本包含更多的特性和改进,但它们已经迭代数次变得相当成熟,因此从学习的层面来看,我们应该直接开始学习 v6 版本。

同时通过 Bundlephobia 可见: v6.3 之前的版本相比 v5 版本包大小要小得多,但是 v6.4 版本之后引入了读取写入和路由相关的数据等方案,才加大了包的体积。

一小时快速回顾 React Router

React Router 的基本使用

参考官方文档的 快速开始。可以帮助你快速掌握 React Router基本使用(但个人感觉官方给的示例对于第一次上手有点难度)。

🚗 功能概述

首先我们思考一下为什么需要前端路由库?

🖥️ 客户端路由(Client Side Routing)

传统多页面

浏览器从服务端请求网页,会下载 CSSJavaScript 资源,并渲染从服务端发送过来的 HTML。当我们点击链接切换路由时,会整页刷新,这些请求都会重新执行。

一小时快速回顾 React Router

这就导致了页面切换加载缓慢,切换断层感强,流畅度不够,用户体验较差。

客户端路由

单页面应用只需初始加载一次,获取所需的所有资源,当我们跳转切换页面时,页面局部刷新或更改,不会再向服务器发送请求。

其优点主要有两方面:

  1. 页面切换快,几乎无感,用户体验较好。
  2. 数据传递层面来讲,单页面应用较为容易,直接在页面组件之间进行数据传输,而多页面应用则依赖 url 传参,或者 CookielocalStorage 等本地存储方案。

但也有相应的缺点:

不利于 SEO搜索引擎优化),客户端路由是根据匹配到的路由通过 JavaScript 动态渲染上去的,初次返回的 HTML 基本是个空壳,因此不利于搜索引擎爬虫爬取。对于这种情况可以使用 SSR 等方案来优化。对于多页面应用来讲就不存在这类问题。

一小时快速回顾 React Router

了解了为什么需要前端路由,我们再考虑一下一个前端路由库应该具备哪些功能:

  • URL 路由匹配:实现能根据 URL 路径匹配对应的页面或组件,且路由为客户端路由,切换页面时不向服务器发送请求,不重新刷新整个页面。
  • 路由跳转与传参:怎么在各个路由之间进行跳转,跳转的时候如何传递、获取参数。
  • 与路由相关的数据处理:在路由跳转的时候,实现相关数据处理,实现不跳转页面的表单提交、组件渲染之前并行加载内部所有嵌套组件的数据、实现加载路由过程中的页面等操作。

👶 创建路由

在开始之前我们需要先引人 react-router-dom 包。

react-router 是核心包,react-router-dom 是在 react-router 的基础上的浏览器端路由库,react-router-native 是在 react-router 的基础上的 React Native 端路由库。

pnpm install react-router-dom

我们要使用客户端路由首先需要创建路由实例。

创建路由实例

主流的创建路由实例的方法有两种,分别是 createBrowserRoutercreateHashRouter,其他创建路由实例的方法可以参考官方文档

createBrowserRouter

推荐创建的路由,也是项目中 99% 的情况下使用的路由模式。

createBrowserRouter 是一个工厂函数,它用于创建一个 Browser Router 对象。

其基本用法如下:

// 创建 History 路由实例
const router = createBrowserRouter([
  // 数组的第一个参数是一个对象数组,里面存的是 Route 对象
  {
    // 路径
    path: "/home",
    // 组件
    element: <Home />,
    // 子路由
    children: [
      {
        index: true,
        element: <Class />,
      },
      {
        path: "team",
        element: <Team />,
      },
    ],
  },
  {
    // 路径
    path: "/about",
    // 组件
    element: <About />,
    errorElement: <Error />,
  },
], {
  // 根路径
  basename: '/app'
});

// 使用 JSX 的形式
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="team" element={<Team />} />
    </Route>
  )
);

第一个参数是一个对象数组,里面存的是 Route 对象,包含属性:

  • path:与 url 匹配的路径
  • element:匹配上路径时渲染的组件
  • children:嵌套子路由,值也是 Route 对象数组
  • errorElement:抛出异常时渲染的组件
  • index:是否是默认子路由,如果为 true 则无需设置 path
  • loader
  • action

第二个参数是一个配置对象,可能会用到的属性有 basename,用来设置客户端路由的根路由,适用于不能部署到域的根目录,只能部署到子目录的情况。

除了上面直接传入配置项的方式,还支持通过 createRoutesFromElementsJSX 的方式创建路由实例。

其原理是使用 DOM History API 来更新 url、管理历史堆栈。history api 可以修改、监听浏览器的 URL,改变的 URL 会发送给服务端,这就导致如果我们进入了某个子路由,然后在该子路由页面点击刷新按钮,URL 就会被发送到服务端,但是单页面应用也就是客户端路由在服务端只有一个 index.html 在根目录下, 因此会报找不到该资源的错,所以 History 模式下需要服务端配置:当找不到匹配路由资源的时候,将所有路由情况都返回 index.html,由客户端路由来判断到底该 URL 要渲染哪些组件。

createHashRouter

不推荐使用哈希路由,除非你不能配置你的服务器。

Hash 模式更改 url 后面的哈希部分,这部分对服务器不可见,意思是再怎么折腾请求服务端那边都是根目录下的 index.html,前端通 过 local.hash 来改变和获取哈希值,当更改哈希值时,浏览器不会重新加载页面,hashchange 事件监听变化,最后实现无刷新的页面跳转。

其余部分与 History 路由基本一致。该路由模式是在 window.pushStatehistory API 标准化之前的解决方案。

使用路由实例

使用路由实例我们通过 <RouterProvider /> 组件来实现,我们将创建的路由实例作为 router 参数传递给 <RouteProvider /> 组件,将其渲染。

// react 将 <RouterProvider /> 渲染在根组件上
ReactDOM.createRoot(document.getElementById("root")).render(
  <RouterProvider
    router={router}
  />
);
特性
嵌套路由

嵌套路由示例

一个路由下可以设置多个子路由,通过 Outlet 来展示子路由的组件。

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* 
        Outlet 组件将会渲染 DashboardMessages 如果路由匹配上 /messages,
        会渲染 DashboardTasks 如果路由匹配上 /tasks
      */}
      <Outlet />
    </div>
  );
}

function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <Dashboard />,
      children: [
        {
          path: "messages",
          element: <DashboardMessages />,
        },
        {
          path: "tasks",
          element: <DashboardTasks />,
        },
      ],
    },
  ]);
  return (
    <RouterProvider
    	router={router}
  	/>
  );
}
动态参数

URL 的片段是可以被解析并提供给各种 api 的。

path="projects/:projectId/tasks/:taskId"

如上面两个前面带 : 的路由片段是动态的,在该路由组件下可以通过 useParams 可获取到 projectstaskId 两个参数:

// 如果当前的路由是 /projects/abc/tasks/3
createBrowserRouter([
  {
    path: "projects/:projectId/tasks/:taskId",
    element: <Task />,
  },
]);

// 组件内获取
function Task() {
  // useParams 返回对应的参数
  const params = useParams();
  params.projectId; // abc
  params.taskId; // 3
}
优先匹配

对于下面两种路由都会匹配上的情况,如 teamIdnew

<Route path="/teams/:teamId" />
<Route path="/teams/new" />

React Router 会选择第二个(/teams/new),也就是更具体的那一个。

错误路由

可以设置当抛出错误时的路由渲染页面。

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    // 抛出错误时渲染的组件
    errorElement: <ErrorPage />,
    children: [
      {
        path: "team",
        element: <Team />,
      },
    ],
  },
]);

errorElement 具有冒泡特性,如果子路由抛出错误,首先会检查当下有无 errorElement,没有则会向上查找,直到根路由。

默认路由

如果在当前路由下没有匹配到对应的路由,我们往往需要设置一个类似 404 的页面,来告知用户该路由不存在,react-router 本身提供了一个默认 404 页面,但是如果我们想要自定义该怎么办?

我们可以在设置路由的 path="*" 来设置当匹配不到路由的时候展示的页面。

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./Home";
import About from "./About";

function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <Home />,
    },
    {
      path: "/about",
      element: <About />,
    },
    {
      // 设置默认路由
      path: "*",
      element: <div>not found</div>,
    },
  ]);
  return <RouterProvider router={router} />;
}

export default App;

当我们进入不存在的路由时,就会默认展示 path=* 提供的组件:

一小时快速回顾 React Router

🚀 路由间跳转

声明式路由和编程式路由是 React Router 中两种不同的路由跳转方式。

声明式跳转

概念

通过使用 JSX 元素,如 <Link /><NavLink /> 等,设置 to 属性,点击该元素来进行跳转的方式就为声明式跳转。

示例

<Link />:允许用户点击跳转到另一个路由。

  • to:要跳转到的路径,可以通过 ../ 的方式跳转到上层组件/路径

  • relative:如果通过 ../ 的方式跳转上层,默认情况下是跳转到上层组件,即 relative 的值默认为 route,如果要跳转到上层路径,则需设置为 path

    <Route path="/" element={<Layout />}>
      <Route path="contacts/:id" element={<Contact />} />
      <Route
        path="contacts/:id/edit"
        element={<EditContact />}
      />
    </Route>;
    
    function EditContact() {
      return (
        // 如果为 path 则是跳转到 contacts/:id,如果为 route 则是跳转到 /
        <Link to=".." relative="path">
          Cancel
        </Link>
      );
    }
    
  • preventScrollReset:决定是否在跳转时将滚动位置重置为窗口顶部

  • replace:是否直接替换历史堆栈中的当前路由

  • state:通过该属性可以存储状态,并传递给跳转后的路由,该状态在 url 中不会体现,需要用 useLocation 获取

  • reloadDocument:整页刷新

<NavLink />:是一种特殊的 <Link />,用来判断当前路由是“活跃的”还是“待定的”。

  • 默认 active 类:会自动为“活跃的路由”设置上 active 类,以便直接用 css 对其进行样式修改

    // jsx
    <nav id="sidebar">
      <NavLink to="/messages" />
    </nav>
    
    // css
    #sidebar a.active {
      color: red;
    }
    
  • 将是否活跃等参数传入 classNamestylechildren 等,以便我们判断进行逻辑处理

    <NavLink
      to="/messages"
      className={({ isActive, isPending }) =>
        isPending ? "pending" : isActive ? "active" : ""
      }
      style={({ isActive, isPending }) => {
        return {
          fontWeight: isActive ? "bold" : "",
          color: isPending ? "red" : "black",
        };
      }}
    >
      {({ isActive, isPending }) => (
        <span className={isActive ? "active" : ""}>Tasks</span>
      )}
    </NavLink>;
    
  • 可以设置 end 属性,来决定是否完全匹配

    完全匹配只的是当只有 URL 完全一样的时候才会标识为活跃状态,如果有超出的路由片段则为待定状态:

NavLinkURLisActive
<NavLink to="/tasks" />/taskstrue
<NavLink to="/tasks" />/tasks/123true
<NavLink to="/tasks" end />/taskstrue
<NavLink to="/tasks" end />/tasks/123false
编程式跳转(命令式跳转)

概念

通过命令代码来进行路由的跳转,可以将跳转命令绑定到事件中。

useNavigate

hooks 返回一个函数实例,以便编程式跳转。

const navigate = useNavigate();
navigate("/session");

返回函数有以下参数:

  • to:要跳转到的路径或在历史堆栈中想要跳转的增量,如 -1

    navigate(-1);
    
  • options:包含 replacestate

对比

声明式路由更加简单直观,可以适用大多数场景,而编程式路由则更加灵活,能满足一些特定的需求。

📮 路由间传参

useLocation

let location = useLocation();

hooks 可以获取 location 对象,包含以下属性:

一小时快速回顾 React Router

  • pathnameurl 的路径部分

  • searchurl? 参数片段

  • hashurl# 及后面的哈希片段

  • state:用来获取跳转前传入的 state,该 state 不会出现在 url 当中,对于一些不想明面表现在 url 中的参数可以使用 state 进行传参。

    一小时快速回顾 React Router

  • key:与路径关联的唯一字符串

useParams

hooks 返回动态参数键值对的对象。

一小时快速回顾 React Router

useSearchParams

hooks 返回一个包含两个元素的数组,第一个元素是一个 URLSearchParams 对象,包含该 url ? 后面跟着的参数,第二个元素是一个函数,用来设置 url ? 后面的参数,酷似 useState 的用法。

一小时快速回顾 React Router

✏️ 手写简易版 BrowserRouter

该实现方法和 v6 的语法不相同,重在体验 React Router 内部的原理。

首先我们需要有两个组件:<BrowserRouter><Route>

一小时快速回顾 React Router

其中 <BrowserRouter> 用来包裹整个路由,内部的 <Route> 表示路由组件,含有 pathelement 属性,分别为匹配的路径和匹配上后渲染的组件。

BorwserRouter 组件

<BrowserRouter> 组件主要完成的功能有如下:

  1. 保存当前路径状态,并传递给它包裹的所有子组件(即 <Route>),初始值为浏览器地址栏中的路径。
  2. 提供更新路径的方法,并传递给它包裹的所有子组件,用来内部更新路径状态。
  3. 监听路径的变化,并因为外部方式(浏览器的回退和前进)使路径变化时实时更新路径状态。

保存当前路径状态

保存当前路径状态,初始值为浏览器地址栏中的路径,提供 setPath,用于给更新路径的方法提供更新手段。

一小时快速回顾 React Router

更新路径的方法

一小时快速回顾 React Router

通过 history.pushState 更新路径,同时更新 path 状态。

history.pushState 方法接收三个参数,第一个参数是 state 对象,传入的 state 对象可以在对应路由下通过 window.history.state 获取;第二个参数是 title,但现在大多数浏览器都忽略次参数,可以传入空字符串;第三个参数是 URL

监听浏览器前进后退并更新路由

通过监听 popstate 事件,更新 path 状态。

调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。

popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮。

一小时快速回顾 React Router

监听到浏览器路由因为浏览器行为发生变化后,获取变化后的路径,更新路径状态。

传递给子组件

通过 createContext 创建上下文,来实现跨组件通信:

一小时快速回顾 React Router

这里创建了两个上下文,一个用来存储更新路径状态的方法,一个用来存储当前路径状态。通过 Provider 传递给子组件。

一小时快速回顾 React Router

Route 组件

<Route> 中获取到配置路由中传入的 pathelement 表示这个路由组件要渲染的路径和对应组件。

一小时快速回顾 React Router

然后通过 useContext 获取当前的路径状态,与各个 <Route> 组件的 path 进行对比,如果相匹配,则将其渲染。

一小时快速回顾 React Router

Route 的 element 组件

在路由渲染组件中,通过 useContext 获取到 HistoryContextpush 方法用来更新路径,并可以传入 state

一小时快速回顾 React Router

若传入了 state,可以在相应路由组件中使用 window.history.state 获取传入的参数。

参考仓库

📗 思考

  1. 实现一个简易版 react-router-dom 的基础功能,并尽可能增加特性,如错误路由、默认路由、嵌套路由等。
  2. 实现路由守卫,如果处于未登录状态进入页面,则跳转到登陆页,并对各个页面进行权限管理,相应角色权限只能进入相应的页面。