NextJS Study

是时候学习下现代的前端开发,之前学过html、css和js的dom操作,这次来系统学习下react、nextjs等前端技术栈。

前置准备

安装nodejs

Debian12:

curl -Lf https://nodejs.org/dist/v20.18.0/node-v20.18.0-linux-x64.tar.xz -o /tmp/node-v20.18.0-linux-x64.tar.xz
apt-get install xz-utils -y
tar -xvf /tmp/node-v20.18.0-linux-x64.tar.xz -C /usr/local
export PATH=$PATH:/usr/local/node-v20.18.0-linux-x64/bin
echo "export PATH=$PATH:/usr/local/node-v20.18.0-linux-x64/bin" >> /etc/profile
echo "export PATH=$PATH:/usr/local/node-v20.18.0-linux-x64/bin" >> /etc/zshrc
node -v&&npm -v

macOS:

brew install node npm
node -v&&npm -v

创建example项目

npm install -g pnpm
yes| npx create-next-app@latest nextjs-dashboard-demo --use-pnpm --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"
cd nextjs-dashboard-demo
sed -i 's/next dev --turbopack/next dev --turbopack -H 127.0.0.1 -p 3000/g' package.json # 修改启动的host
cd nextjs-dashboard-demo
pnpm i #安装依赖
pnpm dev #启动项目

/app: 包含所有的路由、组件、逻辑,是主要代码的所在。 /app/lib: 包含应用使用的函数,例如可复用的utils函数和数据获取函数。 /app/ui: 包含所有的UI组件,例如卡片、表格、表单。 /public: 包含所有的静态资源,例如图片 Config Files: 在根目录还有 next.config.js 等配置文件。如果你使用 create-next-app 初始化项目,那么大部分文件已经预先配置好了。在NextJS的官方example中,不需要修改这个文件。

prettier

npm install -g prettier

vscode安装插件:esbenp.prettier-vscode

settings.json

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "eslint.format.enable": true,
  "files.exclude": {
    "**/.next": false,
    "**/node_modules": false
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

.prettierrc

{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "es5",
  "tabWidth": 2,
  "useTabs": false
}

.prettierignore

node_modules
.next
dist
# shadcd/ui的目录
components/ui/
hooks

命令行

prettier --write .

eslint.config.mjs 配置

import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.config({
    extends: ["next/core-web-vitals", "next/typescript"],
    rules: {
      "react/no-unescaped-entities": "off",
      "@next/next/no-page-custom-font": "off",
      "@typescript-eslint/no-unused-vars": "off",
    },
  }),
];

export default eslintConfig;

简单的TS

export type Invoice = {
  id: string;
  customer_id: string;
  amount: number;
  date: string;
  // In TypeScript, this is called a string union type.
  // It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
  status: 'pending' | 'paid';
};
  • 可以使用Prisma or Drizzle来生成database scheme对应的TS类型。
  • 如果项目中存在TS,NextJS会自动安装必要的依赖和配置(应该是指tsconfig.json)。并且NextJS提供的TypeScript plugin可以帮助你自动补全和保证类型安全。

样式

全局样式

可以使用 /app/ui/global.css 文件将 CSS 规则添加到应用程序中的所有页面 —— 例如 CSS 重置规则(用来消除浏览器默认样式的差异)、链接等 HTML 元素的全栈范围样式等。(接受者是标签选择器,例如body、h1、p等。

您可以在应用程序的任何组件中导入 global.css,但通常最好将其添加到顶级组件中。在 Next.js 中,这是根布局(稍后会详细介绍)。通过导航到 /app/layout.tsx 并导入 global.css 文件,将全局样式添加到您的应用程序:

// /app/layout.tsx
import '@/app/ui/global.css';

golbal.css中的内容如下,主要包含 @tailwind 的三个指令,用于引入 tailwindcss 的基础样式、组件和工具类。

/* /app/ui/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

input[type='number'] {
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type='number']::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

input[type='number']::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

Tailwind 是一个 CSS 框架,允许您直接在 TSX 标记中快速编写实用程序类,从而加快开发过程。通过给元素增加类名称,可以快速实现样式的修改。

当您使用create-next-app启动新项目时,Next.js 会询问您是否要使用 Tailwind。如果您选择yes ,Next.js 将自动安装必要的软件包并在您的应用程序中配置 Tailwind

尽管 CSS 样式是全局共享的,但每个类都单独应用于每个元素。例如 /app/page.tsx,就使用了Tailwind的类名:

// /app/page.tsx
import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
export default function Page() {
  return (
    // These are Tailwind classes:
    <main className="flex min-h-screen flex-col p-6">
      <div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
    // ...
  )
}

CSS Modules

CSS Modules 是一种允许您在组件级别上使用 CSS 的技术,他使 CSS 类默认作用于组件的本地范围,从而降低样式冲突的风险。。Tailwind 和 CSS 模块是设计 Next.js 应用程序样式的两种最常见的方法。使用其中之一取决于您的偏好 - 您甚至可以在同一个应用程序中使用两者!

/* /app/ui/home.module.css */
.shape {
  height: 0;
  width: 0;
  border-bottom: 30px solid black;
  border-left: 20px solid transparent;
  border-right: 20px solid transparent;
}
import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import styles from '@/app/ui/home.module.css';
 
export default function Page() {
  return (
    <main className="flex min-h-screen flex-col p-6">
      <div className={styles.shape} />
    // ...
  )
}

使用 clsx 库切换类名

clsx 是一个小型库,用于在 TSX 中动态切换类名。它允许您在组件中使用条件逻辑来决定是否应该添加或删除类名。

// /app/ui/invoices/status.tsx
import clsx from 'clsx';
 
export default function InvoiceStatus({ status }: { status: string }) {
  return (
    <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',
        },
      )}
    >
    // ...
)}

