服务端渲染 (SSR)

当使用 Element Plus 在 SSR 场景下开发时,您需要在 SSR 期间进行特殊处理,以避免水合错误。

TIP

对于Nuxt 用户,我们提供的 Nuxt 模块 已经包含了对这些问题的处理方案。 您只需要安装它就可以了。

提供一个ID

提供的值用于生成 ElementPlus 中的唯一ID。 因为不同的 IDs 容易发生SSR中的水合率错误, 为了确保服务器端和客户端生成相同的ID, 我们需要将 ID_injection_key 注入到 Vue。

// src/main.js (irrelevant code omitted)
import { createApp } from 'vue'
import { ID_INJECTION_KEY } from 'element-plus'
import App from './App.vue'

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

Teleports

Teleport 被元素加元件中的多个组件内部使用 (例如) ElDialog, ElDrawer, ElTooltip, ElDropdown, ElSelect, ElDatePicker ...),所以在SSR期间需要特殊处理。

在挂载时渲染 Teleport

一个较容易的解决办法是有条件渲染挂载上的 Teleport 。

例如,在Nuxt中使用 ClientOnly 这个组件。

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

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

const isClient = ref(false)

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

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

注入 Teleport 标记

另一种方式是将传送标记注入到你的 HTML 页面末尾的正确位置。

WARNING

Teleport 可能会有一些 SSR问题,所以你应该特别注意以下几种情况。

  1. 基于 ElTooltip 的 teleported 的属性应该是一致的,建议使用默认值。
  2. ElDialog 和 ElDrawer 的 append-to-body 属性值应该是一致的,建议启用 append-to-body
  3. ElSubMenu组件有多层弹出窗口,建议启用 popper-append-to-body

您需要注入靠近 <body> 标签的传送标记。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Element Plus</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

如果您修改了 Namespaceappend-to 属性,您需要调整 #el-popper-container- 值。

// src/entry-server.js (irrelevant code omitted)
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 (irrelevant code omitted)
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)