Nextjs学习笔记

发布于2024年02月01日浏览量112

# NextJs
# 自学笔记
# 前端

项目结构

路由文件 (.jsx/.tsx)

文件描述
layout布局
page
loading加载页面
not-found未找到页面
error错误页面
global-error全局错误页面
template重新渲染页面
default并行路由回退页面

Dashboard Tutorial

基于nextjs官方教程

部分项目结构

/app/lib存放可以复用的函数或者数据结构 /app/ui存放ui组件 /scripts存放脚本

Tailwind

  • 使用import xxx.css文件来应用样式
  • className={xxx.yy}可应用css文件中的某个特定样式 其中clsx是一个便于切换类名的库,使用方法例如
className={clsx( 'inline-flex items-center rounded-full px-2 py-1 text-xs', { 'bg-gray-100 text-gray-500': status === 'pending', 'bg-green-500 text-white': status === 'paid', }, )}

字体和图像

字体

Next.js 会在构建时下载字体文件,并将它们与其他静态资产一起托管,这样当用户访问应用程序时就不会有额外的字体网络请求 例如:

  1. 在font.ts中声明google font字体:export const inter = Inter({ subsets: ['latin'] });
  2. 使用本地字体:export const harmony = localFont({ src: '../../public/fonts/SmileySans.ttf.woff2' });
  3. 在layout.tsx中应用到body或任意元素:<body className={`${inter.className} antialiased`}>{children}</body> (antialiased为可选的,字体边缘抗锯齿)

图像

import Image from 'next/image';以使用nextjs优化图像,但需要注意

  • 确保图像在不同的屏幕尺寸上具有响应性
  • 为不同的设备指定图像大小
  • 防止在加载图像时发生布局偏移
  • 延迟加载用户视口之外的图像 例如:
<Image src="/hero-desktop.png" width={1000} height={760} className="hidden md:block" alt="Screenshots of the dashboard project showing desktop version" />

其中width和height定义了图像的纵横比,在加载时能防止图像偏移,并且比例在不同屏幕尺寸下仍能保持一致

但是对于字体和图像的优化远不止这些,还需多加了解!!

页面和布局

Next使用嵌套式文件夹进行路由匹配,文件夹名为路由,其中的page.tsx和layout.tsx分别对应自身的页面和布局 使用布局的一个好处是,在导航时,只有页面组件会更新,而布局不会重新呈现,称为部分渲染

页面间导航

在Next应用中使用<a>进行导航时,会触发整个页面的重新获取,十分浪费资源 使用import Link from 'next/link';以进行客户端导航 与React的SPA不同,Next会自动切分route,并在第一次加载时加载整个应用的完整代码 所以在这个情况下,每个页面都是独立的,即使有个页面抛出error也不会影响其他页面的运行 Next会自动prefetch the code for the linked route in the background,以优化点击链接后的加载速度

显示激活的links

可用于导航栏

'use client'; import { usePathname } from 'next/navigation'; import clsx from 'clsx'; function() { const pathname = usePathname(); className={clsx({ 'bg-sky-100 text-blue-600': pathname === link.href})} }

获取数据

总体来说,在page统一获取数据,可以使用const data = await Promise.all([ invoiceCountPromise, customerCountPromise, invoiceStatusPromise, ]);的方式同时发出多个请求

静态和动态渲染

静态渲染

使用静态呈现时,数据提取和呈现在生成时(部署时)或重新验证期间在服务器上进行 每当用户访问应用程序时都会先提供缓存的结果

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

动态渲染

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

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

但是最终页面的加载速度取决于最慢的数据获取请求

Streaming

流式处理是一种数据传输技术,它允许将路由分解为更小的“块”,并在它们准备就绪时逐步将它们从服务器流式传输到客户端 在Next中实现流式处理有两种方法:

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

loading.tsx

与page.tsx, layout.tsx处于同级目录

  1. loading.tsx是一个特殊的Next.js文件,它建立在Suspense之上,它允许您创建回退UI,以便在页面内容加载时显示为替换
  2. 静态内容会立即显示出来,用户可以在加载动态内容时进行其他交互
  3. 用户不必等待页面加载完成后才能离开(可中断导航)

