问题背景
在 SSR 应用中,代码会在两个不同环境执行:
- 服务端 (Node.js) : 没有
window对象,无法获取屏幕宽度 - 客户端 (浏览器) : 有
window.innerWidth,可以判断是否为移动端
如果直接在组件初始化时判断 window.innerWidth <= 768,会导致:
- 服务端渲染的 HTML 和客户端期望的 DOM 结构不一致
- Vue 抛出 hydration mismatch 警告或错误
- 页面可能闪烁或强制重新渲染
/**
* 响应式布局就绪 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
}
封装策略
const layout = reactive({
isMobile: false, // 初始值不重要,因为不会被立即使用
mounted: false, // 关键标志:确保服务端和客户端首次渲染一致
})
关键机制:
-
延迟渲染: 通过
v-if="layout.mounted"让组件在服务端和客户端首次渲染时都不显示内容 -
客户端激活: 只有在
onMounted钩子执行后(仅在客户端运行)才:- 设置
mounted = true显示内容 - 执行
layoutCheck()正确判断isMobile
- 设置
-
响应式更新: 监听
resize事件,支持窗口大小变化时动态调整布局
实际使用分析
1. 案例:移动端和PC端使用完全不同的组件,避免服务端渲染错误的结构
.ProductList(v-if="layout.mounted" ref="elRef")
//- 移动端: Swiper 轮播
Swiper(v-if="layout.isMobile" ...)
//- PC端: Sticky 列表
.list-container(v-else ...)
2. 案例:两端布局差异大(侧边栏位置、内容宽度不同),必须等客户端挂载后再渲染
<div v-if="layout.mounted">
<!-- 移动端布局 -->
<div v-if="layout.isMobile">...</div>
<!-- PC端布局 -->
<div v-else>...</div>
</div>
3. 案例:移动端和PC端使用不同的交互组件(按钮 vs 下拉菜单)
.NewsListCategory(v-if="layout.mounted")
//- 移动端: 按钮组
template(v-if="layout.isMobile")
ButtonCategory(...)
//- PC端: 下拉菜单
template(v-else)
a-dropdown(...)
设计意图总结
这个封装避免了:
- ❌ Hydration mismatch 错误: 服务端和客户端渲染结果不一致
- ❌ 闪烁问题: 页面先显示PC版,再切换到移动版
- ❌ SEO 问题: 搜索引擎抓取到错误的移动端/PC端内容
实现了:
- ✅ 一致的初始渲染: 服务端和客户端首次都不显示内容(skeleton 或空白)
- ✅ 正确的布局判断: 只在客户端执行窗口尺寸检测
- ✅ 响应式适配: 支持窗口大小变化时动态调整
为什么不能简化?
// ❌ 错误做法 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 应用的最佳实践: 对于依赖浏览器环境的响应式布局,必须等到客户端挂载后再渲染,以保证服务端和客户端的一致性。