服务器组件
Nuxt 默认会在服务器上渲染你的应用,但随后会将每个组件的 JavaScript 发送到浏览器,并对整个页面进行水合。对于那些在客户端永远不会发生变化的内容密集型组件(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="# Headline" />
</div>
</template>
~/components/islands/ 中的组件也会注册为 island,并且可以直接使用 <NuxtIsland> 进行渲染,例如使用 <NuxtIsland name="MyIsland" /> 渲染 ~/components/islands/MyIsland.vue。
你还可以将 .server.vue 组件与同名的 .client.vue 组件配对,以实现独立的服务器端和客户端实现。在这种情况下,该组件不是 island:客户端部分会正常进行 hydration。
岛屿如何渲染
服务端组件在底层使用 <NuxtIsland>。渲染岛屿会向专用的岛屿端点发起请求,该请求会:
- 在服务端创建一个全新的、隔离的 Vue 应用,仅用于渲染该组件
- 创建一个“岛屿上下文”,你可以在岛屿内部通过
nuxtApp.ssrContext.islandContext访问它 - 再次运行你的插件,除非插件设置了
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 的组件会作为岛屿的一部分在服务器端渲染,然后由主客户端应用进行水合。只有该组件的代码块会发送到客户端;岛屿的其余部分仍保持静态。
设置 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 时)或获取岛失败时渲染的内容。客户端导航往返
在初始的服务器渲染页面加载时,各个岛屿会以内联方式渲染,不会产生额外请求。然而,在客户端导航时,目标页面上的每个岛屿都必须从服务器获取(你可以在网络面板中看到这些请求)。这会带来实际开销:- 在导航期间,岛屿会阻塞网络往返,除非你传递
lazyprop(并提供#fallback插槽)以非阻塞方式渲染它们 - 每个页面包含许多岛屿的应用会在每次导航时发起许多请求
预渲染与缓存
Island 与静态渲染和缓存配合良好:- 在预渲染期间(
nuxt generate或prerender路由规则),Island 响应会被缓存,因此相同的 Island(名称、props 和上下文相同)只会渲染一次,之后重复使用 - 由于 props 会作为 GET 查询参数传递,Island 响应也可以由你的服务器或 CDN 在 Island 端点级别进行缓存
- 两个具有相同 props 的同一 Island 实例会共享一次服务器渲染结果和一个负载条目
prerender 和 noScripts 路由规则结合使用效果很好。需要注意的一点是: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>文档。- 每个嵌套岛屿都会增加额外开销,因此在其他岛屿中嵌套岛屿时请务必谨慎。