优化字体和图像

字体在网站设计中发挥着重要作用,但如果需要获取和加载字体文件,在项目中使用自定义字体可能会影响性能。累积布局偏移是 Google 用于评估网站性能和用户体验的指标。对于字体,当浏览器最初以后备字体或系统字体呈现文本,然后在加载后将其交换为自定义字体时,就会发生布局转换。这种交换可能会导致文本大小、间距或布局发生变化,从而移动其周围的元素。

当您使用next/font模块时,Next.js 会自动优化应用程序中的字体。它在构建时下载字体文件并将它们与其他静态资产一起托管。这意味着当用户访问您的应用程序时,不会出现会影响性能的额外网络请求字体。

添加字体

让我们向您的应用程序添加自定义 Google 字体,看看它是如何工作的!在 /app/ui 文件夹中,创建一个名为 fonts.ts 的新文件。您将使用此文件来保留将在整个应用程序中使用的字体。从 next/font/google 模块导入 Inter 字体 - 这将是您的主要字体。然后,指定您要加载的子集。在这种情况下,“拉丁语”:

// /app/ui/fonts.ts
import { Inter, Lusitana } from 'next/font/google';
 
export const inter = Inter({ subsets: ['latin'] });
 
export const lusitana = Lusitana({
  weight: ['400', '700'],
  subsets: ['latin'],
});

最后将字体添加到 /app/layout.tsx 中的元素:看第二行import和body标签的inter.className

