React Router@3.x 升级到 @6.x 的实施方案

这篇具有很好参考价值的文章主要介绍了React Router@3.x 升级到 @6.x 的实施方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:景明

升级背景

目前公司产品有关 react 的工具版本普遍较低,其中 react router 版本为 3.x(是的,没有看错,3.x 的版本)。而最新的 react router 已经到了 6.x 版本。

为了能够跟上路由的脚步,也为了使用 router 相关的 hooks 函数,一次必不可少的升级由此到来!

版本确定

react-touter 6.x 版本,只对 react 和 react-dom 版本有要求,我们的项目满足条件。

"peerDependencies": {
    "react": ">=16.8",
    "react-dom": ">=16.8"
}

确定使用 react-router-dom: 6.11.1 为目标升级版本。是的,跳过了v4/v5 版本,直接上 v6 一步到位

React Router 使用场景以及变化介绍

组件引用

在 v6 版本,分为了 3 个包(PS:兼容包不算)

  • react-router : 核心包,只提供核心的路由和 hook 函数,不会直接使用
  • react-router-dom :供浏览器/Web 应用使用的 API。依赖于 react-router, 同时将 react-router 的 API 重新暴露出来
  • react-router-native :供 React Native 应用使用的 API。同时将 react-router 的 API 重新暴露出来(无 native 相关项目,与我们无关不管)

从 V6 开始,只需要使用 react-router-dom 即可,不会直接使用 react-router。

对应的是组件引用的变更,如下:

// v3 版本
import { Link } from 'react-router'

// v6 版本后
import { Link } from 'react-router-dom';

路由

Route 类型定义

interface RouteObject {
  path?: string;
  index?: boolean; // 索引路由
  children?: React.ReactNode; // 子路由
  caseSensitive?: boolean; // 区分大小写
  id?: string;
  loader?: LoaderFunction; // 路由元素渲染前执行
  action?: ActionFunction;
  element?: React.ReactNode | null;
  Component?: React.ComponentType | null;
  errorElement?: React.ReactNode | null; // 在 loader / action 过程中抛出异常展示
  ErrorBoundary?: React.ComponentType | null;
  handle?: RouteObject["handle"];
  lazy?: LazyRouteFunction<RouteObject>;
}

path

v6 中使用简化的路径格式。<Route path> 在 v6 中仅支持 2 种占位符:动态:id参数和*通配符。通配符*只能用在路径的末尾,不能用在中间。

// 有效地址
/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*

