資料獲取

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

選擇資料獲取方式

API 層

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

  • 當您使用提供 API 的第三方服務時。
  • 當您從客戶端獲取資料時,您希望有一個在伺服器上運行的 API 層,以避免將資料庫憑證暴露給客戶端。

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

資料庫查詢

當您建立一個全端應用程式時,您還需要編寫與資料庫交互的邏輯。對於像 Postgres 這樣的 關聯式資料庫 (relational databases),您可以使用 SQL 或 ORM

在以下幾種情況下,您需要編寫資料庫查詢:

  • 當建立 API 端點時,您需要編寫與資料庫交互的邏輯。
  • 如果您使用 React 伺服器元件 (Server Components)(在伺服器上獲取資料),您可以跳過 API 層,直接查詢資料庫,而不會冒著將資料庫憑證暴露給客戶端的風險。

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

使用伺服器元件獲取資料

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

  • 伺服器元件支援 JavaScript Promises,原生提供了處理異步任務(如資料獲取)的解決方案。您可以使用 async/await 語法,而不需要 useEffectuseState 或其他資料獲取函式庫。
  • 伺服器元件在伺服器上運行,因此您可以將昂貴的資料獲取和邏輯保留在伺服器上,僅將結果發送給客戶端。
  • 由於伺服器元件在伺服器上運行,您可以直接查詢資料庫,而無需額外的 API 層。這節省了編寫和維護額外代碼的時間。

使用 SQL

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

  • SQL 是查詢關聯式資料庫的行業標準(例如 ORM 在底層生成 SQL)。
  • 對 SQL 有基本了解可以幫助您理解關聯式資料庫的基礎知識,從而將這些知識應用到其他工具中。
  • SQL 功能強大,允許您獲取和操作特定資料。
  • postgres.js 函式庫提供了對 SQL 注入 (SQL injections) 的防護。

如果您以前沒有使用過 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. 資料請求無意中互相阻塞,形成了一個 請求瀑布 (request waterfall)
  2. 預設情況下,Next.js 預渲染 (prerenders) 路由以提高性能,這稱為 靜態渲染 (Static Rendering)。因此,如果您的資料發生變化,它不會反映在您的儀表板中。

讓我們在本章中討論第 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(); // 等待 fetchRevenue() 完成
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // 等待 fetchLatestInvoices() 完成

這種模式並不一定是壞的。在某些情況下,您可能希望使用瀑布,因為您希望在下一個請求之前滿足某個條件。例如,您可能希望先獲取用戶的 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 模式有一個 缺點:如果一個資料請求比其他請求慢,會發生什麼?讓我們在下一章中了解更多。

On this page