Wen Chen
Published on

在單一 domain 底下透過 subPath 部署不同 Next app

Authors
  • avatar
    Name
    Wen Chen

前言

最近馬不停蹄的在將舊專案容器化上雲的時候,意外遇到了新的問題需解決,稍微搜尋一下發現官方有提供解決方式,最終也順利用那個方式處理掉了,但平常沒注意應該不太會看到那個參數。 故記錄下來希望能幫助到有需要的人。

使用的版本是 NextJs : 12.x

需求說明

我們產品本身有 主專案Rewards(兌點)專案 兩者,兌點專案是產品運行一段時間之後額外開發供用戶兌換紅利點數用的,當時開發時就已經獨立拉了一個專案出來做。

而實際部署時,他們希望呈現的方式是 https://mySite.com/ (主站) 、https://mySite.com/rewards (兌點),藉此讓兩個不同專案看起來是在相同網站底下。

過去兌點專案雖然是用 NextJS 做開發,不過所有資料都是在 Client side 去做 fetching,打包時也是直接用 next export 輸出成靜態檔案,基本上行為跟 create-react-app 相差不大,所以之前在處理時,他們是直接把輸出的靜態檔案放在對應的資料夾再設定 Nginx 指定路徑即可。

然而,在這次容器和上雲時,我將兌點專案打包調整成 next build 並將 node server 包在獨立的 container 內透過 Node.js server 來實際 render pages。

捨棄過去輸出成靜態檔案的做法,來實際支援 SSR 的需求。

想了解 next exportnext build 差別者可以看 這篇

但,這樣便出現了一個很直接的問題:我要怎麼將相同 domain 指向不同 container ?

過去專案因為主專案跟兌點專案都是輸出靜態檔案,只要透過 nginx 去指定不同的檔案資料夾即可,但在 NextJs 的架構底下,運維只會替我把 url 指到指定的 app port。

example : https://mySite.com/ => 5073 port

要在單一 domain 下部署不同的 app,在我自己做 side project 時其實大多都是用 cname 做處理的。

像是 部落格 和我的 攝影集

如果沒有一定要放在 subPath 底下的需求,用 cname 應該是比較方便的解決方式。

但既然,有這樣的需求,那要怎麼做呢?


失敗的嘗試

直接先附上最終解法 basePath

待會再聊實作,原先我異想天開,想說直接改掉專案 pages 結構,把他們全部放在 /rewards 之後不知道可不可行,當然結論是不行的。

因為雖然 Load balance 接收到 https://mySite.com/rewards/* 的請求會正確指到兌點專案的 container。

但!因為頁面在載入時,瀏覽器還需要另外到 _next/static/ 載入 js 等相關靜態資源,url 大概會長這樣:

https://mySite.com/_next/static/chunks/ooxxoooxx.js

檔名就不重要了,反正都是經過編譯過檔名,問題出現在上面的路徑,若是單純改掉 pages 的結構,輸出時他還是會將這些資源放在根目錄。

當瀏覽器要去載入相關靜態資源時,Load Balance 讀到 https://mySite.com/ ,便又把 request 導回去主專案的 container,結果就是會得到一堆 404 。

以上說完錯誤的示範,底下開始本文的正解。

實作

基本上從官方文件 看到說明,這個 option 就是要拿來解決我們這個需求的:

To deploy a Next.js application under a sub-path of a domain you can use the basePath config option.

設定的方式非常無腦,基本上去 next.config.jsbasePath 這個設定就好了。而在沒設定的狀況下 basePath 會是 ""

// next.config.js
module.exports = {
  basePath: '/rewards'
}

設定完畢之後,重啟專案,你就會發現原本的 / 首頁變成了 404,因為一但加上 basePath 之後,你的所有路徑都會變成 /basePath/ 之後,原本的 / 就變成了 /rewards/

而前面提到的靜態資源路徑也會自動變成 https://mySite.com/rewards/_next/static/chunks/ooxxoooxx.js

在載入資源時 Load balance 就不會再指錯位置囉。

這裡有幾點要注意:

  1. pages 檔案結構不需要做任何修改,在原路徑底下他會自動加上 basePath 。
  2. next/linknext/router 也不需要做任何修改,假如原本是 "/shoppingCart",他在 output 時會自動變成 /rewards/shoppingCart,不需要手動替每一個Link改位置。

需手動修正地方

雖然大部分不用做任何改動,但還是有些地方需要手動修正,除了官方文件提到的 next/image 以外,我在實作時實際上遇到的是 api 的 url。

因為我們有透過 rewrites 來完成 api url 的 proxy,大概像這樣:

module.exports = {
	async rewrites() {
		return {
			fallback: [
				{
					source: '/api/:path*',
					destination: `${backendDomain}/api/:path*`
				}
			]
		}
	}
}

簡單來說就是把 /api/ 路徑的請求全部代理到由後端指定給我們的 api url,在加上 basePath 功能之後原本 source 雖然沒有特別修改,但他會直接變成 /rewards/api/:path,所以若有統一定義前端 api baseUrl 的地方也要一起加上 /rewards/,這樣才有辦法被 rewrites 正確吃到路徑。

聽起來有點饒口,直接上範例:

修改前:

// 所有前端的 api 都會透過預先定義好的 baseAxios 再往後接 url 去發送
const baseUrl = "/api"

const baseAxios = axios.create({
	baseURL: baseUrl
})

修改後:

// 加上 basePath 後才有辦法被 rewrites 正確捕捉
const baseUrl = "/rewards/api"

const baseAxios = axios.create({
	baseURL: baseUrl
})

後記

基本上 basePath 需設定的內容不多,如果有相關需求的話,希望有幫助到你!

參考資料

https://nextjs.org/docs/app/api-reference/next-config-js/basePath https://nextjs.org/docs/app/api-reference/next-config-js/rewrites https://stackoverflow.com/questions/61724368/what-is-the-difference-between-next-export-and-next-build-in-next-js