// 无效地址
/users/:id?
/tweets/:id(\d+)
/files/*/cat.jpg
/files-*

index

判断该路由是否为索引路由(默认的子路由)。

<Route path="/teams" element={<Teams />}>
  <Route index element={<TeamsIndex />} />
  <Route path=":teamId" element={<Team />} />
</Route>

设置了 index 的 route 不允许存在子路由

loader

在路由组件渲染前执行并传递数据,组件可通过 useLoaderData 获取 loader 的返回值。

createBrowserRouter([
  {
    element: <Teams />,
    path: "/",
    // 打开配置将造成死循环,因为 /view 也会触发 / 的 loader
    // loader: async () => {
    //   return redirect('/view');
    // },
    children: [
      {
        element: <Team />,
        path: "view",
        loader: async ({ params }) => {
          return fetch(`/api/view/${params.id}`);
        },
      },
    ],
  },
]);

需要注意的是,loader 是并行触发,匹配多个 route,这些 route 上如果都存在 loader,都会执行。

想要针对特定的路由,可以采用如下写法:

export const loader = ({ request }) => {
  if (new URL(request.url).pathname === "/") {
    return redirect("/view");
  }
  return null;
};

element/Component

// element?: React.ReactNode | null;
<Route path="/a" element={<Properties />} />

// Component?: React.ComponentType | null;
<Route path="/a" Component={Properties} />

与 v3 相比,v6 是大写开头的 Component。
v6 更推荐采用 element 的方式,可以非常方便的传递 props

中心化配置

在 v6 版本支持中心化配置,可以通过 createHashRouter 进行配置。
使用如下,结构就是 route 的定义:

export const getRoutes = createHashRouter([
    {
        path: '/',
        Component: AuthLayout,
        children: [
            ...commonRouteConfig,
            {
                Component: SideLayout,
                children: [
                    {
                        path: 'metaDataCenter',
                        Component: MetaDataCenter,
                    },
                    {
                        path: 'metaDataSearch',
                        Component: MetaDataSearch,
                    },
                    {
                        path: 'metaDataDetails',
                        Component: MetaDataDetails,
                    },
                    {
                        path: 'dataSourceDetails',
                        Component: MetaDataDetails,
                    },
              }
          ]
    }
]

引入如下:

import { RouterProvider } from 'react-router-dom';

<RouterProvider router={getRoutes} />

与 v3 相比:

  • component -> Component
  • childRoutes -> children
  • 增加 loader
  • name
  • indexRoute ,采用布局 route
  • 在布局组件中,使用 进行占位展示,而不是 children
  • 在 v3 中路径前带 /代表绝对路径,在 v6 中不管带不带都是相对父级的路径,推荐不带 /
  • 配合 RouterProvider 使用

组件化路由

在组件内使用:

  • Routes: 当地址发生变化,Routes 会在 Route 中进行匹配(原v5 中 Switch)
  • Route:子路由信息
// This is a React Router v6 app
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users/*" element={<Users />} />
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Routes>
        <Route path=":id" element={<UserProfile />} />
        <Route path="me" element={<OwnUserProfile />} />
      </Routes>
    </div>
  );
}
  • <Route path><Link to> 是相对父元素的地址。
  • 你可以把 Route 按你想要的任何顺序排列,Routes 会根据当前路由信息进行生成权重,进行排序,在匹配最佳路由
// 动态路由权重,比如 /foo/:id
const dynamicSegmentValue = 3;
// 索引路由权重,也就是加了 index 为 true 属性的路由
const indexRouteValue = 2;
// 空路由权重,当一段路径值为空时匹配,只有最后的路径以 / 结尾才会用到它
const emptySegmentValue = 1;
// 静态路由权重
const staticSegmentValue = 10;
// 路由通配符权重,为负的,代表当我们写 * 时实际会降低权重
const splatPenalty = -2;

路由跳转

useNavigate

declare function useNavigate(): NavigateFunction;

interface NavigateFunction {
  (
    to: To,
    options?: {
      replace?: boolean;
      state?: any;
      relative?: RelativeRoutingType;
    }
  ): void;
  (delta: number): void;
}

在组件内原本采用 history 进行跳转,在 V6 修改成使用 navigate 进行跳转。

import { useNavigate } from "react-router-dom";

function App() {
  let navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}

如果需要替换当前位置而不是将新位置推送到历史堆栈,请使用 navigate(to, { replace: true })。 如果你需要增加状态,请使用 navigate(to, { state })

如果当前正在使用 history 中的 go、goBack 或 goForward 来向后和向前导航,则还应该将它们替换为 navigate 的第一个数字参数,表示在历史堆栈中移动指针的位置

// v3 -> v6
go(-2)} -> navigate(-2)
goBack -> navigate(-1)
goForward -> navigate(1)
go(2) -> navigate(2)

Navigate

declare function Navigate(props: NavigateProps): null;

interface NavigateProps {
  to: To;
  replace?: boolean;
  state?: any;
  relative?: RelativeRoutingType;
}

如果你更喜欢使用声明式 API 进行导航( v5 的 Redirect),v6 提供了一个 Navigate 组件。像这样使用它:

import { Navigate } from "react-router-dom";

function App() {
  return <Navigate to="/home" replace state={state} />;
}

注意:v6 默认使用push逻辑,你可以通过 replaceProps 来更改它。

history

history 库是 v6 的直接依赖项,在大多数情况下不需要直接导入或使用它。应该使用 useNavigate 钩子进行所有导航。

然而在非 tsx 中,如 redux 、 ajax 函数中。我们是无法使用react hooks的。

这个时候可以使用 location ,或者 history 进行跳转。

history.push("/home");
history.push("/home?the=query", { some: "state" });
history.push(
  {
    pathname: "/home",
    search: "?the=query",
  },
  {
    some: state,
  }
);
history.go(-1);
history.back();

location

采用 window.location 对象进行跳转。

window.location.hash = '/'

传参

query

// V3
type Location = {
  pathname: Pathname;
  search: Search;
  query: Query;
  state: LocationState;
  action: Action;
  key: LocationKey;
};

// V6
type Location = {
  pathname: Pathname;
  search: Search;
  state: LocationState;
  key: LocationKey;
};

在 v3 中,我们可以通过 location.query 进行 Url 的参数获取或设置,而在 v6 中是不支持的。

在使用 useNavigate 时,接收一个完整的 pathname,如:/user?name=admin

在我们自己的工具库 dt-utils 中,新增 getUrlPathname 方法用来生成 pathname。

getUrlPathname(pathname: string, queryParams?: {}): string

// example
DtUtils.getUrlPathname('/metaDataSearch', { metaType, search })

获取时使用 getParameterByName 进行获取单个 query param。也新增了 getUrlQueryParams 方法获取所有的 query params

// getParameterByName(name: string, url?: string): string | null
// 需要注意 getParameterByName 返回的是 null。在多数情况下,需要转成 undefined
const standardId = DtUtils.getParameterByName('standardId') || undefined;

// getQueryParams(url: string): Record<string, string>
const query = DtUtils.getUrlQueryParams(location.search);

params

通过 useParams 获取到路由上的参数。

import * as React from 'react';
import { Routes, Route, useParams } from 'react-router-dom';

function ProfilePage() {
  // Get the userId param from the URL.
  let { userId } = useParams();
  // ...
}

function App() {
  return (
    <Routes>
      <Route path="users">
        <Route path=":userId" element={<ProfilePage />} />
      </Route>
    </Routes>
  );
}

state

在进行路由跳转时可以通过传递 state 状态进行传参。

// route 传递
<Route path="/element" element={<Navigate to="/" state={{ id: 1 }} />} />

// link 传递
<Link to="/home" state={state} />

// 跳转传递
navigate('/about', {
    state: {
        id: 1
    }
})

// 获取 state
export default function App() {
  // 通过 location 中的 state 获取
  let location = useLocation();
  const id = location.state.id
  
  return (
    <div className="App">
      <header>首页</header>
      <p>我的id是:{id}</p>
    </div>
  );
}

Outlet

可通过 useOutletContext 获取 outlet 传入的信息。

function Parent() {
  const [count, setCount] = React.useState(0);
  return <Outlet context={[count, setCount]} />;
}
import { useOutletContext } from "react-router-dom";

function Child() {
  const [count, setCount] = useOutletContext();
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}

路由跳转前拦截

在 v3 中使用 setRouteLeaveHook 进行路由的拦截,在 v6 被移除了。

this.props.router.setRouteLeaveHook(this.props.route, () => {
    if (!this.state.finishRule) {
        return '规则还未生效,是否确认取消?';
    }
    return true;
});

在 V6 中采用 usePrompt 进行组件跳转拦截。

需要注意的是,由于 usePrompt 在各浏览器中交互可能不一致。

目前可拦截前进,后退,正常跳转。

刷新页面不可拦截。

/**
 * Wrapper around useBlocker to show a window.confirm prompt to users instead
 * of building a custom UI with useBlocker.
 *
 * Warning: This has *a lot of rough edges* and behaves very differently (and
 * very incorrectly in some cases) across browsers if user click addition
 * back/forward navigations while the confirm is open.  Use at your own risk.
 */
