跳至內容

9

串流

在上一章中,您學習了 Next.js 的不同渲染方法。我們也討論了緩慢的資料擷取如何影響應用程式的效能。讓我們看看如何在資料請求緩慢的情況下改善使用者體驗。

本章節內容...

以下是我們將涵蓋的主題

什麼是串流以及何時 might 使用它。

如何使用 loading.tsx 和 Suspense 實作串流。

什麼是載入骨架。

什麼是路由群組,以及何時 might 使用它們。

在應用程式中放置 Suspense 邊界的位置。

什麼是串流?

串流是一種資料傳輸技術,它允許您將路由分解成更小的「區塊」,並在它們準備好時逐步從伺服器串流到用戶端。

Diagram showing time with sequential data fetching and parallel data fetching

透過串流,您可以防止緩慢的資料請求阻塞整個頁面。這允許使用者在不必等待所有資料載入之前就能看到並與頁面的部分內容互動。

Diagram showing time with sequential data fetching and parallel data fetching

串流與 React 的元件模型相容,因為每個元件都可以被視為一個*區塊*。

您可以透過兩種方式在 Next.js 中實作串流

  1. 在頁面層級,使用 loading.tsx 檔案。
  2. 對於特定元件,使用 <Suspense>

讓我們看看它是如何運作的。

使用 loading.tsx 串流整個頁面

/app/dashboard 資料夾中,建立一個名為 loading.tsx 的新檔案。

/app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}

重新整理 https://127.0.0.1:3000/dashboard,您現在應該會看到

Dashboard page with 'Loading...' text

這裡發生了幾件事情

  1. loading.tsx 是一個基於 Suspense 構建的特殊 Next.js 檔案,它允許您建立在頁面內容載入時顯示的 fallback UI。
  2. 由於 <SideNav> 是靜態的,它會立即顯示。使用者可以在動態內容載入時與 <SideNav> 互動。
  3. 使用者不必等待頁面完成載入即可離開(這稱為可中斷導航)。

恭喜!您剛剛實作了串流。但我們可以做更多事情來改善使用者體驗。讓我們顯示一個載入骨架,而不是 Loading… 文字。

新增載入骨架

載入骨架是簡化的 UI 版本。許多網站使用它們作為佔位符(或 fallback),向使用者指示內容正在載入。您在 loading.tsx 中新增的任何 UI 都將嵌入靜態檔案的一部分,並首先發送。然後,其餘的動態內容將從伺服器串流到客戶端。

在您的 loading.tsx 檔案中,導入一個名為 <DashboardSkeleton> 的新元件。

/app/dashboard/loading.tsx
import DashboardSkeleton from '@/app/ui/skeletons';
 
export default function Loading() {
  return <DashboardSkeleton />;
}

然後,重新整理 https://127.0.0.1:3000/dashboard,您現在應該會看到

Dashboard page with loading skeletons

使用路由群組修復載入骨架錯誤

目前,您的載入骨架也會套用至發票和客戶頁面。

由於 loading.tsx 在檔案系統中的層級高於 /invoices/page.tsx/customers/page.tsx,因此它也會套用至這些頁面。

我們可以使用路由群組來更改。在 dashboard 資料夾內建立一個名為 /(overview) 的新資料夾。然後,將 loading.tsxpage.tsx 檔案移到該資料夾內。

Folder structure showing how to create a route group using parentheses

現在,loading.tsx 檔案將只會套用至您的儀表板總覽頁面。

路由群組允許您將檔案組織成邏輯群組,而不會影響 URL 路徑結構。當您使用括號 () 建立新資料夾時,該名稱將不會包含在 URL 路徑中。因此 /dashboard/(overview)/page.tsx 會變成 /dashboard

在這裡,您正在使用路由群組來確保 loading.tsx 只套用至您的儀表板總覽頁面。然而,您也可以使用路由群組將您的應用程式分成不同的區段(例如 (marketing) 路由和 (shop) 路由),或者在大型應用程式中按團隊區分。

串流元件

到目前為止,您正在串流整個頁面。但您也可以更精細地使用 React Suspense 來串流特定元件。

Suspense 允許您延遲渲染應用程式的部分內容,直到滿足某些條件(例如載入資料)。您可以將動態元件包裝在 Suspense 中。然後,傳遞一個 fallback 元件,以便在載入動態元件時顯示。

如果您還記得緩慢的資料請求 fetchRevenue(),這就是導致整個頁面變慢的請求。您可以使用 Suspense 只串流這個元件,並立即顯示頁面 UI 的其餘部分,而不是阻塞整個頁面。

要這樣做,您需要將資料擷取移到元件中,讓我們更新程式碼來看看它的樣子。

/dashboard/(overview)/page.tsx 中刪除所有 fetchRevenue() 的實例及其資料。

