nextjs(持续学习中)

项目创建

注意node版本18.17以上

mock -> 模拟数据 ; prisma -> 根据数据库架构自动生成类型

mockapi
prisma

npx create-next-app@latest “你的项目名称” --use-npm --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example" 
cd 你的项目名称
npm i
npm run dev
目录说明
app包含应用程序的所有路由、组件和逻辑(主要)
app/lib包含应用程序中使用的函数
app/ui包含应用程序的所有 UI 组件
public包含应用程序的所有静态资产
scripts脚本,使用它来填充数据库
next.config.js配置文件 (无需修改)

css

Tailwind 和 CSS 模块是 Next.js 应用程序样式的两种最常见方式。
无论您使用一个还是另一个都是偏好问题 - 您甚至可以在同一应用程序中同时使用两者!

  1. Tailwind
    Tailwind 是一个 CSS 框架,它允许您直接在 TSX 标记中快速编写实用程序类,从而加快开发过程。
 <div className="h-0 w-0 border-b-[30px] border-l-[20px] border-r-[20px] border-b-black border-l-transparent border-r-transparent" />
  1. CSS modules
    CSS 模块允许您通过自动创建唯一的类名来将 CSS 范围限定为组件,因此您也不必担心样式冲突(scope)。
    在里面 /app/ui ,创建一个新文件,调用 home.module.css 并添加以下 CSS 规则:
.shape {
  height: 0;
  width: 0;
  border-bottom: 30px solid black;
  border-left: 20px solid transparent;
  border-right: 20px solid transparent;
}

然后,在您的 /app/page.tsx 文件中导入样式,并将

您添加的 Tailwind 类名替换为 styles.shape

import styles from '@/app/ui/home.module.css';
<div className={styles.shape} />;
  1. clsx
    根据状态或其他条件有条件地设置元素的样式,轻松切换类名的库
// 导入
import clsx from 'clsx';
// 使用clsx()
<span className={clsx(
        'inline-flex items-center rounded-full px-2 py-1 text-sm',
        {
          'bg-gray-100 text-gray-500': status === 'pending',
          'bg-green-500 text-white': status === 'paid',
        },
      )}
    >
  1. 其他…

优化字体和图像

图像优化文档
字体优化文档
使用图像提高Web性能(MDN)
Web 字体 (MDN)

  • 字体在网站设计中起着重要作用,但如果需要获取和加载字体文件,则在项目中使用自定义字体可能会影响性能
  • 累积布局偏移是 Google 用于评估网站性能和用户体验的指标。 对于字体,当浏览器最初以后备字体或系统字体呈现文本,然后在加载后将其交换为自定义字体时,就会发生布局转换。 这种交换可能会导致文本大小、间距或布局发生变化,从而移动其周围的元素。
  • Next.js 会在您使用该 next/font 模块时自动优化应用程序中的字体。它会在构建时下载字体文件,并将它们与其他静态资产一起托管。这意味着,当用户访问您的应用程序时,不会有额外的网络请求字体,影响性能。(它将字体文件与其他静态资产一起托管,因此没有其他网络请求)
/**
 * 在您的 /app/ui 文件夹中,创建一个名为 fonts.ts .您将使用此文件来保留将在整个应用程序中使用的字体。
 * 从 next/font/google 模块导入 Inter 字体,指定要加载的子集(主字体)
 */

import { Inter,Lusitana } from 'next/font/google';                                         
// 指定子集
export const inter = Inter({ subsets: ['latin'] }); 
// 添加辅助字体Lusitana 
// 指定子集以及指定字体粗细。 
export const lusitana = Lusitana({
    weight: ['400', '700'],
    subsets: ['latin'],
});

在/app/layout.tsx文件中将字体添加到以下 元素中

/**
 * 通过添加到 Inter 元素中 <body> ,字体将应用于整个应用程序。
 * 在这里,您还添加了 Tailwind antialiased 类,该类使字体平滑。
 * 没有必要使用这个类,但它增加了一个很好的触感。
 * 导航到浏览器,打开开发工具并选择 body 元素。
 * 您应该看到 Inter 并且 Inter_Fallback 现在已应用在样式下
 */
import '@/app/ui/global.css';
// 导入字体
import { inter } from '@/app/ui/fonts';                      
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      {/* <body>{children}</body> */}
      {/* 使用字体 */}
      <body className={`${inter.className} antialiased`}>{children}</body>
    </html>
  );
}

