当前位置: 首页 > 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.lryc.cn/news/582511.html

相关文章:

  • 算法训练营DAY29 第八章 贪心算法 part02
  • ubuntu 操作记录
  • Python语言+pytest框架+allure报告+log日志+yaml文件+mysql断言实现接口自动化框架
  • 机制、形式、周期、内容:算法备案抽检复审政策讲解
  • 探索下一代云存储技术:对象存储、文件存储与块存储的区别与选择
  • 光流 | 当前光流算法还存在哪些缺点及难题?
  • ReactNative【实战系列教程】我的小红书 4 -- 首页(含顶栏tab切换,横向滚动频道,频道编辑弹窗,瀑布流布局列表等)
  • 闲庭信步使用图像验证平台加速FPGA的开发:第五课——HSV转RGB的FPGA实现
  • Java连接Emqx实现订阅发布消息
  • 恒创科技:香港站群服务器做seo站群优化效果如何
  • ReactNative【实战】瀑布流布局列表(含图片自适应、点亮红心动画)
  • Rust DevOps框架管理实例
  • ffmpeg下编译tsan
  • iOS 性能测试工具全流程:主流工具实战对比与适用场景
  • cocos2dx3.x项目升级到xcode15以上的iconv与duplicate symbols报错问题
  • CSP-S模拟赛二总结(实际难度大于CSP-S)
  • 力扣 239 题:滑动窗口最大值的两种高效解法
  • Android kotlin 协程的详细使用指南
  • C++--AVL树
  • 微前端框架对比
  • (16)Java+Playwright自动化测试-iframe操作-监听事件和执行js脚本
  • 精益管理与数字化转型的融合:中小制造企业降本增效的双重引擎
  • Nexus zkVM 3.0 及未来:迈向模块化、分布式的零知识证明
  • 生成PDF文件(基于 iText PDF )
  • Android framework修改解决偶发开机时有两个launcher入口的情况
  • Prompt Injection Attack to Tool Selection in LLM Agents
  • 论文略读:Prefix-Tuning: Optimizing Continuous Prompts for Generation
  • C++11标准库算法:深入理解std::find, std::find_if与std::find_if_not
  • Python中os.path和pathlib模块路径操作函数汇总
  • react的条件渲染【简约风5min】