昨天看了 App Router 中一個可以透過建立 slot 設定為平行路由的實作方式,今天再來看一個很類似,但應用情境有點不同的 App Router 的進階用法 - Intercepting Routes。
在正式講到 Next.js App Router 的 Intercepting Routes 前,我們來思考一個需求應該要怎麼實作。
需求內容是「現在有一個頁面,頁面上有篩選內容和依照篩選顯示的列表內容,可以點擊列表內其中一個項目的查看詳細內容」。
目前有兩個做法:
以上這兩個實作方式,都可以呈現最基本的功能,就是顯示點擊項目的詳細內容,但是各有缺點。
第一個方法,由於沒有真的做切頁的動作,雖然可以維持篩選的內容,但是卻無法讓這個詳細內容有獨立的網址。
第二個方式,讓詳細內容有獨立的網址了,但是因為做了一個切換頁面的動作,當返回到列表頁的時候,篩選內容會被清空,也就沒辦法維持前一次篩選的內容。
那如果想要讓兩個方式的優點同時存在的話,可以怎麼做呢?
大家可以先稍微記住目前說的這個實作情境,我們先進到這次的主題。
我們一樣先來認識一下「Intercepting Routes」是什麼。
「Intercepting Routes」指的是攔截路由,那這個攔截路由是在攔截什麼呢?這個攔截路由其實攔截的是原本應該要完整前往另一個頁面的行為
,也就是說 Intercepting Routes 可以讓我們在不離開當前畫面的情況下,在原本的路由載入另一個路由的內容
,這個行為通常會以 Modal 或是 Drawer 呈現。
這裡舉一個小例子來看看。
當我們在一個頁面裡想要從畫面點擊一個按鈕看到 detail 資訊彈窗,通常會透過類似 isOpen 這樣的狀態結合 Modal 下去呈現。
在這樣的狀態下,因為打開 Modal 元件時,網址不會有變動,Modal 是否開啟是由狀態控制,所以當頁面重整時,並不會維持 Modal 開啟的狀態。當點擊返回按鈕時,也不是單純把 Modal 關掉的行為,而是返回開啟 Modal 頁面的上一頁。
但是當我們使用 Intercepting Routes 時,我們若是想實作從頁面裡點擊按鈕會看到 detail 資訊彈窗的功能,可以讓點擊跳出彈窗的這個部分,改成不是單純以元件狀態來呈現,而是以攔截路由的功能來呈現。
這樣呈現功能後,就會變成點擊按鈕,不僅會顯示彈窗,網址也會有相對應的改變,當點擊返回頁面,會回到顯示著按鈕的頁面。當點擊按鈕,顯示彈窗的狀態下,重新整理頁面時,不會回到前一個頁面,會維持在顯示 detail 的頁面。
在這個情境下,真正被攔截的是「前往 detail/1 頁面的導航行為」,並且將它改成以彈窗的形式來顯示 detail 內容。
接著進入正題,來看看 Intercepting Routes 的用法吧!
如果想要使用 Intercepting Routes,會需要準備一個真正的路由頁面,還有攔截時要使用的畫面。
真正的路由頁面,指的是要被攔截的頁面,也就是重新整理後要顯示的頁面,這個頁面我們就用我們知道的建立頁面的方式來建立就好。例如:detail/123 這個路由的頁面,就是在 src/app/detail/[id] 下建立一個 page 檔案。
攔截時要使用的畫面,就需要透過(.)、(..)、(..)(..)或(...)來命名資料夾,去讓進入的路由進行相對應的匹配,進而攔截要前往的路由。
要使用這幾種方法中的哪一種寫法來命名資料夾,會依照想要攔截的範圍與路由段層級來決定。
photo/[id]
,目前所在路由的頁面在 /
這個路由,與 photo/[id]
都一樣在根目錄底下,也就是說位在同一層,所以可以使用(.)photo
。
photo/[id]
,要前往的這個目標路由的頁面在 /list
上,需要往上一層在進入 /photo
,所以要透過在 list/(..)photo/[id]
資料夾底下加 page 檔案建立攔截路由。photo/[id]
,要前往這個路由的頁面在 /content/list
這個路由,這時就需要往外跳出兩層,使用 content/list/(..)(..)photo/[id]/page.tsx
。這裡也使用一個例子來看看 Intercepting Routes 的使用方式。
在這裡我們在 src/app 底下建立一個 list 資料夾,這是顯示 list 的頁面。
import Link from "next/link";
const ListPage = () => {
return (
<div>
<h1>List 頁面</h1>
<Link className="text-blue-500 underline" href="/photo/123">
點我打開攔截頁面
</Link>
</div>
);
};
export default ListPage;
另外也 src/app/photo 底下建立一個 [id] 資料夾,用來顯示 photo 的詳細頁。
const PhotoPage = () => {
return <div>這是正式的 photo 完整頁面(完整跳轉才看到)</div>;
};
export default PhotoPage;
在加入 Intercepting Routes 之前,點擊連結會跳往 /photo 資料夾底下建立的這個頁面
接下來透過 (..) 命名規則來建立 Intercepting Route,因為這個情境是要攔截從 list 資料夾跳出,前往 /photo/[id] 的路由。
const InterceptedRoutePhotoPage = () => {
return <div className="bg-yellow-50 p-3">被攔截的 photo 畫面</div>;
};
export default InterceptedRoutePhotoPage;
加上 Intercepting Route 後,當點擊連結就不會直接進入原本的 photo/[id] 頁面,會先顯示 (..)photo/[id] 的頁面內容,但是網址一樣會顯示 photo/[id],當重新整理頁面,或是直接輸入這個網址時,才會顯示原本 photo/[id] 的頁面。
已經了解使用方式了,這邊再回到一開始提過的需求內容,目標是把兩個實作方法的優點都保存,也就是讓詳細資訊頁有獨立的網址,同時也要讓看到詳細內容後,再返回列表頁面,原本的篩選值不要被重置。這時候就可以使用我們今天看的 Intercepting Routes,並且結合昨天提到過的 Parallel Routes。
我的的清單頁設定的在 src/app/list/page.tsx
,所以進入的網址會是 localhost:3000/list
。
這樣的做法下,我們一樣要準備一個 detail 頁面。
// src/app/list/[id]/page.tsx
"use client";
import DetailContent from "@/app/components/DetailContent";
import { useParams } from "next/navigation";
const DetailPage = () => {
const params = useParams();
return (
<div>
<DetailContent id={Number(params.id)} />
</div>
);
};
export default DetailPage;
設定好這個檔案後,當我們點擊 Detail 就可以進入到像這樣的詳細資訊頁面。
但就如同前面提到過的狀況一樣,因為我們做了切頁的動作,所以當我們點進頁面時,再返回我們原本輸入的篩選值會重置,變成沒有刪選過的列表。
接著使用 Parallel Routes + Intercepting Routes 的方式設定攔截這個 detail 的畫面。
在 list 資料夾底下加上 @modal
資料夾,接著用 (..)list
的寫法建立攔截對應頁面的資料夾,因為是要攔截 list/[id]
這個頁面,所以底下要在建立一個 [id]
資料夾。
// src/app/list/@modal/(..)list/[id]/page.tsx
"use client";
import DetailContent from "@/app/components/DetailContent";
import Modal from "@/app/components/Modal";
import { useParams, useRouter } from "next/navigation";
const DetailModal = () => {
const router = useRouter();
const params = useParams();
return (
<Modal
modalTitle="Detail"
onClose={() => {
router.back();
}}
>
<DetailContent id={Number(params.id)} />
</Modal>
);
};
export default DetailModal;
在 list 資料夾底下的 layout 要再加上 modal prop,modal 這個 Parallel Route 才有效果。
// src/app/list/layout.tsx
const ListLayout = ({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) => {
return (
<div className="p-6">
{children}
{modal}
</div>
);
};
export default ListLayout;
最後就能達到我們期望的需求,查看詳細內容時,不影響原本的篩選值,且有每個詳細內容頁面都有獨立的網址。
完整的參考範例可以參考範例連結
Intercepting Routes 能夠攔截原本完整的頁面導航行為,並改以彈窗或其他形式呈現頁面內容。在處理列表與詳細內容的需求時,這種方式能夠同時解決傳統作法的缺點:單純使用 Modal 雖然能保留篩選狀態,但無法提供獨立網址;而直接切換頁面雖然能產生獨立網址,卻會導致返回列表頁時篩選條件被清空。透過 Parallel Routes 結合 Intercepting Routes,我們不僅能保有詳細頁面的 URL 供使用者分享與直接訪問,又能讓返回列表時維持篩選內容不被重置,達成流暢且兼顧使用者體驗的實作方式。
今天認識什麼是 Parallel Routes 的部分就到這裡告一個段落,雖然以我目前實務上使用的習慣來說,還沒有用到這個進階的用法,因為大多數的實作需求,用一般的路由設定方式都可以辦得到,但是多學一種用法,對於之後遇到類似的情況時,也就能更快反應可以這樣設計路由。
關於路由的部分就到這裡告一個段落,明天將會繼續看 Next.js 的其他內容。