在 /app/page.tsx 文件中找到的

元素,使用辅助字体。

// 使用辅助字体
import { lusitana } from '@/app/ui/fonts';                 

export default function Page() {
  return (
    <p className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}>
            <strong>Welcome to Acme.</strong> This is the example for the{' '}
            <a href="https://nextjs.org/learn/" className="text-blue-500">
              Next.js Learn Course
            </a>
            , brought to you by Vercel.
          </p>
  );
}

在顶级 /public 文件夹下提供静态资产
默认 /public 文件夹会被映射网站的根目录

即/public/img.png -> /img.png

使用常规 HTML(不推荐)

/**
* 使用常规 HTML,必须手动
* 确保您的图像在不同的屏幕尺寸上具有响应性
* 指定不同设备的图像大小
* 防止在加载图像时出现布局偏移
* 延迟加载用户视口之外的图像
* --------------------------------------------------
* 图像优化是 Web 开发中的一个大主题,本身可以被视为一个专业化。
* 您可以使用该 next/image 组件自动优化图像,而不是手动实现这些优化。
* ------------------------------
* 自动图像优化功能:
* 1、防止在加载图像时自动移动布局
* 2、调整图像大小以避免将大图像传送到视口较小的设备。
* 3、默认情况下延迟加载图像(图像在进入视口时加载->懒加载)。
* 4、在浏览器支持的情况下,以现代格式(如 WebP 和 AVIF)提供图像。
*
**/

<img
  src="/hero.png"
  alt="Screenshots of the dashboard project showing desktop version"                                                                                             
/>

使用 next/image 组件自动优化图像(推荐

在 /app/page.tsx 文件中,导入 next/image 组件。

根据image 组件设置的宽高进行设定
默认移动端优先原则,md: 更宽的屏幕 block:以块级元素显示
className=“hidden md:block” 默认hidden 隐藏,md: 更宽的屏幕显示

/**
* 应该设置图片的宽高(避免布局偏移(从无到有))
* 宽高应该与原图的纵横比相同
* 您还会注意到该类 hidden 用于从移动屏幕上的 DOM 中删除图像
* md:block 桌面屏幕上显示图像。
**/
import Image from 'next/image';                                                                                       
export default function Page() {
  return (
    <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
      {/* Add Hero Images Here */}
      <Image
        src="/hero-desktop.png"
        width={1000}
        height={760}
        className="hidden md:block"
        alt="Screenshots of the dashboard project showing desktop version"
      />
      {/* 显示在移动屏幕上,并在桌面上隐藏 */}
       <Image
        src="/hero-mobile.png"
        width={560}
        height={620}
        className="block md:hidden"
        alt="Screenshot of the dashboard project showing mobile version"
      />
    </div>
  );
}

创建布局和页面

特殊文件说明
page.tsx用于导出 React 组件,并且路由是可访问所必需的(不同文件下访问入口)
layout.tsx布局文件(不同文件下的布局入口)
loading.tsx创建后备 UI 以在页面内容加载时显示为替换

创建首页入口文件:/app/dashboard/page.tsx
url地址:http://localhost:3000/dashboard

export default function Page() {
  return <p>Dashboard Page</p>;
}

在Next.js中使用布局的一个好处是,在导航时,只有页面组件会更新,而布局不会重新呈现,这称为部分渲染。
创建首页布局文件:/app/dashboard/layout.tsx

import SideNav from '@/app/ui/dashboard/sidenav';
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
      <div className="w-full flex-none md:w-64">
        <SideNav />
      </div>
      <div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
    </div>
  );
}

页面导航

页面之间链接,通常使用<a> HTML 元素,每次切换页面都会被刷新。
--------------------
在 Next.js 中,可以使用 <Link /> 组件在应用程序中的页面之间进行链接。
<Link> 允许您使用 JavaScript 进行客户端导航。

next/link 如何实现一个Web 应用?

  • Next.js会自动按路由段拆分应用程序。
  • 这与传统的 React SPA 不同,在传统的 React SPA 中,浏览器在初始加载时加载所有应用程序代码
  • 按路由拆分代码意味着页面变得独立。如果某个页面抛出错误,应用程序的其余部分仍将正常工作。
  • 此外,在生产环境中,每当组件出现在浏览器的视口中时Link,Next.js都会在后台自动预取链接路由的代码。当用户单击链接时,目标页面的代码已经在后台加载,这就是使页面转换近乎即时的原因!

