---
title: "服务器组件"
description: "仅在服务器上渲染单个组件，使其 JavaScript 不包含在客户端资源包中。"
canonical_url: "https://nuxt.zhcndoc.com/docs/4.x/guide/concepts/server-components"
---
# 服务器组件

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

Nuxt 默认会在服务器上渲染你的应用，但随后会将每个组件的 JavaScript 发送到浏览器，并对整个页面进行水合。对于那些在客户端永远不会发生变化的内容密集型组件（Markdown 渲染、语法高亮、CMS 输出），这是一种浪费：用户下载、解析并执行代码，而这些代码唯一的作用只是重新生成页面上已经存在的 HTML。

服务器组件（也称为岛屿组件）则反其道而行之。服务器组件在服务器上渲染，其 HTML 会嵌入页面中，并且不会向客户端发送任何 JavaScript。它的依赖项（Markdown 解析器、高亮库）也会留在服务器上。

::tip{icon="i-lucide-newspaper" to="https://roe.dev/blog/nuxt-server-components" target="_blank"}
阅读 Daniel Roe 编写的 Nuxt 服务器组件指南。
::

::video-accordion{title="观看 Learn Vue 关于 Nuxt 服务器组件的视频" videoId="u1yyXe86xJM"}
::

## 启用服务器组件

