渲染模式

了解 Nuxt 中可用的不同渲染模式。

Nuxt 支持不同的渲染模式,通用渲染客户端渲染,同时也提供了混合渲染以及在CDN 边缘服务器上渲染应用的可能性。

浏览器和服务器都可以解释 JavaScript 代码,将 Vue.js 组件转换为 HTML 元素。这个步骤称为“渲染”。Nuxt 同时支持 通用渲染客户端渲染。这两种方法各有优缺点,下面将介绍。

默认情况下,Nuxt 使用 通用渲染 来提供更好的用户体验、性能并优化搜索引擎索引,但你可以在 一行配置 中切换渲染模式。

通用渲染

这一步类似于传统由 PHP 或 Ruby 应用执行的 服务器端渲染。当浏览器请求某个 URL 并启用了通用渲染时,Nuxt 会在服务器环境中运行 JavaScript(Vue.js)代码,并返回一个完全渲染好的 HTML 页面到浏览器。如果页面是预先生成的,Nuxt 也可能从缓存返回一个完全渲染好的 HTML 页面。用户会立即获得应用初始内容的全部信息,这与客户端渲染不同。

当 HTML 文档被下载后,浏览器会解析它,Vue.js 会接管该文档。曾经在服务器上运行的相同 JavaScript 代码现在会在客户端(浏览器)中“再次”运行,从而通过将监听器绑定到 HTML 来启用交互(因此称为 通用渲染)。这称为 水合(Hydration)。当水合完成后,页面即可享受动态界面和页面切换等好处。

通用渲染允许 Nuxt 应用在保留客户端渲染优点的同时提供快速的页面加载时间。此外,由于内容已经存在于 HTML 文档中,爬虫可以无额外开销地对其进行索引。

什么是服务器渲染,什么是客户端渲染?

在通用渲染模式下,常会有人问 Vue 文件的哪些部分在服务器和/或客户端运行,这是很正常的。

app/app.vue
<script setup lang="ts">
const counter = ref(0) // 在服务器和客户端环境中执行

const handleClick = () => {
  counter.value++ // 仅在客户端环境中执行
}
</script>

<template>
  <div>
    <p>计数:{{ counter }}</p>
    <button @click="handleClick">
      增加
    </button>
  </div>
</template>

在初始请求时,由于 counter 被渲染在 <p> 标签内部,因此它会在服务器上初始化。handleClick 的内容在此处不会被执行。在浏览器进行水合时,counter ref 会被重新初始化。handleClick 最终会绑定到按钮上;因此可以合理地推断 handleClick 的主体将始终在浏览器环境中运行。

中间件页面 会在服务器上运行,并在水合期间在客户端运行。插件 可以在服务器或客户端或两者上运行。组件 也可以被强制仅在客户端运行。组合式函数(Composables)工具函数 则根据它们的使用上下文决定运行位置。

服务器端渲染的好处:

  • 性能:用户可以立即访问页面内容,因为浏览器显示静态内容要比显示由 JavaScript 生成的内容更快。与此同时,Nuxt 在水合过程中保留了 Web 应用的交互性。
  • 搜索引擎优化:通用渲染将页面的完整 HTML 内容像传统服务器应用一样交付给浏览器。网络爬虫可以直接索引页面内容,这使得通用渲染成为希望快速被索引内容的优秀选择。

服务器端渲染的缺点:

  • 开发限制:服务器和浏览器环境提供的 API 并不相同,编写可以无缝在两端运行的代码可能比较棘手。幸运的是,Nuxt 提供了指南和特定变量来帮助你确定某段代码在哪执行。
  • 成本:需要有服务器运行以便实时渲染页面。这会像任何传统服务器一样增加月度成本。然而,由于通用渲染使得浏览器在客户端导航时接手,服务器调用得到了大量减少。通过利用边缘端渲染 可以进一步降低成本。

通用渲染非常灵活,几乎适用于任何用例,尤其适合面向内容的网站:博客、营销网站、作品集、电子商务网站和市场平台。