打开/app/ui/dashboard/nav-links.tsx文件, 导入 Link 组件。


// 使用Link
import Link from 'next/link';
// 然后,找到 <a> 标签替换为 <Link>
<Link
   key={link.name}
   href={link.href}
   className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
   <LinkIcon className="w-6" />
   <p className="hidden md:block">{link.name}</p>
</Link>

获取用户的当前路径,Next.js提供了一个钩子函数usePathname()
使用客户端渲染(必须):作用于浏览器
默认情况下,Next.js应用程序使用 React Server 组件(服务端渲染)

'use client'; // 使用客户端渲染(必须)
import {
  UserGroupIcon,
  HomeIcon,
  DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
// 导入钩子函数usePathname
import { usePathname } from 'next/navigation';
// usePathname搭配clsx使用
import clsx from 'clsx';
// 导航列表
const links = [
  { name: 'Home', href: '/dashboard', icon: HomeIcon },
  {
    name: 'Invoices',
    href: '/dashboard/invoices',
    icon: DocumentDuplicateIcon,
  },
  { name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
];

export default function NavLinks() {
  const pathname = usePathname();
   return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <Link
            key={link.name}
            href={link.href}
            className={clsx(
              'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
              {
                'bg-sky-100 text-blue-600': pathname === link.href,
              },
            )}
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

设置数据库

使用@vercel/postgres设置PostgreSQL数据库

  1. 将项目推送到 GitHub
  2. 设置 Vercel 帐户并链接 GitHub 存储库以进行即时预览和部署
  3. 创建项目并将其链接到Postgres 数据库

在上传的项目中,选择Storage
Connect Store → Create New → Postgres → Continue
通过将数据库放置在同一区域或靠近应用程序代码,可以减少数据请求的延迟。
数据库区域一旦启动就无法更改。如果要使用其他区域,则应在创建数据库之前进行设置
导航到 .env.local 选项卡,单击“显示密钥”和“复制代码段”
导航到代码编辑器并将 .env.example 文件重命名为 .env 。粘贴从 Vercel 复制的内容。
重要提示:转到您的 .gitignore 文件并确保 .env 位于忽略的文件中,以防止在推送到 GitHub 时暴露您的数据库机密。
最后,在终端中运行 npm i @vercel/postgres 以安装 Vercel Postgres SDK。

  1. 使用初始数据为数据库设定种子
    文件中 package.json ,将以下行添加到脚本中
"scripts": {
  "build": "next build",
  "dev": "next dev",
  "start": "next start",
  // this
  "seed": "node -r dotenv/config ./scripts/seed.js"
},

在终端运行:npm run seed
将placeholder-data.js数据导入数据库中(初始数据)
在Vercel查看数据是否填充完毕,选择Storage,下滑找到侧边栏Data,可查询表格,以及sql查询

获取数据

  1. 了解一些获取数据的方法:API、ORM、SQL 等。
  2. 服务器组件如何帮助您更安全地访问后端资源。
  3. 什么是网络瀑布。
  4. 如何使用 JavaScript 模式实现并行数据获取。

API 是应用程序代码和数据库之间的中间层

  • 第三方提供的api
  • 如果要从客户端提取数据,则希望在服务器上运行一个 API 层,以避免向客户端公开数据库机密。(在客户端上获取数据时不应直接查询数据库,因为这会暴露数据库机密。)
  • 使用route-handlers 创建 API 终端节点

注:如果你使用的是 React Server 组件(在服务器上获取数据),你可以跳过 API 层,直接查询你的数据库,而不会冒着将数据库机密暴露给客户端的风险。

React Server 组件优点

  1. 服务器组件支持 Promise,为数据获取等异步任务提供更简单的解决方案。 您可以使用 async/await 语法,而无需使用 useEffect、useState 或数据获取库。
  2. 服务器组件在服务器上执行,因此您可以将昂贵的数据提取和逻辑保留在服务器上,并且只将结果发送到客户端。
  3. 如前所述,由于服务器组件在服务器上执行,因此您可以直接查询数据库,而无需额外的 API 层。

在/app/lib/data.ts文件中,导入 sql 函数,此函数允许您查询数据库

// 查询数据库
import { sql } from '@vercel/postgres';

在/app/dashboard/page.tsx文件中

/**
 * Page 是一个异步组件。这允许您用于 await 获取数据。
 * 还有 3 个接收数据的组件: <Card> 、 <RevenueChart> 和 <LatestInvoices> 。
 * 它们目前被注释掉,以防止应用程序出错。
 * 
 */
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>
  );
}

在/app/lib/data.ts文件中引入fetchRevenue 函数
然后,取消对组件的 注释
导航到组件文件 ( /app/ui/dashboard/revenue-chart.tsx ) 并取消注释其中的代码。
检查你的 localhost,你应该能够看到一个使用 revenue 数据的图表。

后续相关自行查看官方文档

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. 数据请求无意中相互阻塞,从而创建了一个请求瀑布(同步)。

“瀑布流”是指一系列网络请求,这些请求依赖于先前请求的完成情况。在数据获取的情况下,每个请求只能在前一个请求返回数据后开始。(影响性能)
--------------------------------------------------------------------------
避免瀑布的常用方法是同时并行启动所有数据请求。(异步)
在 JavaScript 中,您可以使用 Promise.all() or Promise.allSettled() 函数同时启动所有 promise。例如,在 中 data.ts ,我们在 fetchCardData() 函数中使用 Promise.all()
开始同时执行所有数据提取,这可能会导致性能提升
但是,仅依赖这种 JavaScript 模式有一个缺点:如果一个数据请求比其他所有数据请求慢,会发生什么情况?

  1. 默认情况下,Next.js预渲染路由以提高性能,这被称为静态渲染。因此,如果您的数据发生更改,在视图上并不会响应更改

静态和动态渲染

目前dashboard是静态的,并没有实时数据更新

静态渲染

  1. 更快的网站 - 预渲染的内容可以缓存并在全球范围内分发。这确保了世界各地的用户可以更快、更可靠地访问您网站的内容。
  2. 减少服务器负载 - 由于内容已缓存,因此服务器不必为每个用户请求动态生成内容。
  3. SEO - 搜索引擎爬虫更容易对预渲染的内容进行索引,因为内容在页面加载时已经可用。这可以提高搜索引擎排名。

动态渲染

  1. 实时数据 - 动态呈现允许应用程序显示实时或经常更新的数据。这非常适合数据经常更改的应用程序。
  2. 特定于用户的内容 - 更轻松地提供个性化内容(如仪表板或用户配置文件),并根据用户交互更新数据。
  3. 请求时间信息 - 动态呈现允许您访问只能在请求时知道的信息,例如 Cookie 或 URL 搜索参数。

在/app/lib/data.ts 文件中使用unstable_noStore,并将注释解开(测速,人工阻塞)
Segment Config Option

/**
 * 使用在服务器组件或数据获取函数中调用 unstable_noStore 的 Next.js API 来选择退出静态渲染
 * 注意: unstable_noStore 是一个实验性 API,将来可能会更改。
 * 如果您希望在自己的项目中使用稳定的 API,也可以使用 Segment Config Option export const dynamic = "force-dynamic" 。
 */
import { unstable_noStore as noStore } from 'next/cache';
export async function fetchRevenue() {
  // 此处添加 noStore() 以防止响应被缓存.
  noStore();
   try {
    // Artificially delay a response for demo purposes.
    // Don't do this in production :)
	// 解开注释
    console.log('Fetching revenue data...');
    await new Promise((resolve) => setTimeout(resolve, 3000));

    const data = await sql<Revenue>`SELECT * FROM revenue`;
	// 解开注释
    console.log('Data fetch completed after 3 seconds.');

    return data.rows;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch revenue data.');
  }
}
// ...

Streaming( 流)

流式处理是一种数据传输技术,它允许您将路由分解为更小的“块”,并在它们准备就绪时逐步将它们从服务器流式传输到客户端。
通过流式传输,您可以防止缓慢的数据请求阻止您的整个页面。这允许用户查看页面的某些部分并与之交互,而无需等待加载所有数据后才能向用户显示任何 UI。

实现流式处理

  1. 在页面级别,使用 loading.tsx 文件。
  2. 对于特定组件,使用 <Suspense> .

1、在文件夹中 /app/dashboard ,创建一个名为 loading.tsx (特殊文件:页面加载时显示)

// 导入骨架屏ui
import DashboardSkeleton from '@/app/ui/skeletons';
 
export default function Loading() {
  return <DashboardSkeleton />;
}

2、由于 <SideNav> 是静态的,因此会立即显示。用户可以在加载动态内容时与之 交互。
3、用户不必等待页面完成加载后再导航(这称为可中断导航)。

注意:loading.tsx 级别高于 /invoices/page.tsx 和 /customers/page.tsx页面,所以loading.tsx 也应用在了/invoices/page.tsx 和 /customers/page.tsx页面中。

解决:
使用路由组来更改此设置,在/app/dashboard下创建文件夹/(overview) ,将/app/dashboard/page.tsx和/app/dashboard/loading.tsx移入新文件中。

路由组允许您将文件组织到逻辑组中,而不会影响 URL 路径结构。使用括号创建新文件夹时 () ,该名称不会包含在 URL 路径中。所以 /dashboard/(overview)/page.tsx 变成了 /dashboard .
-----------------
在这里,你使用路由组来确保 loading.tsx 仅适用于仪表板概述页面。但是,您也可以使用路由组将应用程序分成多个部分(例如路由 (marketing) 和 (shop) 路由),或者对于较大的应用程序按团队分隔。

4、Suspense 允许您延迟渲染应用程序的某些部分,直到满足某些条件(例如加载数据)。您可以将动态组件包装在 Suspense 中。然后,向它传递一个回退组件,以便在动态组件加载时显示。

如果您还记得缓慢的数据请求, fetchRevenue() 则这是减慢整个页面速度的请求。您可以使用 Suspense 仅流式传输此组件并立即显示页面 UI 的其余部分,而不是阻止您的页面。

在 /dashboard/(overview)/page.tsx 删除所有 fetchRevenue() 实例及其数据,并导入Suspense、RevenueChartSkeleton

// 移除fetchRevenue
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';
// 导入Suspense ,RevenueChartSkeleton 
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';

// const revenue = await fetchRevenue(); 删除
// <RevenueChart revenue={revenue}  /> 修改
<Suspense fallback={<RevenueChartSkeleton />}>
 	<RevenueChart />
</Suspense>

修改/app/ui/dashboard/revenue-chart.tsx文件

import { fetchRevenue } from '@/app/lib/data';
export default async function RevenueChart() { // 移除参数
  const revenue = await fetchRevenue(); // 获取数据
   // ...
  return (
    // ...
  );
}

练习

将card组件包裹在Suspense中,一个一个加载时会出现popping effect,可能会造成视觉上的不和谐。
解决:
1、在/app/dashboard/page.tsx删除card相关的实例及其数据,导入CardsSkeleton 、CardWrapper

// 修改(添加CardsSkeleton )
import { RevenueChartSkeleton,LatestInvoicesSkeleton,CardsSkeleton } from '@/app/ui/skeletons';
// 导入CardWrapper 组件
import CardWrapper from '@/app/ui/dashboard/cards';
// 删掉
// const {totalPaidInvoices,totalPendingInvoices,numberOfInvoices,numberOfCustomers} = await fetchCardData()
 {/* <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"
        /> */}
// 修改成
<Suspense fallback={<CardsSkeleton />}>
   <CardWrapper />
</Suspense>

2、在/app/ui/dashboard/cards.tsx文件中获取数据,将card注释去掉

// ...
import { fetchCardData } from '@/app/lib/data';
 
// ...
 
export default async function CardWrapper() {
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    <>
      <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"
      />
    </>
  );
}

