Nuxt 4 全棧專案架構設計

一個 Dashboard 型專案在開工前定下的三個結構決定:關掉 SSR 的取捨、app 與 server 的 界線該用什麼判準畫、以及拿掉元件路徑前綴換來的代價。

系列・Nuxt 4 全棧實戰筆記・第 1/12 篇

這個系列記的是一個 Dashboard 型專案的實作決定:需要登入才能使用、使用者固定、資料操作密集、對搜尋引擎沒有需求。技術棧是 Nuxt 4 + Supabase + Cloudflare Workers。

選型的理由清單這裡不寫——「生態成熟」「開發體驗好」這類說法無法證偽,對讀者也沒有用。這篇只寫三個有代價的結構決定,它們的共通點是:越晚回頭越貴。

一、關掉 SSR

ssr: false 是最早、也最難回頭的一個。

關掉之後拿到的是:不必處理 hydration 不一致、不必為每個資料存取點區分「這段在伺服器跑還是在瀏覽器跑」、部署形態單純(一包靜態資源加一個 API 層)。對一個所有頁面都要登入的後台來說,這幾件事省下的心力相當可觀。

放棄的是搜尋引擎索引、首屏渲染速度,以及未來想要一部分頁面公開時的彈性。最後這項是真正的成本:等到有一天需要一個公開的分享頁或報表連結,你面對的不是加個設定,而是把資料存取模式整批重做。

你可能會想:那等真的需要的時候再開回來不就好了?設定確實只有一行,難的是那一行後面的東西。關掉 SSR 之後寫出來的每一段資料存取,都預設「這段只在瀏覽器跑、使用者一定已登入」;開回來的那天,這些假設會同時失效,而它們不會集中在一個檔案裡。你改的是一個設定,要驗的是整個應用。

判準很簡單:應用裡有沒有任何一個頁面,將來可能需要不登入就能看。 有的話就不要關。這個專案的答案是沒有,所以關了;如果是部落格或電商,答案永遠是有。

二、app 與 server 的界線

分層的理由不是「關注點分離」——那句話解釋不了任何具體的取捨。真正的判準只有一句:這段程式碼會不會被使用者讀到。

app/ 底下的一切最終都會進到瀏覽器,包含你以為只在建置期用到的常數。所以密鑰、業務規則裡的門檻值、判斷誰能做什麼的邏輯,一律不能放這裡。server/ 則是相反的一側:使用者只能看到它的輸出,看不到它的過程。

shared/ 是為了那些兩邊都需要、而且必須完全一致的東西存在的——最典型的是驗證用的 schema。表單在瀏覽器端擋一次是為了體驗,API 在伺服器端擋一次是為了安全;兩邊各寫一份的話,遲早會漂成兩套規則,而且漂掉的時候不會有人發現。

app/         # 會進到瀏覽器:頁面、元件、composables、client 端型別
server/      # 只在伺服器執行:API、middleware、DB client、密鑰
shared/      # 兩邊共用且必須一致:驗證 schema、共用型別
supabase/    # migration

界線不清楚的代價不是「不好維護」這種抽象說法,而是你不會知道某段邏輯有沒有洩漏出去。放對地方之後,這個問題由目錄本身回答。

三、拿掉元件路徑前綴的代價

Nuxt 預設會把目錄結構編進元件名稱:components/machines/MachineTable.vue 註冊成 <MachinesMachineTable />。這個名稱沒有人想寫,所以很容易就設了:

components: [{ path: '~/components', pathPrefix: false }]

設完之後它變成 <MachineTable />,好寫很多。代價是整個專案的元件名稱變成一個扁平的命名空間——不同目錄下不能再有同名檔案。

這個代價通常在專案中期才會顯現:兩個功能各自有一個 StatusBadge.vue,其中一個會安靜地覆蓋另一個。它不會有錯誤訊息,只會有一個地方顯示成另一個地方的樣子。

所以設了這個選項,就等於接受「元件命名要當成全域命名空間來管」。小專案划算,元件多而分散的專案要先想清楚。

這三個決定的共通點

它們都不是「哪個比較好」的問題,而是用一個現在的方便換一個未來的限制:

決定現在拿到未來受限
ssr: false開發模型單純沒有頁面能公開
app / server 分界洩漏問題由目錄回答共用邏輯必須刻意放進 shared/
pathPrefix: false元件好寫名稱全域唯一

會後悔的通常不是選錯,而是當初沒意識到自己在做交換。這也是為什麼這三件事值得在開工前寫下來——它們之後都不會再提醒你一次。