渲染模式配置
nuxt 支持配置 渲染模式:
- 通用渲染 - 服务端渲染(SSR) 和 水合结合
- 客户端渲染(CSR)- vue 日常开发
- 静态托管 - 只有
index.html页面 - 混合渲染 - 根据页面需求配置渲染模式
- 边缘测渲染(ESR)- 通过 CDN 节点进行渲染,需要云服务CDN 支持
通用渲染
- 请求页面时,在服务端通过
vue渲染完整的静态 HTML,ref响应式属性会完成一次初始化,完成再 HTML 标签中的静态展示渲染 - 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 应用
- 请求页面,客户端从服务端下载初始静态页和 bundleJS
- 根据 bundle JS 和 页面,在客户端完成渲染和交互式流程
开启方式:
ts
export default defineNuxtConfig({ ssr: false, })
优点:
- 开发复杂度低
- 可以直接托管静态服务,因为无需服务端参与渲染
- 代码可以缓存在用户电脑上,无需服务端参与
缺点:
- 首屏渲染需要下载后再进行,较慢
- SEO 问题
静态托管
执行 nuxt generate / nuxt build --prerender 构建
无需响应式交互,只有 index.html、404.html 、200.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 减少了延迟
支持平台:
- Cloudflare Pages使用 git 集成和
nuxt build命令实现零配置。 - Vercel Cloud使用
nuxt build命令和NITRO_PRESET=vercel-edge环境变量。 - Netlify Edge Functions使用
nuxt build命令和NITRO_PRESET=netlify-edge环境变量
数据获取
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 可以用于仅客户端的请求,比如说交互事件是在客户端绑定的,所以只会执行一次请求。
useFetch 和 useAsyncData 适合于有服务端参与的情况:确保 API 调用在服务端进行,数据会转发到客户端来避免执行多次请求。
两者支持的 options 配置:
lazy:延迟请求 ,true/false- 路由跳转时,页面不会等待数据获取后才渲染,而是直接跳转,从客户端发起请求来获取数据
- nuxt 使用
<Suspense>来支持 SSR 页面数据获取后才跳转路由,设置lazy:true后需要用户手动获取控制status来控制页面加载流程显示
server:是否启用服务端请求- 路由跳转时,会等待数据获取后才渲染页面(lazy不同点)
- 可以和
lazy:true同时使用,两者都是让客户端发起请求,但一起使用语义化会更强一点
default(){}:返回默认渲染的内容,执行clear()时也会恢复默认值pick和transform(){}:用于最小化数据负载,pick:['xx','xx']用于粗粒度字段控制,transform(data){}用于细粒度的字段控制- 启用 SSR 情况下,才会最小化负载返回,后续请求和客户端渲染还是返回完整数据,只是
data得到的数据有指定的字段内容
- 启用 SSR 情况下,才会最小化负载返回,后续请求和客户端渲染还是返回完整数据,只是
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:数据获取中
- idle:未开始获取数据,
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,acceptcontent-length,content-md5,content-typex-forwarded-host,x-forwarded-port,x-forwarded-protocf-connecting-ip,cf-ray
SSR 反向传递 Cookie
启用 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 允许您访问 useAsyncData、useLazyAsyncData、useFetch 和 useLazyFetch 的当前缓存值
实现乐观更新,实际是在请求发出前,结合 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.pipeThrough和TextDecoderStream获取流信息 - 通过
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 和 部分插件
- postcss-import:改进
@import规则 - postcss-url:转换
url()语句 - autoprefixer:自动添加前缀
- cssnano:压缩和清除
通过 post css lang 获得语法支持
vue
<style lang="postcss">
/* Write postcss here */
</style>
CSS LCP 优化
- 使用 CDN,使文件更接近您的用户
- 压缩资源,最好使用 Brotli
- 使用 HTTP2/HTTP3 进行交付
- 在同一域上托管您的资产(不要使用不同的子域)
现代部署平台会完成整个流程