Nuxt 学习笔记(二)

渲染模式配置

nuxt 支持配置 渲染模式:

  • 通用渲染 - 服务端渲染(SSR) 和 水合结合
  • 客户端渲染(CSR)- vue 日常开发
  • 静态托管 - 只有 index.html 页面
  • 混合渲染 - 根据页面需求配置渲染模式
  • 边缘测渲染(ESR)- 通过 CDN 节点进行渲染,需要云服务CDN 支持

通用渲染

  1. 请求页面时,在服务端通过 vue 渲染完整的静态 HTML, ref 响应式属性会完成一次初始化,完成再 HTML 标签中的静态展示渲染
  2. HTML 页面到达客户端后,会执行水合操作,使页面具备交互式
  • 重新初始化 ref 等响应式属性
  • 对页面事件监听执行绑定,使页面具备交互能力

事件是在客户端层执行的,而响应式绑定在服务端和客户端都会运行

ts 复制代码
<script setup lang="ts">
const counter = ref(0); // executes in server and client environments

const handleClick = () => {
  counter.value++; // executes only in a client environment
};
</script>

<template>
  <div>
    <p>Count: {{ counter }}</p>
    <button @click="handleClick">Increment</button>
  </div>
</template>

优点

  • seo
  • 首屏速度

存在问题

  • 客户端 API 无法在 服务端调用,可以用 import.meta.server 来区分
  • 服务端压力,但通过水合和 ESR 可以降低很多

客户端渲染

和 Vue 编写代码一致,即 SPA 应用

  1. 请求页面,客户端从服务端下载初始静态页和 bundleJS
  2. 根据 bundle JS 和 页面,在客户端完成渲染和交互式流程

开启方式:

ts 复制代码
export default defineNuxtConfig({ ssr: false, })

优点

  • 开发复杂度低
  • 可以直接托管静态服务,因为无需服务端参与渲染
  • 代码可以缓存在用户电脑上,无需服务端参与

缺点

  • 首屏渲染需要下载后再进行,较慢
  • SEO 问题
静态托管

执行 nuxt generate / nuxt build --prerender 构建

无需响应式交互,只有 index.html404.html200.html

无需使用 nuxt 的动态路由机制

按照以下配置,避免nuxt 为每个页面渲染单独的 html 文件,会只生成 _index.html_

