Next.js 中的客戶端路由

已發表: 2022-03-10
快速總結 ↬ Next.js 有一個基於文件的路由系統,其中每個頁面根據其文件名自動成為一個路由。 每個頁面都是從 pages 目錄導出的默認 React 組件,可用於定義最常見的路由模式。 本文將指導您了解有關 Next.js 中路由的幾乎所有知識,並為您指明相關主題和概念的方向。

超鏈接從一開始就是 Web 的瑰寶之一。 根據 MDN,超鏈接是使 Web成為 Web 的原因。 雖然用於文檔之間的鏈接等目的,但其主要用途是引用可通過唯一網址或 URL 識別的不同網頁。

路由是每個 Web 應用程序的一個重要方面,就像指向 Web 的超鏈接一樣。 它是一種將請求路由到處理它們的代碼的機制。 關於路由,Next.js 頁面由唯一的 URL 路徑引用和識別。 如果 Web 由通過超鏈接互連的導航網頁組成,那麼每個 Next.js 應用程序都由通過路由器互連的可路由頁面(路由處理程序或路由)組成。

Next.js 內置了對路由的支持,但解包起來很麻煩,尤其是在考慮渲染和數據獲取時。 作為理解 Next.js 中客戶端路由的先決條件,有必要對 Next.js 中的路由、渲染和數據獲取等概念有一個概述。

本文將對熟悉 Next.js 並希望了解它如何處理路由的 React 開發人員有所幫助。 您需要具備 React 和 Next.js 的工作知識才能充分利用本文,本文僅介紹 Next.js 中的客戶端路由和相關概念。

路由和渲染

路由和渲染是相輔相成的,將在本文的整個過程中發揮重要作用。 我喜歡 Gaurav 對它們的解釋:

路由是將用戶導航到網站上不同頁面的過程。

渲染是將這些頁面放在 UI 上的過程。 每次您請求到特定頁面的路由時,您也在渲染該頁面,但並非每次渲染都是路由的結果。

花五分鐘時間考慮一下。

關於 Next.js 中的渲染,您需要了解的是,每個頁面都預先與最少的 JavaScript 代碼一起預先渲染,以使其通過稱為水合的過程變得完全交互。 Next.js 如何做到這一點高度依賴於預渲染的形式:靜態生成服務器端渲染,它們都與所使用的數據獲取技術高度耦合,並在生成頁面的 HTML分開。

根據您的數據獲取要求,您可能會發現自己使用內置的數據獲取功能,如getStaticPropsgetStaticPathsgetServerSideProps 、客戶端數據獲取工具(如 SWR、react-query)或傳統的數據獲取方法(如 fetch-on-渲染,獲取然後渲染,渲染為你獲取(帶有懸念)。

預渲染(在渲染之前 -到 UI )是對路由的補充,並且與數據獲取高度耦合 - 在 Next.js 中它自己的整個主題。 因此,儘管這些概念是互補的或密切相關的,但本文將僅關注頁面之間的導航(路由),並在必要時引用相關概念。

說完這些,讓我們從基本要點開始:Next.js 有一個基於頁面概念的基於文件系統的路由器。

跳躍後更多! 繼續往下看↓

頁面

Next.js 中的頁面是自動作為路由可用的 React 組件。 它們作為默認導出從 pages 目錄導出,支持的文件擴展名為.js.jsx.ts.tsx

一個典型的 Next.js 應用程序將具有一個包含頂級目錄的文件夾結構,例如pagespublicstyles 。

 next-app ├── node_modules ├── pages │ ├── index.js // path: base-url (/) │ ├── books.jsx // path: /books │ └── book.ts // path: /book ├── public ├── styles ├── .gitignore ├── package.json └── README.md

每個頁面都是一個 React 組件:

 // pages/books.js — `base-url/book` export default function Book() { return

圖書

}

注意請記住,頁面也可以稱為“路由處理程序”。

自定義頁面

這些是駐留在頁面目錄中但不參與路由的特殊頁面。 它們以下劃線符號為前綴,如_app.js_document.js

  • _app.js
    這是一個位於 pages 文件夾中的自定義組件。 Next.js 使用這個組件來初始化頁面。
  • _document.js
    _app.js一樣, _document.js是 Next.js 用來增強應用程序<html><body>標記的自定義組件。 這是必要的,因為 Next.js 頁面跳過了周圍文檔標記的定義。
 next-app ├── node_modules ├── pages │ ├── _app.js // ️ Custom page (unavailable as a route) │ ├── _document.jsx // ️ Custom page (unavailable as a route) │ └── index.ts // path: base-url (/) ├── public ├── styles ├── .gitignore ├── package.json └── README.md

