## 简介和目标
在这篇博客文章中,我想介绍您在实际场景中需要的最重要的 Next.js 功能。
我创建这篇博客文章作为我自己和感兴趣的读者的参考。而不必阅读整个 nextjs 文档。我认为写一篇精简博客文章包含所有接下来的重要实用功能会更容易,您可以定期访问以刷新您的知识!
我们将在并行构建笔记应用程序的同时一起了解以下功能。
应用路由器
加载和错误处理
服务器操作
数据获取和缓存
流媒体和悬念
平行路线
错误处理
我们最终的笔记应用程序代码将如下所示:
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
请直接跳到最终代码,您可以在这个 Github 存储库 spithacode 中找到。
事不宜迟,让我们开始吧!
在深入开发我们的笔记应用程序之前,我想介绍一些关键的 nextjs 概念,在继续之前了解这些概念非常重要。
App Router 是一个新目录 "/app",它支持许多旧版 "/page" 目录中不可能的功能,例如:
嵌套路由:您可以将文件夹嵌套在另一个文件夹中。页面路径url将遵循相同的文件夹嵌套。例如,假设[noteId]动态参数等于/app/notes/[noteId]/edit/page.tsx对应的url >“1” 是 “/notes/1/edit。
/app Router 之前都应该掌握这个主题。
服务器组件服务器组件基本上是在服务器上呈现的组件。
任何前面没有“use client”指令的组件默认都是服务器组件,包括页面和布局。
服务器组件可以与任何nodejs API或任何要在服务器上使用的组件交互。与
客户端组件不同,可以在服务器组件之前添加async关键字。因此,您可以调用任何异步函数并在渲染组件之前等待它。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
答案可以用几句话来概括
SEO、性能和用户体验。
当用户访问页面时,浏览器会下载网站资源,包括 html、css 和 javascript。由于其大小,JavaScript 包(包括您的框架代码)比其他资源需要更多的时间来加载。
爬虫。
SEO 指标,例如 LCP、TTFB、跳出率...都会受到影响。
客户端组件 只是一个发送到用户浏览器的组件。
客户端组件不仅仅是裸露的 html 和 css 组件。它们需要交互性才能工作,因此实际上不可能在服务器上渲染它们。
交互性由像react(useState,useEffect)这样的javascript框架或仅浏览器或DOM API来保证。
客户端组件声明之前应有“use client”指令。这告诉 Nextjs 忽略它的交互部分(useState、useEffect...)并将其直接发送到用户的浏览器。
/client-component.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
我知道,Nextjs 中最令人沮丧的事情是,如果您错过了 服务器组件 和 客户端组件 之间的嵌套规则,您可能会遇到那些奇怪的错误。
因此,在下一节中,我们将通过展示 服务器组件 和 客户端组件 之间可能的不同嵌套排列来澄清这一点。
我们将跳过这两种排列,因为它们显然是允许的:客户端组件另一个客户端组件和另一个服务器组件内的服务器组件。
您可以导入客户端组件并在服务器组件内正常渲染它们。这种排列很明显,因为 pages 和 layouts 默认情况下是服务器组件。
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
想象一下将客户端组件发送到用户的浏览器,然后等待位于其中的服务器组件来渲染和获取数据。这是不可能的,因为服务器组件已经发送到客户端,那么如何在服务器上渲染它?
这就是为什么 Nextjs 不支持这种类型的排列。
因此请始终记住避免将 服务器组件 导入到 客户端组件 中以将它们渲染为子组件。
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
始终尝试通过在 jsx 树中下推客户端组件来减少发送到用户浏览器的 JavaScript。
不可能直接导入和渲染服务器组件作为客户端组件的子组件,但是有一个解决方法可以利用反应可组合性.
技巧是将 服务器组件 作为 客户端组件 的子级传递到更高级别的服务器组件 (ParentServerComponent)。
我们称之为爸爸把戏:D.
此技巧可确保传递的 服务器组件 在将 客户端组件 发送到用户的浏览器之前在服务器上呈现。
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
我们将在笔记应用程序的 /app/page.tsx 主页看到一个具体示例。
我们将在其中渲染作为客户端组件内的子组件传递的服务器组件。客户端组件可以根据布尔状态变量值有条件地显示或隐藏服务器组件渲染的内容。
服务器操作是一个有趣的nextjs功能,它允许远程调用远程和安全地从客户端组件在服务器上声明的函数.
要声明服务器操作,您只需将“use server”指令添加到函数主体中,如下所示。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
“use server”指令告诉Nextjs该函数包含仅在服务器上执行的服务器端代码。
在底层,Nextjs 传送 操作 ID 并为此操作创建一个保留端点。
因此,当您在 客户端组件 中调用此操作时,Nextjs 将对由 Action Id 标识的操作唯一端点执行 POST 请求,同时传递您在调用请求正文中的操作时传递的序列化参数。
让我们通过这个简化的示例更好地阐明这一点。
我们之前看到,您需要在函数体指令中使用 “use server” 来声明服务器操作。但是如果您需要一次声明一堆服务器操作怎么办?
嗯,您可以在文件头或文件开头使用该指令,如下面的代码所示。
/server/actions.ts
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
请注意,服务器操作应始终标记为异步
所以在上面的代码中,我们声明了一个名为 createLogAction.
该操作负责将日志条目保存在服务器上 /logs 目录下的特定文件中。
文件根据 名称 操作参数命名。
操作附加一个日志条目,其中包含创建日期和消息操作参数。
现在,让我们在 CreateLogButton 客户端组件中使用我们创建的操作。
/components/CreateLogButton.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
按钮组件声明了一个名为 isSubmitting 的本地状态变量,用于跟踪操作是否正在执行。执行操作时,按钮文本从 “登录按钮” 更改为 “正在加载...”。
当我们单击日志按钮组件时,将调用服务器操作。
首先,让我们从创建注释验证架构和类型开始。
由于模型应该处理数据验证,我们将使用一个流行的库来实现此目的,称为 zod。
zod 的酷之处在于其描述性且易于理解的 API,这使得定义模型和生成相应的 TypeScript 成为一项无缝任务。
我们不会在笔记中使用奇特的复杂模型。每个笔记都有一个唯一的 ID、标题、内容和创建日期字段。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
我们还声明了一些有用的附加模式,例如 InsertNoteSchema 和WhereNoteSchema,当我们创建稍后操作模型的可重用函数时,这将使我们的生活变得更轻松。
我们将在内存中存储和操作我们的笔记。
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
我们将注释数组存储在全局 this 对象中,以避免每次将注释常量导入到文件中时丢失数组的状态(页面重新加载...)。
createNote 用例将允许我们将注释插入到注释数组中。将 notes.unshift 方法视为 notes.push 方法的逆方法,因为它将元素推送到数组的开头而不是末尾。
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
我们将使用 updateNote 更新注释数组中给定其 id 的特定注释。它首先查找元素的索引,如果没有找到则抛出错误,并根据找到的索引返回相应的注释。
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
deleteNote 用例函数将用于删除给定笔记 ID 的给定笔记。
该方法的工作原理类似,首先它根据给定的 id 查找注释的索引,如果未找到则抛出错误,然后返回由找到的 id 索引的相应注释。
"use client" import { ServerComponent } from '@/components' // Not allowed :( export const ClientComponent = ()=>{ return ( <> <ServerComponent/> </> ) }
getNote 函数是不言自明的,它只会根据给定的 id 查找一条注释。
import {ClientComponent} from '@/components/...' import {ServerComponent} from '@/components/...' export const ParentServerComponent = ()=>{ return ( <> <ClientComponent> <ServerComponent/> </ClientComponent> </> ) }
由于我们不想将整个笔记数据库推送到客户端,因此我们只会获取可用笔记总数的一部分。因此我们需要实现服务器端分页。
export const Component = ()=>{ const serverActionFunction = async(params:any)=>{ "use server" // server code lives here //... / } const handleClick = ()=>{ await serverActionFunction() } return <button onClick={handleClick}>click me</button> }
因此 getNotes 函数基本上允许我们通过传递 page 参数从服务器获取特定页面。
limit 参数用于确定给定页面上存在的项目数量。
例如:
如果 notes 数组包含 100 个元素,且 limit 参数等于 10。
通过向我们的服务器请求第 1 页,只会返回前 10 项。
search 参数将用于实现服务器端搜索。它将告诉服务器仅返回将 search 字符串作为标题或内容属性中的子字符串的注释。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
此用例将用于获取有关用户最近活动的一些虚假数据。
我们将在 /dashboard 页面中使用此功能。
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
此用例函数将负责获取有关我们笔记中使用的不同标签的统计信息(#something)。
我们将在 /dashboard 页面中使用此功能。
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
我们将使用这个用例函数来返回一些有关某些用户信息的虚假数据,例如姓名、电子邮件......
我们将在 /dashboard 页面中使用此功能。
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
"use client" import { ServerComponent } from '@/components' // Not allowed :( export const ClientComponent = ()=>{ return ( <> <ServerComponent/> </> ) }
在此主页中,我们将演示之前的技巧或解决方法,用于在客户端组件内渲染服务器组件(PaPa技巧:D) .
/app/page.tsx
import {ClientComponent} from '@/components/...' import {ServerComponent} from '@/components/...' export const ParentServerComponent = ()=>{ return ( <> <ClientComponent> <ServerComponent/> </ClientComponent> </> ) }
在上面的代码中,我们声明了一个名为 Home 的 父服务器组件,它负责在我们的应用程序中渲染 "/" 页面。
我们正在导入一个名为 RandomNote 的 Server Component 和一个名为 NoteOfTheDay 的 ClientComponent。
我们将 RandomNote 服务器组件作为子组件传递给 NoteOfTheDay 客户端组件。
/app/components/RandomNote.ts
export const Component = ()=>{ const serverActionFunction = async(params:any)=>{ "use server" // server code lives here //... / } const handleClick = ()=>{ await serverActionFunction() } return <button onClick={handleClick}>click me</button> }
RandomNote 服务器组件的工作原理如下:
它使用 getRandomNote 用例函数获取随机注释。
它呈现由标题和完整注释的部分或子字符串内容组成的注释详细信息。
/app/components/NoteOfTheDay.ts
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
另一侧的 NoteOfTheDay 客户端组件的工作原理如下:
/app/notes/page.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
我们将首先创建 /app/notes/page.tsx 页面,它是一个服务器组件,负责:
获取页面搜索参数,即附加在 URL 末尾 ? 标记后的字符串:http://localhost:3000/notes?page=1&search=Something
将搜索参数传递到名为 fetchNotes.
fetchNotes 函数使用我们之前声明的用例函数 getNotes 来获取当前笔记页面。
您可以注意到,我们正在使用从 "next/cache" 导入的名为 unstable_cache 的实用函数来包装 getNotes 函数。不稳定的缓存函数用于缓存 getNotes 函数的响应。
如果我们确定数据库中没有添加任何注释。每次重新加载页面时都点击它是没有意义的。因此 unstable_cache 函数使用 "notes" 标签标记 getNotes 函数结果,我们稍后可以使用该标签使 "notes" 缓存添加或删除注释。
fetchNotes 函数返回两个值:音符和总计。
NotesList 的 客户端组件,它负责渲染我们的笔记。
为了解决这个问题,我们将使用一个很棒的 Nextjs 功能,称为。
服务器端页面流。
/app/notes/page.tsx 文件旁边创建 loading.tsx 文件来做到这一点。
/app/notes/loading.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
那不是很酷吗:)。只需创建一个loading.tsx 文件,瞧,你就完成了。您的用户体验正在提升到一个新的水平。
/app/notes/components/NotesList.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
注释列表 客户端组件 从其父 服务器组件(即 NotesPage。
接收注释和分页相关数据)然后组件处理渲染当前笔记页面。每个单独的笔记卡均使用 NoteView 组件呈现。
它还使用 Next.js Link 组件提供上一页和下一页的链接,该组件对于预获取下一页和上一页数据至关重要,以便我们拥有一个无缝且快速的客户端侧边导航。
为了处理服务器端搜索,我们使用一个名为useNotesSearch的自定义钩子,它基本上处理当用户在搜索中键入特定查询时触发笔记重新获取输入.
/app/notes/components/NoteView.ts
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
NoteView 组件很简单,它只负责渲染每个单独的笔记卡及其相应的:标题、部分内容以及用于查看笔记详细信息或编辑它的操作链接。
/app/notes/components/hooks/use-notes-search.ts
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
useNotesSearch 自定义挂钩的工作原理如下:
它使用 useState 钩子将 initialSearch 属性存储在本地状态中。
我们使用 useEffect React 钩子在 currentPage 或 debouncedSearchValue 变量值发生变化时触发页面导航。
新的页面 URL 是在考虑当前页面和搜索值的情况下构建的。
当用户在搜索输入中键入内容时,每次字符发生变化时都会调用 setSearch 函数。这会导致短时间内导航过多。
为了避免我们只在用户停止输入其他术语时触发导航,我们将在特定的时间内(在我们的例子中为 300 毫秒)对搜索值进行去抖动。
接下来,让我们浏览一下 /app/notes/create/page.tsx,它是 CreateNoteForm 客户端组件的服务器组件包装器。
/app/notes/create/page.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/notes/create/components/CreateNoteForm.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
CreateNoteForm 客户端组件表单负责从用户检索数据,然后将其存储在本地状态变量(标题、内容)中。
点击提交按钮提交表单后,createNoteAction将与title和content本地状态参数一起提交.
isSubmitting状态布尔变量用于跟踪操作提交状态。
如果 createNoteAction 成功提交且没有任何错误,我们会将用户重定向到 /notes 页面。
/app/notes/create/actions/create-note.action.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
createNoteAction 操作代码很简单,包含文件前面带有 “use server” 指令,指示 Next.js 该操作可在客户端组件中调用。
关于服务器操作,我们应该强调的一点是,只有操作接口被发送到客户端,而不是操作本身内部的代码。
换句话说,操作中的代码将驻留在服务器上,因此我们不应该信任从客户端到我们服务器的任何输入。
这就是为什么我们在这里使用 zod 来使用我们之前创建的模式来验证 rawNote 操作参数。
验证输入后,我们将使用经过验证的数据调用 createNote 用例。
如果笔记创建成功,则会调用 revalidateTag 函数来使标记为 "notes" 的缓存条目失效(记住 unstable_cache 函数用于 /notes 页面)。
笔记详细信息页面根据其唯一 ID 呈现特定笔记的标题和完整内容。除此之外,它还显示一些用于编辑或删除注释的操作按钮。
/app/notes/[noteId]/page.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
首先,我们从页面道具中检索页面参数。在 Next.js 13 中,我们必须等待 params 页面参数,因为它是一个承诺。
完成此操作后,我们将 params.noteId 传递给 fetchNote 本地声明的函数。
/app/notes/[noteId]/fetchers/fetch-note.ts
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
fetchNote 函数使用 unstable_cache 包装我们的 getNote 用例,同时用 “note-details”标记返回的结果 和 note-details/${id} 标签。
“note-details”标签可用于一次性使所有笔记详细信息缓存条目失效。
另一方面,note-details/${id} 标签仅与由其唯一 id 定义的特定注释相关联。因此我们可以使用它来使特定笔记的缓存条目无效,而不是使整个笔记集无效。
/app/notes/[noteId]/loading.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
提醒
loading.tsx 是一个特殊的 Next.js 页面,当笔记详细信息页面在服务器上获取其数据时呈现。
或者换句话说,当fetchNote函数正在执行时,将向用户显示骨架页面而不是空白屏幕。
这个 nextjs 功能称为 页面流。它允许发送动态页面的整个静态父布局,同时逐渐流式传输其内容。
这可以避免在服务器上获取页面的动态内容时阻塞用户界面,从而提高性能和用户体验。
/app/notes/[noteId]/components/DeleteNoteButton.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
现在让我们深入了解 DeleteNoteButton 客户端组件。
该组件负责渲染删除按钮并执行 deleteNoteAction,然后在操作成功执行时将用户重定向到 /notes 页面。
为了跟踪操作执行状态,我们使用本地状态变量isDeleting.
/app/notes/[noteId]/actions/delete-note.action.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
deleteNoteAction 代码的工作原理如下:
/app/notes/[noteId]/edit/page.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
/app/notes/[noteId]/edit/page.tsx 页面是一个服务器组件,它从 params Promise 获取 noteId 参数。
然后它使用 fetchNote 函数获取注释。
成功获取后。它将注释传递给 EditNoteForm 客户端组件。
/app/notes/[noteId]/edit/components/EditNoteForm.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
EditNoteForm 客户端组件接收注释并呈现一个表单,允许用户更新注释的详细信息。
title 和 content 局部状态变量用于存储其相应的输入或文本区域值。
通过更新注释按钮提交表单时。 updateNoteAction 被调用,并以 title 和 content 值作为参数。
isSubmitting 状态变量用于跟踪操作提交状态,允许在操作执行时显示加载指示器。
/app/notes/[noteId]/edit/actions/edit-note.action.ts
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
updateNoteAction 操作的工作原理如下:
/app/dashboard/page.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/dashboard/page.tsx 页面被分解为更小的服务器端组件:NotesSummary、RecentActivity 和 TagCloud.
每个服务器组件独立获取自己的数据。
每个服务器组件都包装在 React Suspense 边界中。
悬念边界的作用是当子服务器组件获取自己的数据时显示后备组件(在我们的例子中是一个骨架)。
或者换句话说,Suspense边界允许我们推迟或延迟其子级的渲染,直到满足某些条件(正在加载子级内的数据)。
因此用户将能够将页面视为一堆骨架的组合。服务器正在传输每个单独组件的响应。
这种方法的一个关键优点是,如果一个或多个服务器组件比另一个组件花费更多时间,可以避免阻塞 UI。
因此,如果我们假设每个组件的单独获取时间分布如下:
当我们点击刷新时,我们首先看到的是 3 个骨架加载器。
1 秒后,RecentActivity 组件将显示。
2 秒后,NotesSummary 将紧随其后,然后是 TagCloud。
所以不要让用户等待 3 秒才能看到任何内容。我们通过首先显示 RecentActivity 将时间缩短了 2 秒。
这种增量渲染方法可以带来更好的用户体验和性能。
各个服务器组件的代码在下面突出显示。
/app/dashboard/components/RecentActivity.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
RecentActivity 服务器组件基本上使用 getRecentActivity 用例函数获取最后的活动,并将它们呈现在无序列表中。
/app/dashboard/components/TagCloud.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
TagCloud 服务器端组件获取然后渲染笔记内容中使用的所有标签名称及其各自的计数。
/app/dashboard/components/NotesSummary.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
NotesSummary 服务器组件在使用 getNoteSummary 用例函数获取摘要信息后呈现摘要信息。
现在让我们进入个人资料页面,在这里我们将介绍一个有趣的 nextjs 功能,称为 并行路由。
并行路线允许我们同时或有条件渲染一个或多个页面在同一布局内。
在下面的示例中,我们将在 /app/profile 相同的布局中渲染 用户信息页面 和 用户注释页面 .
您可以使用命名槽创建并行路由。命名槽完全被声明为子页面,但与普通页面不同,@ 符号应位于文件夹名称之前。
例如,在 /app/profile/ 文件夹中,我们将创建两个命名槽:
现在让我们创建一个布局文件 /app/profile/layout.tsx 文件,它将定义 /profile 页面的布局。
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
正如您从上面的代码中看到的,我们现在可以访问 info 和 notes 参数,其中包含 @info 和 @notes 页面内的内容。
因此 @info 页面将呈现在左侧,@notes 将呈现在右侧。
page.tsx 中的内容(由 children 引用)将呈现在页面底部。
/app/profile/@info/page.tsx
export const ServerComponent = async ()=>{ const posts = await getSomeData() // call any nodejs api or server function during the component rendering // Don't even think about it. No useEffects are allowed here x_x const pasta = await getPasta() return ( <ul> { data.map(d=>( <li>{d.title}</li> )) } </ul> ) }
UserInfoPage 是一个服务器组件,它将使用 getUserInfo 用例函数获取用户信息。
当组件获取数据并在服务器上渲染时(服务器端流式传输),上述后备框架将被发送到用户浏览器。
/app/profile/@info/loading.tsx
"use client" import React,{useEffect,useState} from "react" export const ClientComponent = ()=>{ const [value,setValue] = useState() useEffect(()=>{ alert("Component have mounted!") return ()=>{ alert("Component is unmounted") } },[]) //.......... return ( <> <button onClick={()=>alert("Hello, from browser")}></button> {/* .......... JSX Code ...............*/} </> ) }
同样的事情也适用于 LastNotesPage 服务器端组件。它将获取数据并在服务器上渲染,同时向用户显示骨架 UI
/app/profile/@notes/page.tsx
import { ClientComponent } from '@/components' // Allowed :) export const ServerComponent = ()=>{ return ( <> <ClientComponent/> </> ) }
/app/profile/@notes/loading.tsx
"use client" import { ServerComponent } from '@/components' // Not allowed :( export const ClientComponent = ()=>{ return ( <> <ServerComponent/> </> ) }
现在让我们探索 Nextjs 中的一个非常好的功能 error.tsx 页面。
当您将应用程序部署到生产环境时,您肯定会希望在某个页面抛出未捕获的错误时显示用户友好的错误。
这就是 error.tsx 文件出现的地方。
让我们首先创建一个示例页面,该页面会在几秒钟后抛出未捕获的错误。
/app/error-page/page.tsx
import {ClientComponent} from '@/components/...' import {ServerComponent} from '@/components/...' export const ParentServerComponent = ()=>{ return ( <> <ClientComponent> <ServerComponent/> </ClientComponent> </> ) }
当页面睡眠或等待睡眠函数执行时。将向用户显示以下加载页面。
/app/error-page/loading.tsx
export const Component = ()=>{ const serverActionFunction = async(params:any)=>{ "use server" // server code lives here //... / } const handleClick = ()=>{ await serverActionFunction() } return <button onClick={handleClick}>click me</button> }
几秒钟后,将抛出错误并删除您的页面:(。
为了避免这种情况,我们将创建 error.tsx 文件,该文件导出一个组件,该组件将充当 /app/error-page/page 的 错误边界 .tsx.
/app/error-page/error.tsx
- app/ - notes/ --------------------------------> Server Side Caching Features - components/ - NotesList.tsx - [noteId]/ - actions/ -------------------------> Server Actions feature - delete-note.action.ts - edit-note.action.ts - components/ - DeleteButton.tsx - page.tsx - edit/ - components/ - EditNoteForm.tsx - page.tsx - loading.tsx --------------------> Page level Streaming feature - create/ - actions/ - create-note.action.ts - components/ - CreateNoteForm.tsx - page.tsx - error-page/ - page.tsx - error.tsx --------------------------> Error Boundary as a page feature - dashboard/ ---------------------------> Component Level Streaming Feature - components/ - NoteActivity.tsx - TagCloud.tsx - NotesSummary.tsx - page.tsx - profile/ ----------------------------->[6] Parallel Routes Feature - layout.tsx - page.tsx - @info/ - page.tsx - loading.tsx - @notes/ - page.tsx - loading.tsx - core/ --------------------------> Our business logic lives here - entities/ - note.ts - use-cases/ - create-note.use-case.ts - update-note.use-case.ts - delete-note.use-case.ts - get-note.use-case.ts - get-notes.use-case.ts - get-notes-summary.use-case.ts - get-recent-activity.use-case.ts - get-recent-tags.use-case.ts
在本指南中,我们通过构建实用的笔记应用程序探索了 Next.js 的关键功能。我们涵盖了:
通过将这些概念应用到实际项目中,我们获得了 Next.js 强大功能的实践经验。请记住,巩固理解的最好方法是通过实践。
如果您有任何疑问或想进一步讨论,请随时与我联系。
编码愉快!
以上是Next.js 深入探讨:构建具有高级功能的 Notes 应用程序的详细内容。更多信息请关注PHP中文网其他相关文章!