VueUse SSR 安全使用
VueUse 是 Vue 生态中最大的 Composables 工具库——200+ 个函数覆盖浏览器 API、传感器、状态管理、动画等。但在 Nuxt4 的 SSR 环境中,VueUse 的很多函数依赖浏览器 API(
window、document、localStorage),直接使用会导致服务端报错。本章讲清楚 SSR 环境下使用 VueUse 的正确姿势,以及如何构建自己的 SSR 安全 Composables。
1. SSR 的核心问题
1.1 服务端没有浏览器 API
Nuxt4 的 SSR 在 Node.js 中执行组件的 setup() 函数。Node.js 没有:
| 不可用 API | 依赖它的 VueUse 函数 |
|---|---|
window | useWindowSize、useScroll、useMediaQuery |
document | useEventListener、useMutationObserver |
localStorage | useLocalStorage、useSessionStorage |
navigator | useGeolocation、useOnline、useClipboard |
IntersectionObserver | useIntersectionObserver、useElementVisibility |
直接在 setup() 中调用这些函数,SSR 阶段会抛出 ReferenceError: window is not defined。
1.2 VueUse 的内置安全机制
好消息是,VueUse 大部分函数已经做了 SSR 兼容——它们在服务端返回安全的默认值(空对象、false、0),不会报错。但有些细节需要注意。
2. 常用函数的 SSR 行为
2.1 useLocalStorage / useSessionStorage
const theme = useLocalStorage('theme', 'light')
// SSR 阶段:返回默认值 'light'(不访问 localStorage)
// 客户端 Hydration 后:读取 localStorage 真实值潜在问题:SSR 渲染用默认值('light'),但客户端 localStorage 中可能是 'dark'。Hydration 时值会变化,导致短暂的闪烁(先亮后暗)。
解决方案:使用 Cookie 在 SSR 阶段就获取正确的值(参见第 60 章暗黑模式)。或者使用 useCookie:
// 用 Cookie 替代 localStorage,SSR 也能读到
const theme = useCookie('theme', { default: () => 'light' })2.2 useMediaQuery
const isMobile = useMediaQuery('(max-width: 768px)')
// SSR 阶段:始终返回 false
// 客户端:根据实际视口返回 true/falseSSR 阶段无法知道用户的屏幕宽度。如果你的布局依赖 isMobile 做条件渲染,SSR 和客户端可能渲染不同的内容——导致 Hydration Mismatch。
安全做法:
<template>
<!-- 用 CSS 响应式替代 JS 条件渲染 -->
<div class="hidden md:block">桌面端内容</div>
<div class="md:hidden">移动端内容</div>
</template>CSS 媒体查询在客户端即时生效,不会产生 Hydration Mismatch。只在真正需要 JS 逻辑时才用 useMediaQuery。
2.3 useIntersectionObserver
const target = useTemplateRef<HTMLElement>('target')
const { isIntersecting } = useIntersectionObserver(target, ([entry]) => {
// 元素进入/离开视口
})SSR 阶段:target 是 null,Observer 不会创建。客户端 Hydration 后自动初始化。无需特殊处理。
2.4 useWindowSize
const { width, height } = useWindowSize()
// SSR 阶段:width = Infinity, height = Infinity(VueUse 的默认值)注意 SSR 默认值是 Infinity,不是 0。如果你的代码中有 if (width.value < 768) 这样的判断,SSR 阶段会走 false 分支。
2.5 useEventListener
useEventListener('scroll', () => {
// 处理滚动
})
// SSR 安全:服务端不会添加事件监听器VueUse 的 useEventListener 内部会检查环境,SSR 阶段直接跳过。无需特殊处理。
3. SSR 安全的判断模式
3.1 import.meta.client / import.meta.server
Nuxt4 提供了编译时常量来判断运行环境:
if (import.meta.client) {
// 只在客户端执行
const canvas = document.createElement('canvas')
}
if (import.meta.server) {
// 只在服务端执行
const data = await readFile('./data.json')
}编译时优化:import.meta.client 在服务端构建时被替换为 false,整个 if 块被 Tree Shaking 移除——不会增加 bundle 体积。
3.2 onMounted 保护
onMounted 只在客户端执行(服务端不会调用):
<script setup>
let chart: Chart | null = null
onMounted(async () => {
const { Chart } = await import('chart.js')
chart = new Chart(canvasEl.value, { /* ... */ })
})
onUnmounted(() => {
chart?.destroy()
})
</script>3.3 <ClientOnly> 组件
Nuxt4 内置 <ClientOnly> 组件,包裹的内容只在客户端渲染:
<template>
<ClientOnly>
<VideoPlayer :src="videoUrl" />
<template #fallback>
<div class="skeleton animate-pulse h-64 rounded-lg bg-gray-200" />
</template>
</ClientOnly>
</template>#fallback 插槽在 SSR 阶段渲染,客户端 Hydration 后替换为真实组件。适合重度依赖浏览器 API 的组件(视频播放器、地图、Canvas 绑定的图表)。
3.4 .client 后缀
Nuxt4 约定 .client.vue 后缀的组件只在客户端加载:
components/
├── VideoPlayer.client.vue ← 只在客户端渲染
├── AppHeader.vue ← 双端渲染
└── ServerBanner.server.vue ← 只在服务端渲染
4. 构建 SSR 安全的 Composables
4.1 设计原则
自定义 Composable 要做到 SSR 安全,遵循三条原则:
- 不在顶层访问浏览器 API:
setup()中不能直接用window、document - 提供安全默认值:SSR 阶段返回合理的默认值(false、0、空字符串)
- 延迟初始化:在
onMounted或watch中初始化浏览器相关逻辑
4.2 示例:useVideoPlayer
// composables/useVideoPlayer.ts
export function useVideoPlayer(videoRef: Ref<HTMLVideoElement | null>) {
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
// SSR 安全:onMounted 中初始化
onMounted(() => {
const el = videoRef.value
if (!el) return
useEventListener(el, 'play', () => { isPlaying.value = true })
useEventListener(el, 'pause', () => { isPlaying.value = false })
useEventListener(el, 'timeupdate', () => {
currentTime.value = el.currentTime
})
useEventListener(el, 'loadedmetadata', () => {
duration.value = el.duration
})
})
function play() {
videoRef.value?.play()
}
function pause() {
videoRef.value?.pause()
}
function seek(time: number) {
if (videoRef.value) {
videoRef.value.currentTime = time
}
}
return { isPlaying, currentTime, duration, play, pause, seek }
}4.3 示例:useScrollProgress
// composables/useScrollProgress.ts
export function useScrollProgress() {
const progress = ref(0)
if (import.meta.client) {
useEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop
const scrollHeight = document.documentElement.scrollHeight
const clientHeight = document.documentElement.clientHeight
progress.value = scrollTop / (scrollHeight - clientHeight)
})
}
return { progress }
}import.meta.client 保护浏览器 API 调用,SSR 阶段 progress 始终为 0。
5. 常见陷阱
5.1 Hydration Mismatch
根因:服务端和客户端渲染的 HTML 不一致。
常见触发场景:
new Date()在服务端和客户端返回不同时间Math.random()两端生成不同值useMediaQuerySSR 返回 false,客户端返回 trueuseLocalStorageSSR 返回默认值,客户端返回存储值
排查:Nuxt4 开发模式下会在控制台输出 Hydration Mismatch 警告,包含不匹配的具体位置。
5.2 内存泄漏
SSR 环境中的内存泄漏比 SPA 更严重——SPA 中页面刷新就释放内存,SSR 的 Node.js 进程持续运行。
// ❌ 模块级别的状态会在所有请求间共享
const globalCache = new Map()
// ✅ 在 composable 中使用,每个请求独立
export function useCache() {
const cache = useState('cache', () => new Map())
return cache
}useState 是 Nuxt4 提供的 SSR 安全的状态管理——每个请求有独立的状态实例,不会跨请求污染。
本章小结
- SSR 核心限制:服务端没有
window/document/localStorage,VueUse 大部分函数已内置安全兼容 - 常见函数行为:
useLocalStorage返回默认值(可能闪烁),useMediaQuery返回 false(用 CSS 替代),useEventListener自动跳过 - 保护模式:
import.meta.client(编译时移除)、onMounted(客户端执行)、<ClientOnly>(组件级)、.client.vue(文件级) - 自定义 Composable:不在顶层访问浏览器 API,提供安全默认值,
onMounted中延迟初始化 - 避坑:Hydration Mismatch 用
useCookie替代useLocalStorage,用useState替代模块级变量防止跨请求污染