:遷移踩坑與最佳實踐)
Nuxt.js 詳解三遷移踩坑與最佳實踐這是 Nuxt 系列的最后一篇。前兩篇講了 Nuxt 是什么、怎么用。這篇講實際項目里你會踩的坑——SSR 兼容性、數據水合、性能優化、部署問題以及怎么避開它們。一、SSR 兼容性問題最高頻踩坑問題是什么Nuxt 開啟 SSR 后組件會在服務端先執行一次再到客戶端執行一次。服務端環境里沒有window、document、localStorage、sessionStorage這些瀏覽器對象。只要你的代碼在服務端碰到了這些對象直接報錯ReferenceError: window is not defined ReferenceError: document is not defined錯誤寫法script setup // ? setup 頂層直接用 windowSSR 階段會炸 const width window.innerWidth const token localStorage.getItem(token) /script正確寫法一用 onMountedonMounted只在客戶端執行服務端不跑script setup const width ref(0) onMounted(() { width.value window.innerWidth }) /script正確寫法二用 process.client 判斷if(process.client){// 這段代碼只在客戶端執行consttokenlocalStorage.getItem(token)}Nuxt 3 也支持import.meta.clientif(import.meta.client){consttokenlocalStorage.getItem(token)}正確寫法三用 包裹有些組件只能在客戶端跑比如用到 canvas、地圖 SDK用ClientOnly包起來template ClientOnly MapComponent / template #fallback div地圖加載中.../div /template /ClientOnly /template#fallback是服務端渲染時的占位內容避免白屏。二、第三方庫 SSR 兼容處理問題很多第三方庫圖表庫、編輯器、地圖默認依賴瀏覽器環境在 SSR 階段會報錯。方案一動態導入 ssr:falsescript setup const MonacoEditor defineAsyncComponent(() import(guolao/vue-monaco-editor) ) /script template ClientOnly MonacoEditor / /ClientOnly /template方案二nuxt.config 配置// nuxt.config.tsexportdefaultdefineNuxtConfig({build:{transpile:[vue-monaco-editor]// 讓 Nuxt 處理這個庫的 SSR},vite:{ssr:{noExternal:[some-ssr-unfriendly-lib]// 不走外部化打包進 SSR bundle}}})方案三用插件按需加載// plugins/echarts.client.ts// 文件名帶 .client 后綴只在客戶端加載import{use}fromecharts/coreimport{CanvasRenderer}fromecharts/renderersimport{BarChart}fromecharts/chartsuse([CanvasRenderer,BarChart])exportdefaultdefineNuxtPlugin((){// 初始化邏輯})三、數據水合Hydration問題問題是什么SSR 時服務端渲染了一份 HTML客戶端拿到后會把這份 HTML 和 JS 狀態對齊hydration。如果服務端和客戶端渲染出來的內容不一致就會報 hydration mismatch 警告甚至頁面錯亂。常見觸發場景時間不一致服務端渲染 12:00:00客戶端水合時已經 12:00:01。!-- ? 會出問題 -- template div{{ new Date().toLocaleTimeString() }}/div /template隨機數不一致!-- ? 服務端和客戶端隨機數不同 -- template div驗證碼{{ Math.random() }}/div /template解決方案把不確定的內容放到onMounted里生成script setup const timeStr ref() onMounted(() { timeStr.value new Date().toLocaleTimeString() }) /script template div{{ timeStr || --:--:-- }}/div /template四、useFetch vs useAsyncData 怎么選這是新手最容易困惑的點。特性useFetchuseAsyncData定位封裝好的 HTTP 請求工具通用數據獲取數據來源$fetchHTTP任意異步操作參數URL optionskey handler適用調接口組合多數據源、非 HTTP 數據簡單記法調接口用 useFetch其他場景用 useAsyncData。常見錯誤不用 useFetch 直接 $fetch!-- ? 這樣不會做 SSR 預取還會在客戶端重復請求 -- script setup const data await $fetch(/api/users) /script正確用 useFetch 包一層script setup const { data } await useFetch(/api/users) // SSR 階段預取客戶端復用不重復請求 /script避免重復請求給 key多個組件用同一份數據時給相同的 keyNuxt 會復用緩存而不是重復請求// 組件 Aconst{data}awaituseFetch(/api/config,{key:app-config})// 組件 Bconst{data}awaituseFetch(/api/config,{key:app-config})// 只請求一次第二個復用第一個的結果五、狀態管理最佳實踐SSR 下 Pinia 狀態共享SSR 模式下每次請求是獨立的不能在模塊頂層創建全局單例否則狀態會串到其他用戶。錯誤寫法// ? 模塊頂層創建單例多用戶共享會串數據conststorecreatePinia()正確在setup里調用useXxxStore()Nuxt 會保證每次請求獨立。跨請求狀態用 useStateNuxt 內置useState專門處理 SSR 下的共享狀態自動處理服務端到客戶端的序列化// composables/useCart.tsexportconstuseCart(){returnuseState(cart,()({items:[],total:0,}))}不要用普通的全局變量存狀態——SSR 下會串用戶數據。六、SEO 優化進階基礎設置script setup useSeoMeta({ title: 商品詳情, ogTitle: 商品詳情, description: 商品描述, ogDescription: 商品描述, }) /script動態 SEO數據驅動的 SEO等數據回來再設置script setup const { data: product } await useFetch(/api/products/${route.params.id}) useSeoMeta({ title: () ${product.value?.name} - 我的商城, description: () product.value?.description, }) /script站點全局默認值// nuxt.config.tsexportdefaultdefineNuxtConfig({app:{head:{titleTemplate:%s - 我的商城,meta:[{name:viewport,content:widthdevice-width, initial-scale1},{name:description,content:我的商城默認描述},],}}})sitemap 和 robotsnpx nuxi moduleinstallsitemap// nuxt.config.tsexportdefaultdefineNuxtConfig({modules:[nuxtjs/sitemap],site:{url:https://example.com,},sitemap:{sources:[/api/__sitemap__/urls],}})自動生成/sitemap.xml和/robots.txt。七、圖片優化安裝 NuxtImagenpx nuxi moduleinstallimage使用template NuxtImg src/images/product.jpg width400 height300 formatwebp loadinglazy alt商品圖 / /template效果自動生成多種尺寸的響應式圖片自動轉 WebP 格式體積小 30%-50%懶加載默認開啟生成 srcset 適配不同屏幕八、性能優化路由級緩存routeRules// nuxt.config.tsexportdefaultdefineNuxtConfig({routeRules:{/:{prerender:true},// 構建時預渲染/blog/**:{swr:3600},// 1小時增量緩存/api/heavy/**:{swr:600},// 重計算接口緩存 10 分鐘/admin/**:{ssr:false},// 后臺不走 SSR}})prerender構建時生成靜態 HTML運行時零開銷swrstale-while-revalidate返回緩存的同時后臺刷新兼顧速度和新鮮度ssr: false不走服務端渲染省服務器資源組件懶加載不立即需要的組件用懶加載script setup // 只在需要時才加載編輯器組件 const Editor defineAsyncComponent(() import(~/components/Editor.vue)) /script template ClientOnly Editor v-ifshowEditor / /ClientOnly /template數據預取script setup definePageMeta({ // 進入這個頁面時預取 /api/users不用等組件加載 async middleware() { await useFetch(/api/users) } }) /script九、部署注意事項Node 部署環境變量構建后的產物需要運行時讀取環境變量。構建時寫死的值不會生效要用運行時配置// nuxt.config.tsexportdefaultdefineNuxtConfig({runtimeConfig:{public:{apiBase:process.env.NUXT_PUBLIC_API_BASE||http://localhost:3000}}})啟動時傳入NUXT_PUBLIC_API_BASEhttps://prod-api.example.comnode.output/server/index.mjs靜態站點部署注意SSG 模式下動態路由的頁面要告訴 Nuxt 去預渲染哪些// nuxt.config.tsexportdefaultdefineNuxtConfig({nitro:{prerender:{crawlLinks:true,// 自動爬取頁面里的鏈接routes:[/sitemap.xml],}}})或者用routeRules指定routeRules:{/blog/**:{prerender:true}}反向代理配置用 Nginx 反代 Nuxt 應用server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }十、常見報錯排查1.window is not defined原因SSR 階段用了瀏覽器 API。解決用process.client判斷或放onMounted里。2.Hydration text mismatch原因服務端和客戶端渲染內容不一致時間、隨機數、依賴客戶端狀態的數據。解決把不確定內容放onMounted或用ClientOnly包裹。3.useFetch 重復請求原因沒有 SSR 預取或 key 重復。解決確保useFetch在setup頂層 await 調用不要包在函數里。4. 第三方庫報Cannot read properties of undefined原因庫依賴瀏覽器環境。解決用ClientOnly包裹或配vite.ssr.noExternal。5. 部署后 502 / 端口不對原因Nitro 默認 3000 端口被占用或沒配對。解決PORT8080 node .output/server/index.mjs指定端口。6. 靜態生成后動態路由 404原因SSG 模式下動態路由沒有被預渲染。解決配置nitro.prerender.crawlLinks或手動指定 routes。十一、結語三篇文章走完了 Nuxt 的完整認知鏈路是什么Vue 之上的全棧框架解決 SSR、SEO、路由工程化、前后端一體怎么用約定式路由、自動導入、useFetch、Pinia、server/api、部署踩什么坑SSR 兼容性、hydration、第三方庫、性能、部署核心心法一條凡是涉及瀏覽器 API 的代碼先想服務端階段會不會執行到這里。這一條想通了Nuxt 大半的坑都不會踩。本系列共三篇第一篇Vue 開發者為什么要關注 Nuxt第二篇從零搭建一個 Nuxt 項目第三篇遷移踩坑與最佳實踐本文