ts 复制代码
export default defineNuxtConfig({ ssr:false, hooks: { 'prerender:routes' ({
routes }) { routes.clear() // 不构建路由页面 }, }, })

看生成文件没有什么 html ,有点搞不懂

混合渲染

为不同路由配置不同渲染模式:

  • ssr
  • 缓存规则
  • 重定向
  • cors 支持
ts 复制代码
export default defineNuxtConfig({
  routeRules: {
    // Homepage pre-rendered at build time
    "/": { prerender: true },
    // Products page generated on demand, revalidates in background, cached until API response changes
    "/products": { swr: true },
    // Product pages generated on demand, revalidates in background, cached for 1 hour (3600 seconds)
    "/products/**": { swr: 3600 },
    // Blog posts page generated on demand, revalidates in background, cached on CDN for 1 hour (3600 seconds)
    "/blog": { isr: 3600 },
    // Blog post page generated on demand once until next deployment, cached on CDN
    "/blog/**": { isr: true },
    // Admin dashboard renders only on client-side
    "/admin/**": { ssr: false },
    // Add cors headers on API routes
    "/api/**": { cors: true },
    // Redirects legacy urls
    "/old-page": { redirect: "/new-page" },
  },
});

支持参数如下:

  • redirect: string - 定义服务器端重定向。
  • ssr: boolean - 禁用应用程序部分 HTML 的服务器端渲染,并使用 ssr: false 使其仅在浏览器中渲染。
  • cors:boolean - 使用 cors: true 自动添加 CORS 头 - 您可以通过使用 headers 覆盖来自定义输出。
  • headers: object - 为您网站的某些部分添加特定的头部信息,cors 等。
  • swr: number | boolean - 为服务器响应添加缓存头并在服务器或反向代理上缓存一段可配置的 TTL(生存时间)。Nitro 的 node-server 预设能够缓存完整响应。当 TTL 过期时,缓存的响应将被发送,同时页面将在后台重新生成。如果使用 true,将添加一个 stale-while-revalidate 头,但不带 MaxAge。
  • isr:number|boolean - 行为与 swr 相同,不同之处在于我们能够将响应添加到支持此功能的平台(目前是 Netlify 或 Vercel)上的 CDN 缓存中。如果使用 true,则内容会在 CDN 中持续到下一次部署。
  • prerender: boolean - 在构建时预渲染路由并将其作为静态资产包含在构建中(无需动态生成的界面)。
  • noScripts:boolean - 禁用 Nuxt 脚本和 JS 资源提示的渲染,用于您网站的某些部分。
  • appMiddleware: string | string[] | Record<string, boolean> - 允许您定义应或不应在应用程序的 Vue 应用部分(即非 Nitro 路由)中为页面路径运行的中间件。

Cache-Control: max-age=600, stale-while-revalidate=30(过期后30s陈旧接受)

stale-while-revalidate 指令通常与 max-age 指令一起使用。max-age 指定了缓存的最大存储时间(以秒为单位),超过这个时间后缓存被认为是陈旧的*。stale-while-revalidate 指定了在缓存过期后,客户端愿意接受陈旧响应的时间长度。

边缘侧渲染(部署方案)

在 CDN 边缘服务器上完成渲染流程,不是一种渲染方案,而是部署目标,支持使用混合渲染

请求页面时,会在经过 CDN 服务器上完成渲染操作,不到达最终服务器

  • 减少服务器压力
  • 通过 CDN 减少了延迟

支持平台:

数据获取

GitHub - unjs/ofetch: 😱 A better fetch API. Works everywhere.

GitHub - sveltejs/devalue: Gets the job done when JSON.stringify can't

使用了 devalue 支持特殊数据的序列化和反序列化

支持数据获取、原生获取,捕获钩子、SSE、node端https代理

  • 使用 ofetch 来支持底层数据请求
  • 使用 devalue 支持特殊数据 Date、Map等特殊类型,从服务器到客户端的序列化,支持循环引用、重复数据的处理
    • 如果服务端返回的对象定义 { toJSON(){} } 函数属性,则使用用户自定义序列化方法,devalue 不参与序列化,此时处理可以使用 superjson 来协助序列化和反序列化

数据获取有三个 API

  • $fetch:类似于 fetch 请求,基于 ofetch 库封装,但是可能会执行两次,导致水合和交互事件出现问题(服务端渲染执行 + 客户端水合执行)
  • useFetch(api,options)$fetch + useAsyncData 的封装,但是在通用渲染(SSR) 情况下只请求一次,进行安全 SSR 请求
  • useAsyncData(api/callback,[callback]):类似 useFetch,提供细粒度控制
    • 对于页面中的动态请求,可以使用 useAsyncData + fetch/$fetch/axios 来完成
    • 像 Promise ,可以执行异步请求,只有最终返回的数据才是最后暴露出来的结果

$fetch 可以用于仅客户端的请求,比如说交互事件是在客户端绑定的,所以只会执行一次请求。

useFetchuseAsyncData 适合于有服务端参与的情况:确保 API 调用在服务端进行,数据会转发到客户端来避免执行多次请求。

两者支持的 options 配置

  • lazy:延迟请求 ,true/false
    • 路由跳转时,页面不会等待数据获取后才渲染,而是直接跳转,从客户端发起请求来获取数据
    • nuxt 使用 <Suspense> 来支持 SSR 页面数据获取后才跳转路由,设置 lazy:true 后需要用户手动获取控制 status 来控制页面加载流程显示
  • server:是否启用服务端请求
    • 路由跳转时,会等待数据获取后才渲染页面(lazy不同点)
    • 可以和 lazy:true 同时使用,两者都是让客户端发起请求,但一起使用语义化会更强一点
  • default(){}:返回默认渲染的内容,执行 clear() 时也会恢复默认值
  • picktransform(){}:用于最小化数据负载,pick:['xx','xx'] 用于粗粒度字段控制,transform(data){} 用于细粒度的字段控制
    • 启用 SSR 情况下,才会最小化负载返回,后续请求和客户端渲染还是返回完整数据,只是 data 得到的数据有指定的字段内容
  • key:数据的缓存键,配合 useNuxtData来获取缓存数据,可以做为 default() 的默认值,也可以用于乐观更新的场景,获取缓存数据插入,如果异常再恢复旧数据
    • useFetch 默认为 URL 为键
    • useAsyncData 第一参数字符串则为键,如果是函数,则以 文件名+行号 为键
  • immediate:是否立即获取数据,即是否在进入页面时自动执行请求,还是通过 execute/refresh 手动执行
  • watch:指定要监听变化执行的响应式依赖,通常用于翻页、筛选等会触发重新请求的场景

API 返回内容

  • data:基于 ref 封装的响应结果
  • refresh/execute:执行数据重新请求/刷新,在 SSR 情况下在客户端运行
  • clear:执行数据清理
    • data 设置为 undefined(如果 options.default() 声明则执行该函数获取默认值)
    • error 返回值设置为 undefined
    • status设置为 "idle"
  • error:基于 ref 封装,数据 获取失败时会错误对象,
  • status:基于 ref 封装,当前请求状态 "idle""pending""success""error"
    • idle:未开始获取数据,immediate:false 时会有状态
    • pending:数据获取中
vue 复制代码
<script setup lang="ts">
const { data } = await useFetch("/api/data");
// 更细粒度控制
const {} = await useAsyncData(() => $fetch("/api/data"));

async function handleFormSubmit() {
  const res = await $fetch("/api/submit", {
    method: "POST",
    body: {
      // My form data
    },
  });
}
</script>

服务端数据获取

通常采用 useAsyncData 配合 $fetch 来完成

nuxt 会获取异步数据,之后通过服务端渲染页面后返回

vue 复制代码
<script setup>
const { data: posts } = await useAsyncData("posts", () => $fetch("/api/posts"));
</script>
<template>
  <div>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.name }}</li>
    </ul>
    <h1>About</h1>
    <TestText></TestText>
  </div>
</template>

可以看到请求只返回了渲染后页面,没有 /api/post 的接口请求

客户端数据获取

如果打算在客户端渲染数据,可以使用 $fetch 去获取数据

让数据在客户端获取并渲染

vue 复制代码
<script setup>
let posts = ref([]);
onMounted(async () => {
  posts.value = await $fetch("/api/posts");
});
</script>
<template>
  <div>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.name }}</li>
    </ul>
    <h1>About</h1>
    <TestText></TestText>
  </div>
