Nuxt 的 SSR 與 Nitro:方便,但別讓它替你劃架構邊界
Nuxt 的 SSR 與 Nitro:方便,但別讓它替你劃架構邊界
Nuxt 是 Vue 世界的 meta-framework,把 routing、SSR、static generation、server engine 全部整合進一個盒子,今年七月 Nuxt 4 也正式出了。它確實好用,好用到你會開始把所有東西往裡面塞。我對它的態度跟對多數 meta-framework 一樣:方便那層我很愛用,但方便不該決定架構邊界。這篇想講的就是 Nuxt 兩個最容易被誤用的地方,SSR 的資料抓取,跟 Nitro 那層 server。
SSR、static、hybrid:先想清楚每條 route 該怎麼長
Nuxt 不是只有 SSR 一種模式。同一個專案裡,行銷頁可以 prerender 成純靜態、丟到 CDN 邊緣就好;需要即時資料的 dashboard 走 SSR;某些幾乎不變的內容可以設 cache、隔一段時間再重新生成。這套 per-route 規則就是 hybrid rendering,用 routeRules 一條一條指定,是 Nuxt 真正值得花時間搞懂的地方。多數人效能問題不是框架慢,是整站無腦開 SSR,把本來可以靜態的頁也每次都拿去 server 算一遍。
真正會咬人的是資料抓取。useAsyncData 跟 useFetch 的行為要先有正確的心智模型:首次請求在 server 端跑,抓到的資料會序列化進 HTML 一起送下來,client 端 hydrate 的時候直接接手那份 payload,不會再打一次。這個設計很漂亮,但前提是你得照它的規矩用。
最常見的兩個坑。一是 double fetch,你在元件裡用了 useFetch,又在某個 onMounted 或 watcher 裡自己再 $fetch 一次相同的資料,server 抓一次、client 又抓一次,使用者就看你白白多打一輪。二是更危險的洩漏:useAsyncData 的 handler 是會在 server 跑,但寫法不對也會被帶去 client。如果你在那個 callback 裡直接讀環境變數裡的 API key、直接連內部服務的位址,而這段邏輯沒有被正確關在 server-only 的範圍,它就有機會被打包進送給瀏覽器的 bundle。這不是 Nuxt 的錯,是你把 server-only 的東西寫在了會 hydrate 的路徑上。
判斷原則其實很簡單:那份資料的「來源」能不能見光。能見光的(公開 API、CDN 上的 JSON)放哪都行;不能見光的(內部位址、密鑰、需要特權的查詢)就只能待在 server 端,前端只該拿到 already 裁好的結果。
Nitro 當 BFF 很方便,但方便不是邊界
Nitro 是 Nuxt 底下的 server engine,你在 server/api/ 底下開的那些 server route,就是跑在 Nitro 上。它能做的事比想像多:同一層既是 SSR 的資料來源,也能當一個輕量的 BFF gateway。前端要資料,不直接打第三方,而是打自己的 Nitro route,那層去聚合下游、補上認證、決定回傳什麼,外部請求不直接碰資料層。這個配置我跑過,是真的省力,密鑰留在 server、call chain 集中、前端只拿到裁好的東西。
問題不在它做不到,而在它太容易做到,於是邊界開始糊。框架的 server 那層有個結構性的誘惑:它同時看得到「給瀏覽器的聚合」跟「面向後端的風險」,兩件事寫在同一個目錄、同一種 defineEventHandler 裡,久了你會分不清哪個 route 是在替前端裁資料、哪個是在扛內部服務的存取。一個 server/api/ 底下混著「給 lobby 用的欄位拼裝」跟「直接帶著 DB 憑證查表」,code review 的人很難一眼看出風險落在哪。
所以我自己還是傾向獨立的 BFF。不是說 Nitro 不能扛這個角色,小專案、團隊就幾個人、心裡那條線守得住,用 Nitro 當 BFF 完全合理,少養一個服務。但一旦系統開始長大,我會把「面向後端、扛風險」那層拉出去,讓 Nitro 回去專心做它最擅長的:SSR 跟給前端的輕量聚合。分開不是為了多一層,是為了讓「誰面向瀏覽器、誰面向後端」這條界線,由架構決定,而不是由「反正寫在這裡最快」決定。
Nuxt 把很多事變簡單,這是它的價值。但簡單跟邊界是兩回事。它讓你「能」把聚合、密鑰、下游存取通通塞進 server route,不代表你「該」這麼做。方便是工具給你的禮物,邊界是你自己要守的紀律。跑得起來是一回事,設計有沒有道理是另一回事。
重點整理
- Nuxt 的 hybrid rendering 用
routeRules逐 route 指定 SSR / static / cache,多數效能問題來自全站無腦開 SSR。 useAsyncData/useFetch首次在 server 抓、hydrate 後 client 接手,搞錯會 double fetch 或把 server-only 的密鑰帶進 client bundle。- 判斷資料放哪只問一句:它的來源能不能見光,不能見光的就只能待 server。
- Nitro server route 可同時當 SSR 來源與輕量 BFF,外部請求不直接碰資料層,這配置很方便。
- 但方便不該劃架構邊界,系統一大就把「面向後端、扛風險」那層獨立出去,別讓它跟給瀏覽器的聚合混在同一個目錄。
觀點 Sheng,內文 Claude 協助 · 列入 20260613 blog 翻新計劃,新漆未乾。