在 2026 年的 Vue 生态中,服务端渲染(SSR)已成为中大型项目的标配。作为组件库开发者,如果你的库在 Nuxt 或 VitePress 中一引入就报 window/document is not defined,那说明你的组件库对 BOM 和 DOM 的隔离做得不够彻底。
很多人认为只要把操作扔进 onMounted 就万事大吉了。但真相远比这复杂。
- 为什么不能只盯着
window?
在 SSR 环境(Node.js 或 Edge Workers)中,我们不仅没有 window(BOM),更没有 document(DOM)。
一个有经验的开发者在做环境检查时,会意识到环境伪造或部分缺失的风险。例如,某些测试环境可能会模拟 window 但没有 document。因此,严谨的组件库底层判断通常是这样的:
typescript
// 严谨的客户端判断:BOM 与 DOM 必须共存
export const isClient = typeof window !== 'undefined' &&
typeof document !== 'undefined' &&
!!window.document.createElement;
请谨慎使用此类代码。
- 生命周期里的“禁区”:BOM/DOM 操作
Vue 的生命周期在 SSR 阶段只执行 setup() 和 onBeforeMount() 。
核心铁律: 在 onMounted 之前的任何地方,直接引用 BOM 或 DOM 对象都是自寻死路。
typescript
// ❌ 错误示例:模块顶层引用 BOM/DOM
const hasTouch = 'ontouchstart' in window;
const isRetina = window.devicePixelRatio > 1;
export default {
setup() {
// ❌ 错误示例:setup 顶层引用 DOM
const bodyWidth = document.body.clientWidth;
onMounted(() => {
// ✅ 只有这里才是绝对安全的 BOM/DOM 操作区
console.log(window.location.href);
});
}
}
请谨慎使用此类代码。
- 环境判断:硬编码宏 vs 运行时检查
这是开发者最容易混淆的地方,取决于你是在写“项目”还是写“组件库”。
A. 组件库:运行时动态检查
由于组件库作为 NPM 包分发,无法预知最终的构建引擎,必须依赖 typeof window 这种运行时检查。这保证了代码在任何环境下都能通过逻辑判断避开非法引用。
B. 项目开发:构建时硬编码宏
在 Nuxt/Vite 项目中,官方推荐使用 import.meta.client。
它的本质是“编译宏”:
- 在生成 Server Bundle 时,它被硬编码为
false; - 在生成 Client Bundle 时,它被写死为
true。
这种硬编码配合 Tree-shaking,能物理删除不属于该端的代码块。例如,客户端专用的复杂逻辑在服务端产物中会被彻底“剪掉”,既保护了代码安全,又优化了服务端性能。
- 编写“功能阉割版”逻辑的艺术
为了让组件库优雅运行,我们需要编写“环境识别型”的代码。
策略:服务端提供“静态占位”,客户端挂载后“二次激活”。
typescript
export default {
setup() {
// 1. 定义安全默认值(避免初次渲染时依赖 DOM 测量)
const containerHeight = ref(0);
// 2. 逻辑分流
if (isClient) {
// 这里可以访问 BOM 逻辑(如监听 scroll),但依然不能碰还没挂载的 DOM
console.log(window.navigator.userAgent);
}
onMounted(() => {
// 3. 此时 DOM 已就绪,进行真实的测量与副作用绑定
const rect = document.getElementById('comp-id')?.getBoundingClientRect();
containerHeight.value = rect?.height || 0;
});
return { containerHeight };
}
}
请谨慎使用此类代码。
-
总结:SSR 适配的三层境界
-
第一层(防崩) :识别出所有对
window、document、navigator、location等 BOM/DOM 对象的直接引用,并将其包裹在环境判断或onMounted中。 -
第二层(防污染) :意识到服务器是长期运行的进程,严禁在模块顶层定义响应式全局单例,防止不同请求间的数据交叉泄露。
-
第三层(同构) :确保服务端渲染出的“死代码(HTML)”与客户端初次运行生成的虚拟 DOM 结构完全一致,避免 Hydration Mismatch。
下一篇,我们将聊聊最核心的问题:既然服务端已经渲染好了,为什么客户端还要从 setup 重新跑一遍? 揭秘“水合(Hydration)”背后的代价。