Next.js14从入门到实战012:NextJS基础篇之页面获取数据
现在,您已经创建了数据库并为其添加了数据,让我们来讨论为应用程序获取数据的不同方法,并构建dashboard概览页面。
本章你将学习到
- 了解一些获取数据的方法:应用程序接口、ORM、SQL 等。
- 服务器组件如何帮助您更安全地访问后端资源。
- 什么是网络瀑布
- 如何使用 JavaScript Pattern实现并行数据获取。
选择获取数据的方式
API layer 应用程序接口层
应用程序接口是应用程序代码和数据库之间的中间层。您可能会在以下几种情况下使用 API:
- 如果您使用的是提供 API 的第三方服务。
- 如果要从客户端获取数据,则需要在服务器上运行 API 层,以避免向客户端暴露数据库密钥。
在 Next.js 中,您可以使用路由控制器来创建 API 。
数据库查询
在创建全栈应用程序时,您还需要编写与数据库交互的逻辑。对于 Postgres 这样的关系型数据库,可以使用 SQL 或 Prisma 这样的 ORM 来实现。
当然,在一些情况下,您还需要编写数据库查询:
- 创建 API 端点时,您需要编写与数据库交互的逻辑。
- 如果使用的是 React 服务端组件(在服务器上获取数据),则可以跳过 API 层,直接查询数据库,而不必冒着向客户端暴露数据库机密的风险。
让我们进一步了解 React 服务器组件。
使用服务端组件获取数据
默认情况下,Next.js 应用程序使用 React 服务端组件。使用服务器组件获取数据是一种相对较新的方法,使用它们有一些好处:
-
服务端组件支持
promises
,为数据获取等异步任务提供了更简单的解决方案。您可以使用async/await
语法,而无需使用useEffect
、useState
或数据获取库。 -
服务端组件在服务器上执行,因此可以在服务器上保留昂贵的数据获取和逻辑,只将结果发送给客户端。
-
如前所述,由于服务端组件是在服务器上执行的,因此可以直接查询数据库,而无需额外的 API 层。
使用SQL
在dashboard项目中,您将使用 Vercel Postgres SDK 和 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
文件中保留了所有数据查询,你可以将它们导入到组件中。
为dashboard 页面获取数据
现在,您已经了解了获取数据的不同方法,让我们为为dashboard页面获取数据。打开 /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
) 并取消对其中代码的注释。检查本地主机,应该可以看到使用 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
文件,查看函数的返回值。
怎样?如果你已经写完了,可以核对下下面的答案
// /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,
fetchCardData,
} from '@/app/lib/data';
export default async function Page() {
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices();
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
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>
);
}
你现在已经获取了仪表盘概览页面的所有数据。您的页面应该是这样的
不过......有两件事您需要注意:
- 数据请求无意中相互阻塞,形成了请求瀑布。
- 默认情况下,Next.js 会预先渲染路由以提高性能,这就是所谓的静态渲染。因此,如果数据发生变化,它不会反映在dashboard上。
什么是请求瀑布?
瀑布指的是一连串依赖于前一个请求完成的网络请求。就数据获取而言,只有在前一个请求返回数据后,才能开始每个请求。
例如,在 fetchLatestInvoices()
开始运行之前,我们需要等待 fetchRevenue()
执行,依此类推。
// /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.js
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 模式。
不过,这种模式有一个缺点:如果一个数据请求比其他所有数据请求都慢,该怎么办?
转载自:https://juejin.cn/post/7360879591236681762