</template>

获取头部信息

可以借助 useFetch 和 服务端请求来完成,在服务端解析 cookie,作为响应返回

(避免 CSRF 导致cookie信息被盗取,仅代理需要的头部信息)

javascript 复制代码
// 客户端
const { data } = await useFetch("/api/echo");

// 服务端 server/api/echo.get.ts
export default defineEventHandler((event) => parseCookies(event));

还有一种方案,就是客户端请求页面时,服务端通过 useRequestHeader 捕获请求头,将请求头作为参数使用

javascript 复制代码
const headers = useRequestHeaders(["cookie"]); // 在客户端获取是空,只有服务端获取才有值

async function getCurrentUser() {
  return await $fetch("/api/me", { headers });
}

不可代理的头部信息:

  • host, accept
  • content-length, content-md5, content-type
  • x-forwarded-host, x-forwarded-port, x-forwarded-proto
  • cf-connecting-ip, cf-ray

启用 SSR 时,请求是从服务端发出的,如果用 $fetch,不会包含浏览器头部信息,使用 useFetch 时nuxt 使用 useRequestFetch 代理头部和 Cookie 内容

如果服务端请求后要将头部信息 cookie 代理响应回客户端,需要自行封装请求逻辑实现(借助 h3 插件)

javascript 复制代码
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* Get the response from the server endpoint */
  // 获取数据
  const res = await $fetch.raw(url)
  /* Get the cookies from the response */
  // 得到响应的头部信息
  const cookies = res.headers.getSetCookie()
  /* Attach each cookie to our incoming Request */
  // 将头部信息作为 设置到响应头,返回给客户端
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* Return the data of the response */
  return res._data
}
javascript 复制代码
<script setup lang="ts">
// This composable will automatically pass cookies to the client
const event = useRequestEvent()

