banner
AgedCoffee

AgedCoffee

ReactRouter6-4中的懶加載

原文地址:Lazy Loading Routes in React Router 6.4+

React Router 6.4 + 中的懶加載路由#

React Router 6.4 引入了“數據路由器”的概念,主要關注點是將數據獲取與渲染分離,以消除渲染 + 獲取鏈和隨之而來的旋轉加載圖標。

這些鏈條通常被稱為 “瀑布”,但我們正在重新思考這個術語,因為大多數人聽到 “瀑布” 就會想象尼亞加拉大瀑布,所有的水都會落在一個漂亮的大瀑布中。但是,“一次性” 似乎是加載數據的好方法,那麼為什麼要討厭瀑布呢?也許我們應該追求它們?

實際上,我們想要避免的 “瀑布” 看起來更像上面的標題圖片,並且類似於樓梯。水流下來一點,然後停止,再下來一點,然後停止,依此類推。現在想象一下,在那個樓梯中每一個步驟都是一個加載旋轉器。這不是我們想給用戶的 UI 類型!因此,在本文(以及希望之外),我們使用 “鏈” 這個術語來表示固有順序的獲取,並且每個獲取都被前面的獲取所阻塞。

Render + Fetch Chains#

如果你還沒有閱讀過“Remixing React Router”一文或者去年 Reactathon 上 Ryan 的“When to Fetch”演講,那麼在深入閱讀本文之前,建議先查看這些內容。它們涵蓋了我們引入數據路由概念背後的很多背景知識。

簡而言之,當你的路由器不知道你的數據需求時,你最終會得到鏈接請求,並且隨著渲染子組件,“發現” 了後續數據需求。

將數據獲取與組件耦合會導致渲染 + 獲取鏈

但是引入數據路由器可以讓您並行獲取數據並一次性渲染所有內容:

路由獲取並行化請求,消除了緩慢的渲染 + 獲取鏈

為了實現這一點,數據路由器將您的路由定義從渲染週期中提取出來,以便我們的路由器可以提前識別嵌套數據需求。

// app.jsx
import Layout, { getUser } from `./layout`
import Home from `./home`
import Projects, { getProjects } from `./projects`
import Project, { getProject } from `./project`

const routes = [
  {
    path: '/',
    loader: () => getUser(),
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'projects',
        loader: () => getProjects(),
        element: <Projects />,
        children: [
          {
            path: ':projectId',
            loader: ({ params }) => getProject(params.projectId),
            element: <Project />,
          },
        ],
      },
    ],
  },
]

但這也有一個缺點。到目前為止,我們已經討論了如何優化數據獲取,但我們還必須考慮如何優化 JS 捆綁包的獲取!通過上面的路由定義,雖然我們可以並行地獲取所有數據,但我們阻塞了數據獲取的開始,因為要下載包含所有加載器和組件的 Javascript 捆綁包。

考慮一個用戶在 / 路由上進入您的網站:

單個 JS 捆綁包阻止了數據獲取

React.lazy 能拯救我們嗎?#

React.lazy 提供了一種優秀的原語來分塊組件樹,但它遭受著與數據路由器試圖消除的獲取和渲染緊密耦合的相同問題 😕。這是因為當你使用 React.lazy () 時,你會為你的組件創建一個異步塊,但 React 直到渲染懶惰組件才開始獲取該塊。

// app.jsx
const LazyComponent = React.lazy(() => import('./component'))

function App() {
  return (
    <React.Suspense fallback={<p>Loading lazy chunk...</p>}>
      <LazyComponent />
    </React.Suspense>
  )
}

React.lazy () 調用會產生類似的渲染 + 獲取鏈。

因此,雖然我們可以在數據路由器中利用 React.lazy (),但最終會引入一個鏈來下載組件。Ruben Casas 撰寫了一篇很棒的文章,介紹了一些使用 React.lazy () 在數據路由器中進行代碼拆分的方法。但是從這篇文章中可以看出,手動進行代碼拆分仍然有點冗長和繁瑣。因為 DX 不夠好,所以我們收到了@rossipedia 提出的建議(和初始 POC 實現)。這個建議很好地概述了當前面臨的挑戰,並讓我們開始思考如何在 RouterProvider 中引入一流的代碼拆分支持。我們要向這兩位(以及其他優秀社區成員)致以巨大的讚揚,感謝他們積極參與 React Router 演進 🙌。

介紹 Route.lazy#

如果我們希望懶加載與數據路由器良好地配合,我們需要能夠在渲染週期之外引入惰性。就像我們將數據獲取從渲染週期中提取出來一樣,我們也希望將路由獲取提取出來。

如果你退後一步,看待路由定義,它可以分為三個部分:

  • 路徑匹配字段,例如路徑、索引和子節點
  • 數據加載 / 提交字段,如加載器和操作
  • 渲染字段,如元素和錯誤元素