頁面之間的鏈接

Next.js 從next/link API 公開了一個Link組件,可用於在頁面之間執行客戶端路由轉換。

 // Import the <Link/> component import Link from "next/link"; // This could be a page component export default function TopNav() { return ( <nav> <Link href="/">Home</Link> <Link href="/">Publications</Link> <Link href="/">About</Link> </nav> ) } // This could be a non-page component export default function Publications() { return ( <section> <TopNav/> {/* ... */} </section> ) }

Link組件可以在任何組件內部使用,無論是否是頁面。 當以上面示例中最基本的形式使用時, Link組件將轉換為具有href屬性的超鏈接。 (更多關於Link在下面的下一個/鏈接部分。)

路由

Next.js 基於文件的路由系統可用於定義最常見的路由模式。 為了適應這些模式,每條路由都根據其定義進行分離。

索引路線

默認情況下,在您的 Next.js 應用程序中,初始/默認路由是pages/index.js ,它會自動作為/用作應用程序的起點。 使用localhost:3000的基本 URL,可以在瀏覽器中應用程序的基本 URL 級別訪問此索引路由。

索引路由自動充當每個目錄的默認路由,並且可以消除命名冗餘。 下面的目錄結構公開了兩個路由路徑: //home

 next-app └── pages ├── index.js // path: base-url (/) └── home.js // path: /home

嵌套路由的消除更為明顯。

嵌套路由

pages/book這樣的路由是一層深度。 更深入的是創建嵌套路由,這需要嵌套的文件夾結構。 使用https://www.smashingmagazine.com的基本 URL,您可以通過創建類似於以下的文件夾結構來訪問路線https://www.smashingmagazine.com/printed-books/printed-books

 next-app └── pages ├── index.js // top index route └── printed-books // nested route └── printed-books.js // path: /printed-books/printed-books

或者通過索引路徑消除路徑冗餘,並在https://www.smashingmagazine.com/printed-books訪問印刷書籍的路徑。

 next-app └── pages ├── index.js // top index route └── printed-books // nested route └── index.js // path: /printed-books

動態路由在消除冗餘方面也發揮著重要作用。

動態路線

在前面的示例中,我們使用索引路由訪問所有印刷書籍。 要訪問單個書籍,需要為每本書創建不同的路徑,例如:

 // ️ Don't do this. next-app └── pages ├── index.js // top index route └── printed-books // nested route ├── index.js // path: /printed-books ├── typesript-in-50-lessons.js // path: /printed-books/typesript-in-50-lessons ├── checklist-cards.js // path: /printed-books/checklist-cards ├── ethical-design-handbook.js // path: /printed-books/ethical-design-handbook ├── inclusive-components.js // path: /printed-books/inclusive-components └── click.js // path: /printed-books/click

這是高度冗餘、不可擴展的,並且可以通過動態路由進行補救,例如:

 // Do this instead. next-app └── pages ├── index.js // top index route └── printed-books ├── index.js // path: /printed-books └── [book-id].js // path: /printed-books/:book-id

括號語法 - [book-id] - 是動態段,並且不僅限於文件。 它也可以與下面示例中的文件夾一起使用,從而使作者在路徑/printed-books/:book-id/author中可用。

 next-app └── pages ├── index.js // top index route └── printed-books ├── index.js // path: /printed-books └── [book-id] └── author.js // path: /printed-books/:book-id/author

路由的動態段作為查詢參數公開,可以在路由中涉及的任何連接組件中使用useRouter()鉤子的query對象訪問該參數 - (有關此內容的更多信息,請參見下一個/路由器 API 部分)。

 // printed-books/:book-id import { useRouter } from 'next/router'; export default function Book() { const { query } = useRouter(); return ( <div> <h1> book-id <em>{query['book-id']}</em> </h1> </div> ); }
 // /printed-books/:book-id/author import { useRouter } from 'next/router'; export default function Author() { const { query } = useRouter(); return ( <div> <h1> Fetch author with book-id <em>{query['book-id']}</em> </h1> </div> ); }

