昨天我們介紹了如何使用 layout 來建立共用的頁面架構,今天要接著來看另一個與路由系統密切相關的重要功能,那就是「內部連結(<Link>
)」的使用方式。
在開發網頁時,說到「連結」通常會直覺想到使用<a>
標籤。但如果是使用 React 或 Next.js 這類具備「客戶端路由系統(client-side routing)」的框架,就不能單純使用原生的<a>
標籤來實作頁面跳轉,否則會導致整個頁面重新載入,導致失去 SPA 的優勢。
在這個情況下,如果我們需要維持在客戶端上渲染頁面的功能,我們就需要使用 React Router 或 Next.js 所提供的 <Link>
元件,讓頁面在不重整的情況下完成跳轉,並保留目前應用所採用的渲染模式。
今天我們一樣從 React 的實作方式開始看內部連結的部分,如果想要在 React 專案中維持 CSR 的行為,需要使用 react-router-dom 提供的 <Link>
元件來取代原生 <a>
標籤。
如果只是單純放一個內部頁面的連結,做法非常簡單,就是把 <Link>
引入,並透過 to
屬性指定要導向的路由路徑即可,例如下面這個例子:
import { Link } from "react-router-dom";
const AboutPage = () => {
return (
<div>
<h1>About</h1>
<p>This is the about page.</p>
<Link to="/about/other-pages">Other Page</Link>
</div>
);
}
export default AboutPage
如果想要使用動態的路由,可以組上變數後,放到 Link 的 to
屬性上。
<Link to={`/about/${id}`}>go to about</Link>
也可以用物件語法的寫法,將動態的部分帶到 Link 上。
<Link
to={{
pathname: `/about/${id}`,
}}
>
go to about<
</Link>
這裡的物件寫法,還可以帶入 hash 或 query string。
<Link
to={{
pathname: "/new-page",
search: "?type=1",
hash: "#top",
}}
>
Shoes
</Link>
在 React 上可以透過 Link 帶上一些動態參數和 query string 的部分,當然也就有方法可以從對應的頁面取得路由相關的內容。在 React 專案中,如果要取得路由上動態參數的部分,可以透過引入 react-router-dom 的 useParams 和 useSearchParams 來取得。useParams 可以取得路由上動態參數的部分,useSearchParams 則可以取得 query string 的部分。
在 React 中,如果我們透過 <Link>
建立了帶有動態參數或 query string 的內部連結,在實務上,也會有在對應的頁面中取得這些路由資訊的需求,這時候就可以透過引入 react-router-dom 的 useParams 和 useSearchParams 來取得。
- useParams: 用來取得路由中的動態參數(例如 /about/${id}
中的 id)。
- useSearchParams: 用來取得 query string(例如 /new-page?type=1
中的 type)。
接下來直接透過實際的使用例子來看看取得路由動態參數的方式。
import { useParams, useSearchParams } from 'react-router-dom';
const Detail = () => {
// 取得params
const params = useParams();
// searchParams可以取得query, setSearchParams可以更新query
const [searchParams, setSearchParams] = useSearchParams();
// 取得 params
console.log('params: ', params.id); // params: 10
// 取得 query
console.log('query: ', searchParams.get('type')); //query: a
return (
<div>
<h1>Detail</h1>
</div>
);
}
export default Detail
當進入的網址為 http://localhost:3002/#/about/10?type=a 時,因為 10 的部分是動態參數的部分,所以透過 useParams 會取得 10,為 query string 部分的 type 則可以透過 useSearchParams 取得,在這個情境下會取得 a。
到這裡為止都只提到了 React 中的內部連結用法,接下來當然就是要看看我們的男主角 Next.js 的用法囉。
在 Next.js 中,不論是使用 Page Router 還是 App Router,一樣都是使用 <Link >
來達到內部連結的效果。雖然在 <Link >
的使用上,Page Router 和 App Router 的差異不大,但實際上還是有些許差異。
接下來除了看 Next.js 中如何使用內部連結之外,也會來看一下 Page Router 和 App Router 在使用 <Link>
時,相同和相異之處的部分有哪些。
<Link>
使用部分<Link>
在 Next.js 中的 Page Router 和 App Router 要設定內部連結時,一樣都是使用 <Link>
,只是這個 Link 不是從 react-router-dom 引入,而是從 next/link 引入。還有一個不同的地方是,使用 Link 時,帶入連結網址使用的屬性不是 to,改成使用 href。
import Link from "next/link";
const AboutA = () => {
return (
<div>
<p>About A</p>
<Link href="/about">
go to about
</Link>
</div>
);
};
export default AboutA;
在 Next.js 的 Page Router 和 App Router 中,如果要使用動態路由和加上 query string 的話,一樣也會使用相同的方式,把相關資訊帶到 <Link>
中。
可以透過 template 語法將變數部分組成要前往的路由路徑,再帶到 to 屬性上。
<Link href={`/about/${id}`}>go to about</Link>
也可以用物件的方式把相關資訊帶過去。
<Link href={{ pathname: '/about/[id]', query: { id: 1 } }}>
go about
</Link>
<Link>
使用部分大多數的一些基本的 <Link>
用法,不論是在 Page Router 還是在 App Router 上都一樣,但還是有些微的差距。接下來也來看看差異的部分有哪些?
在這個部分,不論是 Page Router 還是 App Router,都會依照是 CSR 還是 SSR 或 SSG 的渲染模式,而用不同的方式去取得路由中動態變數和 query string 的部分。但是取得方式的寫法,會依照是 Page Router 或 App Router 有不同。
∙ CSR 模式
如果是使用 Page Router,在 CSR 模式下,可以引入 next/router
的 useRouter 來取得動態路由或 query string。
例如以下的寫法,當進入的網址為 http://localhost:3002/#/about/10?type=a 時,就可以透過 useRouter 取得 id 和 type 的資訊。
import { useRouter } from "next/router";
const Detail = () => {
const router = useRouter();
console.log(router.query); // {type: 'a', id: '10'}
return (
<div>
<h1>Detail</h1>
</div>
);
};
export default Detail;
如果是 App Router,在 CSR 模式下,則要透過引入 next/navigation
的 useParams
和 useSearchParams
來取得路由中動態的部分及 query string。
當進入的網址為 http://localhost:3002/#/about/10?type=a
用以下的寫法,就能從 params 取得動態路由的內容,從 searchParams 則可以取得 query string 的部分。
"use client";
import { useParams, useSearchParams } from "next/navigation";
const Detail = () => {
const params = useParams();
const searchParams = useSearchParams();
const id = params.id;
const type = searchParams.get("type");
console.log(id, type); // '10', 'a'
return (
<div>
<h1>Detail</h1>
</div>
);
};
export default Detail;
∙ SSR 模式
在 SSR 模式沒辦法使用 hook,如果要在 Page Router 下取得網址上的動態參數和 query 的話,就需要使用 getServerSideProps
。
export async function getServerSideProps({ params, query }) {
return {
props: {
id: params.id,
type: query.type || null,
},
};
}
const Detail = ({ id, type }) => {
return (
<div>
<p>Post ID: {id}</p>
<p>Query: {type}</p>
</div>
);
};
export default Detail;
在 App Router 下,在取得方法上就相對地簡單,只要從 page 檔案中的 fucntion 解構出 params 和 searchParams,在伺服器上就可以取得相對應的值。但要注意的是這裡的 params 和 searchParams 都是 Promise,所以想要取得值的內容,需要用 await。
const Detail = async ({ params, searchParams }) => {
const { id } = await params
const { type } = await searchParams
console.log(id, type); // '10', 'a'
return (
<div>
<h1>Detail</h1>
</div>
);
};
export default Detail;
∙ SSG 模式下
在 SSG 這種渲染模式中,情況會稍微有點不一樣,會需要讓動態路由(例如:/detail/[id])變成是可預期的動態路由。因為在 SSG 模式下,會在 build 階段就產出完整 HTML,所以在這個階段,Next.js 必須要知道有哪些動態路由參數(例如:/detail/[id]
內有哪些 id),用來預先生成對應的頁面。因此我們會需要透過一些方式主動告訴 Next.js 哪些路徑要被產出成靜態頁。
也是因為如此,當有完全無法預期的動態內容時,就無法使用 SSG。例如使用者動態新增的資料(如會員 ID、留言 ID)、或者查詢參數(query string)這類在 build 階段無法得知的資訊,都無法提前對應生成對應的 HTML。
使用 Page Router 時,如果要在 SSG 這種渲染模式中,動態路由變成可預期的動態路由,需要搭配 getStaticProps
和 getStaticPaths
一起使用,這樣才能讓 Next.js 知道可能會有哪些動態資料。
export async function getStaticPaths() {
return {
// 這裡會需要放入可預期的動態資料
paths: [{ params: { id: "1" } }],
fallback: false,
};
}
export async function getStaticProps({ params }) {
return {
props: {
id: params.id,
},
};
}
const Detail = ({ id }) => {
return (
<div>
Post ID: {id}
</div>
);
};
export default Detail;
如果路由中的動態參數(如: id)無法預先得知,這時可以改用 ISR,搭配 fallback: 'blocking' 的方式來達成靜態頁面的延遲生成與快取。
當 fallback 設為 'blocking' 時,若使用者輸入的 id 不在 getStaticPaths 所定義的列表中,Next.js 會在伺服器端同步產生對應的 HTML 頁面,產生完成後才回傳給使用者。這個頁面會被快取起來,讓下一次訪問相同路徑的使用者能直接獲得靜態頁面,不需要重新生成。
export async function getStaticPaths() {
return {
paths: [{ params: { id: "1" } }],
// 改成 blocking
fallback: 'blocking'
};
}
export async function getStaticProps({ params }) {
return {
props: {
id: params.id,
},
revalidate: 60, // 60 秒後允許再重新生成新版本
};
}
在 App Router 和 Page Router 使用 SSG 渲染模式時一樣,需要另外搭配一個 function 去建立一些可預期的動態參數內容,但是 App Router 要使用的 function 是 generateStaticParams
。取出來的方式和 SSR 的時候一樣,是透過解構 page 中的 function 取得。
export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }];
}
export default async function Detail({ params }) {
const { id } = await params;
console.log(id);
return (
<div>
<h1>Detail</h1>
</div>
);
}
另外,需要注意的部分是如果想要維持 SSG 這個渲染模式,不可以使用到 searchParams
的話,如果使用了解構出來的 searchParams
就會被視為是動態渲染,也就無法維持 SSG,在 build 的時候也就不會產生出 HTML。
這裡也透過觀察 Link
的 prefetch 機制,來了解 Page Router 和 App Router 在這部分上的差異。
不論 Page Router 還是 Page Router 在頁面上如果有使用 <Link>
的話,都會做一個 prefetch 的動作,但是在進行 prefetch 的時機點和 prefetch 的內容上,有些許差異。
在 Page Router 中,當滑鼠滑過 Link,就會先加載這個 Link 對應頁面的 JavaScript chunk。
在 App Router 中,則是只要頁面有出現 Link 就會預先加載對應頁面的 RSC Payload。
不論是使用 React 還是 Next.js,善用 <Link>
都是維持單頁應用(SPA) 的關鍵之一。尤其在 Next.js 的 App Router 中,Link 的功用不只是在於導航功能,更牽涉到 RSC payload 的 prefetch,能讓頁面的效能變得更好。
今天 Link 的內容就到這裡告一個段落,明天我們會接著來看搭配 router 客製化定義錯誤頁面的部分。