关于编写不会导致水合不匹配(hydration mismatch)的 Vue 代码的更多示例,请参阅 Vue 官方文档
当导入依赖浏览器 API 且有副作用的库时,请确保导入它的组件仅在客户端被调用。打包器不会对包含副作用的模块的导入进行 tree-shake。

客户端渲染

传统的 Vue.js 应用默认是在浏览器(或“客户端”)中渲染的。然后,Vue.js 在浏览器下载并解析包含创建当前界面的指令的所有 JavaScript 代码后生成 HTML 元素。

客户端渲染的好处:

  • 开发速度:在完全客户端上开发时,我们不必担心代码的服务器兼容性,例如使用仅属于浏览器的 API(如 window 对象)。
  • 更便宜:运行服务器会增加基础设施成本,因为你需要在支持 JavaScript 的平台上运行。我们可以在任何静态服务器上托管仅客户端应用,提供 HTML、CSS 和 JavaScript 文件即可。
  • 离线:由于代码全部在浏览器中运行,当网络不可用时它仍能良好工作。

客户端渲染的缺点:

  • 性能:用户必须等待浏览器下载、解析并运行 JavaScript 文件。根据网络下载和用户设备的解析与执行速度,这可能需要一些时间并影响用户体验。
  • 搜索引擎优化:通过客户端渲染交付的内容的索引和更新比服务器渲染的 HTML 文档更耗时。这与我们讨论的性能缺点有关,因为搜索引擎爬虫不会在第一次尝试时等待界面完全渲染来索引页面。使用纯客户端渲染时,你的内容在搜索结果页面中显示和更新会更慢。

客户端渲染适合高度交互的 Web 应用,这些应用不需要被索引或用户访问频繁。它可以利用浏览器缓存在后续访问中跳过下载阶段,例如 SaaS、后台管理应用或在线游戏

你可以在 nuxt.config.ts 中启用仅客户端渲染:

nuxt.config.ts
export default defineNuxtConfig({
  ssr: false,
})
如果你确实使用了 ssr: false,你还应该在 ~/spa-loading-template.html 中放置一个 HTML 文件,用你希望用于渲染加载屏幕的 HTML 内容,该加载屏幕会在应用水合之前显示。
SPA 加载模板 中查看详情

部署静态客户端渲染应用

如果你使用 nuxt generatenuxt build --prerender 命令将应用部署到静态托管,那么默认情况下,Nuxt 会将每个页面渲染为单独的静态 HTML 文件。

如果你使用 nuxt generatenuxt build --prerender 对应用进行预渲染,那么输出文件夹中将不会包含任何服务器,因此你将无法使用任何服务器端点。如果你需要服务器功能,请改用 nuxt build

如果你完全使用客户端渲染,那么这可能是不必要的。你可能只需要一个 index.html 文件,以及 200.html404.html 回退文件,并可以让你的静态网页托管服务为所有请求返回这些文件。

为此,我们可以改变路由的预渲染方式。只需在 nuxt.config.tshooks 中添加如下内容:

nuxt.config.ts
export default defineNuxtConfig({
  hooks: {
    'prerender:routes' ({ routes }) {
      routes.clear() // 不生成任何路由(除了默认项)
    },
  },
})

这将生成三个文件:

  • index.html
  • 200.html
  • 404.html

200.html 和 404.html 是什么?

静态托管服务需要一个用于客户端路由和缺失路径的 HTML 壳层。Nuxt 为此会输出两个 SPA 回退文件:

  • 200.html:当路径未命中时,如果你希望客户端路由器来处理 URL,就提供这个文件。
  • 404.html:当托管服务需要保持 404 状态但仍然加载你的应用时,就提供这个文件。

nuxt generatenuxt build --prerender 会将它们写入 .output/public/。仅执行不带预渲染的 nuxt build 不会生成这些文件。如果使用混合路由规则,请通过路由规则添加这些回退文件,或者在需要它们时运行预渲染构建。将你的托管服务指向提供商所期望的那个文件。

服务器端渲染错误页面

