Nuxt.js 详解(三):迁移踩坑与最佳实践

📅 2026/7/31 8:09:43 👁️ 阅读次数 📝 编程学习
Nuxt.js 详解(三):迁移踩坑与最佳实践

Nuxt.js 详解(三):迁移踩坑与最佳实践

这是 Nuxt 系列的最后一篇。前两篇讲了 Nuxt 是什么、怎么用。这篇讲实际项目里你会踩的坑——SSR 兼容性、数据水合、性能优化、部署问题,以及怎么避开它们。

一、SSR 兼容性问题(最高频踩坑)

问题是什么

Nuxt 开启 SSR 后,组件会在服务端先执行一次,再到客户端执行一次。服务端环境里没有windowdocumentlocalStoragesessionStorage这些浏览器对象。

只要你的代码在服务端碰到了这些对象,直接报错:

ReferenceError: window is not defined ReferenceError: document is not defined

错误写法

<script setup> // ❌ setup 顶层直接用 window,SSR 阶段会炸 const width = window.innerWidth const token = localStorage.getItem('token') </script>

正确写法一:用 onMounted

onMounted只在客户端执行,服务端不跑:

<script setup> const width = ref(0) onMounted(() => { width.value = window.innerWidth }) </script>

正确写法二:用 process.client 判断

if(process.client){// 这段代码只在客户端执行consttoken=localStorage.getItem('token')}

Nuxt 3 也支持import.meta.client

if(import.meta.client){consttoken=localStorage.getItem('token')}

正确写法三:用 包裹

有些组件只能在客户端跑(比如用到 canvas、地图 SDK),用<ClientOnly>包起来:

<template> <ClientOnly> <MapComponent /> <template #fallback> <div>地图加载中...</div> </template> </ClientOnly> </template>

#fallback是服务端渲染时的占位内容,避免白屏。

二、第三方库 SSR 兼容处理

问题

很多第三方库(图表库、编辑器、地图)默认依赖浏览器环境,在 SSR 阶段会报错。

方案一:动态导入 + ssr:false

<script 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}from'echarts/core'import{CanvasRenderer}from'echarts/renderers'import{BarChart}from'echarts/charts'use([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 请求工具通用数据获取
数据来源$fetch(HTTP)任意异步操作
参数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

多个组件用同一份数据时,给相同的 key,Nuxt 会复用缓存而不是重复请求:

// 组件 Aconst{data}=awaituseFetch('/api/config',{key:'app-config'})// 组件 Bconst{data}=awaituseFetch('/api/config',{key:'app-config'})// 只请求一次,第二个复用第一个的结果

五、状态管理最佳实践

SSR 下 Pinia 状态共享

SSR 模式下,每次请求是独立的,不能在模块顶层创建全局单例,否则状态会串到其他用户。

错误写法:

// ❌ 模块顶层创建单例,多用户共享会串数据conststore=createPinia()

正确:在setup里调用useXxxStore(),Nuxt 会保证每次请求独立。

跨请求状态用 useState

Nuxt 内置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:'width=device-width, initial-scale=1'},{name:'description',content:'我的商城默认描述'},],}}})

sitemap 和 robots

npx nuxi moduleinstallsitemap
// nuxt.config.tsexportdefaultdefineNuxtConfig({modules:['@nuxtjs/sitemap'],site:{url:'https://example.com',},sitemap:{sources:['/api/__sitemap__/urls'],}})

自动生成/sitemap.xml/robots.txt

七、图片优化

安装 NuxtImage

npx nuxi moduleinstallimage

使用

<template> <NuxtImg src="/images/product.jpg" width="400" height="300" format="webp" loading="lazy" 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,运行时零开销
  • swr(stale-while-revalidate):返回缓存的同时后台刷新,兼顾速度和新鲜度
  • ssr: false:不走服务端渲染,省服务器资源

组件懒加载

不立即需要的组件用懒加载:

<script setup> // 只在需要时才加载编辑器组件 const Editor = defineAsyncComponent(() => import('~/components/Editor.vue')) </script> <template> <ClientOnly> <Editor v-if="showEditor" /> </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_BASE=https://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 重复。
解决:确保useFetchsetup顶层 await 调用,不要包在函数里。

4. 第三方库报Cannot read properties of undefined

原因:库依赖浏览器环境。
解决:用<ClientOnly>包裹,或配vite.ssr.noExternal

5. 部署后 502 / 端口不对

原因:Nitro 默认 3000 端口,被占用或没配对。
解决PORT=8080 node .output/server/index.mjs指定端口。

6. 静态生成后动态路由 404

原因:SSG 模式下动态路由没有被预渲染。
解决:配置nitro.prerender.crawlLinks或手动指定 routes。

十一、结语

三篇文章走完了 Nuxt 的完整认知链路:

  1. 是什么:Vue 之上的全栈框架,解决 SSR、SEO、路由工程化、前后端一体
  2. 怎么用:约定式路由、自动导入、useFetch、Pinia、server/api、部署
  3. 踩什么坑:SSR 兼容性、hydration、第三方库、性能、部署

核心心法一条:凡是涉及浏览器 API 的代码,先想"服务端阶段会不会执行到这里"。这一条想通了,Nuxt 大半的坑都不会踩。


本系列共三篇:

  • 第一篇:Vue 开发者为什么要关注 Nuxt
  • 第二篇:从零搭建一个 Nuxt 项目
  • 第三篇:迁移踩坑与最佳实践(本文)