服务端渲染(SSR)

在 SSR 场景使用 Uniboot UI 时,需要做额外处理以避免水合(hydrate)错误。

TIP

Nuxt 用户可使用我们提供的 Nuxt 模块,其中已包含相关处理,安装即可。

注入 ID

该值用于在 Uniboot UI 内生成唯一 ID。SSR 下若服务端与客户端 ID 不一致,容易引发水合错误,因此需要向 Vue 注入 ID_INJECTION_KEY

main.ts
ts
// 无关代码已省略
import { createApp } from 'vue'
import { ID_INJECTION_KEY } from 'uniboot-ui'
import App from './App.vue'

const app = createApp(App)
app.provide(ID_INJECTION_KEY, {
  prefix: 1024,
  current: 0,
})

注入 ZIndex

SSR 开发中可能因 z-index 导致水合错误,建议注入初始值以避免。

main.ts
ts
// 无关代码已省略
import { createApp } from 'vue'
import { ZINDEX_INJECTION_KEY } from 'uniboot-ui'
import App from './App.vue'

const app = createApp(App)
app.provide(ZINDEX_INJECTION_KEY, { current: 0 })

Teleports

Uniboot UI 中多个组件(如 UDialog、UDrawer、UTooltip、UDropdown、USelect、UDatePicker 等)内部使用了 Teleport,SSR 时需要特别处理。

仅在客户端挂载后再渲染 Teleport

较简单的做法是在挂载后再渲染 Teleport。

例如在 Nuxt 中使用 ClientOnly

html
<client-only>
  <u-tooltip content="the tooltip content">
    <u-button>tooltip</u-button>
  </u-tooltip>
</client-only>

或:

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

const isClient = ref(false)

onMounted(() => {
  isClient.value = true
})
</script>

<template>
  <u-tooltip v-if="isClient" content="the tooltip content">
    <u-button>tooltip</u-button>
  </u-tooltip>
</template>

注入 teleport 标记

另一种做法是将 teleport 生成的标记注入到最终 HTML 的正确位置。

需要将其放在靠近 <body> 标签处。

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Uniboot UI</title>
    <!--preload-links-->
  </head>
  <body>
    <!--app-teleports-->
    <div id="app"><!--app-html--></div>
    <script type="module" src="/src/entry-client.js"></script>
  </body>
</html>

TIP

若修改了命名空间append-to 属性,需要相应调整 #el-popper-container- 相关占位。

src/entry-server.js
js
// 无关代码已省略
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'

export async function render(url, manifest) {
  // ...
  const ctx = {}
  const html = await renderToString(app, ctx)
  const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
  const teleports = renderTeleports(ctx.teleports)

  return [html, preloadLinks, teleports]
}

function renderTeleports(teleports) {
  if (!teleports) return ''
  return Object.entries(teleports).reduce((all, [key, value]) => {
    if (key.startsWith('#el-popper-container-')) {
      return `${all}<div id="${key.slice(1)}">${value}</div>`
    }
    return all
  }, teleports.body || '')
}
server.js or prerender.js
js
// 无关代码已省略
const [appHtml, preloadLinks, teleports] = await render(url, manifest)

const html = template
  .replace('<!--preload-links-->', preloadLinks)
  .replace('<!--app-html-->', appHtml)
  .replace(/(\n|\r\n)\s*<!--app-teleports-->/, teleports)