在上一章中,你通过流式处理提高了dashboard页面的初始加载性能。现在,让我们进入 /invoices 页面,学习如何添加搜索和分页!
在本章中,你将学到……
- 了解如何使用 Next.js API:
searchParams、usePathname和useRouter。 - 使用 URL 搜索参数实施搜索和分页。
开始代码
在 /dashboard/invoices/page.tsx 文件中,粘贴以下代码:
import Pagination from '@/app/ui/invoices/pagination';
import Search from '@/app/ui/search';
import Table from '@/app/ui/invoices/table';
import { CreateInvoice } from '@/app/ui/invoices/buttons';
import { lusitana } from '@/app/ui/fonts';
import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
import { Suspense } from 'react';
export default async function Page() {
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search placeholder="Search invoices..." />
<CreateInvoice />
</div>
{/* <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense> */}
<div className="mt-5 flex w-full justify-center">
{/* <Pagination totalPages={totalPages} /> */}
</div>
</div>
);
}
花一些时间熟悉页面和你将要使用的组件:
<Search/>允许用户搜索特定发票<Pagination/>允许用户在发票页面之间导航<Table/>显示发票
您的搜索功能将跨越客户端和服务器。当用户在客户端搜索发票时,将更新 URL 参数,在服务器上获取数据,并在服务器上用新数据重新渲染表格。
为什么使用URL搜索参数?
如上所述,您将使用 URL 搜索参数来管理搜索状态。如果您习惯于使用客户端状态,那么这种模式可能会让您感到陌生。
使用 URL 参数进行搜索有几个好处:
- 可收藏和共享的 URL:由于搜索参数包含在 URL 中,因此用户可以将应用程序的当前状态(包括搜索查询和筛选器)加入书签,以便将来参考或共享。
- 服务器端渲染和初始加载:URL 参数可直接在服务器上消耗,以呈现初始状态,从而更容易处理服务器呈现。
- 分析和跟踪:直接在 URL 中进行搜索查询和筛选,可以更轻松地跟踪用户行为,而无需额外的客户端逻辑。
添加搜索功能
这些是用于实现搜索功能的 Next.js 客户端钩子:
useSearchParams- 允许访问当前 URL 的参数。例如,该 URL/dashboard/invoices?page=1&query=pending的搜索参数如下:{page: '1', query: 'pending'}usePathname- 读取当前 URL 的路径名。例如,对于路径/dashboard/invoices,usePathname将返回'/dashboard/invoices'useRouter- 以编程方式在客户端组件内的路由之间进行导航。您可以使用多种方法
下面是实施步骤的简要概述:
- 获取用户输入
- 使用搜索参数更新 URL
- 保持 URL 与输入字段同步
- 更新表格以反映搜索查询
1. 获取用户输入
进入 <Search> 组件 ( /app/ui/search.tsx ),你就会发现:
"use client"- 这是一个客户端组件,这意味着您可以使用事件监听器和钩子。<input>- 这是搜索输入。
创建一个新的 handleSearch 函数,并为 <input> 元素添加一个 onChange 监听器。只要输入值发生变化, onChange 就会调用 handleSearch 。
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
export default function Search({ placeholder }: { placeholder: string }) {
function handleSearch(term: string) {
console.log(term);
}
return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
Search
</label>
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
);
}
在 "开发工具 "中打开控制台,然后在搜索字段中键入,测试是否正常工作。您应该会看到搜索词被记录到控制台中。
好极了!您获取到了用户的搜索输入。现在,您需要用搜索词更新 URL。
2. 用搜索参数更新URL
从 'next/navigation' 中导入 useSearchParams 钩子,并将其赋值给一个变量:
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
console.log(term);
}
// ...
}
在 handleSearch, 中,使用新的 searchParams 变量创建一个新的 URLSearchParams 实例。
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
}
// ...
}
URLSearchParams 是一个 Web API,它提供了用于操作 URL 查询参数的实用方法。您可以使用它来获取参数字符串,而不是创建一个复杂的字符串字面量,如 ?page=1&query=a 。
接下来,根据用户的输入 set 参数字符串。如果输入为空,则要 delete :
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
}
// ...
}
现在您已经有了查询字符串。您可以使用 Next.js 的 useRouter 和 usePathname 钩子来更新 URL。
从 'next/navigation' 中导入 useRouter 和 usePathname ,并在 handleSearch 中使用 useRouter() 中的 replace 方法:
// /app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, usePathname, useRouter } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}
}
以下是详细情况:
${pathname}是当前路径,在你的例子中是"/dashboard/invoices"。- 当用户在搜索栏中输入时,
params.toString()会将输入内容转换为 URL 友好格式。 replace(${pathname}?${params.toString()})使用用户的搜索数据更新 URL。例如,/dashboard/invoices?query=lee表示用户搜索 "Lee"。- 由于 Next.js 的客户端导航功能(在页面间导航一章中已有介绍),URL 的更新无需重新加载页面。
3. 保持URL和输入同步
为确保输入字段与 URL 同步,并在共享时填充,可以通过读取 searchParams 将 defaultValue 传递给输入:
// /app/ui/search.tsx
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
受控 vs. 非受控 如果您要使用状态来管理输入的值,您可以使用
value属性使其成为受控组件。这意味着 React 将管理输入的状态。 不过,由于您没有使用状态,因此可以使用defaultValue。这意味着本地输入将管理自己的状态。这也没有问题,因为你是将搜索查询保存到 URL 而不是状态。
4. 更新表格
最后,您需要更新表格组件,以显示搜索的查询结果。
返回发票页面。
页面组件接受一个名为 searchParams 的道具,因此您可以将当前 URL 参数传递给 <Table> 组件。
// /app/dashboard/invoices/page.tsx
import Pagination from '@/app/ui/invoices/pagination';
import Search from '@/app/ui/search';
import Table from '@/app/ui/invoices/table';
import { CreateInvoice } from '@/app/ui/invoices/buttons';
import { lusitana } from '@/app/ui/fonts';
import { Suspense } from 'react';
import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search placeholder="Search invoices..." />
<CreateInvoice />
</div>
<Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense>
<div className="mt-5 flex w-full justify-center">
{/* <Pagination totalPages={totalPages} /> */}
</div>
</div>
);
}
如果您导航到 <Table> 组件,就会看到 query 和 currentPage 这两个参数被传递到 fetchFilteredInvoices() 函数,该函数会返回与查询匹配的发票。
// /app/ui/invoices/table.tsx
// ...
export default async function InvoicesTable({
query,
currentPage,
}: {
query: string;
currentPage: number;
}) {
const invoices = await fetchFilteredInvoices(query, currentPage);
// ...
}
完成这些更改后,请进行测试。如果您搜索一个术语,您将更新 URL,URL 将向服务器发送一个新请求,服务器将获取数据,然后只返回与您的查询相匹配的发票。
何时使用
useSearchParams()钩子,何时使用searchParams属性?您可能已经注意到,您使用了两种不同的方法来提取搜索参数。使用其中一种还是另一种取决于您是在客户端还是在服务器端工作。
<Search>是一个客户端组件,因此您使用useSearchParams()钩子从客户端访问参数。<Table>是一个服务器组件,可以获取自己的数据,因此可以将searchParamsprop 从页面传递给该组件。一般来说,如果要从客户端读取参数,请使用
useSearchParams()钩子,因为这样可以避免返回服务器。
防抖Debouncing
恭喜您您已经使用 Next.js 实现了搜索!不过,您还可以做一些事情来优化它。
在 handleSearch 函数中,添加以下 console.log :
// /app/ui/search.tsx
function handleSearch(term: string) {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}
然后在搜索栏中输入 "Emil",查看开发工具中的控制台。发生了什么?
Searching... E
Searching... Em
Searching... Emi
Searching... Emil
每次按键都会更新 URL,因此每次按键都会查询数据库!由于我们的应用程序规模较小,这并不是什么问题,但试想一下,如果您的应用程序有成千上万的用户,每个用户每次按键都会向您的数据库发送一个新请求。
Debouncing是一种限制函数触发速度的编程方法。在我们的例子中,您只想在用户停止键入时查询数据库。
如何Debouncing
- 触发事件:当发生应予以解除的事件(如搜索框中的按键)时,计时器就会启动。
- 等待:如果在计时器到期前发生新事件,则重置计时器。
- 执行:如果计时器倒计时结束,则执行退保功能。
您可以通过多种方式实现去抖,包括手动创建自己的去抖函数。为了简单起见,我们将使用一个名为 use-debounce 的库。
npm i use-debounce
在您的 <Search> 组件中,导入一个名为 useDebouncedCallback 的函数:
// /app/ui/search.tsx
// ...
import { useDebouncedCallback } from 'use-debounce';
// Inside the Search Component...
const handleSearch = useDebouncedCallback((term) => {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}, 300);
该函数将封装 handleSearch 的内容,并且仅在用户停止键入后的特定时间(300 毫秒)后运行代码。
现在再次键入搜索栏,打开开发工具中的控制台。你应该会看到以下内容:
Searching... Emil
通过防抖,可以减少发送到数据库的请求数量,从而节省资源。
添加分页
引入搜索功能后,您会发现表格每次只显示 6 张发票。这是因为 data.ts 中的 fetchFilteredInvoices() 函数每页最多返回 6 张发票。
通过添加分页功能,用户可以浏览不同页面,查看所有发票。让我们看看如何使用 URL 参数实现分页,就像使用搜索一样。
导航至 <Pagination/> 组件,您会发现这是一个客户端组件。您不想在客户端获取数据,因为这会暴露您的数据库机密(请记住,您使用的不是 API 层)。相反,您可以在服务器上获取数据,并将其作为道具传递给组件。
在 /dashboard/invoices/page.tsx 中,导入名为 fetchInvoicesPages 的新函数,并将 searchParams 中的 query 作为参数传递:
// /app/dashboard/invoices/page.tsx
// ...
import { fetchInvoicesPages } from '@/app/lib/data';
export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string,
page?: string,
},
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
const totalPages = await fetchInvoicesPages(query);
return (
// ...
);
}
fetchInvoicesPages 根据搜索查询返回页面总数。例如,如果有 12 张发票与搜索查询匹配,每页显示 6 张发票,那么页面总数就是 2。
接下来,将 totalPages prop 传递给 <Pagination/> 组件:
/app/dashboard/invoices/page.tsx
// ...
export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
const totalPages = await fetchInvoicesPages(query);
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search placeholder="Search invoices..." />
<CreateInvoice />
</div>
<Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense>
<div className="mt-5 flex w-full justify-center">
<Pagination totalPages={totalPages} />
</div>
</div>
);
}
导航至 <Pagination/> 组件并导入 usePathname 和 useSearchParams 钩子。我们将用它来获取当前页面并设置新页面。请确保取消注释该组件中的代码。由于您尚未执行 <Pagination/> 逻辑,您的应用程序将暂时中断。让我们现在就开始吧!
/app/ui/invoices/pagination.tsx
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
// ...
}
接下来,在 <Pagination> 组件中创建一个名为 createPageURL 的新函数。与搜索类似,您将使用 URLSearchParams 设置新的页码,并使用 pathName 创建 URL 字符串。
/app/ui/invoices/pagination.tsx
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
// ...
}
以下是详细情况:
createPageURL创建当前搜索参数的实例- 然后,它会将 "页面 "参数更新为所提供的页码
- 最后,它会使用路径名和更新的搜索参数构建完整的 URL
<Pagination> 组件的其余部分代码,在本课程中,我们将不作详细介绍,但你可以通过查看 createPageURL 被调用位置的代码来学习。
最后,当用户键入新的搜索查询时,您希望将页码重置为 1。您可以通过更新 <Search> 组件中的 handleSearch 函数来实现这一目标:
/app/ui/search.tsx
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
export default function Search({ placeholder }: { placeholder: string }) {
const searchParams = useSearchParams();
const { replace } = useRouter();
const pathname = usePathname();
const handleSearch = useDebouncedCallback((term) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}, 300);
摘要
恭喜您,您刚刚使用 URL Params 和 Next.js API 实现了搜索和分页功能!
总之,在本章中
- 使用 URL 搜索参数而不是客户端状态来处理搜索和分页。
- 在服务器上获取数据。
- 使用
useRouter路由器钩子,以实现更流畅的客户端过渡。
这些开发方法与您在使用客户端 React 时可能有所不同,但现在你已经能够使用 URL 搜索参数并了解了如何将此状态提交到服务端。