3、刷新页面,您应该会看到所有卡同时加载。当您希望同时加载多个组件时,可以使用此模式。

搜索与分页

您的搜索功能将跨越客户端和服务器。当用户在客户端搜索发票时,URL 参数将被更新,数据将在服务器上获取,并且表格将使用新数据在服务器上重新呈现。

客户端钩子说明
useSearchParams允许您访问当前 URL 的参数
usePathname允许您读取当前 URL 的路径名
useRouter以编程方式启用客户端组件内的路由之间的导航

“use client” - 这是一个客户端组件,这意味着您可以使用事件侦听器和钩子。

在/app/ui/search.tsx文件中创建一个新 handleSearch 函数,并向 元素添加一个 onChange 侦听器。

'use client';
 
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
 
export default function Search({ placeholder }: { placeholder: string }) {
  // this
  function handleSearch(term: string) {
    console.log(term);
  }
 
  return (
   // ...
   <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);
        }}
      />
    // ...
  );
}

使用搜索参数更新 URL

1、在内部 handleSearch, 使用新 searchParams 变量创建一个新 URLSearchParams 实例。

URLSearchParams 是一个 Web API,它提供用于操作 URL 查询参数的实用工具方法。
您可以使用它来获取 params 字符串,而不是创建复杂的字符串文字 ?page=1&query=a 。

