跳到內容

7

資料擷取

既然您已建立並植入資料庫,讓我們討論您可以為應用程式擷取資料的不同方式,並建構您的儀表板總覽頁面。

在本章中...

以下是我們將涵蓋的主題

了解一些資料擷取的方法:API、ORM、SQL 等。

伺服器元件如何協助您更安全地存取後端資源。

什麼是網路瀑布流。

如何使用 JavaScript 模式實作平行資料擷取。

選擇如何擷取資料

API 層

API 是應用程式碼和資料庫之間的中介層。在幾種情況下,您可能會使用 API

  • 如果您使用提供 API 的第三方服務。
  • 如果您從用戶端擷取資料,您會希望有一個在伺服器上執行的 API 層,以避免將您的資料庫密碼洩露給用戶端。

在 Next.js 中,您可以使用路由處理器建立 API 端點。

資料庫查詢

當您建立全端應用程式時,您也需要編寫邏輯來與資料庫互動。對於關聯式資料庫(例如 Postgres),您可以使用 SQL 或 ORM 來執行此操作。

在幾種情況下,您必須編寫資料庫查詢

  • 當建立 API 端點時,您需要編寫邏輯來與資料庫互動。
  • 如果您使用 React 伺服器元件(在伺服器上擷取資料),您可以略過 API 層,並直接查詢資料庫,而不會有將資料庫密碼洩露給用戶端的風險。

讓我們進一步了解 React 伺服器元件。

使用伺服器元件擷取資料

預設情況下,Next.js 應用程式使用React 伺服器元件。使用伺服器元件擷取資料是一種相對較新的方法,使用它們有幾個好處

  • 伺服器元件支援 JavaScript Promise,為非同步任務(如資料擷取)提供原生解決方案。您可以使用 async/await 語法,而無需 useEffectuseState 或其他資料擷取函式庫。
  • 伺服器元件在伺服器上執行,因此您可以將昂貴的資料擷取和邏輯保留在伺服器上,僅將結果傳送到用戶端。
  • 由於伺服器元件在伺服器上執行,因此您可以直接查詢資料庫,而無需額外的 API 層。這使您無需編寫和維護額外的程式碼。

使用 SQL

對於您的儀表板應用程式,您將使用 postgres.js 函式庫和 SQL 編寫資料庫查詢。我們將使用 SQL 有幾個原因

  • SQL 是查詢關聯式資料庫的產業標準(例如,ORM 在底層產生 SQL)。
  • 對 SQL 有基本的了解可以幫助您了解關聯式資料庫的基礎知識,讓您可以將您的知識應用於其他工具。
  • SQL 功能多樣,可讓您擷取和操作特定資料。
  • postgres.js 函式庫提供針對 SQL 注入攻擊 的保護。

如果您之前沒有使用過 SQL,請別擔心 – 我們已為您提供查詢。

前往 /app/lib/data.ts。在這裡您會看到我們正在使用 postgressql 函式 允許您查詢資料庫

/app/lib/data.ts
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...

您可以在伺服器上的任何位置呼叫 sql,例如伺服器元件。但為了讓您更輕鬆地瀏覽元件,我們將所有資料查詢都保留在 data.ts 檔案中,您可以將它們匯入到元件中。

注意:如果您在第 6 章中使用了自己的資料庫供應商,則需要更新資料庫查詢以與您的供應商搭配使用。您可以在 /app/lib/data.ts 中找到查詢。

為儀表板總覽頁面擷取資料

現在您已了解擷取資料的不同方式,讓我們為儀表板總覽頁面擷取資料。導航至 /app/dashboard/page.tsx,貼上以下程式碼,並花一些時間探索它