Suspense

<Suspense fallback={<RevenueChartSkeleton />}> <RevenueChart /> </Suspense>

搜索和分页

搜索

用于实现搜索功能的 Next.js 客户端钩子:

  • useSearchParams允许访问当前URL的参数。例如,此/dashboard/invoices?page=1&query=pending的搜索参数为{page: '1', query: 'pending'}
  • usePathname允许读取当前URL的路径名。例如,对于路由/dashboard/invoices``usePathname,将返回'/dashboard/invoices'
  • useRouter以编程方式在客户端组件内的路由之间启用导航

捕获用户输入

搜索框组件使用'use client'以确认为客户端组件,此时才可使用事件侦听等钩子

因为是客户端组件,此时console.log的位置为浏览器控制台中;否则在代码终端处

使用搜索参数更新url

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

保持url与输入同步

在输入框加入defaultValue={searchParams.get('query')?.toString()}

更新数据显示

export default async function Page({ searchParams,}: { searchParams?: { query?: string; page?: string; }; }) { const query = searchParams?.query || ''; const currentPage = Number(searchParams?.page) || 1; // ... return ( <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />} > <Table query={query} currentPage={currentPage} /> </Suspense> )

记得debounce

分页

在分页组件中获取传入组件的总页数,并根据当前params中的数据确定页码

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

更改数据

服务器操作

React服务器操作允许直接在服务器上运行异步代码。它们消除了创建API端点来改变数据的需要。相反,可以直接编写在服务器上执行的异步函数,并从客户端或服务器组件调用这些函数

表单与服务器操作

// Server Component export default function Page() { // Action async function create(formData: FormData) { 'use server'; // Logic to mutate data... } // Invoke the action using the "action" attribute return <form action={create}>...</form>; }

好处:渐进式增强 - 即使在客户端上禁用了js,表单也能正常工作

示例

创建发票

新建actions.ts用于导出所有服务器函数,在头部添加'use client'; 创建发票路由/invoices/create/page.tsx

'use client'; import { z } from 'zod'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; const FormSchema = z.object({ id: z.string(), customerId: z.string(), amount: z.coerce.number(), status: z.enum(['pending', 'paid']), date: z.string(), }); const CreateInvoice = FormSchema.omit({ id: true, date: true }); export async function createInvoice(formData: FormData) { const { customerId, amount, status } = CreateInvoice.parse({ customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), }); await sql; // 插入数据库操作 revalidatePath('/dashboard/invoices'); redirect('/dashboard/invoices'); }

最后在表单中调用<form action={createInvoice}> 注意:

  1. 此处使用了zod进行数据验证
  2. next有客户端路由器缓存机制来储存一定时间内的路由段,以减少向服务器发送的请求数量。但是此时我们修改了数据,所以需要清除缓存并触发对服务器的新请求,使用revalidatePath函数
  3. 在数据更新后重定向页面,使用redirect函数

更新发票

由于更新的发票id是变化的,所以此处使用方括号包裹文件夹来创建动态路由/invoices/[id]/edit/page.tsx

import Form from '@/app/ui/invoices/edit-form'; import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data'; export default async function Page({ params }: { params: { id: string } }) { const id = params.id; const [invoice, customers] = await Promise.all([ fetchInvoiceById(id), fetchCustomers(), ]); // ... }

但为了在编辑表单中使用更新数据的函数,不能直接像之前一样使用<form action={updateInvoice(id)}>,而应该使用

import { updateInvoice } from '@/app/lib/actions'; export default function EditInvoiceForm({ invoice, customers,}: { invoice: InvoiceForm; customers: CustomerField[]; }) { const updateInvoiceWithId = updateInvoice.bind(null, invoice.id); return ( <form action={updateInvoiceWithId}> <input type="hidden" name="id" value={invoice.id} /> </form> ); }

删除发票

直接将删除按钮包裹在表单中,如下

import { deleteInvoice } from '@/app/lib/actions'; // ... export function DeleteInvoice({ id }: { id: string }) { const deleteInvoiceWithId = deleteInvoice.bind(null, id); return ( <form action={deleteInvoiceWithId}> <button className="rounded-md border p-2 hover:bg-gray-100"> <span className="sr-only">Delete</span> <TrashIcon className="w-4" /> </button> </form> ); }

错误处理

!! 记得使用try-catch包裹服务器操作

处理所有错误

可以创建文件error.tsx作为同级位置路由出现错误时的UI 示例:

'use client'; import { useEffect } from 'react'; export default function Error({ error, reset,}: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { // Optionally log the error to an error reporting service console.error(error); }, [error]); return ( <main className="flex h-full flex-col items-center justify-center"> <h2 className="text-center">Something went wrong!</h2> <button onClick={ // Attempt to recover by trying to re-render the invoices route () => reset() } > Try again </button> </main> ); }
  1. error.tsx必须是一个客户端组件,使用'use client';
  2. error参数:js原生Error对象
  3. reset参数:重置错误边界的函数,执行时会尝试重新呈现路由段

处理404错误

可以创建文件not-found.tsx,在代码中调用notFound()方法即可进入

提高可访问性

使用eslint

package.jsonscripts下添加"lint": "next lint",然后执行npm run lint

表单验证

因框架不同,具体实现详见教程

身份验证

具体实现结合自身需求,主要思路为使用中间件middleware.ts拦截路由

元数据

元数据类型

标题元数据:负责浏览器选项卡上显示的网页标题

<title>Page Title</title>

描述元数据:提供网页内容的简要概述,通常显示在搜索引擎结果中

<meta name="description" content="A brief description of the page content." />

关键字元数据:包括与网页内容相关的关键字,帮助搜索引擎索引页面

<meta name="keywords" content="keyword1, keyword2, keyword3" />

Open Graph 元数据:增强了网页在社交媒体平台上共享时的表示方式,提供标题、描述和预览图像等信息

<meta property="og:title" content="Title Here" />
<meta property="og:description" content="Description Here" />
<meta property="og:image" content="image_url_here" />

网站图标元数据:将网站图标连接到网页

<link rel="icon" href="path/to/favicon.ico" />

添加元数据

next有一个元数据API,可用于定义应用程序元数据。有两种方法可以向应用程序添加元数据:

  • 基于配置
    • 导出layout.jspage.js文件中的静态元数据对象,或使用动态generateMetadata函数
  • 基于文件
    • favicon.icoapple-icon.jpgicon.jpg:用于网站图标和图标
    • opengraph-image.jpg以及twitter-image.jpg:用于社交媒体图像
    • robots.txt:提供有关搜索引擎抓取的说明
    • sitemap.xml:提供有关网站结构的信息

网站图标和 Open Graph 图像

/public文件夹中有两个图像:favicon.icoopengraph-image.jpg 将这些图像移动到/app文件夹的根目录,next将自动识别这些文件并将其用作图标和OG图像

页面标题和说明

可以包含来自任何OR文件的元数据对象,以添加其他页面信息如标题和描述。layout.js中的任何元数据都将由使用它的所有页面继承

import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Acme Dashboard', description: 'The official Next.js Course Dashboard, built with App Router.', metadataBase: new URL('https://next-learn-dashboard.vercel.sh'), };

每个页面都可以创建属于自己的元数据,同时也可以使用模板:

import { Metadata } from 'next'; export const metadata: Metadata = { title: { template: '%s | Acme Dashboard', default: 'Acme Dashboard', }, description: 'The official Next.js Learn Dashboard built with App Router.', metadataBase: new URL('https://next-learn-dashboard.vercel.sh'), };

此时在invoices页面中添加元数据export const metadata: Metadata = { title: 'Invoices',};,即可看到页面标题为Invoices | Acme Dashboard

后续拓展

详见Learn Next.js: Next Steps | Next.js (nextjs.org)

-

My Nocturzone

LEON の 熬夜空间