Skip to content

SSR 兼容性

VitePress 在构建时使用服务端渲染(SSR),某些第三方库或组件可能存在兼容性问题。本文档介绍常见问题及解决方案。

什么是 SSR

SSR(Server-Side Rendering)是在服务器端生成 HTML 内容的技术。VitePress 使用 SSR 来:

  • 生成静态 HTML 文件
  • 提供更好的 SEO
  • 加快首屏加载速度

常见兼容性问题

1. 访问浏览器 API

在 SSR 阶段,windowdocumentnavigator 等浏览器 API 不存在。

错误示例:

vue
<script setup>
// 错误:SSR 阶段 window 不存在
const width = window.innerWidth
</script>

解决方案:

vue
<script setup>
import { ref, onMounted } from 'vue'

const width = ref(0)

onMounted(() => {
  // 正确:仅在客户端执行
  width.value = window.innerWidth
})
</script>

2. 使用 ClientOnly 组件

对于需要浏览器环境的组件,使用 ClientOnly 包裹:

vue
<ClientOnly>
  <BrowserDependentComponent />
</ClientOnly>

3. 条件导入

动态导入仅客户端使用的模块:

vue
<script setup>
import { onMounted, ref } from 'vue'

const Component = ref(null)

onMounted(async () => {
  // 仅在客户端导入
  const module = await import('some-client-library')
  Component.value = module.default
})
</script>

<template>
  <component :is="Component" v-if="Component" />
</template>

检测 SSR 环境

使用 import.meta

ts
// 检测是否在服务端
if (import.meta.env.SSR) {
  // 服务端代码
} else {
  // 客户端代码
}

使用 onMounted 钩子

vue
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 这里的代码只在客户端执行
  console.log('Client only')
})
</script>

常见库的处理

1. 图表库(如 ECharts)

vue
<script setup>
import { ref, onMounted } from 'vue'

const chartRef = ref(null)
let echarts = null

onMounted(async () => {
  // 动态导入
  echarts = await import('echarts')
  
  const chart = echarts.init(chartRef.value)
  chart.setOption({
    // 配置项
  })
})
</script>

<template>
  <div ref="chartRef" style="height: 400px"></div>
</template>

2. 代码高亮(如 Prism)

vue
<script setup>
import { onMounted, ref } from 'vue'

const codeRef = ref(null)

onMounted(async () => {
  const Prism = await import('prismjs')
  Prism.highlightElement(codeRef.value)
})
</script>

<template>
  <pre><code ref="codeRef">const hello = 'world'</code></pre>
</template>

3. 地图库(如 Leaflet)

vue
<script setup>
import { onMounted, ref } from 'vue'
import 'leaflet/dist/leaflet.css'

const mapRef = ref(null)

onMounted(async () => {
  const L = await import('leaflet')
  
  const map = L.map(mapRef.value).setView([51.505, -0.09], 13)
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map)
})
</script>

<template>
  <div ref="mapRef" style="height: 400px"></div>
</template>

vitepress 配置选项

ssr.external

排除不兼容 SSR 的依赖:

ts
// docs/.vitepress/config.mts
export default defineConfig({
  vite: {
    ssr: {
      external: ['some-library', 'another-library']
    }
  }
})

ssr.noExternal

强制打包某些依赖:

ts
export default defineConfig({
  vite: {
    ssr: {
      noExternal: ['element-plus']
    }
  }
})

调试 SSR 问题

1. 使用 vitepress dev --ssr

在开发模式下测试 SSR:

bash
npx vitepress dev docs --ssr

2. 查看构建错误

构建时会显示具体的 SSR 错误:

bash
npm run build

3. 添加错误边界

vue
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((e) => {
  error.value = e.message
  return false
})
</script>

<template>
  <div v-if="error" class="error">
    渲染错误: {{ error }}
  </div>
  <slot v-else />
</template>

最佳实践

1. 延迟加载

对于大型库,使用延迟加载:

vue
<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

<template>
  <Suspense>
    <template #default>
      <HeavyComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

2. 使用 provide/inject

避免在组件初始化时访问浏览器 API:

ts
// docs/.vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    // 在客户端提供全局状态
    if (!import.meta.env.SSR) {
      app.provide('browserInfo', {
        width: window.innerWidth,
        height: window.innerHeight
      })
    }
  }
}

3. 使用 CSS 替代 JS

尽可能使用 CSS 实现效果:

css
/* 使用 CSS 变量 */
:root {
  --vh: 1vh;
}

/* 移动端适配 */
.full-height {
  height: 100vh;
  height: calc(var(--vh, 1vh) * 100);
}

常见错误及解决

ReferenceError: window is not defined

原因:在 SSR 阶段访问了 window 对象

解决

vue
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 在这里使用 window
  console.log(window.innerWidth)
})
</script>

document is not defined

原因:在 SSR 阶段访问了 document 对象

解决

ts
if (!import.meta.env.SSR) {
  document.getElementById('app')
}

原因:在 SSR 阶段访问了 navigator 对象

解决

ts
const isMobile = !import.meta.env.SSR && navigator.userAgent.includes('Mobile')

相关资源

下一步

了解 MPA 模式 作为 SSR 的替代方案。

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献