使用 Catch All 路由擴展動態路由段

您已經看到了與[book-id].js相同的前一個示例中的動態路由段括號語法。 這種語法的美妙之處在於它使Catch-All Routes更進一步。 您可以從名稱中推斷出它的作用:它捕獲所有路由。

當我們查看動態示例時,我們了解到它如何幫助消除文件創建冗餘,以便通過單個路徑訪問具有其 ID 的多本書。 但我們還可以做一些別的事情。

具體來說,我們有路徑/printed-books/:book-id ,目錄結構:

 next-app └── pages ├── index.js └── printed-books ├── index.js └── [book-id].js

如果我們更新路徑以包含更多的類別(如類別),我們最終可能會得到以下內容: /printed-books/design/:book-id/printed-books/engineering/:book-id ,或者更好的是/printed-books/:category/:book-id

讓我們添加發行年份: /printed-books/:category/:release-year/:book-id 。 你能看到一個模式嗎? 目錄結構變為:

 next-app └── pages ├── index.js └── printed-books └── [category] └── [release-year] └── [book-id].js

我們用命名文件代替了動態路由,但不知何故仍然以另一種形式的冗餘告終。 好吧,有一個解決方法:Catch All Routes 消除了對深度嵌套路由的需求:

 next-app └── pages ├── index.js └── printed-books └── [...slug].js

它使用相同的括號語法,只是它以三個點為前綴。 想想像 JavaScript 擴展語法這樣的點。 您可能想知道:如果我使用包羅萬象的路線,我如何訪問類別( [category] ​​)和發布年份( [release-year] )。 兩種方式:

  1. 在印刷書籍示例的情況下,最終目標是書籍,並且每個書籍信息都將附帶其元數據,或者
  2. “slug”段作為查詢參數數組返回。
 import { useRouter } from 'next/router'; export default function Book() { const { query } = useRouter(); // There's a brief moment where `slug` is undefined // so we use the Optional Chaining (?.) and Nullish coalescing operator (??) // to check if slug is undefined, then fall back to an empty array const [category, releaseYear, bookId] = query?.slug ?? []; return ( <table> <tbody> <tr> <th>Book Id</th> <td>{bookId}</td> </tr> <tr> <th>Category</th> <td>{category}</td> </tr> <tr> <th>Release Year</th> <td>{releaseYear}</td> </tr> </tbody> </table> ); }

以下是路線/printed-books/[…slug]的更多示例:

小路查詢參數
/printed-books/click.js {“蛞蝓”:[“點擊”]}
/printed-books/2020/click.js {“蛞蝓”:[“2020”,“點擊”]}
/printed-books/design/2020/click.js {“蛞蝓”:[“設計”,“2020”,“點擊”]}

與 catch-all 路由一樣,路由/printed-books將拋出 404 錯誤,除非您提供備用索引路由。

 next-app └── pages ├── index.js └── printed-books ├── index.js // path: /printed-books └── [...slug].js

這是因為包羅萬象的路線是“嚴格的”。 它要么匹配一個蛞蝓,要么拋出一個錯誤。 如果您想避免在 catch-all 路由旁邊創建索引路由,則可以使用可選的 catch-all 路由

使用可選的 Catch-All 路由擴展動態路由段

語法與 catch-all-routes 相同,但使用雙方括號。

 next-app └── pages ├── index.js └── printed-books └── [[...slug]].js

在這種情況下,包羅萬象的路由(slug)是可選的,如果不可用,則回退到路徑/printed-books ,使用[[…slug]].js路由處理程序呈現,沒有任何查詢參數。

在索引路線旁邊使用 catch-all,或單獨使用可選的 catch-all 路線。 避免同時使用包羅萬象和可選包羅萬象的路線。

路由優先級

能夠定義最常見的路由模式的能力可能是“黑天鵝”。 路線衝突的可能性是一個迫在眉睫的威脅,尤其是當您開始處理動態路線時。

當這樣做有意義時,Next.js 會以錯誤的形式讓您了解路由衝突。 如果沒有,它會根據路由的特殊性對路由應用優先級。

例如,在同一級別上有多個動態路由是錯誤的。

 // This is an error // Failed to reload dynamic routes: Error: You cannot use different slug names for the // same dynamic path ('book-id' !== 'id'). next-app └── pages ├── index.js └── printed-books ├── [book-id].js └── [id].js

如果您仔細查看下面定義的路線,您會注意到發生衝突的可能性。

 // Directory structure flattened for simplicity next-app └── pages ├── index.js // index route (also a predefined route) └── printed-books ├── index.js ├── tags.js // predefined route ├── [book-id].js // handles dynamic route └── [...slug].js // handles catch all route

例如,嘗試回答這個問題:什麼路由處理路徑/printed-books/inclusive-components

  • /printed-books/[book-id].js ,或
  • /printed-books/[…slug].js

答案在於路由處理程序的“特異性”。 首先是預定義路由,然後是動態路由,然後是包羅萬象的路由。 您可以將路由請求/處理模型視為具有以下步驟的偽代碼:

  1. 是否有可以處理路由的預定義路由處理程序
    • true — 處理路由請求。
    • false — 轉到 2。
  2. 是否有可以處理路由的動態路由處理程序
    • true — 處理路由請求。
    • false — 轉到 3。
  3. 是否有可以處理路由的包羅萬象的路由處理程序
    • true — 處理路由請求。
    • false — 拋出未找到的 404 頁面。

因此, /printed-books/[book-id].js獲勝。

以下是更多示例:

路線路由處理程序路線類型
/printed-books /printed-books 索引路由
/printed-books/tags /printed-books/tags.js 預定義路線
/printed-books/inclusive-components /printed-books/[book-id].js 動態路由
/printed-books/design/inclusive-components /printed-books/[...slug].js 包羅萬象的路線

next/link API

next/link API 將Link組件公開為執行客戶端路由轉換的聲明性方式。

 import Link from 'next/link' function TopNav() { return ( <nav> <Link href="/">Smashing Magazine</Link> <Link href="/articles">Articles</Link> <Link href="/guides">Guides</Link> <Link href="/printed-books">Books</Link> </nav> ) }

Link組件將解析為常規的 HTML 超鏈接。 也就是說, <Link href="/">Smashing Magazine</Link>將解析為<a href="/">Smashing Magazine</a>

href屬性是Link組件唯一需要的屬性。 有關Link組件上可用道具的完整列表,請參閱文檔。

Link組件還有其他機制需要注意。

具有動態路段的路線

在 Next.js 9.5.3 之前, Link到動態路由意味著您必須同時為Link提供hrefas屬性,如下所示:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href="/printed-books/[printed-book-id]" as={`/printed-books/${printedBook.id}`} > {printedBook.name} </Link> )); }

