Vue3 组件库 SSR 适配指南:除了 onMounted,你还得绕过这些坑

49 阅读3分钟

在 2026 年的 Vue 生态中,服务端渲染(SSR)已成为中大型项目的标配。作为组件库开发者,如果你的库在 Nuxt 或 VitePress 中一引入就报 window/document is not defined,那说明你的组件库对 BOM 和 DOM 的隔离做得不够彻底。

很多人认为只要把操作扔进 onMounted 就万事大吉了。但真相远比这复杂。

  1. 为什么不能只盯着 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;

请谨慎使用此类代码。

  1. 生命周期里的“禁区”: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);
    });
  }
}

请谨慎使用此类代码。

  1. 环境判断:硬编码宏 vs 运行时检查

这是开发者最容易混淆的地方,取决于你是在写“项目”还是写“组件库”。

A. 组件库:运行时动态检查

由于组件库作为 NPM 包分发,无法预知最终的构建引擎,必须依赖 typeof window 这种运行时检查。这保证了代码在任何环境下都能通过逻辑判断避开非法引用。

B. 项目开发:构建时硬编码宏

在 Nuxt/Vite 项目中,官方推荐使用 import.meta.client
它的本质是“编译宏”:

  • 在生成 Server Bundle 时,它被硬编码为 false
  • 在生成 Client Bundle 时,它被写死为 true

这种硬编码配合 Tree-shaking,能物理删除不属于该端的代码块。例如,客户端专用的复杂逻辑在服务端产物中会被彻底“剪掉”,既保护了代码安全,又优化了服务端性能。

  1. 编写“功能阉割版”逻辑的艺术

为了让组件库优雅运行,我们需要编写“环境识别型”的代码。
策略:服务端提供“静态占位”,客户端挂载后“二次激活”。

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 };
  }
}

请谨慎使用此类代码。

  1. 总结:SSR 适配的三层境界

  2. 第一层(防崩) :识别出所有对 windowdocumentnavigatorlocation 等 BOM/DOM 对象的直接引用,并将其包裹在环境判断或 onMounted 中。

  3. 第二层(防污染) :意识到服务器是长期运行的进程,严禁在模块顶层定义响应式全局单例,防止不同请求间的数据交叉泄露。

  4. 第三层(同构) :确保服务端渲染出的“死代码(HTML)”与客户端初次运行生成的虚拟 DOM 结构完全一致,避免 Hydration Mismatch。

下一篇,我们将聊聊最核心的问题:既然服务端已经渲染好了,为什么客户端还要从 setup 重新跑一遍?  揭秘“水合(Hydration)”背后的代价。