解决 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 应用的最佳实践: 对于依赖浏览器环境的响应式布局,必须等到客户端挂载后再渲染,以保证服务端和客户端的一致性。

相关推荐
贾铭1 小时前
如何实现一个网页版的剪映(二)
前端·后端
用户600071819101 小时前
【翻译】Rozenite 构建解析:注入机制全揭秘
前端
失迭1 小时前
Cloudflare Tunnel + Zero Trust 稳定接入 Netcup VPS SSH
前端·javascript·github
一叶渡江1 小时前
Ghost docker安装踩坑
前端·cms
光影少年2 小时前
CSS盒模型是什么?box-sizing有什么作用?
前端·css
Dev7z2 小时前
基于晶体塑性理论的FCC单晶本构模型数值实现与验证(硕士级别)
前端
前端嘣擦擦2 小时前
避坑笔记:Chrome 144+ SVG 事件失效问题
前端·javascript·chrome·笔记·svg2
秋天的一阵风2 小时前
🧠 空数组的迷惑行为:为什么 every 为真,some 为假?
前端·javascript·面试
渔舟唱晚@2 小时前
React 19 核心 Hooks 深度解析
前端·react.js·前端框架