儘管這允許 Next.js 為動態參數插入 href,但它很繁瑣、容易出錯並且有些必要,現在隨著 Next.js 10 的發布,大多數用例都得到了修復。

此修復程序也是向後兼容的。 如果您一直在使用ashref ,則沒有任何問題。 要採用新語法,請丟棄href屬性及其值,並將 as 屬性重命名as href ,如下例所示:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={`/printed-books/${printedBook.id}`}>{printedBook.name}</Link> )); }

請參閱自動解析 href。

passHref Prop 的用例

仔細看看下面的片段:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; // Say this has some sort of base styling attached function CustomLink({ href, name }) { return <a href={href}>{name}</a>; } export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={`/printed-books/${printedBook.id}`} passHref> <CustomLink name={printedBook.name} /> </Link> )); }

passHref強制Link組件將href屬性向下傳遞給CustomLink子組件。 如果Link組件覆蓋了返回超鏈接<a>標記的組件,則這是強制性的。 您的用例可能是因為您正在使用樣式組件之類的庫,或者您需要將多個子組件傳遞給Link組件,因為它只需要一個子組件。

請參閱文檔以了解更多信息。

網址對象

Link組件的href屬性也可以是具有query等屬性的 URL 對象,該對象會自動格式化為 URL 字符串。

使用printedBooks對象,下面的示例將鏈接到:

  1. /printed-books/ethical-design?name=Ethical+Design and
  2. /printed-books/design-systems?name=Design+Systems
 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/${printedBook.id}`, query: { name: `${printedBook.name}` }, }} > {printedBook.name} </Link> )); }

如果在pathname中包含動態段,則還必須將其作為屬性包含在查詢對像中,以確保在pathname中插入查詢:

 import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; // In this case the dynamic segment `[book-id]` in pathname // maps directly to the query param `book-id` export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/[book-id]`, query: { 'book-id': `${printedBook.id}` }, }} > {printedBook.name} </Link> )); }

