這個系列記的是一個 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 | 元件好寫 | 名稱全域唯一 |
會後悔的通常不是選錯,而是當初沒意識到自己在做交換。這也是為什麼這三件事值得在開工前寫下來——它們之後都不會再提醒你一次。