服务器组件

仅在服务器上渲染单个组件,使其 JavaScript 不进入客户端 bundle。

Nuxt 默认会在服务器上渲染你的应用,但随后会把每个组件的 JavaScript 发送到浏览器,并对整个页面进行 hydration。对于那些内容密集型组件(Markdown 渲染、语法高亮、CMS 输出)来说,如果它们在客户端永远不会变化,这就是在浪费工作:用户下载、解析并执行的代码,其唯一作用只是重现页面上已经存在的 HTML。

服务器组件(也称为岛屿组件)则相反。服务器组件会在服务器上渲染,其 HTML 会嵌入到页面中,并且不会把任何 JavaScript 发送到客户端。它的依赖项(例如 Markdown 解析器、高亮库)也同样只保留在服务器上。

阅读 Daniel Roe 关于 Nuxt 服务器组件的指南。

启用服务端组件

组件岛由 experimental.componentIslands 控制。默认值为 'auto',这会在你的应用包含服务端组件或岛时自动启用该功能,因此在大多数情况下你不需要任何配置。

如果你想使用远程岛或选择性客户端水合,请显式设置该选项:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    componentIslands: {
      selectiveClient: true, // 或 `'deep'`,以启用 `nuxt-client`
      remoteIsland: false, // 允许从远程来源渲染岛
    },
  },
})
服务端组件仍标记为实验性功能。你可以在 GitHub 上关注路线图。

.server.vue 组件

为组件添加 .server 后缀,使其成为一个独立的服务端组件:

Directory Structure
-| app/
---| components/
-----| HighlightedMarkdown.server.vue

像使用其他组件一样使用它:

app/pages/example.vue
<template>
  <div>
    <!--
      在服务端渲染;Markdown 解析和高亮
      库不会包含在你的客户端包中
     -->
    <HighlightedMarkdown markdown="# 标题" />
  </div>
</template>

~/components/islands/ 中的组件也会被注册为 islands,并且可以直接使用 <NuxtIsland> 渲染,例如 <NuxtIsland name="MyIsland" /> 对应 ~/components/islands/MyIsland.vue

你也可以将 .server.vue 组件与同名的 .client.vue 组件配对,用于分别实现服务端和客户端。在这种情况下,该组件不是 island:客户端部分会正常进行 hydration。

服务端组件(以及 islands)必须只有一个根元素。(HTML 注释也会被视为元素。)

岛屿是如何渲染的

服务端组件在底层使用 <NuxtIsland>。渲染一个岛屿会向专门的岛屿端点发起请求,该端点会:

  • 在服务器上创建一个新的、隔离的 Vue 应用,仅用于渲染该组件
  • 创建一个可在岛屿内通过 nuxtApp.ssrContext.islandContext 访问的 “island context”
  • 重新运行你的插件,除非它们设置了 env: { islands: false }(对象语法插件)

由于岛屿与应用的其余部分是隔离的:

  • 你不能在页面和岛屿之间共享状态(provide/inject、Pinia、useState);请改为通过 props 传递数据
  • 岛屿内部的 useRoute() 反映的是岛屿自身的请求,而不是用户当前所在的页面。如果岛屿需要路由信息,请显式传入,可以通过 props,或者通过 <NuxtIsland> 上的 context prop 传入(在岛屿内从 nuxtApp.ssrContext.islandContext 读取)
  • 渲染岛屿时不会运行路由中间件

Props 会被序列化并作为 GET 查询参数 发送。这使得岛屿响应可以被缓存,但这也意味着:

  • props 必须可 JSON 序列化
  • props 受 URL 长度限制,因此避免传递大量数据
  • props 可能会出现在服务器访问日志、CDN 缓存和 Referer 请求头中
由于 props 来自请求(URL 查询参数或 body),请将它们视为不可信输入。Nuxt 会拒绝最可能被意外泄露的 props:岛屿未声明的顶层 as(未声明的 prop 会作为属性透传到岛屿根元素上),以及在启用 vue.runtimeCompiler 时,props 中任意位置出现的 template。除此之外,请避免将尚未验证的 props 传入动态组件解析(<component :is>h()resolveDynamicComponent(),或多态的 as / asChild prop),因为一个字符串可以解析为任何已注册组件或 HTML 元素。组件未声明的 props 会作为属性透传到其单一根元素上,因此根元素是多态组件的岛屿(例如来自 reka-ui / @nuxt/ui)可能会接收到你未绑定的属性。请在这类岛屿上设置 defineOptions({ inheritAttrs: false }),或声明你接受的 props。要根据调用方输入切换组件,请通过导入组件的允许列表来映射一个区分值,而不是传递原始 prop:
<script setup lang="ts">
import type { Component } from 'vue'
import CardA from './CardA.vue'
import CardB from './CardB.vue'

const props = defineProps<{ variant: string }>()
const allowed: Record<string, Component> = { a: CardA, b: CardB }
const component = allowed[props.variant] ?? CardA
</script>

<template>
  <component :is="component" />