declare function usePrompt({ when, message }: {
    when: boolean;
    message: string;
}): void;
export { usePrompt as unstable_usePrompt };

针对这个功能,封装了一个 usePrompt

import { unstable_usePrompt } from 'react-router-dom';
import useSyncState from '../useSyncState';

/**
 * 拦截路由改变
 * @param {boolean} [initWhen = true] 是否弹框
 * @param {string} [message = ''] 弹框内容
 * @returns {(state: boolean, callback?: (state: boolean) => void) => void}
 */
const usePrompt = (initWhen = true, message = '') => {
    const [when, setWhen] = useSyncState(initWhen);
    unstable_usePrompt({ when, message });
    return setWhen;
};

export default usePrompt;

// example
import usePrompt from 'dt-common/src/components/usePrompt';

const EditClassRule = (props: EditClassRuleProps) => {
    const setWhen = usePrompt(
        checkAuthority('DATASECURITY_DATACLASSIFICATION_CLASSIFICATIONSETTING'),
        '规则还未生效,是否确认取消?'
    );

    return (
        <EditClassRuleContent {...(props as EditClassRuleContentProps)} setFinishRule={setWhen} />
    );
};

router Props 注入

路由注入

在 V3 中 router 会给每一个匹配命中的组件注入相关的 router props