组件岛屿由 [`experimental.componentIslands`](https://nuxt.zhcndoc.com/docs/4.x/guide/going-further/experimental-features#componentislands) 控制。默认值为 `'auto'`，当你的应用包含服务器组件或岛屿时，它会自动启用此功能，因此大多数情况下无需进行任何配置。

如果希望使用远程岛屿或选择性客户端水合，请显式设置该选项：

```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
  experimental: {
    componentIslands: {
      selectiveClient: true, // 或使用 'deep'，以启用 `nuxt-client`
      remoteIsland: false, // 允许从远程来源渲染岛屿
    },
  },
})
```

::read-more{icon="i-simple-icons-github" to="https://github.com/nuxt/nuxt/issues/19772" target="_blank"}
服务器组件仍处于实验阶段。你可以在 GitHub 上查看路线图。
::

## `.server.vue` 组件

为组件添加 `.server` 后缀，即可将其设为独立的服务器组件：

```bash [目录结构]
-| app/
---| components/
-----| HighlightedMarkdown.server.vue
```

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

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

`~/components/islands/` 内的组件也会注册为岛屿，可以直接使用 [`<NuxtIsland>`](https://nuxt.zhcndoc.com/docs/4.x/api/components/nuxt-island) 渲染，例如对 `~/components/islands/MyIsland.vue` 使用 `<NuxtIsland name="MyIsland" />`。

你也可以将 `.server.vue` 组件与同名的 `.client.vue` 组件配对，以实现[独立的服务器端和客户端实现](https://nuxt.zhcndoc.com/docs/4.x/directory-structure/app/components#paired-with-a-client-component)。在这种情况下，该组件不是岛屿：客户端部分会正常进行水合。

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

## 岛屿如何渲染

服务器组件在底层使用 [`<NuxtIsland>`](https://nuxt.zhcndoc.com/docs/4.x/api/components/nuxt-island)。渲染岛屿时会向专用的岛屿端点发送请求，该端点会：

- 在服务端创建一个**全新的、隔离的 Vue 应用**，仅用于渲染该组件
- 创建一个“岛屿上下文”，你可以在岛屿内部通过 `nuxtApp.ssrContext.islandContext` 访问它
- 再次运行你的插件，除非插件设置了 `env: { islands: false }`（对象语法插件）

由于岛屿与应用的其余部分相互隔离：

- 你无法在页面和岛屿之间共享状态（provide/inject、Pinia、`useState`）；请改用 props 传递数据
- 岛屿内部的 [`useRoute()`](https://nuxt.zhcndoc.com/docs/4.x/api/composables/use-route) 反映的是岛屿自身的请求，而不是用户所在的页面。如果岛屿需要路由信息，请通过 props 或 `<NuxtIsland>` 上的 `context` prop 显式传入（在岛屿内部通过 `nuxtApp.ssrContext.islandContext` 读取）
- 渲染岛屿时不会运行路由中间件

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

- props 必须可被 JSON 序列化
- props 受 URL 长度限制，因此应避免传递大量数据
- props 可能会出现在服务器访问日志、CDN 缓存和 `Referer` 请求头中

::note
由于 props 来自请求（URL 查询参数或请求体），请将其视为不可信输入。Nuxt 会拒绝最可能被意外泄露的 props：岛屿未声明的顶层 `as`（未声明的 prop 会作为属性透传到岛屿的根元素），以及在启用 `vue.runtimeCompiler` 时，props 中任意位置的 `template`。除此之外，避免将未经验证的 props 传入动态组件解析（`<component :is>`{lang="vue"}、`h()`、`resolveDynamicComponent()`，或多态的 `as` / `asChild` prop），因为字符串可能会解析为任何已注册的组件或 HTML 元素。

将 props 视为不可信输入也包括其大小：在使用数值或数组类型的 prop 驱动 `v-for` 或其他随其值增长的工作前，应先进行校验或限制。Nuxt 会将岛屿渲染中的数值型 `v-for` 源限制为最多迭代 10,000 次，但这只是安全网，并不能代替校验。

组件未声明的 props 会作为属性透传到其单一根元素上，因此根元素是多态组件的岛屿（例如来自 `reka-ui` / `@nuxt/ui`）可能会收到你未绑定的属性。请在这类岛屿上设置 `defineOptions({ inheritAttrs: false })`{lang="ts"}，或声明你接受的 props。与任何 Vue 组件一样，透传属性也包括 `onclick` 或 `onfocus` 等事件处理器属性，浏览器会将其作为 JavaScript 执行，因此不要将你无法控制的对象（例如 `route.query`）展开到服务器组件上，或传入 `<NuxtIsland>`{lang="vue"} 的 `props` 中。

若要根据调用方输入切换组件，请通过导入组件的允许列表映射判别值，而不是直接传递原始 prop：

```vue
<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。

::read-more{to="https://nuxt.zhcndoc.com/docs/4.x/api/components/nuxt-island"}
阅读完整的 `<NuxtIsland>` API 文档，了解 props、插槽、事件和已知限制。
::

## 使用 `nuxt-client` 进行选择性水合

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

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

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

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

::warning
仅在本地 `.vue` SFC 上使用 `nuxt-client`。像 [`<NuxtLink>`](https://nuxt.zhcndoc.com/docs/4.x/api/components/nuxt-link) 这样的内置组件会跳过岛屿转换。在客户端导航后，你可能会看到 `Failed to locate Teleport target`，或者链接消失且没有错误。请将内置组件包装在你自己的 `.vue` 文件中，并将 `nuxt-client` 添加到该包装组件上。请参阅 [#29251](https://github.com/nuxt/nuxt/issues/29251) 和 [#26002](https://github.com/nuxt/nuxt/issues/26002)。
::

## 插槽

如果在岛组件中声明了插槽，就可以将插槽传递给岛组件。插槽内容由父组件提供，因此它属于主客户端应用，并且**具有交互性**（它被包裹在一个设置了 `display: contents;` 的 `<div>` 中）。

`<NuxtIsland>` 保留了 `#fallback` 插槽，用于指定岛加载前（设置了 `lazy` 时）或获取岛失败时渲染的内容。

## 客户端导航往返

在初始的服务器渲染页面加载时，各个岛屿会以内联方式渲染，不会产生额外请求。然而，在**客户端导航**时，目标页面上的每个岛屿都必须从服务器获取（你可以在网络面板中看到这些请求）。这会带来实际开销：

- 在导航期间，岛屿会阻塞网络往返，除非你传递 `lazy` prop（并提供 `#fallback` 插槽）以非阻塞方式渲染它们
- 每个页面包含许多岛屿的应用会在每次导航时发起许多请求

岛屿最适合用于通过完整页面加载访问的页面（内容页和营销页），或者每页岛屿数量较少的场景。如果某个组件需要在客户端频繁更新，那么岛屿可能不是合适的工具。

当指向[仅限服务器的页面](https://nuxt.zhcndoc.com/docs/4.x/directory-structure/app/pages#server-only-pages)的 `<NuxtLink>` 被预取时，其岛屿响应也会一并预取，因此往返请求会在用户导航之前发生，而不是在导航期间发生。预取的响应会根据岛屿名称、props 和上下文进行缓存，这与岛屿挂载时使用的键相同。预取失败并非致命错误：岛屿会在挂载时再次获取。

::note
这适用于渲染整个 `.server.vue` 页面 的岛屿。页面内使用的岛屿仍会在挂载时获取。
::

## 预渲染和缓存

Island 与静态渲染和缓存配合良好：

- 在预渲染期间（`nuxt generate` 或 `prerender` 路由规则），Island 响应会被缓存，因此相同的 Island（名称、props 和上下文相同）只会渲染一次，之后重复使用
- 由于 props 会作为 GET 查询参数传递，Island 响应也可以由你的服务器或 CDN 在 Island 端点级别进行缓存
- 两个具有相同 props 的同一 Island 实例会共享一次服务器渲染结果和一个负载条目

请注意，Island 响应仅根据名称、props 和上下文作为键进行缓存，这正是它们能够独立于所在页面进行缓存的原因；这也是它们无法访问当前路由的原因（见上文）。

如果你正在构建一个以静态内容为主的网站，Island 与 `prerender` 和 `noScripts` 路由规则结合使用效果很好。

::read-more{to="https://nuxt.zhcndoc.com/docs/4.x/guide/recipes/mostly-static-sites"}
请参阅以静态内容为主的网站配方，了解如何结合使用预渲染、`noScripts`、岛屿和延迟水合。
::

有一点需要注意：岛屿插槽和 `nuxt-client` 组件依赖一个小型内联脚本，在水合之前将传送的内容移动到正确位置。在通过 [`noScripts`](https://nuxt.zhcndoc.com/docs/4.x/guide/going-further/features#noscripts) 渲染的路由上，该脚本会被省略，因此完全具备交互性的 `nuxt-client` 组件不会在此处进行水合。普通静态岛屿不受影响。

## 当前限制

服务器组件目前处于实验阶段，一些待完善的问题已在公开 issue 中进行跟踪：

- 服务器专用组件和岛屿组件的大多数功能（例如插槽和 `nuxt-client` 组件）仅适用于单文件组件。
- 使用岛屿可能会大幅增加构建时生成的代码块数量（[#34855](https://github.com/nuxt/nuxt/issues/34855)）。
- 使用 webpack 和 Rspack 时，服务器组件插槽中的作用域 `:slotted()` 样式可能会失效，因为服务器端和客户端构建可能生成不同的作用域 ID（[#31510](https://github.com/nuxt/nuxt/issues/31510)）。
- 模板 ref 无法从父组件引用服务器组件内部的元素（[#31512](https://github.com/nuxt/nuxt/issues/31512)）。
- `inject`/`provide` 不会跨越岛屿边界，因此无法从页面向独立服务器组件注入内容（[#22751](https://github.com/nuxt/nuxt/issues/22751)）。
- 通过自动生成的包装组件渲染的服务器组件不会暴露加载和错误事件；如果需要其 `error` 事件和 `refresh()` 方法，请直接使用 `<NuxtIsland>`（[#25744](https://github.com/nuxt/nuxt/issues/25744)）。
- [`useId`](https://vuejs.org/api/composition-api-helpers#useid) 在岛屿内存在已知限制；请参阅 [`<NuxtIsland>` 文档](https://nuxt.zhcndoc.com/docs/4.x/api/components/nuxt-island#known-limitations)。
- 每个嵌套岛屿都会增加额外开销，因此请谨慎在岛屿内嵌套其他岛屿。

::read-more{to="https://nuxt.zhcndoc.com/docs/4.x/directory-structure/app/components#server-components"}
请在组件目录文档中了解更多服务器组件文件约定。
::


## Sitemap

See the full [sitemap](https://nuxt.zhcndoc.com/sitemap.md) for all pages.
