Nuxt.js 详解(三):迁移踩坑与最佳实践 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 项目第三篇迁移踩坑与最佳实践本文

本月热点