在進行前端專案的開發時,常常都會利用 layout 讓頁面整體的架構固定下來,並且將共用邏輯集中在 layout 處,使整體的頁面架構更好維護及開發。layout 這個看似很普通的東西,其實在 Page Router 和 App Router 的寫法並不相同。
今天就讓我們從回顧 React 的 layout 寫法,看回我們的男主角 Next.js 中 Page Router 和 App Router 的寫法。
正式進入 Next.js 的 Page Router 和 App Router 的 layout 設定前,我們先來快速回顧一下寫 react 時,要怎麼設定 layout。
因為 React 是用 config 檔案來設定 Router,所以如果想要依照 Route 別設定 layout,會需要將相關設定內容寫到 config 檔案內,如下:
const Router = createHashRouter([
{
path: "/",
element: <HomePage />,
},
{
path: "about",
element: <Layout />,
children: [
{
path: "",
element: <About />,
},
{
path: "other-page",
element: <OtherPage />,
},
],
},
]);
在 Layout 檔案內,則需要使用 Outlet
標示出在 layout 中要依照網址不同而顯示不一樣的區塊。
例如這樣寫:
import { Outlet } from "react-router-dom";
const Layout = () => {
return (
<div>
<h1>Main Title</h1>
<div>
<Outlet /> // route 切換時對應到的畫面
</div>
</div>
);
}
export default Layout
當切換到 /about 時,或是切換到 about/other-page,主要變動的區塊就會是 Outlet 的部分,Main Title 文字的部分不會因為切換到 /about 或 about/other-page 而被影響。也就是說在這個寫法下,這兩個網址都會顯示 Main Title 這個文字的部分。
看到這裡可能有人會想說既然在 Next.js 下是使用檔案系統來定義路由,那從 React 透過 config 定義路由的模式轉換為檔案結構的方式後,是否也能以此類推,在定義路由的資料夾下透過設定 layout 檔案,讓該資料夾下的所有頁面共用?這確實是比較理想和直覺的做法,不過很可惜的是,Page Router 並不支援這樣的用法,只有 App Router 沒有支援這個做法。
接著就來看看 Page Router 和 App Router 要怎麼實作 layout 的部分。
如果在 Page Router 模式下使用 layout,無法單純透過檔案系統來建立,只能透過自定義的方式來讓 layout 套用,例如在頁面中定義 getLayout
函式,或在 _app
檔案中使用被當作 layout 元件。
當每一頁只需要使用同一種架構,可以設定一個當作 layout 使用的元件,並且設定在入口檔案的部分,如下:
先建立一個要來當作所有路由都共用的 layout 元件。
const MainLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<div className="h-12 w-full bg-gray-100 flex items-center justify-center">
page Logo
</div>
{children}
</div>
);
};
export default MainLayout;
再把這個檔案包裝於 _app
檔案中的 Component 外面,這樣使用後,進入到每個路由時,就都會組合這個 layout 元件把畫面渲染出來,所以不論切換到哪個頁面,都會有 header 的部分。
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import MainLayout from "@/components/MainLayout";
export default function App({ Component, pageProps }: AppProps) {
return (
<MainLayout>
<Component {...pageProps} />
</MainLayout>
);
}
就會呈現以下這樣的結果:
前面提到的使用方式是直接在 _app
檔案中包一層 layout 元件,讓每個頁面都套用相同的 layout。這種做法適用於所有路由對應到的頁面都共用同一個版型的情境。但在實際應用上,並不會每次都這麼剛好只有這麼單純的使用情境,還是有可能會需要在指定的路由使用特定 layout,或是在套用主要 layout 的子路由中又需要額外再使用一層 layout。
這種情況下,也就無法單純在 _app
檔案中全域套用一個 layout,而是需要更靈活的作法。這種時候我們就會需要使用 getLayout 這個函式來為每個頁面指定該頁面要使用的 layout。
首先一樣需要先建立 layout 檔案,這裡建立一個會顯示橘色外框的 SubLayout 檔案。
const SubLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="border border-amber-300">
<div>{children}</div>
</div>
);
};
export default SubLayout;
再透過 getLayout 把這個 SubLayout 使用於要用的頁面上,要使用的頁面都需要這樣加上要使用的 layout。
import SubLayout from "@/components/SubLayout";
import Link from "next/link";
const AboutA = () => {
return (
<div>
<p>About A</p>
<Link className="underline text-blue-400" href="/">
{" "}
home
</Link>
</div>
);
};
// 在這裡加上 SubLayout
AboutA.getLayout = function getLayout(page: React.ReactNode) {
return <SubLayout>{page}</SubLayout>;
};
export default AboutA;
最後還有一個步驟很重要,那就是需要再把 getLayout 綁定在 _app
檔案上,這樣當切換頁面時,才會呼叫到 getLayout,並把要使用的 SubLayout 給套用上。
export default function App({ Component, pageProps }: AppProps) {
const getLayout = (Component as any).getLayout || ((page) => page);
return <MainLayout>{getLayout(<Component {...pageProps} />)}</MainLayout>;
}
最後呈現的效果就會是這樣,只要切換到有使用這個 SubLayout 的 Route,都有出現橘色外框,灰底 header 的部分則是因為我們原本就有使用 MainLayout,所以都會維持顯示。
看完 Page Router 的部分後,也來看看 Page Router 的部分。
在 App Router 中,如果要使用 layout 來套用共用的網頁結構和邏輯不需要像 Page Router 這樣有那麼多步驟,因為 App Router 有支援巢狀資料夾結構套用 layout 的功能,所以只需要在 app 資料夾
底下建立名為 layout 的檔案
就可以讓對應的 route 套用。
如果要在 / 路由
下處理整個頁面的 layout,可以在 app 資料夾下建立名為 layout 的檔案,如果要在 /about 路
由下建立另一個名為 layout 也是在 about 資料夾底下在建立一個 Layout 檔案就可以了。
這邊來看一個使用 layout 檔案對巢狀路由結構設定。
如圖這樣設定後,也就能針對 / 路由
設定一個共用的 layout,還有針對 /about 路由
也設定一個 layout。
在 layout 檔案中的設定非常單純,只需要把 children 這個 prop 帶進去就好,這個 children 就是對應當前路由段(Route Segment) 下要顯示的畫面,Next.js 會自動幫你將內容放進來。我們可以在這裡自由地包裝 layout,例如:放入 sidebar、navbar 等元件組合 layout。
const AboutLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="flex">
<div className="w-[200px] h-[calc(100vh-60px)] bg-amber-200">
sidebar menu
</div>
<div className="p-6">{children}</div>
</div>
);
};
export default AboutLayout;
在這個例子中分別在 app 資料夾底層和 about 資料個底層各加了一個 layout 檔案,呈現的狀況就會是以下這樣。
以實際畫面來看的話,橘框的部分套用的是 app 資料夾底層的 layout,也就是在 / 這個路由底下,都會套用設定了藍色的 header 的 layout;紫色框的部分則是套用 about 資料夾底下的 layout,也就是 /about 路由段
除了會套用到藍色的 header 外,也會套用 about 資料夾下 layout 檔案內的黃底 sidebar menu。
無論是在 React、Page Router,還是 App Router 中,都可以使用 layout 讓畫面邏輯更加一致與好維護。
在 React 中會需要搭配 Outlet 與設定 config 配置 layout。Page Router 則是因為沒有支援巢狀 layout 的緣故,需要透過 _app.tsx 或 getLayout 函式來套用 layout。而 App Router 因為支援 layout 套用在巢狀結構的檔案系統中,使得定義 layout 的這個部分變得比 Page Router 還更直覺,在 App Router 中,只需要使用透過命名為 layout 的檔案,就能讓 layout 自然地依照資料夾架構套用。
明天我們會接著看在 React、Page Router 及 App Router 中的 使用方式及差異!