// /app/layout.tsx
import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${inter.className} antialiased`}>{children}</body>
    </html>
  );
}

通过将Inter添加到<body>元素,该字体将应用于整个应用程序(因为字体相关的css属性默认会继承给子元素)。在这里,您还添加了 Tailwind antialiased 平滑字体的类。没有必要使用这个类,但它增加了一个不错的感觉。可以到Google 字体(简体中文)搜索更多字体。

添加图像

Next.js 可以在顶级 /public 文件夹下提供静态资源,例如图像。 /public 内的文件可以在您的应用程序中引用。使用常规 HTML,您可以添加图像,如下所示:

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

但是,这意味着您必须手动:

  • 确保您的图像在不同的屏幕尺寸上都能响应。(响应式布局)
  • 指定不同设备的图像尺寸。
  • 防止图像加载时布局发生变化。
  • 延迟加载用户视口之外的图像。

图像优化是 Web 开发中的一个大主题,其本身可以被视为一个专业领域。您可以使用next/image组件自动优化图像,而不是手动实现这些优化。

<Image>组件是<img>标签的扩展,他有一些自动优化的措施,例如:

  • 加载图像时自动防止布局移动。
  • 调整图像大小以避免将大图像传送到具有较小视口的设备。
  • 默认情况下延迟加载图像(图像在进入视口时加载)。
  • 以现代格式(例如WebP)提供图像和AVIF ,当浏览器支持时。

/app/page.tsx 文件中,从 next/image 导入组件。然后,在注释下添加图片:

// /app/page.tsx
import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
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>
    //...
  );
}

在这里,您将width设置为1000 , height设置为760像素。最好设置图像的width和height以避免布局移位,这些宽高比应该与源图像相同。您还会注意到 hidden 类用于从移动屏幕上的 DOM 中删除图像,以及 md:block 用于在桌面屏幕上显示图像。类似的 block md:hidden 用于在移动屏幕上显示图像,而在平板上隐藏图像。

布局和页面

Next.js 使用文件系统路由,其中​​文件夹用于创建嵌套路由。每个文件夹代表一个映射到 URL 段的路由段。您可以使用layout.tsx和page.tsx文件为每个路由创建单独的UI。

创建页面

page.tsx是一个特殊的 Next.js 文件,它导出 React 组件,并且需要它才能访问路由。在您的应用程序中,您已经有一个页面文件: /app/page.tsx —— 这是与路径/关联的主页。要创建嵌套路由,您可以将文件夹相互嵌套并在其中添加 page.tsx 文件。例如:/app/dashboard/page.tsx 与 /dashboard 路径关联。让我们创建页面来看看它是如何工作的!

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

现在,确保开发服务器正在运行并访问 http://localhost:3000/dashboard。您应该看到"仪表板页面"文本。

创建布局

仪表板具有某种跨多个页面共享的导航。在 Next.js 中,您可以使用特殊的layout.tsx文件来创建在多个页面之间共享的 UI。

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

/app/layout.tsx 是根布局,并且是必须的。

优化导航

传统的<a> HTML 元素会导致全页面刷新。在 Next.js 中,您可以使用用于在应用程序中的页面之间链接的组件。允许您使用 JavaScript 进行客户端导航。

import {
  UserGroupIcon,
  HomeIcon,
  DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
 
// ...
 
export default function NavLinks() {
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <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 自动按路线段对应用程序进行代码分割。这与传统的 React SPA不同,浏览器在初始加载时加载所有应用程序代码。

按路由拆分代码意味着页面变得孤立。如果某个页面抛出错误,应用程序的其余部分仍然可以工作。

此外,在生产中,只要组件出现在浏览器的视口中,Next.js 就会自动在后台预取链接路由的代码。当用户单击链接时,目标页面的代码将已经在后台加载,这使得页面转换几乎是即时的!

常见的 UI 模式是显示活动链接以向用户指示他们当前所在的页面。为此,您需要从 URL 获取用户的当前路径。 Next.js 提供了一个名为usePathname()的钩子,您可以使用它来检查路径并实现此模式。由于 usePathname() 是一个钩子,因此您需要将 nav-links.tsx 转换为客户端组件。将 React 的"use client"指令添加到文件顶部,然后从 next/navigation 导入 usePathname(),并且使用clsx库在链接处于活动状态时有条件地应用类名。

'use client';
 
import {
  UserGroupIcon,
  HomeIcon,
  DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
 
// ...
 
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>
        );
      })}
    </>
  );
}

使用服务器组件(Server Components)获取数据

默认情况下,Next.js 应用程序使用 React Server Components。使用服务器组件获取数据是一种相对较新的方法,使用它们有一些好处:

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

NextJS的教程主要是如何使用SQL查询数据,但我想用API来获取数据。这里整理下通用的一些东西。

// /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() { // async方法,从而可以使用async/await语法
  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>
  );
}

// 获取数据的一个例子,主要关注如何使用Promise和setTimeout
export async function fetchRevenue() {
  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.');
  }
}

可以使用 Promise.all()Promise.allSettled() 来避免请求瀑布:

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,
    ]);
    // ...
  }
}

静态渲染和动态渲染

什么是静态渲染?

通过静态渲染,数据获取和渲染发生在构建时(部署时)或重新验证数据时在服务器上。每当用户访问您的应用程序时,就会提供缓存的结果。静态渲染有几个好处:

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

什么是动态渲染?

通过动态呈现,内容会在请求时(当用户访问页面时)在服务器上为每个用户呈现。动态渲染有几个好处:

  • 实时数据 - 动态渲染允许您的应用程序显示实时或经常更新的数据。这对于数据经常变化的应用程序来说是理想的选择。
  • 用户特定的内容 - 更容易提供个性化内容(例如仪表板或用户配置文件),并根据用户交互更新数据。
  • 请求时间信息 - 动态呈现允许您访问只能在请求时知道的信息,例如 cookie 或 URL 搜索参数。

Streaming 渐进式渲染

通过流式传输,您可以防止缓慢的数据请求阻塞整个页面。这允许用户查看页面的部分内容并与之交互,而无需等待所有数据加载后再向用户显示任何 UI。流式处理与 React 的组件模型配合得很好,因为每个组件都可以被视为一个块。在 Next.js 中实现流式传输有两种方法:

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

使用 loading.tsx 文件

loading.tsx 是一个基于 Suspense 构建的特殊 Next.js 文件,它允许您创建后备 UI 以在页面内容加载时显示为替换。

最基本的:

// /app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}

添加加载骨架:加载骨架是 UI 的简化版本。许多网站使用它们作为占位符(或后备)来向用户指示内容正在加载。您在loading.tsx中添加的任何UI都将作为静态文件的一部分嵌入,并首先发送。然后,其余的动态内容将从服务器流式传输到客户端。

// /app/dashboard/loading.tsx
import DashboardSkeleton from '@/app/ui/skeletons';
 
export default function Loading() {
  return <DashboardSkeleton />;
}


// /app/ui/skeletons.tsx
export default function DashboardSkeleton() {
  return (
    <>
      <div
        className={`${shimmer} relative mb-4 h-8 w-36 overflow-hidden rounded-md bg-gray-100`}
      />
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <CardSkeleton />
        <CardSkeleton />
        <CardSkeleton />
        <CardSkeleton />
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        <RevenueChartSkeleton />
        <LatestInvoicesSkeleton />
      </div>
    </>
  );
}

现在还有个问题,这个loading的骨架界面会对下面的页面也生效。我们可以通过路由组来改变这一点。在仪表板文件夹内创建一个名为 /(overview) 的新文件夹。然后,将 loading.tsx 和 page.tsx 文件移至文件夹内:路由组允许您将文件组织到逻辑组中,而不影响 URL 路径结构。当您使用括号()创建新文件夹时,该名称不会包含在 URL 路径中。因此/dashboard/(overview)/page.tsx变为/dashboard 。

流式传输动态组件

使用 <Suspense> 包裹原来的组件,并添加fallback为骨架即可。

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 { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
 
export default async function Page() {
  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">
        <Suspense fallback={<RevenueChartSkeleton />}>
          <RevenueChart />
        </Suspense>
        <LatestInvoices latestInvoices={latestInvoices} />
      </div>
    </main>
  );
}

部分预渲染Partial Prerendering (PPR)和增量静态生成Incremental Static Regeneration (ISR)

Next.js 14 引入了部分预渲染的实验版本 - 一种新的渲染模型,允许您在同一路径中结合静态和动态渲染的优点。例如:

当用户访问某条路线时:

  • 提供包含导航栏和产品信息的静态路由的外壳,确保快速初始加载。
  • 外壳留下了一些空洞,动态内容(例如购物车和推荐产品)将异步加载。
  • 这些空洞将并行移步加载,从而减少总体加载时间

部分预渲染如何工作?

部分预渲染使用 React 的 Suspense(您在上一章中了解过)来推迟应用程序的渲染部分,直到满足某些条件(例如加载数据)。Suspense fallback 与静态内容一起嵌入到初始 HTML 文件中。在构建时(或revalidation期间),静态内容被预渲染以创建静态外壳。动态内容的呈现被推迟,直到用户请求该页面。

将组件包装在 Suspense 中并不会使组件本身变得动态,而是 Suspense 被用作静态和动态代码之间的边界。

启用部分预渲染

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  /* config options here */
   experimental: {
    ppr: 'incremental',
  },
};

export default nextConfig;

然后在page.tsx中增加这个

export const experimental_ppr = true;

就是这样。您可能在开发中看不到应用程序的差异,但您应该注意到生产中的性能改进。 Next.js 将预渲染路由的静态部分,并推迟动态部分,直到用户请求它们。部分预渲染的优点在于您无需更改代码即可使用它。只要您使用 Suspense 包装路线的动态部分,Next.js 就会知道路线的哪些部分是静态的,哪些部分是动态的。我们相信 PPR 有潜力成为 Web 应用程序的默认渲染模型,汇集了静态站点和动态渲染的优点。然而,它仍处于实验阶段。我们希望将来能够稳定它,并使其成为 Next.js 构建的默认方式。

使用客户端组件添加搜索和分页,并通过客户端重定向修改url,并保持input的输入和input关联,以及输入的防抖

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination

Route Handlers

在 Next.js 中,您可以使用路由处理程序创建 API 端点。https://nextjs.org/docs/app/building-your-application/routing/route-handlers

其他文档

https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic

https://nextjs.org/docs/app/building-your-application/data-fetching

https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration

https://nextjs.org/docs/app/building-your-application/caching#data-cache

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props