SSR 兼容性
SSR 兼容性
Valaxy 使用 SSG(静态站点生成)构建你的站点,它在构建时通过 Vue 的服务端渲染(SSR)将页面渲染为 HTML。这意味着组件在构建期间运行在 Node.js 环境中,此时像 window、document 和 navigator 这样的浏览器 API 是不可用的。
从旧版 `vite-ssg` 引擎升级
Valaxy 曾内置基于 JSDOM 的 vite-ssg 引擎,已在 v1.0 中移除(见 #706)。JSDOM 在 SSR 期间默默提供了 window、document 和 navigator,因此那些在渲染时访问这些全局对象的代码看似"能正常工作"。而 Valaxy SSG 引擎以纯字符串渲染、没有 DOM,因此同样的代码现在会抛错或导致水合错误。
如果你正在升级,且某个主题或 addon 在 SSR 期间依赖 DOM,请用下文的方式守护所有仅浏览器的访问。
为什么会发生水合不匹配
在 SSG 生成静态 HTML 后,Vue 会在浏览器中"水合"它——附加事件监听器并使其具有交互性。如果服务器渲染的 HTML 与客户端渲染的内容不同,你会收到水合不匹配警告。
常见原因:
| 原因 | 示例 |
|---|---|
| 模板中使用仅浏览器 API | {{ window.innerWidth }} |
| 时间/地区相关的值 | {{ new Date().toLocaleString() }} |
| 浏览器扩展修改 HTML | 广告拦截器注入元素 |
| 非标准 HTML 嵌套 | <p> 嵌套在 <p> 中,<div> 在 <a> 中 |
<ClientOnly>
使用内置的 <ClientOnly> 组件包裹仅浏览器的内容。它的内容只在客户端渲染。
<template>
<ClientOnly>
<BrowserOnlyComponent />
</ClientOnly>
</template>使用 #fallback 插槽在 SSR/SSG 期间显示占位内容:
<template>
<ClientOnly>
<HeavyChart :data="chartData" />
<template #fallback>
<div class="chart-placeholder">
加载图表中...
</div>
</template>
</ClientOnly>
</template>defineClientComponent
对于在导入时(而不仅仅是渲染时)访问浏览器 API 的第三方库,使用 defineClientComponent。它会延迟 import() 直到组件在浏览器中挂载。
<script setup>
import { defineClientComponent } from 'valaxy'
const MyBrowserLib = defineClientComponent(
() => import('some-browser-only-lib')
)
</script>
<template>
<MyBrowserLib />
</template>你可以传递 props 和回调函数:
<script setup>
import { defineClientComponent } from 'valaxy'
const EchartsChart = defineClientComponent(
() => import('vue-echarts'),
[
{ option: chartOption, autoresize: true }, // props
{ default: () => h('div', '加载中...') }, // children/slots
],
(mod) => {
// 模块加载后调用
console.log('vue-echarts 已加载', mod)
},
)
</script>
<template>
<EchartsChart />
</template>onMounted + ref 模式
对于只需要在逻辑中(而不是第三方导入中)使用浏览器 API 的简单情况,使用 Vue 的 onMounted:
<script setup>
import { onMounted, ref } from 'vue'
const screenWidth = ref(0)
onMounted(() => {
screenWidth.value = window.innerWidth
})
</script>
<template>
<p>屏幕宽度:{{ screenWidth }}</p>
</template>import.meta.env.SSR
使用 import.meta.env.SSR 标志(由 Vite 提供)来有条件地执行代码:
if (!import.meta.env.SSR) {
// 这段代码只在浏览器中运行
document.addEventListener('scroll', handleScroll)
}这在 composables 或 setup 函数中保护仅浏览器的副作用时很有用。
基于 CSS 的响应式渲染
避免在响应式布局中使用 v-if 配合响应式视口值——这会导致水合不匹配,因为服务器无法知道视口大小。改用 CSS:
<!-- 不好:会导致水合不匹配 -->
<template>
<MobileNav v-if="isMobile" />
<DesktopNav v-else />
</template>
<!-- 好:使用 CSS 媒体查询 -->
<template>
<MobileNav class="mobile-only" />
<DesktopNav class="desktop-only" />
</template>
<style>
.mobile-only {
display: block;
}
.desktop-only {
display: none;
}
@media (min-width: 768px) {
.mobile-only {
display: none;
}
.desktop-only {
display: block;
}
}
</style>主题和插件开发者提示
- 始终使用
pnpm demo:build(SSG 构建)进行测试——pnpm demo(开发模式)不会捕获 SSR 问题。 - 使用
<ClientOnly>或defineClientComponent包裹所有仅浏览器的第三方组件。 - 永远不要在
<script setup>块的顶层访问window、document或navigator——将其移到onMounted中。 - 如果库提供服务端安全的构建版本(例如
import lib from 'lib/dist/ssr'),优先使用它而不是用<ClientOnly>包裹。 - 在 composables 中使用
import.meta.env.SSR来处理条件副作用。
Teleport 弹层
Valaxy 在应用根节点外提供独立的 #valaxy-teleports 容器,开发模式与 SSG 均可使用。主题和插件的弹窗、菜单应指定此目标:
<Teleport to="#valaxy-teleports">
<div v-if="open" role="dialog">弹窗内容</div>
</Teleport>Reka UI 的 DropdownMenuPortal、TooltipPortal 等组件同样传入 to="#valaxy-teleports"。容器由框架 HTML 模板提供,不要在 Vue 应用内部重复创建。SSG 会将服务端生成的弹层内容和水合占位节点注入容器内部。
旧插件的 to="body" 保留兼容处理,但新代码应使用独立容器,避免与应用 DOM 混合;参见 Vue SSR Teleports。只在客户端挂载后创建 DOM 的第三方弹窗不参与 SSR Teleport 水合。
生产回归检查:先运行 pnpm docs:build,再运行 pnpm e2e:ssg。测试使用生产预览,覆盖桌面和手机上的连续路由切换与弹层开关。