Next.js 实战笔记 2.0:深入 App Router 高阶特性与布局解构

Next.js 实战笔记 2.0:深入 App Router 高阶特性与布局解构

上一篇笔记:

  • Next.js 实战笔记 1.0:架构重构与 App Router 核心机制详解

上篇笔记主要回顾了一些 Next12 到 Next15 的一些变化,这里继续学习/复习一些已有或者是新的变化

turbo 的补充

在实际运行的过程当中,我发现使用 yarn dev --turbo 运行,编译并不稳定——不确定是因为我的 Mac 还是 intel 的原因,毕竟现在很多的优化都是针对 M 芯片做的,总之目前还是 fallback 到了默认的开发模式……

其他保留页面

除了 page.jslayout.js 之外,NextJS 还有其他两个保留页面

报错页面

也就是 error.js,大体的实现如下:

"use client";
import React from "react";const MealsErrorPage = () => {return (<main className="error"><h1>An Error Occurred!</h1><p>Fail to fetch meal data. Please try again later.</p></main>);
};export default MealsErrorPage;

需要注意的是, error.js 必须要使用 use client,因为这个页面即会处理 server end 的异常,也会处理 client end 的异常

它的作用与 layout 类似,在当前/兄弟姐妹/子页面出现异常后,会渲染当前页面

not found

大体实现如下:

import React from "react";const NotFoundPage = () => {return (<main className="not-found"><h1>Not Found</h1><p>Could not find the page you are looking for.</p></main>);
};export default NotFoundPage;

error.js 类似,不过在组件内调用 notFound(); 也可以重定向到当前页面

表单

其实这部分不完全是 NextJS 的内容,更多的是 React 19 提出的新功能。这里会基于 NextJS 中的实现进行讨论,React 的话,等到 NextJS 的内容过完了后,重新过一遍 React18 和 19 的新特性

提交表单

之前在使用 React 的表单时,提交事件其实不由 action 触发,而是通过 onClick + preventDefault() 可以绕过 action 进行实现。不过目前 NextJS 目前则可以直接通过 action 在 server end 完成表单的提交,并且将表单中有的数据包成 formData 作为参数

下面是一个简单的实现:

