React [email protected] 升級到 @6.x 的實施方案

来源:https://www.cnblogs.com/dtux/archive/2023/08/09/17616258.html
-Advertisement-
Play Games

>我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:景明 ## 升級背景 目前公司產品有關 react 的工具版本普遍較低,其中 react router ...


我們是袋鼠雲數棧 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)
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

file

  • 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);

總結

該篇文章中主要記錄了,我們項目從 [email protected] 升級到 @6.x 遇到的一些問題以及相關的解決方案,也簡單講解了 v3 與 v6 的部分差異,歡迎大家討論提出相關的問題~


最後

歡迎關註【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 背景 生產上有個導報表功能,工作了很長一段時間一直都很穩,沒出現過什麼問題,最近運營同學突然反饋導出來的數據和實際的對不上,經過排查發現導出的數據有重覆,也有的沒導出來。 由於我們提前生成好數據(每天會truncate重新生成),所以導出的邏輯非常簡單,不需要關聯很多表撈數據,只需要從一張表查 ...
  • # 一、前言 原有的業務系統跑在MySQL主從架構中,高可用通過腳本完成,但存在切換數據丟失和切換不及時風險,調研了高可用更穩定的MGR後,準備入手一試。本篇文章主要記錄GreatSQL從單機擴展到MGR的詳細過程,遇到的問題及解決方法。 # 二、基礎環境 伺服器角色如下 | IP | 埠 | 主 ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202308/3076680-20230807132720267-1631745639.png) # 1. 計算一年有多少天 ## 1.1. Oracle sql語句實例 ```sql select 'Days ...
  • 一、升級webview版本 (1). 下載需要更新的Webview apk。如果不能翻牆可以用下載好的版本(相容32/64位):Webview-115.0.5790.138 (2). 在路徑\aosp\external\chromium-webview\prebuilt\下替換arm或arm64架構 ...
  • 本文介紹了Android13中的凍結進程功能,它是一種重要的資源管理策略,可以提高系統性能和穩定性,同時最大限度地節省設備的資源和電池消耗。 文章討論瞭如何合理分配資源,包括CPU、記憶體等,以提高設備性能和用戶體驗。此外,文章還提到了凍結進程對應用程式線程的影響,並介紹了Android13與Andr... ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 思路分析 在開始動手之前,分析一下整個功能的實現過程: 根據圖片大小創建 canvas1 畫布,並將原圖片直接定位在 canvas1 上; 在畫布上添加一個蒙層,以區分當前 canvas 圖像是被裁剪的原圖像; 在蒙層上方,對裁剪區域(鼠 ...
  • Avue 是一個基於Element-plus低代碼前端框架,它使用JSON 配置來生成頁面,可以減少頁面開發工作量,極大提升效率; 雖然Avue官網上面都有這些配置說明,但是如果剛開始接觸不熟悉框架的話需要很久才找到自己需要的參數配置,為了方便自己今後查找使用,現將一些開發中常用的配置梳理在下 一、 ...
  • 這幾天在學vue3, 用Element-plus 加 vue3 搭了個框架,在這裡記錄一下項目搭建中遇到的問題。 1、使用 Element-plus 的 icon 圖標,顯示不出來 首先,用命令行中安裝 Element-plus 的圖標: npm install @element-plus/icon ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...