/app/dashboard/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';
 
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">
        {/* <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">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

上面的程式碼已刻意註解掉。我們現在將開始範例說明每個部分。

  • page 是一個 async 伺服器元件。這允許您使用 await 擷取資料。
  • 還有 3 個元件接收資料:<Card><RevenueChart><LatestInvoices>。它們目前已註解掉,尚未實作。

<RevenueChart/> 擷取資料

為了為 <RevenueChart/> 元件擷取資料,請從 data.ts 匯入 fetchRevenue 函式,並在您的元件內呼叫它

/app/dashboard/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 { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

接下來,讓我們執行以下操作

  1. 取消註解 <RevenueChart/> 元件。
  2. 導航至元件檔案 (/app/ui/dashboard/revenue-chart.tsx) 並取消註解其中的程式碼。
  3. 檢查 localhost:3000,您應該會看到一個使用 revenue 資料的圖表。
Revenue chart showing the total revenue for the last 12 months

讓我們繼續匯入更多資料並將其顯示在儀表板上。

<LatestInvoices/> 擷取資料

對於 <LatestInvoices /> 元件,我們需要取得最新的 5 筆發票,並依日期排序。

您可以擷取所有發票,並使用 JavaScript 對其進行排序。在我們的資料量小的情況下,這不是問題,但是隨著應用程式的成長,它可能會顯著增加每次請求傳輸的資料量以及排序所需的 JavaScript。

您可以改用 SQL 查詢來僅擷取最後 5 筆發票,而不是在記憶體中排序最新的發票。例如,這是來自 data.ts 檔案的 SQL 查詢

/app/lib/data.ts
// Fetch the last 5 invoices, sorted by date
const data = await sql<LatestInvoiceRaw[]>`
  SELECT invoices.amount, customers.name, customers.image_url, customers.email
  FROM invoices
  JOIN customers ON invoices.customer_id = customers.id
  ORDER BY invoices.date DESC
  LIMIT 5`;

在您的頁面中,匯入 fetchLatestInvoices 函式

/app/dashboard/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 { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

然後,取消註解 <LatestInvoices /> 元件。您還需要取消註解 <LatestInvoices /> 元件本身的相關程式碼,該程式碼位於 /app/ui/dashboard/latest-invoices

如果您造訪您的 localhost,您應該會看到僅從資料庫傳回最後 5 筆。希望您開始看到直接查詢資料庫的優勢!

Latest invoices component alongside the revenue chart

練習:為 <Card> 元件擷取資料

現在輪到您為 <Card> 元件擷取資料了。卡片將顯示以下資料

  • 收取的發票總金額。
  • 待處理的發票總金額。
  • 發票總數。
  • 客戶總數。

同樣地,您可能會想擷取所有發票和客戶,並使用 JavaScript 來操作資料。例如,您可以使用 Array.length 來取得發票和客戶的總數

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

但是使用 SQL,您可以僅擷取您需要的資料。它比使用 Array.length 稍長,但這意味著在請求期間需要傳輸的資料更少。這是 SQL 替代方案

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

您需要匯入的函式稱為 fetchCardData。您將需要解構從函式傳回的值。

提示

  • 檢查卡片元件以查看它們需要哪些資料。
  • 檢查 data.ts 檔案以查看函式傳回的內容。

準備就緒後,展開下方的切換按鈕以取得最終程式碼

太棒了!您現在已擷取儀表板總覽頁面的所有資料。您的頁面應如下所示

Dashboard page with all the data fetched

但是...有兩件事您需要注意

  1. 資料請求正在無意間互相封鎖,產生請求瀑布流
  2. 預設情況下,Next.js 預先渲染路由以提高效能,這稱為 靜態渲染。因此,如果您的資料變更,它將不會反映在您的儀表板中。

讓我們在本章中討論第 1 點,然後在下一章中詳細了解第 2 點。

什麼是請求瀑布流?

「瀑布流」指的是一系列網路請求,這些請求依賴於先前請求的完成。在資料擷取的情況下,每個請求只能在前一個請求傳回資料後開始。

Diagram showing time with sequential data fetching and parallel data fetching

例如,我們需要等待 fetchRevenue() 執行完畢,然後 fetchLatestInvoices() 才能開始執行,依此類推。

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // wait for fetchLatestInvoices() to finish

這種模式不一定不好。在某些情況下,您可能需要瀑布流,因為您希望在發出下一個請求之前滿足條件。例如,您可能想要先擷取使用者的 ID 和個人資料資訊。取得 ID 後,您可能會繼續擷取他們的朋友列表。在這種情況下,每個請求都取決於前一個請求傳回的資料。

但是,這種行為也可能是無意的,並影響效能。

平行資料擷取

避免瀑布流的常見方法是同時啟動所有資料請求 – 平行處理。

在 JavaScript 中,您可以使用 Promise.all()Promise.allSettled() 函式來同時啟動所有 Promise。例如,在 data.ts 中,我們在 fetchCardData() 函式中使用 Promise.all()

/app/lib/data.ts
export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`SELECT
         SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
         SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
         FROM invoices`;
 
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

透過使用此模式,您可以

  • 同時開始執行所有資料擷取,這比等待每個請求在瀑布流中完成更快。
  • 使用可以應用於任何函式庫或框架的原生 JavaScript 模式。

但是,僅依賴此 JavaScript 模式有一個缺點:如果一個資料請求比所有其他資料請求都慢,會發生什麼情況?讓我們在下一章中了解更多資訊。

您已完成章節7

您已了解在 Next.js 中擷取資料的一些不同方法。

下一步

8:靜態和動態渲染

了解 Next.js 中的不同渲染模式。