跳至內容

7

資料獲取

現在您已經建立並填充了資料庫,讓我們來討論為應用程式獲取資料的不同方法,並建構您的儀表板概覽頁面。

本章內容...

以下是我們將涵蓋的主題

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

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

什麼是網路瀑布圖。

如何使用 JavaScript 模式實現平行資料獲取。

選擇如何獲取資料

API 層

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

  • 如果您正在使用提供 API 的第三方服務。
  • 如果您是從客戶端獲取資料,您需要一個在伺服器上執行的 API 層,以避免將資料庫機密洩露給客戶端。

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

資料庫查詢

當您在建立全端應用程式時,您也需要撰寫與資料庫互動的邏輯。對於像 Postgres 這樣的關聯式資料庫,您可以使用 SQL 或ORM來完成。

在某些情況下,您必須撰寫資料庫查詢:

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

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

使用伺服器元件擷取資料

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

  • 伺服器元件支援 promise,為資料擷取等非同步任務提供更簡單的解決方案。您可以使用 async/await 語法,而無需使用 useEffectuseState 或資料擷取函式庫。
  • 伺服器元件在伺服器上執行,因此您可以將耗費資源的資料擷取和邏輯保留在伺服器上,只將結果傳送到客戶端。
  • 如前所述,由於伺服器元件在伺服器上執行,您可以直接查詢資料庫,而無需額外的 API 層。

使用 SQL Vercel Postgres SDK和 SQL 撰寫資料庫查詢。我們使用 SQL 有幾個原因:

  • SQL 是查詢關聯式資料庫的行業標準(例如,ORM 在底層會產生 SQL)。
  • 具備 SQL 的基本概念能幫助您理解關聯式資料庫的基礎知識,讓您將這些知識應用到其他工具上。
  • SQL 功能廣泛,可讓您擷取和操作特定資料。
  • Vercel Postgres SDK 提供了防止 SQL 注入 的保護機制。

如果您以前沒有使用過 SQL,不用擔心,我們已經為您準備好了查詢語句。

請前往 /app/lib/data.ts,您會看到我們從 @vercel/postgres 導入了 sql 函式。這個函式允許您查詢資料庫。

/app/lib/data.ts
import { sql } from '@vercel/postgres';
 
// ...

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

注意:如果您在第六章中使用了自己的資料庫供應器,則需要更新資料庫查詢才能與您的供應器搭配使用。您可以在 /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 是一個 **非同步** 元件。這允許您使用 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();
  // ...
}

然後,取消註釋 <RevenueChart/> 元件,前往元件檔案 (/app/ui/dashboard/revenue-chart.tsx) 並取消註釋其中的程式碼。檢查您的 localhost,您應該可以看到使用 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

如果您瀏覽您的本地主機,您應該會看到資料庫只回傳最新的 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 會**預先渲染**路由以提升效能,這稱為**靜態渲染**。因此,如果您的資料發生變化,儀表板上將不會反映出來。

我們將在本章討論第一點,然後在下一章詳細探討第二點。

什麼是請求瀑布流?

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

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 中不同的渲染模式。