數據路由器在關鍵路徑上真正需要的是匹配路徑字段,因為它需要能夠識別給定 URL 匹配的所有路由。匹配後,我們已經有了異步導航正在進行中,所以沒有理由不能在該導航期間獲取路由信息。然後,在完成數據獲取之前,我們不需要渲染方面的內容,因為直到數據獲取完成之前我們才會呈現目標路線。是的,這可能會引入 “鏈” 的概念(加載路線,然後加載數據),但這是一個可選的杠杆作用,在需要時可以拉動以解決初始加載速度和隨後導航速度之間的權衡問題。

以下是使用上面的路由結構和在一個路由定義中使用新的 lazy () 方法(在 React Router v6.9.0 中可用)的示例:

// app.jsx
import Layout, { getUser } from `./layout`;
import Home from `./home`;

const routes = [{
  path: '/',
  loader: () => getUser(),
  element: <Layout />,
  children: [{
    index: true,
    element: <Home />,
  }, {
    path: 'projects',
    lazy: () => import("./projects"), // 💤 Lazy load!
    children: [{
      path: ':projectId',
      lazy: () => import("./project"), // 💤 Lazy load!
    }],
  }],
}]

// projects.jsx
export function loader = () => { ... }; // formerly named getProjects

export function Component() { ... } // formerly named Projects

// project.jsx
export function loader = () => { ... }; // formerly named getProject

export function Component() { ... } // formerly named Project

你問的是導出功能組件嗎?從這個惰性模塊中導出的屬性會逐字添加到路由定義中。因為導出一個元素很奇怪,所以我們增加了在路由對象上定義組件而不是元素的支持(但別擔心,元素仍然可以使用!)。

在這種情況下,我們選擇將佈局和首頁路由留在主要捆綁包中,因為這是我們用戶最常用的入口點。但是,我們已經將項目導入和:projectId 路由的導入移動到它們自己的動態導入中,在沒有導航到那些路由時不會被加載。

初始加載時,生成的網絡圖大致如下:

lazy () 方法允許我們縮減關鍵路徑捆綁包

現在我們的關鍵路徑捆綁包僅包括我們認為對於進入網站最為關鍵的那些路由。然後,當用戶點擊鏈接到 /projects/123 時,我們通過 lazy () 方法並行獲取這些路由,並執行它們返回的加載器方法:

我們在導航時並行地惰性加載路由

這讓我們在某種程度上兼顧了兩全其美的效果,因為我們能夠將關鍵路徑捆綁到相關的首頁路由中。然後在導航時,我們可以匹配路徑並獲取所需的新路由定義。

高級用法和優化#

一些敏銳的讀者可能會感到一些 🕷️ 蜘蛛俠般的直覺,認為這裡隱藏著一些鏈接。這是最優網絡圖嗎?事實證明不是!但考慮到我們沒有編寫多少代碼就得到了它,它還是相當不錯的 😉。

在上面的例子中,我們的路由模塊包括我們的加載器和組件,這意味著在開始加載程序之前,我們需要下載兩者的內容。實際上,在 React Router SPA 中,您的加載程序通常非常小,並且會訪問外部 API,其中大部分業務邏輯都存在。另一方面,組件定義了整個用戶界面,包括所有與其相關聯的用戶交互 - 它們可能會變得相當大。

單一路由文件會阻止組件下載後的數據獲取

阻止加載程序(可能正在對某個 API 進行 fetch () 調用)通過 JS 下載大型組件樹似乎很愚蠢。如果我們能把這個 👆 變成這個 👇 會怎樣?

我們可以通過將組件提取到它自己的文件中來解除數據獲取的阻塞

好消息是,您只需進行最少量的代碼更改即可實現!如果在路由上靜態定義了一個加載器 / 操作,則它將與 lazy () 並行執行。這使我們可以通過將加載器和組件分開成不同的文件來解耦加載器數據獲取和組件塊下載:

const routes = [
  {
    path: 'projects',
    async loader({ request, params }) {
      let { loader } = await import('./projects-loader')
      return loader({ request, params })
    },
    lazy: () => import('./projects-component'),
  },
]

在路由上靜態定義的任何字段都將始終優先於從 lazy 返回的任何內容。因此,雖然您不應該同時定義靜態加載程序並從 lazy 返回加載程序,但如果這樣做,則會忽略懶惰版本,並獲得控制台警告。

這個靜態定義的加載器概念還為直接內聯代碼打開了一些有趣的可能性。例如,也許你有一個單獨的 API 端點,它知道如何基於請求 URL 獲取給定路由的數據。你可以以最小捆綁成本內聯所有加載器,並在數據獲取和組件(或路由模塊)塊下載之間實現完全並行化。

const routes = [
  {
    path: 'projects',
    loader: ({ request }) => fetchDataForUrl(request.url),
    lazy: () => import('./projects-component'),
  },
]

看啊,媽媽,沒有加載器塊!

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。