React Router@3.x 升级到 @6.x 的实施方案

  • location: 当前 url 的信息
  • params: 路由参数,刷新不会重置
  • route:所有路由配置信息
  • routerParams: 路由参数,刷新后重置
  • router:router 实例,可以调用其中的各种方法,常用的有:push、go、goBack
  • routes:当前路由面包屑

注入 props 在 V6 是没有的。

withRouter 注入

v3 中的 withRouter 将 react-router 的 history、location、match 三个对象传入props对象上。

在 v6 上 withRouter 这个方法也是没有的。

实现 withRouter

在 v6 中,提供了大量 hooks 用于获取信息。

获取 location 的 useLocation。获取路由 params 的 useParams,获取 navigate 实例的 useNavigate 等。

实现了一个 withRouter 的高阶函数,用于注入这 3 个 props。
这里没有直接传入,采用 router 对象的原因是:

  1. 考虑 props 的覆盖,像 params 都是大概率出现的名字。
  2. 原有使用中,大部分都是直接从 this.props 点出来,可以与升级前有个区分,避免混淆。
import React from 'react';
import {
    useNavigate,
    useParams,
    useLocation,
    Params,
    NavigateFunction,
    Location,
} from 'react-router-dom';

export interface RouterInstance {
    router: {
        params: Readonly<Params<string>>;
        navigate: NavigateFunction;
        location: Location;
    };
}

function withRouter<P extends RouterInstance = any, S = any>(
    Component: typeof React.Component<P, S>
) {
    return (props: P) => {
        const params = useParams();
        const navigate = useNavigate();
        const location = useLocation();

        const router: RouterInstance['router'] = {
            params,
            navigate,
            location,
        };

        return <Component {...props} router={router} />;
    };
}

export default withRouter;

// example
export default withRouter<IProps, IState>(Sidebar);

总结

该篇文章中主要记录了,我们项目从 react-router@3.x 升级到 @6.x 遇到的一些问题以及相关的解决方案,也简单讲解了 v3 与 v6 的部分差异,欢迎大家讨论提出相关的问题~


最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star文章来源地址https://www.toymoban.com/news/detail-634724.html

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko

