7
章節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
語法,而無需使用useEffect
、useState
或資料擷取函式庫。 - 伺服器元件在伺服器上執行,因此您可以將耗費資源的資料擷取和邏輯保留在伺服器上,只將結果傳送到客戶端。
- 如前所述,由於伺服器元件在伺服器上執行,您可以直接查詢資料庫,而無需額外的 API 層。
使用 SQL和 SQL 撰寫資料庫查詢。我們使用 SQL 有幾個原因:
- SQL 是查詢關聯式資料庫的行業標準(例如,ORM 在底層會產生 SQL)。
- 具備 SQL 的基本概念能幫助您理解關聯式資料庫的基礎知識,讓您將這些知識應用到其他工具上。
- SQL 功能廣泛,可讓您擷取和操作特定資料。
- Vercel Postgres SDK 提供了防止 SQL 注入
的保護機制。
如果您以前沒有使用過 SQL,不用擔心,我們已經為您準備好了查詢語句。
請前往 /app/lib/data.ts
,您會看到我們從 @vercel/postgres
導入了 sql
import { sql } from '@vercel/postgres';
// ...
您可以在任何伺服器元件中呼叫 sql
。但為了讓您更輕鬆地瀏覽元件,我們已將所有資料查詢保留在 data.ts
檔案中,您可以將它們導入到元件中。
注意:如果您在第六章中使用了自己的資料庫供應器,則需要更新資料庫查詢才能與您的供應器搭配使用。您可以在
/app/lib/data.ts
中找到這些查詢。
擷取儀表板概覽頁面的資料
現在您已經了解了不同的資料擷取方式,讓我們來擷取儀表板概覽頁面的資料。請前往 /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
函式,並在您的元件內呼叫它。
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
資料的圖表。


讓我們繼續導入更多資料查詢!
正在擷取 <LatestInvoices/>
的資料
對於 <LatestInvoices />
元件,我們需要取得按日期排序的最新 5 張發票。
您可以擷取所有發票,然後使用 JavaScript 排序。由於我們的資料量很小,這不成問題,但隨著應用程式的增長,每次請求傳輸的資料量和排序所需的 JavaScript 都會顯著增加。
您可以使用 SQL 查詢來僅擷取最新的 5 張發票,而不是在記憶體中排序所有最新的發票。例如,這是 data.ts
檔案中的 SQL 查詢
// 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
函式
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 張發票。希望您開始看到直接查詢資料庫的優點!


練習:擷取 <Card>
元件的資料
現在輪到您擷取 <Card>
元件的資料。卡片將顯示以下資料
- 已收發票總額。
- 待處理發票總額。
- 發票總數。
- 客戶總數。
同樣地,您可能會想擷取所有發票和客戶,然後使用 JavaScript 來操作資料。例如,您可以使用 Array.length
來取得發票和客戶的總數
const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;
但是使用 SQL,您可以只擷取所需的資料。雖然比使用 Array.length
稍微長一點,但這表示在請求期間需要傳輸的資料更少。以下是 SQL 的替代方案
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
您需要導入的函式稱為 fetchCardData
。您需要解構從函式回傳的值。
提示
- 檢查卡片元件以查看它們需要哪些資料。
- 檢查
data.ts
檔案以查看函式回傳的內容。
準備好之後,展開下面的切換開關以查看最終程式碼
太棒了!您現在已經擷取了儀表板概覽頁面的所有資料。您的頁面應該如下所示


然而... 有兩件事您需要注意
- 資料請求會意外地互相阻塞,造成**請求瀑布**。
- 預設情況下,Next.js 會**預先渲染**路由以提升效能,這稱為**靜態渲染**。因此,如果您的資料發生變化,儀表板上將不會反映出來。
我們將在本章討論第一點,然後在下一章詳細探討第二點。
什麼是請求瀑布流?
「瀑布流」指的是一系列網路請求,後續請求取決於先前請求的完成。在資料擷取的情況下,每個請求只能在前一個請求返回資料後才能開始。


例如,我們需要等待 `fetchRevenue()` 執行完畢後,`fetchLatestInvoices()` 才能開始運行,依此類推。
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()`
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 模式有一個**缺點**:如果其中一個資料請求比其他所有請求都慢,會發生什麼事?
這有幫助嗎?