默认情况下,404.html 是一个空壳,因此你的 error.vue(及其布局和数据)只有在客户端应用启动后才会显示。你可以改为预渲染它:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    prerenderErrorPages: true,
  },
})

Nuxt 随后会在构建时使用一个合成的 404 错误渲染 error.vue,并将结果写入 404.html。无论托管服务从哪个 URL 提供该文件,它都会在原位置完成水合。如果你的托管服务能够提供这些页面,请传入一个介于 400 和 599 之间的状态码数组([404, 500])来生成其他页面。

由于每个缺失路径都提供同一个文件,错误页面不能依赖请求:useRoute()useRequestURL() 以及使用当前路径获取的数据,会在预渲染 HTML 中保留构建时的值,并在水合时得到修正。将特定于请求的标记包裹在 <ClientOnly> 中,并使用 import.meta.prerender 跳过特定于请求的数据获取:

error.vue
<script setup lang="ts">
import type { NuxtError } from '#app'

defineProps<{ error: NuxtError }>()

const route = useRoute()

const { data: suggestions } = await useAsyncData(
  'error-suggestions',
  () => $fetch('/api/suggestions', { query: { path: route.path } }),
  // at build time the path is `/404.html`, so fetch on the client instead
  { server: !import.meta.prerender },
)
</script>

<template>
  <div>
    <h1>{{ error.statusCode }}</h1>
    <ClientOnly>
      <p>Nothing found at {{ route.path }}</p>
    </ClientOnly>
    <ul v-if="suggestions">
      <li
        v-for="suggestion in suggestions"
        :key="suggestion"
      >
        {{ suggestion }}
      </li>
    </ul>
  </div>
</template>

import.meta.prerender 只有在生成页面时才为 true,因此当应用以服务器模式部署时,同一个 error.vue 仍然会在服务器端渲染其数据。

跳过客户端回退文件生成

在对客户端渲染的应用进行预渲染时,Nuxt 默认会生成 index.html200.html404.html 文件。但是,如果你需要在构建中阻止生成这些文件中的任意一个(或全部),可以使用 Nitro'prerender:generate' 钩子。

nuxt.config.ts
// @errors: 2353 7006
export default defineNuxtConfig({
  ssr: false,
  nitro: {
    hooks: {
      'prerender:generate' (route) {
        const routesToSkip = ['/index.html', '/200.html', '/404.html']
        if (routesToSkip.includes(route.route)) {
          route.skip = true
        }
      },
    },
  },
})

混合渲染

混合渲染允许针对每个路由使用不同的缓存规则(使用 路由规则),并决定服务器在特定 URL 的新请求时应该如何响应。

以前 Nuxt 应用的每个路由/页面和服务器必须使用相同的渲染模式,即通用或客户端渲染。在许多情况下,有些页面可以在构建时生成,而其他页面应以客户端渲染。例如,考虑一个带有管理员部分的内容网站。每个内容页面应主要为静态并生成一次,而管理员部分需要登录并更像是一个动态应用。

Nuxt 包含路由规则和混合渲染支持。使用路由规则,你可以为一组 Nuxt 路由定义规则、更改渲染模式或基于路由分配缓存策略!

Nuxt 服务器将自动注册相应的中间件并使用 Nitro 缓存层 为路由封装缓存处理程序。

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 主页在构建时预渲染
    '/': { prerender: true },
    // 产品页面按需生成,后台重新验证,缓存直到 API 响应变化
    '/products': { swr: true },
    // 产品详情页面按需生成,后台重新验证,缓存 1 小时(3600 秒)
    '/products/**': { swr: 3600 },
    // 博客列表页面按需生成,后台重新验证,CDN 缓存 1 小时(3600 秒)
    '/blog': { isr: 3600 },
    // 博客文章页面按需生成一次,直到下次部署,CDN 缓存
    '/blog/**': { isr: true },
    // 管理仪表盘仅在客户端渲染
    '/admin/**': { ssr: false },
    // 为 API 路由添加 cors 头
    '/api/**': { cors: true },
    // 重定向旧链接
    '/old-page': { redirect: '/new-page' },
  },
})

