Next-js 的路由爲什麼這麼奇怪?
Next.js 是 React 的全棧框架,主打服務端渲染,也就是 SSR(Server Side Rendering)。
它有一套非常強大但也很奇怪的路由機制。
這套路由機制是什麼樣的?爲什麼又說很奇怪呢?
我們試一下就知道了。
先創建個 Next.js 項目:
npx create-next-app@latest
執行 create-next-app,輸入一些信息,Next.js 項目就創建好了。
進入項目,執行 npm run dev,把它跑起來:
瀏覽器訪問可以看到這個頁面,就代表跑成功了:
在項目下可以看到 src 下有個 app 目錄,下面有 page.tsx:
我們添加幾個目錄:
guang/liu/page.tsx
export default function Liu() {
return <div>666</div>
}
guang/shuai/page.tsx
export default function Shuai() {
return <div>帥</div>
}
然後瀏覽器訪問下:
可以看到,添加了幾個目錄,就自動多了幾個對應的路由。
這就是 Next.js 的基於文件系統的路由。
剛學了 page.tsx 是定義頁面的,那如果多個頁面有公共部分呢?
比如這種菜單和導航:
肯定不是每個頁面一份。
這種定義在 layout.tsx 裏。
app/layout.tsx 是定義最外層的佈局:
也就是 html 結構,還有 title、description 等信息:
在網頁的 html 源碼裏可以看到這些:
我們改一下試試:
不止是根路由可以定義 layout,每一級都可以:
export default function Layout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<div>
左側菜單
</div>
<div>{children}</div>
</div>
)
}
我們給 guang/shuai 這個頁面添加了佈局,效果是這樣的:
Next.js 會自動在 page.tsx 組件的外層包裹 layout.tsx 組件。
有的同學可能會注意到有個漸變背景,這個是 global.css 裏定義的,我們把它去掉:
然後繼續看:
我們可以使用 Link 組件在不同路由之間導航:
有的同學說,這些都很正常啊。
那接下來看點不那麼正常的:
如果我希望定義 /dong/111/xxx/222 (111、222 是路徑裏的參數)這樣的路由的頁面呢?
應該如何寫?
這樣:
interface Params {
params: {
param1: string;
param2: string;
}
}
export default function Xxx(
{ params }: Params
) {
return <div>
<div>xxx</div>
<div>參數:{JSON.stringify(params)}</div>
</div>
}
路徑中的參數的部分使用 [xxx] 的方式命名。
Next 會把路徑中的參數取出來傳入組件裏:
這種叫做動態路由。
那如果我希望 /dong2/a/b/c 和 /dong2/a/d/e 都渲染同一個組件呢?
這樣寫:
interface Params {
params: {
dong: string;
}
}
export default function Dong2(
{ params }: Params
) {
return <div>
<div>dong2</div>
<div>參數:{JSON.stringify(params)}</div>
</div>
}
[...dong] 的語法就是用來定義任意層級路由的,叫做 catch-all 的路由。
可以看到,/dong2 下的任意的路由,都會渲染這個組件。
那我直接訪問 /dong2 呢?
可以看到,404 了。
但這種也可以支持,再加一箇中括號,改成 [[...dong]] 就好了:
這樣 /dong2 也會渲染這個組件,只不過參數是空:
這種 [[...dong]] 的路由叫做 optional catch-all。
可以看到,Next.js 項目的目錄可不只是單純的目錄,都是有對應的路由含義的。
那如果我就是想加個單純的目錄,不包括在路由裏呢?
這樣寫:
我在 dong 和 dong2 的外層添加了一個 (dongdong) 的目錄,那之前的路由會變麼?
試了下,依然沒變。
也就是說只要在目錄名外加上個 (),就不計入路由,只是分組用的,這叫做路由組。
現在,我們一個 layout 下渲染了一個 page。
那如果我想一個 layout 渲染多個 page 呢?
這樣寫:
guang2 下有 3 個 page,page.tsx、@aaa/page.tsx、@bbb/page.tsx
分別會以 children、aaa、bbb 的參數傳入 layout.tsx
layout.tsx
export default function Layout({
children,
aaa,
bbb
}: {
children: React.ReactNode,
aaa: React.ReactNode,
bbb: React.ReactNode
}) {
return (
<div>
<div>{children}</div>
<div>{aaa}</div>
<div>{bbb}</div>
</div>
)
}
page.tsx
export default function Page() {
return <div>page</div>
}
@aaa/page.tsx
export default function Aaa() {
return <div>aaa</div>
}
@bbb/page.tsx
export default function Bbb() {
return <div>bbb</div>
}
渲染出來是這樣的:
可以看到,在 layout 裏包含了 3 個 page 的內容,都渲染出來了,這種叫做平行路由。
有的同學會問,那 /guang2/@aaa 可以訪問麼?
是不可以的。
此外,Next.js 還有一個很強大的路由機制:
之前有這樣一個路由:
我們在它平級定義個路由:
import Link from "next/link";
export default function Ccc() {
return <div>
<div>
<Link href="/guang/liu">to 666</Link>
</div>
<div>ccc</div>
</div>
}
點擊鏈接跳轉到 /guang/liu
這沒啥問題。
但如果我在 ccc 下加一個 (..)liu 的目錄:
這時候再試一下:
可以看到,這次渲染的 Liu 組件就被替換了,但要是刷新的話還是之前的組件。
很多同學會有疑惑,這個有啥用?
舉個場景的例子就明白了。
比如一個表格,點擊每個條目,都會跳出編輯彈窗,這個編輯頁面可以分享,分享出去打開的是完整的編輯頁面。
再比如登錄,一些頁面點擊登錄會彈出登錄彈窗,但如果把這個登錄鏈接分享出去,打開的是完整的登錄頁面。
也就是說在不同場景下,可以重寫這個 url 渲染的組件,這個就是攔截路由的用處。
用法也很簡單,因爲要攔截的是上一級的 /guang/liu 的路由,所以前面就要加一個 (..)
同理,還有 (.)xx 代表攔截當前目錄的路由,(..)(..)xx 攔截上一級的上一級的路由,(...)xxx 攔截根路由。
這個攔截路由,在特定場景下很有用。
這些就是頁面相關的路由機制,是不是還挺強大的?
當然,這些路由機制不只是頁面可以用,Next.js 還可以用來定義 Get、Post 等接口。
只要把 page.tsx 換成 route.ts 就好了:
import { NextResponse, type NextRequest } from 'next/server'
const data: Record<string, any> = {
1: {
name: 'guang',
age: 20
},
2: {
name: 'dong',
age: 25
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id');
return NextResponse.json(!id ? null : data[id])
}
我們定義了 /guang3 的路由對應的 get 方法,根據 id 來取數據。
訪問下:
前面學的那些路由,都可以用來 route.ts 上。
比如這樣:
[id] 定義動態路由參數,而 [...yyy] 是匹配任意的路由。
route.ts 的 GET 方法裏,同樣是通過 params 來取:
import { NextResponse, type NextRequest } from 'next/server'
interface Params {
params: {
id: string;
yyy: string;
}
}
export async function GET(request: NextRequest, {
params
}: Params) {
return NextResponse.json({
id: params.id,
yyy: params.yyy
})
}
感受到爲啥 Next.js 被叫做全棧框架,而不是 SSR 框架了麼?
因爲它除了可以用來渲染 React 組件外,還可以定義接口。
這樣,我們就把 Next.js 的路由機制過了一遍。
這種路由機制叫做 app router,也就是最頂層是 app 目錄:
之前還有種 page router,最頂層目錄是 page。
這倆只不過是兩種文件、目錄命名規則而已,我們只學 app router 就好了,它是最新的路由機制。
總結下我們學了什麼:
aaa/bbb/page.tsx 可以定義 /aaa/bbb 的路由。
aaa/[id]/bbb/[id2]/page.tsx 中的 [id] 是動態路由參數,可以在組件裏取出來。
aaa/[...xxx]/page.tsx 可以匹配 /aaa/xxx/xxx/xxx 的任意路由,叫做 catch-all 的動態路由。但它不匹配 /aaa
aaa/[[...xxx]]/page.tsx 同上,但匹配 /aaa,叫做 optional catch-all 的動態路由。
aaa/(xxx)/bbb/page.tsx 中的 (xxx) 只是分組用,不參與路由,叫做路由組
aaa/@xxx/page.tsx 可以在 layout.tsx 裏引入多個,叫做平行路由
aaa/(..)/bbb/page.js 可以攔截 /bbb 的路由,重寫對應的組件,但是刷新後依然渲染原組件,叫做攔截路由。
這些路由機制確實看起來挺奇怪的,它會導致 Next.js 的項目看起來這樣:
相比這種基於文件系統的路由,大家可能更熟悉 React Router 那種編程式路由:
Next.js 這種聲明式的路由其實熟悉了還是很方便的。
不需要單獨再維護路由了,目錄就是路由,一目瞭然。
而且這些看似奇怪的語法,細想一下也很正常:
比如 [xxx],一般匹配 url 中的參數都是這種語法。
而 [...xxx] 只是在其中加個一個 ...,這個 ... 在 js 裏就是任意參數的意思,所以用來匹配任意路由。
再加一箇中括號 [[...xxx]] 代表可以不帶參數,這個也是很自然的設計。
(.)xx、(..)xxx 這裏的 . 和 .. 本來就是文件系統裏的符號,用來做攔截路由也挺自然的。
路由組 (xxx) 加了個括號來表示分組,平行路由 @xxx 加了 @ 來表示可以引入多個 page,都是符合直覺的設計。
所以說,Next.js 基於文件系統實現這套路由機制,用的這些奇怪的語法,其實都是挺合理的設計。
總結
我們學習了 Next.js 的路由機制,它是基於文件系統來定義接口或頁面的路由。
Next.js 的路由機制挺強大的,支持的功能很多。
比如這樣:
有動態路由參數 [xx]、catch-all 的動態路由 [...xxx]、optional catch-all 的動態路由 [[...xxx]]、路由組 (xxx)、平行路由 @xxx、攔截路由 (..)xxx。
這些語法乍看比較奇怪,但是細想一下,都是挺合理的設計。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YNnliv3RGc6NRXE5UtyhRQ