## 簡介與目標
在這篇部落格文章中,我想介紹您在實際場景中需要的最重要的 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 的 Clientonent。
我們將 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本地狀態參數一起提交.
和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
函數用於備註詳情頁
筆記詳細資料頁面根據其唯一 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 ...............*/} </> ) }
完成此操作後,我們將 params.noteId
傳遞給/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」標記傳回的結果> 和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 和TagCloudRecentActivity 和TagCloud
RecentActivity.每個伺服器元件獨立取得自己的資料。
每個伺服器元件都包裝在 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中文網其他相關文章!