2、set 基于用户输入的参数字符串。如果输入为空,则需要 delete
3、使用 Next.js useRouter 和 usePathname hooks 来更新 URL。

${pathname} 是当前路径,即 “/dashboard/invoices” 。
params.toString() 将此输入转换为 URL 友好的格式,即query=xxx

4、由于 Next.js 的客户端导航,因此无需重新加载页面即可更新 URL。
5、保持url和输入同步

 <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()}
      />
// 如果使用 state 来管理输入的值,则可以使用该 value 属性使其成为受控组件。这意味着 React 将管理输入的状态。
// 由于您没有使用 state,因此可以使用 defaultValue .这意味着本机输入将管理自己的状态。这没关系,因为您将搜索查询保存到 URL 而不是状态。

6、搜索防抖(减少发送到数据库的请求数,从而节省资源)

npm i use-debounce

/app/ui/search.tsx

'use client';
 
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, usePathname, useRouter } from 'next/navigation';
// 防抖
import { useDebouncedCallback } from 'use-debounce';
 
export default function Search() {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();
 
  const handleSearch = useDebouncedCallback((term) => {
    const params = new URLSearchParams(searchParams);
    // 设置页数为1(分页器)
    params.set('page', '1');
    if (term) {
      params.set('query', term);
    } else {
      params.delete('query');
    }
    // 使用用户的搜索数据更新 URL
    replace(`${pathname}?${params.toString()}`);
  }, 300);
}

