服务器组件

仅在服务器上渲染单个组件,使其 JavaScript 不包含在客户端资源包中。

Nuxt 默认会在服务器上渲染你的应用,但随后会将每个组件的 JavaScript 发送到浏览器,并对整个页面进行水合。对于那些在客户端永远不会发生变化的内容密集型组件(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 后缀,即可将其设为独立的服务器组件:

目录结构
-| app/
---| components/
-----| HighlightedMarkdown.server.vue

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

app/pages/example.vue
<template>
  <div>
    <!--
      在服务器上渲染;markdown 解析和高亮
      库不会包含在客户端构建包中
     -->
    <HighlightedMarkdown markdown="# Headline" />
  </div>
</template>

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

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

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

岛屿如何渲染

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

  • 在服务端创建一个全新的、隔离的 Vue 应用,仅用于渲染该组件
  • 创建一个“岛屿上下文”,你可以在岛屿内部通过 nuxtApp.ssrContext.islandContext 访问它
  • 再次运行你的插件,除非插件设置了 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 查询参数或请求体),请将其视为不可信输入。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 的组件会作为岛屿的一部分在服务器端渲染,然后由主客户端应用进行水合。只有该组件的代码块会发送到客户端;岛屿的其余部分仍保持静态。

设置 selectiveClient: 'deep' 后,还可以向 nuxt-client 组件传递插槽。这些插槽会在服务器端渲染,并且在客户端不具备交互性

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

插槽

如果在岛组件中声明了插槽,就可以将插槽传递给岛组件。插槽内容由父组件提供,因此它属于主客户端应用,并且具有交互性(它被包裹在一个设置了 display: contents;<div> 中)。<NuxtIsland> 保留了 #fallback 插槽,用于指定岛加载前(设置了 lazy 时)或获取岛失败时渲染的内容。

客户端导航往返

在初始的服务器渲染页面加载时,各个岛屿会以内联方式渲染,不会产生额外请求。然而,在客户端导航时,目标页面上的每个岛屿都必须从服务器获取(你可以在网络面板中看到这些请求)。这会带来实际开销:
  • 在导航期间,岛屿会阻塞网络往返,除非你传递 lazy prop(并提供 #fallback 插槽)以非阻塞方式渲染它们
  • 每个页面包含许多岛屿的应用会在每次导航时发起许多请求
岛屿最适合用于通过完整页面加载访问的页面(内容页和营销页),或者每页岛屿数量较少的场景。如果某个组件需要在客户端频繁更新,那么岛屿可能不是合适的工具。

预渲染与缓存

Island 与静态渲染和缓存配合良好:
  • 在预渲染期间(nuxt generateprerender 路由规则),Island 响应会被缓存,因此相同的 Island(名称、props 和上下文相同)只会渲染一次,之后重复使用
  • 由于 props 会作为 GET 查询参数传递,Island 响应也可以由你的服务器或 CDN 在 Island 端点级别进行缓存
  • 两个具有相同 props 的同一 Island 实例会共享一次服务器渲染结果和一个负载条目
请注意,Island 响应仅根据名称、props 和上下文作为键进行缓存,这正是它们能够独立于所在页面进行缓存的原因;这也是它们无法访问当前路由的原因(见上文)。如果你正在构建一个以静态内容为主的网站,Island 与 prerendernoScripts 路由规则结合使用效果很好。
请参阅以静态内容为主的网站方案,了解如何结合使用预渲染、noScripts、Island 和延迟 hydration。
需要注意的一点是:Island 插槽和 nuxt-client 组件依赖一段小型内联脚本,在 hydration 之前将传送的内容重新定位到适当位置。在使用 noScripts 渲染的路由上,该脚本会被省略,因此完全交互式的 nuxt-client 组件不会在那里进行 hydration。普通的静态 Island 不受影响。

当前限制

服务器组件目前处于实验阶段,一些待完善的问题已在公开 issue 中进行跟踪:
  • 大多数仅限服务器组件和岛屿组件的功能(例如插槽和 nuxt-client 组件)目前仅适用于单文件组件。
  • 应用的全局样式会随每个岛屿响应一起发送,并且在某些配置中,岛屿资源可能会被加载两次(#29591)。
  • 使用岛屿可能会显著增加构建时生成的分块数量(#34855)。
  • 服务器组件插槽中的 :slotted() 样式会被忽略(#31510)。
  • 模板引用无法从父组件引用服务器组件内部的元素(#31512)。
  • inject/provide 不会跨越岛屿边界,因此无法将在页面中注入的内容传递到独立的服务器组件中(#22751)。
  • 通过自动生成的包装器渲染的服务器组件不会暴露加载和错误事件;如果需要使用其 error 事件和 refresh() 方法,请直接使用 <NuxtIsland>#25744)。
  • useId 在岛屿内部存在已知限制;请参阅 <NuxtIsland> 文档
  • 每个嵌套岛屿都会增加额外开销,因此在其他岛屿中嵌套岛屿时请务必谨慎。
在组件目录文档中详细了解服务器组件的文件约定。