/app/dashboard/(overview)/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data'; // remove fetchRevenue
 
export default async function Page() {
  const revenue = await fetchRevenue() // delete this line
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    // ...
  );
}

然後,從 React 導入 <Suspense>,並將其包裝在 <RevenueChart /> 周圍。您可以傳遞一個名為 <RevenueChartSkeleton> 的 fallback 元件。

/app/dashboard/(overview)/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
 
export default async function Page() {
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <Card title="Collected" value={totalPaidInvoices} type="collected" />
        <Card title="Pending" value={totalPendingInvoices} type="pending" />
        <Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
        <Card
          title="Total Customers"
          value={numberOfCustomers}
          type="customers"
        />
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        <Suspense fallback={<RevenueChartSkeleton />}>
          <RevenueChart />
        </Suspense>
        <LatestInvoices latestInvoices={latestInvoices} />
      </div>
    </main>
  );
}

最後,更新 <RevenueChart> 元件以擷取其自身的資料,並移除傳遞給它的 prop。

/app/ui/dashboard/revenue-chart.tsx
import { generateYAxis } from '@/app/lib/utils';
import { CalendarIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
// ...
 
export default async function RevenueChart() { // Make component async, remove the props
  const revenue = await fetchRevenue(); // Fetch data inside the component
 
  const chartHeight = 350;
  const { yAxisLabels, topLabel } = generateYAxis(revenue);
 
  if (!revenue || revenue.length === 0) {
    return <p className="mt-4 text-gray-400">No data available.</p>;
  }
 
  return (
    // ...
  );
}
 

現在重新整理頁面,您應該會幾乎立即看到儀表板資訊,同時會為 <RevenueChart> 顯示一個 fallback 骨架畫面。

Dashboard page with revenue chart skeleton and loaded Card and Latest Invoices components

練習:串流 <LatestInvoices>

現在輪到您了!透過串流 <LatestInvoices> 元件來練習您剛才學到的內容。

fetchLatestInvoices() 從頁面移到 <LatestInvoices> 元件。將元件包裝在具有名為 <LatestInvoicesSkeleton> 的 fallback 的 <Suspense> 邊界中。

準備好後,展開切換開關以查看解決方案程式碼。

將元件分組

太棒了!您快要完成了,現在您需要將 <Card> 元件包裝在 Suspense 中。您可以為每個卡片分別獲取資料,但這可能會導致卡片載入時產生*跳動*效果,這對使用者來說視覺上可能會感到突兀。

那麼,您將如何解決這個問題?

要創造更*錯開*的效果,您可以使用包裝器元件將卡片分組。這表示靜態的 <SideNav/> 會先顯示,然後是卡片,依此類推。

在您的 page.tsx 檔案中

  1. 刪除您的 <Card> 元件。
  2. 刪除 fetchCardData() 函式。
  3. 導入一個名為 <CardWrapper /> 的新**包裝器**元件。
  4. 導入一個名為 <CardsSkeleton /> 的新**骨架**元件。
  5. <CardWrapper /> 包裝在 Suspense 中。
/app/dashboard/page.tsx
import CardWrapper from '@/app/ui/dashboard/cards';
// ...
import {
  RevenueChartSkeleton,
  LatestInvoicesSkeleton,
  CardsSkeleton,
} from '@/app/ui/skeletons';
 
export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <Suspense fallback={<CardsSkeleton />}>
          <CardWrapper />
        </Suspense>
      </div>
      // ...
    </main>
  );
}

然後,移至檔案 /app/ui/dashboard/cards.tsx,導入 fetchCardData() 函式,並在 <CardWrapper/> 元件內呼叫它。請確保取消註釋此元件中所有必要的程式碼。

/app/ui/dashboard/cards.tsx
// ...
import { fetchCardData } from '@/app/lib/data';
 
// ...
 
export default async function CardWrapper() {
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    <>
      <Card title="Collected" value={totalPaidInvoices} type="collected" />
      <Card title="Pending" value={totalPendingInvoices} type="pending" />
      <Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
      <Card
        title="Total Customers"
        value={numberOfCustomers}
        type="customers"
      />
    </>
  );
}

重新整理頁面,您應該會看到所有卡片同時載入。當您希望多個元件同時載入時,可以使用這種模式。

決定 Suspense 邊界的放置位置

展望未來

串流和伺服器元件為我們提供了處理資料提取和載入狀態的新方法,最終目標是改善終端使用者體驗。

在下一章中,您將學習部分預渲染,這是一種以串流為核心構建的新 Next.js 渲染模型。

您已完成本章9

您已學習如何使用 Suspense 和載入骨架來串流元件。

下一步

10:部分預渲染

搶先了解部分預渲染 - 一種以串流構建的全新實驗性渲染模型。