/app/dashboard/invoices/page.tsx

// 页面组件接受一个名为 searchParams 的 prop,因此您可以将当前的 URL 参数传递给组件 <Table>
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 (
    // ...
       <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
        <Table query={query} currentPage={currentPage} />
      </Suspense>
    // ...
  );
}

查看/app/ui/invoices/table.tsx,无需修改

添加分页
/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 (
    // ...
    <Pagination totalPages={totalPages} />
    // ...
  );
}

在/app/ui/invoices/pagination.tsx文件中取消注释,导入usePathname, useSearchParams并使用,创建一个名为 createPageURL。与搜索类似,您将用于 URLSearchParams 设置新的页码,并 pathName 创建 URL 字符串。

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()}`;
  };
 
  // ...
}

最后,当用户键入新的搜索查询时,您需要将页码重置为 1。
/app/ui/search.tsx

const handleSearch = useDebouncedCallback((term) => {
    const params = new URLSearchParams(searchParams);
    // 设置页数为1
    params.set('page', '1');
    if (term) {
      params.set('query', term);
    } else {
      params.delete('query');
    }
    replace(`${pathname}?${params.toString()}`);
  }, 300);

服务器操作数据

zod

错误处理

如何使用特殊 error.tsx 文件捕获路由段中的错误,并向用户显示回退 UI。
如何使用 notFound 函数和 not-found 文件处理 404 错误(对于不存在的资源)。

提高可访问性

表单验证
Next.js 配合使用 eslint-plugin-jsx-a11y 以实现辅助功能最佳实践。
使用服务器操作实现服务器端验证
使用 useFormState 钩子显示表单错误

在部署应用程序之前在本地运行 lint 以捕获可访问性问题
在 package.json 文件中添加 next lint 为脚本

"scripts": {
    "build": "next build",
    "dev": "next dev",
    "seed": "node -r dotenv/config ./scripts/seed.js",
    "start": "next start",
    "lint": "next lint"
},
npm run lint

身份验证

使用 NextAuth.js 向应用添加身份验证
使用中间件重定向用户并保护您的路由
使用 React useFormStatus 并 useFormState 处理待处理的状态和表单错误

安全网站通常使用多种方式来检查用户的身份。例如,输入您的用户名和密码后,该网站可能会向您的设备发送验证码或使用外部应用程序,例如 Google Authenticator。这种 2 因素身份验证 (2FA) 有助于提高安全性。即使有人知道了您的密码,如果没有您的唯一令牌,他们也无法访问您的帐户。
身份验证是关于确保用户是他们所说的人。您正在用用户名和密码等东西来证明您的身份。
授权是下一步。确认用户身份后,授权将决定允许他们使用应用程序的哪些部分。

正在安装与 Next.js 14 兼容的 NextAuth.js beta 版本。

npm install next-auth@beta

为应用程序生成密钥。此密钥用于加密 cookie,确保用户会话的安全性

openssl rand -base64 32
  • 14
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值