</template>

更改岛屿的 props 会触发一次网络请求,在服务器上重新渲染该组件,并就地更新其 HTML。

阅读完整的 <NuxtIsland> API 文档,包括 props、插槽、事件和已知限制。

使用 nuxt-client 的选择性水合

岛屿默认是静态的,但你可以通过添加 nuxt-client 属性来对其中的单个组件进行水合。这需要启用 experimental.componentIslands.selectiveClient

app/components/ServerWithClient.server.vue
<template>
  <div>
    <HighlightedMarkdown markdown="# Headline" />
    <!-- Counter 将在客户端加载并完成水合 -->
    <Counter
      nuxt-client
      :count="5"
    />
  </div>
</template>

标记了 nuxt-client 的组件会作为岛屿的一部分在服务端渲染,然后由主客户端应用进行水合。只有它的 chunk 会发送到客户端;岛屿的其余部分仍保持静态。

selectiveClient 设置为 'deep' 还允许向 nuxt-client 组件传递插槽。这些插槽会在服务端渲染,并且在客户端上是不可交互的

仅在本地 .vue SFC 上使用 nuxt-client。像 <NuxtLink> 这样的内置组件会跳过岛屿转换。客户端导航后,你可能会看到 Failed to locate Teleport target,或者链接消失但没有报错。请将内置组件包裹在你自己的 .vue 文件中,并在该包装组件上使用 nuxt-client。参见 #29251#26002

插槽

如果在 island 中声明了插槽,则可以将插槽传递给 island 组件。插槽内容由父级提供,因此它属于主客户端应用,并且可交互的(它被包裹在一个带有 display: contents;<div> 中)。

<NuxtIsland> 保留了 #fallback 插槽,用于指定在 island 加载前(当设置了 lazy 时)或在获取 island 失败时渲染的内容。

客户端导航往返

在初始的服务端渲染页面加载中,island 会内联渲染,因此不会产生额外请求。然而,在客户端导航中,目标页面上的每个 island 都必须从服务器获取(你可以在网络面板中看到这些请求)。这会带来实际成本:

  • 在导航期间,island 会阻塞一次网络往返,除非你传入 lazy 属性(并配合 #fallback 插槽)以非阻塞方式渲染它们
  • 对于每个页面包含许多 island 的应用,每次导航都会发出很多请求

island 最适合那些通过完整页面加载到达的页面(内容页和营销页),或者每页 island 数量较少的页面。如果某个组件需要在客户端频繁更新,island 可能就不是合适的工具。

预渲染与缓存

Island 与静态和缓存渲染配合得很好:

  • 在预渲染期间(nuxt generateprerender 路由规则),island 响应会被缓存,因此相同的 island(相同的名称、props 和 context)只会渲染一次并复用
  • 由于 props 通过 GET 查询参数传递,island 响应也可以在 island 端点层面被你的服务器或 CDN 缓存
  • 同一个 island 的两个实例如果具有相同的 props,会共享一次服务端渲染和一条 payload 条目

请注意,island 响应仅按名称、props 和 context 进行键控,这正是它们能够独立于所在页面被缓存的原因;这也是为什么它们无法看到当前路由(见上文)。

如果你正在构建一个大部分为静态的网站,island 可以很好地与 prerendernoScripts 路由规则结合使用。

有关将预渲染、noScripts、island 和延迟 hydration 结合使用的内容,请参见 mostly-static site 配方。

需要注意的一点交互:island 插槽和 nuxt-client 组件依赖一个小的内联脚本,在 hydration 之前将 teleport 的内容重新定位到正确位置。在使用 noScripts 渲染的路由上,这个脚本会被省略,因此完全交互式的 nuxt-client 组件将不会在这里进行 hydration。普通的静态 island 不受影响。

当前限制

服务器组件仍处于实验阶段,一些粗糙边界已在公开 issue 中跟踪:

  • 服务器专用组件和孤岛组件的大多数特性,例如插槽和 nuxt-client 组件,仅适用于单文件组件。
  • 你的应用的全局样式会随每个孤岛响应一起发送,在某些设置中孤岛资源可能会被加载两次(#29591)。
  • 使用孤岛会显著增加构建时生成的 chunk 数量(#34855)。
  • :slotted() 样式在服务器组件插槽中会被忽略(#31510)。
  • 模板 ref 无法从父组件引用服务器组件内部的元素(#31512)。
  • inject/provide 不会跨越孤岛边界,因此从页面向一个独立的服务器组件注入是行不通的(#22751)。
  • 通过自动生成包装器渲染的服务器组件不会暴露加载和错误事件;如果你需要其 error 事件和 refresh() 方法,请直接使用 <NuxtIsland>#25744)。
  • useId 在孤岛内部有已知限制;请参阅 <NuxtIsland> 文档
  • 每个嵌套孤岛都会增加额外开销,因此在孤岛内部嵌套其他孤岛时要格外小心。
在组件目录文档中阅读更多关于服务器组件文件约定的内容。