// 请求数据返回,设置 cookie
const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))

onMounted(() => console.log(document.cookie))
</script>

useNuxtData 缓存和乐观更新

useNuxtData 允许您访问 useAsyncDatauseLazyAsyncDatauseFetchuseLazyFetch 的当前缓存值

实现乐观更新,实际是在请求发出前,结合 useNuxtData(key) 的数据进行乐观修改

然后根据请求结果

  • 成功:配合 refreshNuxtData(key) 更新缓存数据
  • 失败:回滚请求的数据信息
javascript 复制代码
<script setup lang="ts">
const newTodo = ref('')
let previousTodos = []

// 1.返回 todos 键的缓存数据
const { data: todos } = useNuxtData('todos')

async function addTodo () {
  // 2. 请求方法
  await $fetch('/api/addTodo', {
    method: 'post',
    body: {
      todo: newTodo.value,
    },
    onRequest () {
      // 记录之前的数据
      previousTodos = todos.value

      // 修改 todos 实现乐观插入
      todos.value = [...todos.value, newTodo.value]
    },
    onResponseError () {
      // 请求失败,回滚 todos
      todos.value = previousTodos
    },
    async onResponse () {
      // 请求成功,使用 refreshNuxtData 更新缓存
      await refreshNuxtData('todos')
    },
  })
}
</script>

优势:

  • 通过 key 复用请求且支持组件共享缓存数据
  • 服务端预获取数据,避免客户端二次请求,提升首屏速度
  • 支持响应式和 Typescript 集成
  • 类似 Pinia 的全局数据状态管理

POST SSE 实现

TextDecoderStream - Web API | MDN

ReadableStream.pipeThrough() - Web API | MDN

  • 通过 $fetch 请求数据
  • 使用返回的 response.pipeThroughTextDecoderStream 获取流信息
  • 通过 while 循环获取所有结果
javascript 复制代码
// Make a POST request to the SSE endpoint
const response =
  (await $fetch) <
  ReadableStream >
  ("/chats/ask-ai",
  {
    method: "POST",
    body: {
      query: "Hello AI, how are you?",
    },
    responseType: "stream",
  });

// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader();

// Read the chunk of data as we get it
while (true) {
  const { value, done } = await reader.read();

  if (done) {
    break;
  }

  console.log("Received:", value);
}

生命周期钩子

服务端插件钩子

server/plugins 目录下使用 defineNitroPlugin 创建 nitro 插件,可以捕获生命周期钩子进行操作

例如对客户端响应内容执行篡改

javascript 复制代码
// my-nuxt-app/server/plugins/extend-html.ts
export default defineNitroPlugin((nitroApp) => {
  // 渲染html之前的钩子
  nitroApp.hooks.hook("render:html", (html, { event }) => {
    html.head.push(
      `<meta name="viewport" content="width=device-widht;initial-scale:1;maximum-scale:1;minimum-scale:1;fit-viewport:cover;" />`,
    );

    // 响应消息钩子
    nitroApp.hooks.hook("render:response", (res) => {
      console.log(res);
    });
  });
});

全局配置钩子

适合用于全局构建配置的场景

javascript 复制代码
export default defineNuxtConfig({
  hooks: {
    "build:manifest": (manifest) => {
      // find the app entry, css list
      const css = Object.values(manifest).find(
        (options) => options.isEntry,
      )?.css;
      if (css) {
        // start from the end of the array and go to the beginning
        for (let i = css.length - 1; i >= 0; i--) {
          // if it starts with 'entry', remove it from the list
          if (css[i].startsWith("entry")) {
            css.splice(i, 1);
          }
        }
      }
    },
  },
});

样式相关

全局样式表通常放在 app/assets/css 目录下存储

vue 复制代码
<script>
// 第三方 css
import "animate.css";