路由规则

以下是你可以使用的不同属性:

  • redirect: string - 定义服务器端重定向。
  • ssr: boolean - 禁用应用部分区域的 HTML 服务器端渲染,使其仅在浏览器中渲染,并使用 ssr: false
  • cors: boolean - 使用 cors: true 自动添加 cors 头;你可以通过使用 headers 覆盖来定制输出
  • headers: object - 为网站的部分区域添加指定的头;例如你的资源
  • swr: number | boolean - 为服务器响应添加缓存头,并在服务器或反向代理上根据可配置的 TTL(生存时间)进行缓存。Nitro 的 node-server 预设能够缓存完整响应。当 TTL 过期后,将发送缓存的响应,同时在后台重新生成页面。如果使用 true,则会添加 stale-while-revalidate 头,但不设置 MaxAge。
  • isr: number | boolean - 行为与 swr 相同,但对于支持此功能的平台,我们能够将响应添加到 CDN 缓存中(目前支持 Netlify 或 Vercel)。如果使用 true,内容将保留在 CDN 中,直到下一次部署。
  • prerender: boolean - 在构建时预渲染路由,并将其作为静态资源包含在构建结果中
  • noScripts: boolean - 禁用网站部分区域的 Nuxt 脚本和 JS 资源提示。更多信息请参阅 noScripts
  • appMiddleware: string | string[] | Record<string, boolean> - 允许你定义在 Vue 应用部分中页面路径应运行或不应运行的中间件(即不包括 Nitro 路由)
使用 isrswr 的路由还会在 HTML 旁生成 _payload.json 文件。客户端导航会加载这些缓存的 payload,而不是重新获取数据。更多关于 payload 提取 的内容。

使用 ssr: false 时的服务器 Bundle 大小

ssr: false 覆盖的路由只会在浏览器中渲染,因此 Nuxt 会从服务器 Bundle 中排除其页面组件。只要覆盖每个可到达该页面的路径的规则都能在构建时解析,就会应用此规则,包括 /products/** 规则下的 pages/products/[id].vue 等动态路由。

当任何可到达该页面的路径仍可能在服务器上渲染时,页面会保留在服务器 Bundle 中:

  • 更具体的规则会在仅客户端渲染的规则下方某处重新启用 SSR('/admin/**': { ssr: false }'/admin/report': { ssr: true }
  • 页面具有别名,或具有使用绝对路径声明的子页面,且该别名或子页面位于仅客户端渲染区域之外
  • 页面是一个父级外壳,所渲染的子页面仍在服务器上渲染

这仅是一项构建时优化;它不会改变服务器发送到浏览器的内容。

在可能的情况下,路由规则会自动应用到部署平台的原生规则中,以获得最佳性能(目前支持 Netlify 和 Vercel)。

注意:使用 nuxt generate 时不支持混合渲染。

示例:

Nuxt Vercel ISR

在 Vercel 上部署并使用混合渲染的 Nuxt 应用示例。

边缘端渲染

边缘端渲染(Edge-Side Rendering,ESR)是 Nuxt 引入的一项强大功能,它允许通过内容分发网络(CDN)的边缘服务器更接近用户地渲染你的 Nuxt 应用。通过利用 ESR,你可以确保性能提升和延迟降低,从而提供更好的用户体验。

使用 ESR 时,渲染过程被推送到网络的“边缘”——CDN 的边缘服务器。注意,ESR 更像是一个部署目标,而不是一种实际的渲染模式。

当发出页面请求时,请求不会一路到达原始服务器,而是在最近的边缘服务器被拦截。该服务器为页面生成 HTML 并将其发送回用户。此过程将数据需要传输的物理距离最小化,从而“降低延迟并更快加载页面”。

边缘端渲染之所以成为可能,是因为有 Nitro,即驱动 Nuxt 的服务引擎。它为 Node.js、Deno、Cloudflare Workers 等提供跨平台支持。

您可以利用 ESR 的当前平台有:

请注意,混合渲染可以在使用带有路由规则的边缘端渲染时使用。