到了这里,关于React Router@3.x 升级到 @6.x 的实施方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 明厨亮灶监控实施方案 opencv

    明厨亮灶监控实施方案通过python+opencv网络模型图像识别算法,一旦发现现场人员没有正确佩戴厨师帽或厨师服,及时发现明火离岗、不戴口罩、厨房抽烟、老鼠出没以及陌生人进入后厨等问题生成告警信息并进行提示。OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机

    2024年02月10日
    浏览(40)
  • Jmeter的自动化测试实施方案

    前言: Jmeter是目前最流行的一种测试工具,基于此工具我们搭建了一整套的自动化方案,包括了脚本添加配置、本地配置和运行、服务器配置等内容,完成了自动化测试闭环,通过这种快捷简便高效的方式,希望可以解决自动化测试上手难的痛点。下面闲言少叙,我们直接切

    2024年02月10日
    浏览(50)
  • 基于DataHub元数据血缘管理实施方案

    目录 1. 元数据管理实施方案总览 2. 元数据分类 2.1 技术元数据 2.2 业务元数据 3. 元数据标签体系  基础标签  数仓标签  业务标签 潜在标签 4. 表元数据 4.1  基于pull机制抽取元数据 web端ui方式 cli端yml方式 yml解析 yml模板 4.2. RESET-API方式 API-MEDTADA人工构建模板 5. 血缘元数据

    2024年02月08日
    浏览(52)
  • Oracle到DM实时数据同步实施方案

    目录 1 项目概述 2 需求分析 3 实施操作 3.1 历史数据全量同步 3.2 增量数据实时同步 4 问题总结 4.1 字符型非空约束 4.2 字符型唯一索引尾部空格 将Oracle 11g RAC生产环境数据同步到DM8分析环境,Oracle数据库大小1.5T,日增归档100G,DM数据库为新建库。 初始同步表数70多张,其中

    2024年02月13日
    浏览(43)
  • 电信联通5G共建共享方案实施及验证

    一、情况概述 随着2019年9月9日中国电信集团与联通签署《5G网络共建共享框架合作协议书》,电信与联通在全国范围内合作共建5G接入网络。根据合作协议,联通运营公司将与中国电信在全国范围内合作共建一张5G接入网络, 双方划定区域,分区建设,各自负责在划定区域内的

    2024年01月25日
    浏览(42)
  • 案例分享:西河水库安全监测信息化系统实施方案

    一、项目概述 1.1项目背景 西河水库信息化工作已开展多年,但是由于西河水库监测设备都已经老化或者损坏,现有设备已渐渐不能满足新时期西河水库信息化和现代化发展需求。因此,灌区管理局拟在运用现代信息和通信技术手段感测、分析、整合水库运行核心系统的各项

    2024年02月09日
    浏览(57)
  • AMD老电脑超频及性能提升方案及实施

    收拾电子元件的时候找到了若干古董的CPU 其中有一个X3 440 是原来同学主板烧了之后给我的,我从网上配了AM2 昂达主板,然后又买了AMD兼容内存,组成了win7 64位电脑,用起来非常不错,我把硬件配置和升级过程说明下,先看现在的配置: 处理器: 其中前端总线和总线速度都

    2024年02月05日
    浏览(50)
  • 省三医院新门诊大楼网络安全建设项目实施方案

    省三医院新门诊大楼网络安全建设项目实施方案 甲方:省三医院 乙方:武汉埃郭信息技术有限公司1组吴冰冰 项目背景 省三医院新建一栋门诊大楼,地址位于原三医院东南处空地。为了响应国家建设数字医疗,构建医联网、医共体的号召,新门诊大楼现有计算机网络,实现

    2024年02月08日
    浏览(48)
  • 中小型企业网络解决方案的设计和实施

    第一章  网络需求分析 该公司总部设在上海,总部有人力资源部、财务部和研发部,共三个部门,每个部门平均 16 台终端。分部设在深圳,有生产部、生产部、客服部,也有三个部门,每个部门平均 254 台终端。要求将终端互联在一起。 所有机器能上互联网;分部和总部网

    2024年02月06日
    浏览(55)
  • 电子产品如何设计和实施上下电压力测试方案?

    目录 一、引言 二、测试方案步骤 三、设计方案介绍 3.1 设计思路 3.2 实现方法说明 3.2.1 测试板原理框图 3.2.2 跳变沿检测法 3.2.2.1 接线方式 3.2.2.2 检测流程 3.2.2.3 参考代码  3.2.3 字符串匹配法 3.2.3.1 接线方式 3.2.3.2 检测流程 3.2.3.3 实现代码 3.2.4 继电器控制法 3.2.4.1 接线方式

    2024年02月03日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包