// 1. 静态导入,支持服务端
import "~/assets/css/first.css";

// 2. 动态导入css,不支持服务端
import("~/assets/css/first.css");
</script>

<style>
/* 3. css导入方案  */
@import url("~/assets/css/second.css");
</style>

全局添加

全局添加样式表,可以在 nuxt.config.ts 中配置 css 属性,样式表会应用到所有加载的页面

对于预处理器 scss 可以和 vite 一样使用 processorOption.scss.additionalData 直接全局注入

通常用于添加全局样式和变量值,本质就是给每个文件头注入这句代码

typescript 复制代码
export default defineNuxtConfig({
  css: ["~/assets/css/global.css", "animate.css"],
  vite: {
    css: {
      // 添加代码
      preprocessorOptions: {
        scss: {
          additionalData: '@use "~/assets/_colors.scss" as *;',
        },
      },
      // 加快预处理器编译
      preprocessorMaxWorkers: true, // number of CPUs minus 1
    },
  },
});

样式表会应用到所有页面

样式表会映射项目的css应用路径,所以不支持配置外部CDN 样式

外部样式表

通过 nuxt 配置完成外部样式表的动态添加,本质是指定对应的头部标签属性

typescript 复制代码
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: "2025-07-15",
  app: {
    head: {
      // 注意是数组
      link: [
        {
          rel: "stylesheet",
          href: "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css",
        },
      ],
    },
  },
});

也可以在组件中动态添加,类似 VueUse 提供的 useHead 函数

vue 复制代码
<script setup>
useHead({
  title: "关于",
  link: [
    {
      rel: "stylesheet",
      href: "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css",
    },
  ],
});
</script>

存在同名样式表时:

  • 优先级 - 全局样式表 > 组件添加样式表,全局样式表会覆盖组件样式表的添加
    • 如果父子组件均存在同名样式表,样式表会叠加引入,但不会重复请求

字体样式

字体样式通常存放在 /public/fonts 目录下引入

typescript 复制代码
html {
  font-family:'xxx.woff',
  src: url('fonts/xxx.woff') format('woff')
}

网络加载字体加载需要使用额外模块配置,对于 unocss 内置了预定字体加载方案

字体 CLS 加载优化,可以使用 Fontaine 为 nuxt 开发的模块

组件内样式

和 vue 一致,支持对象、scoped、module、v-bind

module样式如下:

vue 复制代码
<template>
  <p :class="$style.red">This should be red</p>
</template>

<style module>
.red {
  color: red;
}
</style>

postcss支持

内置 postcss 和 部分插件

通过 post css lang 获得语法支持

vue 复制代码
<style lang="postcss">
/* Write postcss here */
</style>

CSS LCP 优化

  • 使用 CDN,使文件更接近您的用户
  • 压缩资源,最好使用 Brotli
  • 使用 HTTP2/HTTP3 进行交付
  • 在同一域上托管您的资产(不要使用不同的子域)

现代部署平台会完成整个流程

参考内容

相关推荐
南境十里·墨染春水2 小时前
C++笔记·-- STL unordered_map
开发语言·c++·笔记
勤劳的进取家2 小时前
SSH配置
运维·网络·学习
三品吉他手会点灯2 小时前
C语言学习笔记 - 17.C编程预备计算机专业知识 - 数据类型
c语言·笔记·学习
亿元程序员2 小时前
Cocos视频拼图,拼图游戏的最后一块碎片,支持原生!
前端
噜噜噜阿鲁~2 小时前
python学习笔记 | 7.4、高级特性-生成器
笔记·python·学习
Rabbit_QL2 小时前
【前端工具链小白篇】前端工具链全景:Node、npm、Vite 各管什么
前端·npm·node.js
身如柳絮随风扬2 小时前
前端基础进阶:Node.js + ES6 + Axios + Vue 全面入门指南
前端·node.js·es6
handler012 小时前
【Linux 笔记】GDB 调试速查手册
linux·运维·c语言·c++·笔记
九思十安2 小时前
HNU2026-算法设计与分析-笔记 3 摊还分析
笔记·算法