解决 Nuxt SSR (服务端渲染) 环境下的水合错误 (Hydration Mismatch)

问题背景

在 SSR 应用中,代码会在两个不同环境执行:

  1. 服务端 (Node.js) : 没有 window 对象,无法获取屏幕宽度
  2. 客户端 (浏览器) : 有 window.innerWidth,可以判断是否为移动端

如果直接在组件初始化时判断 window.innerWidth <= 768,会导致:

  • 服务端渲染的 HTML 和客户端期望的 DOM 结构不一致
  • Vue 抛出 hydration mismatch 警告或错误
  • 页面可能闪烁或强制重新渲染
js 复制代码
/**
 * 响应式布局就绪 composable
 * @param breakpoint 断点宽度,≤ 该值认为是移动端页面,> 该值认为是pc端页面,默认 768px
 * @returns 布局对象
 */
export function useLayoutReady(breakpoint = 768) {
  // 运行环境不一致,导致水合错误
  // globalThis['innerWidth'] <= breakpoint
  const layout = reactive({
    // 是否为移动端页面
    isMobile: false,
    // 是否已挂在,已执行 onMounted
    mounted: false,
  })

  const layoutCheck = () => {
    if (typeof window !== 'undefined') {
      layout.isMobile = window.innerWidth <= breakpoint
    }
  }

  onMounted(() => {
    layout.mounted = true
    layoutCheck()
    window.addEventListener('resize', layoutCheck)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', layoutCheck)
  })

  return layout
}

封装策略

arduino 复制代码
const layout = reactive({
  isMobile: false,      // 初始值不重要,因为不会被立即使用
  mounted: false,       // 关键标志:确保服务端和客户端首次渲染一致
})

关键机制:

  1. 延迟渲染 : 通过 v-if="layout.mounted" 让组件在服务端和客户端首次渲染时都不显示内容

  2. 客户端激活 : 只有在 onMounted 钩子执行后(仅在客户端运行)才:

    • 设置 mounted = true 显示内容
    • 执行 layoutCheck() 正确判断 isMobile
  3. 响应式更新 : 监听 resize 事件,支持窗口大小变化时动态调整布局

实际使用分析

1. 案例:移动端和PC端使用完全不同的组件,避免服务端渲染错误的结构

csharp 复制代码
.ProductList(v-if="layout.mounted" ref="elRef")
  //- 移动端: Swiper 轮播
  Swiper(v-if="layout.isMobile" ...)
  //- PC端: Sticky 列表
  .list-container(v-else ...)

2. 案例:两端布局差异大(侧边栏位置、内容宽度不同),必须等客户端挂载后再渲染

xml 复制代码
<div v-if="layout.mounted">
  <!-- 移动端布局 -->
  <div v-if="layout.isMobile">...</div>
  <!-- PC端布局 -->
  <div v-else>...</div>
</div>

3. 案例:移动端和PC端使用不同的交互组件(按钮 vs 下拉菜单)

scss 复制代码
.NewsListCategory(v-if="layout.mounted")
  //- 移动端: 按钮组
  template(v-if="layout.isMobile")
    ButtonCategory(...)
  //- PC端: 下拉菜单
  template(v-else)
    a-dropdown(...)

设计意图总结

这个封装避免了:

  1. Hydration mismatch 错误: 服务端和客户端渲染结果不一致
  2. 闪烁问题: 页面先显示PC版,再切换到移动版
  3. SEO 问题: 搜索引擎抓取到错误的移动端/PC端内容

实现了:

  1. 一致的初始渲染: 服务端和客户端首次都不显示内容(skeleton 或空白)
  2. 正确的布局判断: 只在客户端执行窗口尺寸检测
  3. 响应式适配: 支持窗口大小变化时动态调整

为什么不能简化?

ini 复制代码
// ❌ 错误做法 1: 直接判断会导致服务端报错
const isMobile = ref(window.innerWidth <= 768)

// ❌ 错误做法 2: 即使加 typeof 判断,服务端渲染 false,客户端可能是 true
const isMobile = ref(typeof window !== 'undefined' && window.innerWidth <= 768)
// 服务端渲染: <div class="pc-layout">...</div>
// 客户端期望: <div class="mobile-layout">...</div>
// 结果: Hydration mismatch!

// ✅ 正确做法: 延迟到客户端挂载后再显示
const layout = useLayoutReady()
// 模板: v-if="layout.mounted"
// 服务端和客户端首次都不渲染,避免水合错误

这个设计体现了 Nuxt SSR 应用的最佳实践: 对于依赖浏览器环境的响应式布局,必须等到客户端挂载后再渲染,以保证服务端和客户端的一致性。

相关推荐
默默学前端5 分钟前
ES6模板语法与字符串处理详解
前端·ecmascript·es6
lxh011313 分钟前
记忆函数 II 题解
前端·javascript
我不吃饼干20 分钟前
TypeScript 类型体操练习笔记(三)
前端·typescript
华仔啊24 分钟前
除了防抖和节流,还有哪些 JS 性能优化手段?
前端·javascript·vue.js
CHU72903531 分钟前
随时随地学新知——线上网课教学小程序前端功能详解
前端·小程序
清粥油条可乐炸鸡33 分钟前
motion入门教程
前端·css·react.js
这是个栗子36 分钟前
【Vue3项目】电商前台项目(四)
前端·vue.js·pinia·表单校验·面包屑导航
前端Hardy40 分钟前
Electrobun 正式登场:仅 12MB,JS 桌面开发迎来轻量化新方案!
前端·javascript·electron
树上有只程序猿40 分钟前
新世界的入场券,不再只发给程序员
前端·人工智能
confiself1 小时前
deer-flow前端分析
前端