上面的例子有路徑:

  1. /printed-books/ethical-design
  2. /printed-books/design-systems

如果您檢查 VSCode 中的href屬性,您會發現LinkProps類型,其href屬性是Url類型,如前所述,它可以是stringUrlObject

VSCode 中檢查的 LinkProps 類型的屏幕截圖
在 VSCode 中檢查LinkProps 。 (大預覽)

檢查UrlObject進一步導致具有屬性的接口:

在 VSCode 中檢查的 <code>UrlObject</code> 的屏幕截圖
在 VSCode 中檢查 UrlObject。 (大預覽)

您可以在 Node.js URL 模塊文檔中了解有關這些屬性的更多信息。

哈希的一個用例是鏈接到頁面中的特定部分。

 import Link from 'next/link'; const printedBooks = [{ name: 'Ethical Design', id: 'ethical-design' }]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/${printedBook.id}`, hash: 'faq', }} > {printedBook.name} </Link> )); }

超鏈接將解析為/printed-books/ethical-design#faq

在文檔中了解更多信息。

next/router API

如果next/link是聲明性的,那麼next/router是必須的。 它公開了一個useRouter鉤子,允許訪問任何功能組件內的router對象。 您可以使用此掛鉤手動執行路由,尤其是在某些情況下, next/link不夠,或者您需要“掛鉤”到路由中。

 import { useRouter } from 'next/router'; export default function Home() { const router = useRouter(); function handleClick(e) { e.preventDefault(); router.push(href); } return ( <button type="button" onClick={handleClick}>Click me</button> ) }

useRouter是一個 React 鉤子,不能與類一起使用。 需要類組件中的router對象嗎? withRouter使用。

 import { withRouter } from 'next/router'; function Home({router}) { function handleClick(e) { e.preventDefault(); router.push(href); } return ( <button type="button" onClick={handleClick}>Click me</button> ) } export default withRouter(Home);

router對象

useRouter鉤子和withRouter高階組件都返回一個路由器對象,該對象具有pathnamequeryasPathbasePath等屬性,為您提供有關當前頁面的 URL 狀態的信息, localelocalesdefaultLocale提供有關活動的、受支持的或當前的默認語言環境。

路由器對像還具有諸如push等方法,用於通過將新 URL 條目添加到歷史堆棧中來導航到新 URL, replace類似於 push ,但替換當前 URL 而不是將新 URL 條目添加到歷史堆棧中。

了解有關路由器對象的更多信息。

使用next.config.js自定義路由配置

這是一個常規的 Node.js 模塊,可用於配置某些 Next.js 行為。

 module.exports = { // configuration options }

請記住在更新next.config.js時重新啟動服務器。 了解更多。

基本路徑

有人提到 Next.js 中的初始/默認路由是pages/index.js和路徑/ 。 這是可配置的,您可以將默認路由設置為域的子路徑。

 module.exports = { // old default path: / // new default path: /dashboard basePath: '/dashboard', };

這些更改將在您的應用程序中自動生效,所有/路徑都路由到/dashboard

此功能只能與 Next.js 9.5 及更高版本一起使用。 了解更多。

尾隨斜線

默認情況下,每個 URL 的末尾都沒有尾部斜杠。 但是,您可以使用以下命令進行切換:

 module.exports = { trailingSlash: true };
 # trailingSlash: false /printed-books/ethical-design#faq # trailingSlash: true /printed-books/ethical-design/#faq

基本路徑和斜杠功能都只能用於 Next.js 9.5 及更高版本。

結論

路由是 Next.js 應用程序中最重要的部分之一,它體現在基於頁面概念的基於文件系統的路由中。 頁面可用於定義最常見的路由模式。 路由和渲染的概念密切相關。 在您構建自己的 Next.js 應用程序或處理 Next.js 代碼庫時,學習本文的課程。 並查看以下資源以了解更多信息。

相關資源

  • 頁面的 Next.js 官方文檔
  • Next.js 獲取數據的官方文檔
  • 用於 next.config.js 的 Next.js 官方文檔
  • Next.js 10:自動解析href
  • Next/link 的 Next.js 官方文檔
  • Next/router 的 Next.js 官方文檔