当前位置: 首页 > news >正文

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.dtcms.com/a/269915.html

相关文章:

  • SQLShift 重磅更新:支持 SQL Server 存储过程转换至 GaussDB!
  • 从深度学习的角度看自动驾驶
  • 半连接海外云策略:混合架构下的全球业务协同方案
  • 香港站群服务器价格怎么样?
  • 保姆级tomcat的页面部署(静态)
  • 【世纪龙科技】汽车零部件检验虚拟实训室-数字赋能职业教育
  • PHP诞生30周年
  • 文件传输安全保障:探索Hash校验的不同方法
  • 使用阿里云/腾讯云安装完成mysql使用不了
  • JavaScript中的Request详解:掌握Fetch API与XMLHttpRequest
  • 单稳态触发器Multisim电路仿真——硬件工程师笔记
  • imx6ull-裸机学习实验11——高精度延时实验
  • 铝板矫平机:精密平整的关键设备
  • AI 在生活中的应用:深度解析与技术洞察
  • [2025CVPR]SGC-Net:开放词汇人机交互检测的分层粒度比较网络解析
  • Java教程:【程序调试技巧】入门
  • Leetcode 3604. Minimum Time to Reach Destination in Directed Graph
  • Windows安装docker+Dify本地部署
  • IB智慧公交系统的设计与实现
  • Python之--列表
  • 【AI大模型】PyTorch Autograd 实战
  • 测量认知革命:Deepoc大模型如何重构示波器的存在形态
  • Cursor配置DeepSeek调用MCP服务实现任务自动化
  • Flutter编译安卓应用时遇到的compileDebugJavaWithJavac和compileDebugKotlin版本不匹配的问题
  • GC4344:高性能音频 DAC 芯片解析
  • 【ASP.NET Core】深入理解Controller的工作机制
  • Android T startingwindow使用总结
  • 【AI智能体】智能音视频-硬件设备基于 WebSocket 实现语音交互
  • ReactNative【实战系列教程】我的小红书 4 -- 首页(含顶栏tab切换,横向滚动频道,频道编辑弹窗,瀑布流布局列表等)
  • 深入解读MCP:构建低延迟、高吞吐量通信中间件