7
章節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
語法,而無需useEffect
、useState
或其他資料擷取函式庫。 - 伺服器元件在伺服器上執行,因此您可以將昂貴的資料擷取和邏輯保留在伺服器上,僅將結果傳送到用戶端。
- 由於伺服器元件在伺服器上執行,因此您可以直接查詢資料庫,而無需額外的 API 層。這使您無需編寫和維護額外的程式碼。
使用 SQL
對於您的儀表板應用程式,您將使用 postgres.js 函式庫和 SQL 編寫資料庫查詢。我們將使用 SQL 有幾個原因
- SQL 是查詢關聯式資料庫的產業標準(例如,ORM 在底層產生 SQL)。
- 對 SQL 有基本的了解可以幫助您了解關聯式資料庫的基礎知識,讓您可以將您的知識應用於其他工具。
- SQL 功能多樣,可讓您擷取和操作特定資料。
postgres.js
函式庫提供針對 SQL 注入攻擊 的保護。
如果您之前沒有使用過 SQL,請別擔心 – 我們已為您提供查詢。
前往 /app/lib/data.ts
。在這裡您會看到我們正在使用 postgres
。sql
函式 允許您查詢資料庫
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
,貼上以下程式碼,並花一些時間探索它
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
函式,並在您的元件內呼叫它
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:3000
,您應該會看到一個使用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
。
如果您造訪您的 localhost,您應該會看到僅從資料庫傳回最後 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 預先渲染路由以提高效能,這稱為 靜態渲染。因此,如果您的資料變更,它將不會反映在您的儀表板中。
讓我們在本章中討論第 1 點,然後在下一章中詳細了解第 2 點。
什麼是請求瀑布流?
「瀑布流」指的是一系列網路請求,這些請求依賴於先前請求的完成。在資料擷取的情況下,每個請求只能在前一個請求傳回資料後開始。


例如,我們需要等待 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()
或 Promise.allSettled()
函式來同時啟動所有 Promise。例如,在 data.ts
中,我們在 fetchCardData()
函式中使用 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 模式有一個缺點:如果一個資料請求比所有其他資料請求都慢,會發生什麼情況?讓我們在下一章中了解更多資訊。
這有幫助嗎?