服务器组件
Nuxt 默认会在服务器上渲染你的应用,但随后会把每个组件的 JavaScript 发送到浏览器,并对整个页面进行 hydration。对于那些内容密集型组件(Markdown 渲染、语法高亮、CMS 输出)来说,如果它们在客户端永远不会变化,这就是在浪费工作:用户下载、解析并执行的代码,其唯一作用只是重现页面上已经存在的 HTML。
服务器组件(也称为岛屿组件)则相反。服务器组件会在服务器上渲染,其 HTML 会嵌入到页面中,并且不会把任何 JavaScript 发送到客户端。它的依赖项(例如 Markdown 解析器、高亮库)也同样只保留在服务器上。
启用服务端组件
组件岛由 experimental.componentIslands 控制。默认值为 'auto',这会在你的应用包含服务端组件或岛时自动启用该功能,因此在大多数情况下你不需要任何配置。
如果你想使用远程岛或选择性客户端水合,请显式设置该选项:
export default defineNuxtConfig({
experimental: {
componentIslands: {
selectiveClient: true, // 或 `'deep'`,以启用 `nuxt-client`
remoteIsland: false, // 允许从远程来源渲染岛
},
},
})
.server.vue 组件
为组件添加 .server 后缀,使其成为一个独立的服务端组件:
-| app/
---| components/
-----| HighlightedMarkdown.server.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。
岛屿是如何渲染的
服务端组件在底层使用 <NuxtIsland>。渲染一个岛屿会向专门的岛屿端点发起请求,该端点会:
- 在服务器上创建一个新的、隔离的 Vue 应用,仅用于渲染该组件
- 创建一个可在岛屿内通过
nuxtApp.ssrContext.islandContext访问的 “island context” - 重新运行你的插件,除非它们设置了
env: { islands: false }(对象语法插件)
由于岛屿与应用的其余部分是隔离的:
- 你不能在页面和岛屿之间共享状态(provide/inject、Pinia、
useState);请改为通过 props 传递数据 - 岛屿内部的
useRoute()反映的是岛屿自身的请求,而不是用户当前所在的页面。如果岛屿需要路由信息,请显式传入,可以通过 props,或者通过<NuxtIsland>上的contextprop 传入(在岛屿内从nuxtApp.ssrContext.islandContext读取) - 渲染岛屿时不会运行路由中间件
Props 会被序列化并作为 GET 查询参数 发送。这使得岛屿响应可以被缓存,但这也意味着:
- props 必须可 JSON 序列化
- props 受 URL 长度限制,因此避免传递大量数据
- props 可能会出现在服务器访问日志、CDN 缓存和
Referer请求头中
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。
使用 nuxt-client 的选择性水合
岛屿默认是静态的,但你可以通过添加 nuxt-client 属性来对其中的单个组件进行水合。这需要启用 experimental.componentIslands.selectiveClient。
<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 generate或prerender路由规则),island 响应会被缓存,因此相同的 island(相同的名称、props 和 context)只会渲染一次并复用 - 由于 props 通过 GET 查询参数传递,island 响应也可以在 island 端点层面被你的服务器或 CDN 缓存
- 同一个 island 的两个实例如果具有相同的 props,会共享一次服务端渲染和一条 payload 条目
请注意,island 响应仅按名称、props 和 context 进行键控,这正是它们能够独立于所在页面被缓存的原因;这也是为什么它们无法看到当前路由(见上文)。
如果你正在构建一个大部分为静态的网站,island 可以很好地与 prerender 和 noScripts 路由规则结合使用。
需要注意的一点交互: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>文档。- 每个嵌套孤岛都会增加额外开销,因此在孤岛内部嵌套其他孤岛时要格外小心。