export default function ShareMealPage() {const shareMeal = async (formData) => {// use server must be an async function"use server";const meal = {creator: formData.get("name"),creator_email: formData.get("email"),title: formData.get("title"),summary: formData.get("summary"),instructions: formData.get("instructions"),image: formData.get("image"),};console.log(meal);};return (<><header className={classes.header}><h1>Share your <span className={classes.highlight}>favorite meal</span></h1><p>Or any other meal you feel needs sharing!</p></header><main className={classes.main}><form className={classes.form} action={shareMeal}></form></main></>);
}

服务端输出的结果:

这里需要注意的是,如果组件本身使用了 use client,那么在方法内使用 use server 就会报错……

useFormStatus

这里简单的提一下使用方法,就是一个返回的 pending 可以更灵活的运用

const { pending, data, method, action } = useFormStatus();

具体的使用案例如下:

"use client";import React from "react";
import { useFormStatus } from "react-dom";const MealsFormSubmit = () => {const { pending } = useFormStatus();return (<button disabled={pending}>{pending ? "Submitting" : "Share Meal"}</button>);
};export default MealsFormSubmit;

我这里是单独拆了一个组件出来使用,这个方法和官方提供的使用方法类似:

import { useFormStatus } from "react-dom";
import action from "./actions";function Submit() {const status = useFormStatus();return <button disabled={status.pending}>Submit</button>;
}export default function App() {return (<form action={action}><Submit /></form>);
}

具体的操作,React 在内部已经实现了,只要通过 action 进行触发,就可以顺利地监听到表单的状态变化

useFormState

目前 React 官方是把 useFormState 重命名成了 useActionState,并且用法是一样的——除了后者是从 react 中导入,前者是 react-dom 中导入:

In earlier React Canary versions, this API was part of React DOM and called useFormState.

但是我看了下,不知道为啥用 useActionState 会报错,用 useFormState 暂时没问题。介于我用的这个版本,useFormState 还没有被移除,因此暂时就使用了 useFormState

hook 的 signature 如下:

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

同理,因为是 hook,所以也需要使用 use client

具体使用方法如下:

"use client";import ImagePicker from "@/components/meals/image-picker";
import classes from "./page.module.css";
import { shareMeal } from "@/lib/action";
import MealsFormSubmit from "@/components/meals/meals-form-submit";
import { useFormState } from "react-dom";export default function ShareMealPage() {const [state, formAction] = useFormState(shareMeal, { message: null });return (<><header className={classes.header}><h1>Share your <span className={classes.highlight}>favorite meal</span></h1><p>Or any other meal you feel needs sharing!</p></header><main className={classes.main}><form className={classes.form} action={formAction}><p className={classes.actions}>{state.message && <p>{state.message}</p>}</p></form></main></>);
}

shareMeal 的实现如下:

export const shareMeal = async (prevState, formData) => {const meal = {creator: formData.get("name"),creator_email: formData.get("email"),title: formData.get("title"),summary: formData.get("summary"),instructions: formData.get("instructions"),image: formData.get("image"),};if (isInvalidText(meal.title) ||isInvalidText(meal.summary) ||isInvalidText(meal.instructions) ||isValidEmail(meal.creator_email) ||isValidEmail(meal.creator) ||!meal.creator_email.includes("@") ||!meal.image ||meal.image.size === 0) {return {message: "Invalid input",};}await saveMeal(meal);redirect("/meals/");
};

这部分其实没什么特别好深入挖掘的,使用方法和官方文档基本一致,属于跟着官方文档实现就好了,大体需要注意的地方有:

  • form 的 action 需要使用 useFormState 返回的第二个值,这样方便 React 进行监听
  • 原本的 action fn 第一个参数需要接受 initialState 作为第一个参数

💡:我个人觉得,将 useFormStateuseFormStatus 封装成一个通用的 custom hook,保证全局的 initialState 一致,这样处理起来可能会更加的高效,也可以更好地减少 boilerplate 代码

缓存

这部分主要是使用 revalidatePath() 这个方法,在进行重定向的时候,去清除 NextJS 中存在的缓存

说实话,这部分的内容可能真的是要多做一点 deploy 之后,才有更多的感觉。目前我有一个小项目是通过 NextJS+github actions 部署到 GH Pages 上的,我只能说似乎是因为 use client 的关系,页面还是会零零碎碎的去 fetch 一些小的 JS 文件。只不过因为页面整体的内容比较少,加载速度还是比较快——大概在 100-200ms 之间,因此目前我还没有花太多的时间和心力去研究 deploy 这部分的内容

dynamic metadata

metadata 的内容在 1.0 中已经提过了,这里讲的是动态的 metadata 的实现方式,主要是通过这个 generateMetadata 的方法自动生成的。 generateMetadata 也是一个保留词,具体使用方法如下:

export const generateMetadata = async ({ params }) => {const meal = await getMeal(params.mealSlug);return {title: meal.title,description: meal.summary,};
};

路由

这里再多提一些关于路由的内容,更多更完整的内容,还是可以到官方文档: **Project structure and organization** 中去去查找,并且自己测试试验,再根据项目需求判断是否需要

parallel routes

个人感觉,parallel routes 是一个更方便管理子组件的一种实现。官方文档中说了,parallel routes 的实现必须要依赖于 layout.js ,而且 parallel routes,也就是用 @folder 这种语法,会生成独立的 slot,但是不会生成独立的 URL

如下面这个案例:

@archive@latest 会作为两个独立的 slots,可以在 layout.js 中获取,但是它的路径还是在 localhost:3000/archive 下,单独访问 localhost:3000/archive/@archive 或是 localhost:3000/archive/@latest 会报错,因为 NextJS 内部并没有实现对应的路径

具体的排列方式如下:

import React from "react";const ArchiveLayout = ({ archive, latest }) => {return (<div><h1>News Archive</h1><section id="archive-filter">{archive}</section><section id="archive-latest">{latest}</section></div>);
};export default ArchiveLayout;

这种情况下, archivelatest 的内容会被并排渲染:

parallel routes + 动态路由

现在总体来说,需求还是比较明确的:

  • archive 显示按照年月分类的文档
  • latest 显示最近的几个文档

按照 NextJS 的结构,那么文档目录就应该是现在这个样子的:

不过这就造成了一个问题:

这是因为,parallel routes 中的路径存在不匹配的情况—— @archive 下有 [year],但是 @latest 下面没有,NextJS 没有办法完美匹配路径,因此就抛出了异常

这种情况下解决方式有两种:

  1. @latest 下也创立对应的 [year] 结构

    缺点就是语意不明确,而且会增加很多无意义的结构

    在当前的业务情况下,@latest 默认只会显示最近的几条数据,并不需要根据 年/月 进行搜索

  2. 使用 default.js

    default.js 是 parallel route 的 fallback 页面,具体实现如下:

    💡 这里的 default.js 中的内容和 page.js 完全一致,因此后期实现中将 page.js 删除了

最终渲染效果如下:

刚开始看到这个 @ 的用法还是不太理解,后面回顾了一下过去做的几个项目,发现这个 slots 还是可以比较好的解决过去项目中,我碰到的几个痛点:

  • 超大表单
    这个在填写付款方法、地址的时候经常碰上,不过我们那时候的业务场景更复杂一些,总体上来说大概会有 6-7 个 steps,每个 steps 的路径一致,但是表单不一样
  • 同一个路径中根据不同条件渲染不同内容

catch all route

其实 NextJS 还是提供了其他的不同实现方法,这个业务场景下,因为只有 年/月 的搜查,其实创建对应的文件夹结构也不是不行,而且对于 NotFound 的支持会更好一些。不过案例中选择用了 catch all route 这个也比较常见实现进行学习

组件部分的实现比较简单:

import NewsList from "@/app/_components/news-list";
import {getAvailableNewsMonths,getAvailableNewsYears,getNewsForYear,getNewsForYearAndMonth,
} from "@/app/_lib/news";
import Link from "next/link";
import React from "react";const FilteredNewsPage = ({ params }) => {const filter = params.filter;const selectedYear = filter?.[0];const selectedMonth = filter?.[1];let news;let links = getAvailableNewsYears();if (selectedYear && !selectedMonth) {news = getNewsForYear(selectedYear);links = getAvailableNewsMonths(selectedYear);} else if (selectedYear && selectedMonth) {news = getNewsForYearAndMonth(selectedYear, selectedMonth);links = [];}let newsContent = <p>No news found for the selected period.</p>;if (news?.length) {newsContent = <NewsList news={news} />;}return (<><header id="archive-header"><nav><ul>{links.map((link) => {const href = selectedYear? `/archive/${selectedYear}/${link}`: `/archive/${link}`;return (<li key={link}><Link href={href}>{link}</Link></li>);})}</ul></nav></header>{newsContent}</>);
};export default FilteredNewsPage;

这里需要注意的是 params 的返回值,从字符串变成了数组。这是 catch all 的特性,也就是拦截所有的 params

目录结构如下:

需要注意的是这种情况下, @archive 下的 page.js 就会导致冲突,因为 [[...filter]] 本身就拦截了所有的路径——前面也提到过了

最终效果如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/88739.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/88739.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TCP 传输时 sk_buff 的 clone 和 unclone

周一有位朋友咨询个问题&#xff0c;问题本身不重要&#xff0c;但牵扯出的细节却是非常有趣。 Linux 内核协议栈的 skb 设计非常高效和精巧&#xff0c;多个 skb 可以指向同一块 data&#xff0c;这就是 clone&#xff0c;当 data 不止一个 skb 指示时&#xff0c;任何一个 s…

【51单片机】51单片机学习笔记-课程简介

00. 目录 文章目录00. 目录01. 学习哪种类型的单片机02. 学习单片机方法03. 学习单片机硬件设备04. 学习单片机软件设备05. 学完单片机能做什么06. 附录01. 学习哪种类型的单片机 单片机的型号那么多&#xff0c;该如何选择一款合适的进行学习呢&#xff1f;这里给读者首推的当…

【Docker基础】Docker端口映射(-p参数)深度解析与实践指南

目录 前言 1 Docker网络基础 1.1 Docker网络模型概述 1.2 容器网络隔离性 2 端口映射基础 2.1 端口映射概念 2.2 为什么需要端口映射 3 -p参数详解 3.1 基本语法 3.2 四种映射格式 3.2.1 完整格式 3.2.2 省略宿主机IP 3.2.3 随机宿主机端口 3.2.4 指定协议类型 …

2、鸿蒙Harmony Next开发:ArkTS语言

目录 什么是ArkTS&#xff1f; ArkTS的发展趋势 ArkTS的定位及约束 ArkTS的对UI的拓展 1、UI描述 2、状态管理&#xff1a; ArkTS语法基础 基本知识&#xff1a;声明 基本知识&#xff1a;类型 基本知识&#xff1a;空安全 基本知识&#xff1a;类型安全与类型推断 …

【Elasticsearch】function_score

如果你希望在 Elasticsearch 查询中降低某些特定 `id` 的文档评分,可以通过 `function_score` 查询结合 `script_score` 函数来实现。`script_score` 允许你使用自定义脚本对文档的评分进行调整。 以下是一个示例,展示如何降低某些特定 `id` 的文档评分: 示例场景 假设我们…

vscode打开stm32CubeIDE的项目的注释问题

文章目录 目的是为消除红色底线打开命令面板&#xff1a;CtrlShiftP 搜索并打开&#xff1a;C/C: Edit Configurations (JSON) 修改并添加。&#xff08;注意里面的版本号&#xff09; {"configurations": [{"name": "Win32","includePath&…

ESP32使用freertos更新lvgl控件内容

LVGL不是线程安全&#xff0c;所有 lv_xxx方法只能在GUI主线程调用。 freertos都是线程池&#xff0c;子线程&#xff0c;不能直接更新lvgl&#xff0c;不然看门狗被触发&#xff0c;死机。 推荐方法案例&#xff1a; 假如搜索wifi列表得到参数是wifi_options&#xff0c;需要通…

OBOO鸥柏丨满天星(MTSTAR)多媒体信息发布系统技术解析

初次启动欢迎您使用鸥柏(OBOO)满天星(MTSTAR)多媒体信息发布系统&#xff0c;在使用本系统的独立服务器模式前&#xff0c;我们需要完成设备的一些必须设置教程技术说明。其总体流程分为两步&#xff1a;录入本地服务器IP地址->连接网络您获取到的OBOO鸥柏满天星(MTSTAR)液晶…

数据结构:栈、队列、链表

目录 栈 ​队列 链表 栈 栈数据结构特点&#xff1a;先入栈的数据后出&#xff0c;此数据结构常用的方法有&#xff1a;入栈push、出栈pop、查看栈顶元素peek等&#xff0c;下方示例以数组实现栈结构。 package com.ginko.datastructure; import lombok.extern.slf4j.Slf4j…

Python-难点-uinttest

1 需求要求&#xff1a;unittest.TestCase放在列表中&#xff0c;列表存储的是脚本文件名import使用动态加载方式&#xff1a;importlib.import_module()unittest.TestLoader使用loadTestsFromModule()2 接口3 示例4 参考资料

开源 python 应用 开发(五)python opencv之目标检测

最近有个项目需要做视觉自动化处理的工具&#xff0c;最后选用的软件为python&#xff0c;刚好这个机会进行系统学习。短时间学习&#xff0c;需要快速开发&#xff0c;所以记录要点步骤&#xff0c;防止忘记。 链接&#xff1a; 开源 python 应用 开发&#xff08;一&#xf…

ABP VNext + OpenTelemetry + Jaeger:分布式追踪与调用链可视化

ABP VNext OpenTelemetry Jaeger&#xff1a;分布式追踪与调用链可视化 &#x1f680; &#x1f4da; 目录ABP VNext OpenTelemetry Jaeger&#xff1a;分布式追踪与调用链可视化 &#x1f680;背景与动机 &#x1f31f;环境与依赖 &#x1f4e6;必装 NuGet 包系统架构概览…

C语言中整数编码方式(原码、反码、补码)

在 C 语言中&#xff0c;原码、反码、补码的运算规则与其编码特性密切相关&#xff0c;核心差异体现在符号位是否参与运算、进位如何处理以及减法是否能转化为加法等方面。以下是三者的运算规则及特点分析&#xff08;以 8 位整数为例&#xff0c;符号位为最高位&#xff09;&a…

js二维数组如何变为一维数组

在 JavaScript 中&#xff0c;将二维数组转换为一维数组&#xff08;扁平化&#xff09;有多种方法&#xff0c;可根据数组结构复杂度、性能需求和兼容性选择。以下是最常用的实现方式&#xff1a; 1. 使用 flat() 方法&#xff08;ES2019&#xff09; MDN释义&#xff1a;flat…

Claude code在Windows上的配置流程

前言 昨天在服务器上配置好了 Claude code&#xff0c;发现其编码性能和效率都非常不错。 然而&#xff0c;尝试用它修改带 UI 界面的客户端程序时颇为不便&#xff0c;因为服务器没有图形化界面&#xff0c;无法直接将应用界面直接显示到开发机上&#xff0c;调试起来颇为不…

手把手教你用YOLOv10打造智能垃圾检测系统

无需编程基础&#xff01;手把手教你用YOLOv10打造智能垃圾检测系统 垃圾分类不再难&#xff0c;AI助手秒识别 你是否曾站在分类垃圾桶前犹豫不决&#xff1f;塑料瓶是可回收还是其他垃圾&#xff1f;外卖餐盒到底该丢哪里&#xff1f;随着垃圾分类政策推广&#xff0c;这样的困…

batchnorm类

1. 伪代码&#xff1a;2. python代码&#xff1a;3. 测试&#xff1a;4. 加深理解&#xff1a;以 为例&#xff0c;x3&#xff0c;可见输出的batchnorm后y0.2627.查看模型记录的均值及方差&#xff0c;计算y0.286799&#xff0c;理解是大致这样的计算过程。&#xff08;为什么数…

SpringBoot项目保证接口幂等的五种方法!

1. 幂等概述 1.1 深入理解幂等性 在计算机领域中&#xff0c;幂等&#xff08;Idempotence&#xff09;是指任意一个操作的多次执行总是能获得相同的结果&#xff0c;不会对系统状态产生额外影响。在Java后端开发中&#xff0c;幂等性的实现通常通过确保方法或服务调用的结果…

SQL新手入门详细教程和应用实例

SQL(Structured Query Language)是用于管理和操作关系型数据库的标准语言。它允许你创建、查询、更新和删除数据。本教程将从基础概念开始,逐步引导你上手SQL,并提供详细的应用实例。教程基于标准SQL语法,实际使用时需根据数据库系统(如MySQL、SQLite或PostgreSQL)调整。…

DVWA-LOW级-SQL手工注入漏洞测试(MySQL数据库)+sqlmap自动化注入-小白必看(超详细)

首次使用DVWA的靶场&#xff0c;咋们先从最低级别的LOW开始&#xff0c;因为之前玩过一下墨者学院&#xff0c;对sql注入有一点认识和理解&#xff0c;所以先从sql的盲注开始&#xff1b; 1、测试注入点是否存在sql注入的漏洞&#xff1b; &#xff08;1&#xff09;首先我们…