news 2026/6/23 22:19:15

Nuxt.js如何系统性解决Vue SSR落地难题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nuxt.js如何系统性解决Vue SSR落地难题

1. 为什么“开箱即用的 SSR”在 Vue 生态里是个伪命题,而 Nuxt.js 真正解决的是什么问题

你刚学完 Vue.js,兴致勃勃地想把项目部署到生产环境,结果被同事一句“你这还是 CSR(客户端渲染),首屏白屏太久,SEO 友好度为零”泼了盆冷水。你打开浏览器开发者工具,Network 标签页里看到 index.html 文件体积只有 1KB,后面跟着十几个 JS chunk,首屏内容全靠 JavaScript 下载、解析、执行后才渲染出来——这确实是典型的 CSR 行为。你立刻搜“Vue SSR 怎么做”,翻到 Vue 官方文档的vue-server-renderer章节,读了三遍,越看越懵:需要手写 webpack 配置区分 client/server 构建入口、要自己管理 bundle renderer、得处理 Vuex 状态序列化与反序列化、还要手动注入 window 对象模拟、服务端路由匹配逻辑得自己重写……最后你关掉页面,默默点开 Nuxt.js 官网首页,看到那句 “The Intuitive Vue Framework” 和那个巨大的 “Create a new Nuxt app” 按钮,心里松了口气——但你可能没意识到,Nuxt.js 并不是“帮你做了 SSR”,而是系统性地消除了 SSR 在 Vue 生态中落地的所有非业务障碍

这不是一个简单的工具封装问题。Vue 本身是纯粹的视图层框架,它不规定路由怎么写、状态怎么管、API 怎么调、构建流程怎么配。这种自由带来了极高的上手门槛,也导致 SSR 成为一个需要跨多个知识域协同作战的工程难题。Nuxt.js 的核心价值,恰恰在于它用一套约定(convention)覆盖了所有这些“不该由业务开发者操心”的环节:它内置了基于 Vue Router 的服务端路由自动注册机制,内置了 Vuex 或 Pinia 的服务端状态预取(asyncData / useAsyncData),内置了 webpack 的双端(client + server)构建配置,甚至内置了 Node.js 服务的最小运行时(nuxt dev / nuxt build + nuxt start)。它不强制你用它的语法,但只要你遵守它的文件结构(pages/、layouts/、middleware/、plugins/),它就自动为你生成可直接部署的 SSR 应用。我第一次用 Nuxt.js 搭建一个带用户登录态和商品列表的电商首页时,从npx nuxi@latest init my-shopnpm run dev启动并看到服务端渲染出的完整 HTML,只用了 17 分钟——其中 12 分钟花在了给商品卡片写 CSS 上。这不是魔法,而是把原本需要 3 天才能理清的构建链路、服务端生命周期、数据预取时机等隐性知识,全部固化为可预测、可复现、可调试的代码骨架。

所以,当你看到热搜词里有人问“ssr种ssrr是一个东西吗”,答案很明确:不是。SSR(Server-Side Rendering)是一种架构模式,指 HTML 在服务器端生成并返回给浏览器;而 “ssrr” 是一个拼写错误或语音误听,没有任何技术含义。真正值得深究的是:为什么 Vue.js 官方不直接把 SSR 做成createApp().mount('#app').enableSSR()这样一行代码?因为 SSR 不是 Vue 的功能开关,而是整个应用架构的重构。它要求你重新思考数据获取的时机(是在组件挂载前?还是在路由解析后?)、状态的生命周期(服务端生成的状态如何安全地传递给客户端?)、错误的捕获边界(服务端渲染失败,是返回 500 还是降级为 CSR?)、以及构建产物的形态(你最终部署的不是一个静态 HTML,而是一个 Node.js 服务进程)。Nuxt.js 就是这个重构过程的“操作系统内核”,它不替代 Vue,而是让 Vue 能在一个为 SSR 优化的运行时环境中,自然地发挥其响应式优势。

提示:很多初学者误以为“装了 Nuxt.js 就等于实现了 SSR”。这是危险的错觉。Nuxt.js 默认启用的是 Universal Rendering(即 SSR + SSG 混合模式),但如果你在nuxt.config.ts中将ssr: false,它会退化为纯 CSR 模式,此时所有页面都由客户端 JavaScript 渲染,和你直接用 Vite + Vue 写出来的效果完全一致。判断是否真正在用 SSR,最简单的方法是禁用浏览器 JavaScript,然后刷新页面——如果能看到完整的页面结构(文字、图片占位符、导航栏),说明 SSR 生效;如果只看到一个空的<div id="app"></div>,那就是 CSR。

2. 从零启动一个真正能跑通 SSR 的 Nuxt 项目:那些被官方文档悄悄省略的关键步骤

现在,我们来亲手搭一个最小可行的 SSR 项目。别急着敲npx nuxi init,先明确一个前提:Nuxt 3(当前稳定版)默认使用 Nitro 作为服务端运行时,它不再依赖 Express/Koa 等传统 Node.js 框架,而是自研了一套轻量、高性能的 HTTP 服务引擎。这意味着你不需要再手动npm install express,也不需要写server.js来启动服务。但这也带来了一个新手极易踩的坑:本地开发时,npm run dev启动的是一个开发服务器,它内部集成了热重载和模块代理;而生产构建后,npm run build生成的.output/server/index.mjs是一个独立的、可直接用node .output/server/index.mjs启动的二进制服务。这两者的行为差异,是很多“本地能跑,上线就白屏”的根源。

我们一步步来:

2.1 初始化与基础结构确认

执行:

npx nuxi@latest init my-ssr-app cd my-ssr-app npm install

初始化完成后,检查项目根目录下的关键文件:

  • app.vue:这是整个应用的根组件,相当于 Vue 项目的main.js入口。它必须包含<NuxtPage />组件,这是 Nuxt 的页面占位符。
  • pages/目录:所有以.vue结尾的文件,都会被自动注册为路由。例如pages/index.vue对应/pages/about.vue对应/about。这是 Nuxt 的核心约定,也是 SSR 路由匹配的基础。
  • nuxt.config.ts:这是项目的“宪法”,所有全局配置都在这里。它默认导出一个对象,其中ssr: true是开启 SSR 的开关(Nuxt 3 默认为true,但显式写出更清晰)。

此时,如果你直接npm run dev,会看到一个空白页面。别慌,这是因为app.vue里只有<NuxtPage />,而pages/目录下还没有任何.vue文件。Nuxt 的路由系统是“按需生成”的,没有页面文件,就没有路由,也就没有内容可渲染。

2.2 编写第一个 SSR 页面:理解useAsyncData的执行时机

pages/目录下创建index.vue

<template> <div> <h1>Welcome to My SSR App</h1> <p>Current time: {{ currentTime }}</p> <p>Server timestamp: {{ serverTime }}</p> </div> </template> <script setup> // 这个钩子会在服务端和客户端都执行,但行为不同 const { data: currentTime } = await useAsyncData('currentTime', () => { return new Date().toISOString() }) // 这个钩子只在服务端执行一次,客户端跳转时不会重复调用 const { data: serverTime } = await useAsyncData('serverTime', () => { return new Date().toISOString() }, { serverOnly: true // 关键!指定只在服务端运行 }) </script>

保存后,刷新浏览器。你会看到两行时间戳,它们看起来一模一样。但请打开浏览器的 Network 标签页,找到index.html的响应体,右键“View Page Source”,搜索Current time。你会发现,HTML 源码里已经包含了<p>Current time: 2024-06-15T08:23:45.123Z</p>这样的文本——这就是服务端渲染的结果。而serverTime的值,也已嵌入 HTML 中。

useAsyncData是 Nuxt 数据获取的基石。它背后的工作流是:

  1. 用户请求/路由;
  2. Nuxt 服务端(Nitro)根据pages/index.vue找到对应组件;
  3. 在服务端执行setup()函数,遇到await useAsyncData(...),暂停执行,发起数据请求(此处是同步的new Date());
  4. 数据返回后,将data属性序列化为 JSON 字符串,并注入到 HTML 的<script>window.__NUXT__ = {...}</script>全局变量中;
  5. HTML 发送给浏览器;
  6. 浏览器加载 JS,客户端 Vue 实例启动,从window.__NUXT__中读取预取的数据,直接填充到响应式状态中,避免重复请求。

这个过程,就是 SSR 的核心价值:把“等待数据”的时间,从客户端的“下载 JS → 执行 JS → 发起请求 → 等待响应 → 渲染 DOM”,提前到了服务端的“接收请求 → 获取数据 → 生成 HTML → 返回 HTML”。用户看到的,永远是“有内容”的页面,而不是一个 loading 动画。

2.3 配置nuxt.config.tsssr开关与devtools的真实作用

打开nuxt.config.ts,你会看到类似这样的代码:

export default defineNuxtConfig({ devtools: { enabled: true }, ssr: true, })

ssr: true是明确告诉 Nuxt:“请启用服务端渲染”。虽然它是默认值,但显式声明是良好实践。如果你把它设为falseuseAsyncData将只在客户端执行,serverOnly: true的选项会失效,整个应用退化为 CSR。

devtools: { enabled: true }这个配置,常被误解为“开启 Vue Devtools 插件”。其实不然。它开启的是Nuxt Devtools,这是一个独立于浏览器插件的、深度集成在 Nuxt 开发服务器中的调试面板。当你在npm run dev状态下访问http://localhost:3000/_devtools,就能看到一个专门针对 Nuxt 的控制台,里面可以实时查看:

  • 当前路由的useAsyncDatauseFetch等数据钩子的执行状态和返回值;
  • 所有已注册的composables(组合式函数)的调用链;
  • middleware(中间件)的执行顺序和耗时;
  • plugins(插件)的加载状态。

这才是真正对 SSR 开发有帮助的调试工具。至于“vue.js devtools插件下载 edge”,那是另一个维度的事:它用于调试客户端 Vue 组件的响应式状态、事件监听、props 传递等,对服务端逻辑无能为力。两者是互补关系,而非替代关系。

注意:nuxt.config.ts中的runtimeConfigpublicRuntimeConfig是两个容易混淆的概念。runtimeConfig里的变量(如数据库密码、API 密钥)只存在于服务端,不会被打包进客户端 JS,因此是安全的;而publicRuntimeConfig里的变量(如 API 基础 URL、应用名称)会被序列化到window.__NUXT__中,客户端 JS 可以读取。千万别把敏感信息放在publicRuntimeConfig里。

3.nuxt.config.ts不是配置文件,而是你的 SSR 应用的“中央调度室”

很多人把nuxt.config.ts当成一个普通的 JSON 配置文件,只用来改改ssr: truedev: false。这是对 Nuxt 架构的严重误读。这个文件,实际上是整个 SSR 应用的“编译期指令集”,它决定了你的代码在构建时如何被解析、如何被注入、以及最终生成的服务端逻辑长什么样。它不是运行时配置,而是构建时的元编程入口。

3.1modules数组:如何让第三方库“原生支持” SSR

假设你想在项目里集成@nuxtjs/tailwindcss,官方文档告诉你npm install -D @nuxtjs/tailwindcss,然后在nuxt.config.tsmodules数组里加上'@nuxtjs/tailwindcss'。为什么加在这里就能工作?因为 Nuxt 的模块系统,本质上是一套“构建时插件机制”。

当你在modules里声明一个模块时,Nuxt 会在构建阶段(nuxt build)自动执行该模块导出的setup()函数。这个函数可以:

  • 注册新的composables(如useTailwind);
  • 修改 webpack/vite 的构建配置(如添加 PostCSS 插件);
  • 注入新的serverMiddleware(服务端中间件);
  • 甚至修改nuxt.config.ts本身的配置项。

@nuxtjs/tailwindcss为例,它的setup()函数会:

  1. 自动在tailwind.config.js中注入 Nuxt 特定的content路径,确保 Tailwind 能扫描到pages/components/等目录下的 class 名;
  2. @tailwind指令注入到app.vue<style>标签中;
  3. 在服务端构建时,预编译所有用到的 Tailwind class,生成一个极小的 CSS 文件,避免客户端运行时解析。

这整个过程,对开发者是完全透明的。你不需要知道postcss是什么,也不需要手动配置purgeCSS。模块系统把“让一个 UI 框架适配 SSR”的复杂性,封装成了一个npm install + modules.push()的原子操作。

3.2serverHandlers:在 SSR 服务里嵌入自定义 API 端点

Nuxt 3 的 Nitro 运行时,允许你在不引入 Express 的情况下,直接在项目里定义 API 路由。这极大简化了前后端分离的开发流程。你不需要再维护一个独立的api-server项目,所有业务逻辑都可以和页面逻辑共存于同一个代码库。

在项目根目录下创建server/api/hello.get.ts

// server/api/hello.get.ts export default defineEventHandler(() => { return { message: 'Hello from SSR server!' } })

保存后,在pages/index.vue中,你可以这样调用:

<script setup> const { data } = await useFetch('/api/hello') </script>

注意,这个/api/hello请求,在服务端渲染时,会由 Nitro 服务内部直接处理,走的是 Node.js 进程内的函数调用,毫秒级响应;而在客户端水合(hydration)后,如果用户点击导航又回到首页,这个请求会变成一个真实的 HTTP 请求,发往http://localhost:3000/api/hello。Nuxt 会自动处理这种“同构请求”的代理逻辑。

serverHandlers的强大之处在于,它可以访问服务端的完整上下文。比如,你想获取当前请求的 IP 地址:

// server/api/ip.get.ts export default defineEventHandler((event) => { const ip = event.node.req.socket.remoteAddress return { ip } })

或者,你想在 API 层做 JWT 验证:

// server/api/protected.get.ts export default defineEventHandler(async (event) => { const token = getCookie(event, 'auth_token') if (!token) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) } const user = await verifyJWT(token) return { user } })

这些逻辑,全部运行在服务端,且与你的页面数据获取(useAsyncData)共享同一套认证、日志、错误处理机制。这才是真正的“全栈一体化”。

3.3app.head:为什么 SEO 标签必须在服务端注入

nuxt.config.ts中,你可以这样配置全局<head>

export default defineNuxtConfig({ app: { head: { title: 'My Awesome SSR App', meta: [ { name: 'description', content: 'A demo of Nuxt SSR in action' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] } } })

这些配置,会在每个页面的 HTML<head>中,静态地插入对应的标签。但如果你需要动态的 SEO 标签(比如每个文章页面显示不同的标题和描述),就必须在页面组件里做:

<!-- pages/blog/[id].vue --> <script setup> const route = useRoute() const { data: post } = await useAsyncData(`post-${route.params.id}`, () => $fetch(`/api/posts/${route.params.id}`)) // 这段代码会在服务端执行,并将生成的 <title> 和 <meta> 标签,直接写入 HTML 的 <head> 中 useHead({ title: post.value?.title || 'Blog Post', meta: [ { name: 'description', content: post.value?.excerpt || '' } ] }) </script>

useHead是 Nuxt 提供的响应式 Head 管理 API。它的精妙之处在于:当post.value在服务端被useAsyncData获取到后,useHead会立即将其计算出的<title><meta>标签,注入到当前页面的 HTML<head>中。这意味着搜索引擎爬虫抓取到的,就是带有正确文章标题和摘要的 HTML,而不是一个通用的“我的博客”标题。这是 SSR 对 SEO 最直接、最有效的贡献。

提示:useHead的响应式能力,是通过 Nuxt 的onServerPrefetch生命周期钩子实现的。它确保了 Head 标签的更新,与数据获取的完成严格同步。如果你在onMounted(仅客户端执行)里调用useHead,那么服务端生成的 HTML 里就不会有这些动态标签,SEO 效果将大打折扣。

4. 从开发到部署:SSR 应用的构建产物、服务启动与性能监控全链路

当你完成了本地开发,准备将 SSR 应用部署到生产环境时,Nuxt 3 的构建流程会给你一个“惊喜”:它不再生成一堆静态文件,而是生成一个可执行的 Node.js 服务包。这个转变,是理解 SSR 部署模型的关键。

4.1nuxt build的产物解剖:.output/目录里的秘密

执行npm run build后,Nuxt 会在项目根目录下生成一个.output/目录。这是整个 SSR 应用的“发布包”,其结构如下:

.output/ ├── public/ # 静态资源,如 favicon.ico, _nuxt/ 下的 JS/CSS ├── server/ # 服务端代码 │ ├── index.mjs # 主服务入口,可直接用 node 运行 │ └── chunks/ # 服务端运行时依赖的代码块 └── nitro.json # Nitro 运行时的元信息配置

重点看server/index.mjs。它不是一个普通的 JS 文件,而是一个经过 Vite 预构建、Tree-shaking、代码分割后的、高度优化的 Node.js 模块。它内部已经打包了:

  • Nitro 运行时核心;
  • 你所有的server/api/端点逻辑;
  • 你所有的pages/组件和服务端路由匹配逻辑;
  • 你所有的plugins/middleware/
  • 甚至包括node_modules中被实际用到的依赖(如unenvdestr等)。

这意味着,你部署时,不需要把整个node_modules上传到服务器。你只需要上传.output/目录,然后在服务器上执行node .output/server/index.mjs,服务就启动了。这极大地简化了部署流程,也避免了因服务器 Node.js 版本、node_modules安装差异导致的运行时错误。

4.2 生产环境启动:nuxt startnode .output/server/index.mjs的等价性

Nuxt 提供了npm run start脚本,它底层执行的就是node .output/server/index.mjs。你可以直接在服务器上运行后者,效果完全一样。但nuxt start的优势在于,它会自动读取.output/nitro.json中的配置,比如hostport,并设置正确的环境变量。

为了确保服务在后台稳定运行,你需要一个进程管理器。最轻量的选择是pm2

# 在服务器上安装 pm2 npm install -g pm2 # 启动 Nuxt 服务 pm2 start .output/server/index.mjs --name "my-nuxt-app" # 查看日志 pm2 logs "my-nuxt-app"

pm2会自动处理进程崩溃重启、CPU 内存监控、日志轮转等运维任务。你不需要自己写forever脚本或systemd服务单元文件。

4.3 SSR 性能监控:如何定位“首屏慢”的真正元凶

SSR 的最大优势是首屏快,但最大的风险是“服务端渲染慢”。当用户访问/,如果服务端花了 2 秒才返回 HTML,那无论你的 JS 多快,用户体验都是差的。所以,监控 SSR 的服务端性能,比监控客户端性能更重要。

Nuxt 3 内置了nitro的日志系统,你可以在nuxt.config.ts中开启详细日志:

export default defineNuxtConfig({ nitro: { devProxy: {}, // 开启请求耗时日志 logging: { requests: true } } })

启动服务后,你会在终端看到类似这样的日志:

✔ GET / (214ms) 200 ✔ GET /_nuxt/entry.123456.js (12ms) 200

这里的214ms,就是服务端渲染/页面的总耗时。如果这个数字经常超过 500ms,你就需要深入排查。

排查路径非常清晰:

  1. 检查useAsyncData的数据源:用console.time('api-call')包裹你的$fetch调用,看网络请求本身耗时多少。如果是调用外部 API,考虑增加缓存($fetch(..., { cache: 'force-cache' }))或使用服务端缓存(如 Redis)。
  2. 检查组件渲染逻辑:在pages/index.vuesetup()函数开头加console.time('render'),结尾加console.timeEnd('render')。如果渲染耗时高,说明你的模板过于复杂,或者有大量计算属性在服务端执行。解决方案是:将复杂计算移到onMounted(客户端执行),或使用defineComponentsetup()中的onServerPrefetch钩子进行更精细的控制。
  3. 检查serverMiddleware:如果你写了自定义中间件,确保它们没有阻塞 I/O 操作。Node.js 是单线程的,一个同步的fs.readFileSync就能让整个服务卡住。

我曾经遇到一个案例:一个新闻首页的 SSR 渲染耗时高达 1.8 秒。排查发现,问题出在一个plugins/analytics.client.ts插件里,它试图在服务端也执行window.navigator.userAgent的解析。由于服务端没有window对象,这段代码抛出了异常,而异常处理逻辑又触发了额外的日志写入,形成了恶性循环。解决方案很简单:给这个插件加上mode: 'client'选项,确保它只在浏览器端加载。

注意:Nuxt 3 的useAsyncData默认开启了lazy: false,这意味着它会阻塞页面渲染,直到数据获取完成。如果你有一些非关键数据(比如侧边栏的推荐文章),可以设置lazy: true,让它在页面渲染后再异步获取,从而降低首屏 TTFB(Time to First Byte)。

5. SSR 的边界与陷阱:什么时候不该用 Nuxt.js,以及如何优雅降级

Nuxt.js 是 SSR 的利器,但它不是银弹。在某些场景下,强行使用 SSR 反而会增加复杂度、降低性能、甚至损害用户体验。识别这些边界,是资深开发者与新手的本质区别。

5.1 交互密集型应用:SSR 可能成为性能瓶颈

想象一个实时协作的白板应用,用户每画一笔,都要通过 WebSocket 向服务端发送坐标,服务端再广播给所有在线用户。这类应用的核心是低延迟、高频率的客户端交互。如果用 Nuxt.js 做 SSR:

  • 首次加载时,服务端需要渲染一个“空画布”,这毫无意义;
  • 用户开始绘画后,所有后续交互都发生在客户端,服务端渲染的初始状态很快就被覆盖;
  • 更糟的是,为了保持服务端和客户端状态的一致,你可能需要在useAsyncData里拉取历史绘图数据,而这部分数据可能非常大(几 MB 的 SVG 路径字符串),导致 HTML 体积暴涨,首屏加载变慢。

在这种场景下,纯 CSR(Vite + Vue)是更优解。你可以用vite-plugin-pwa做离线缓存,用@vueuse/coreuseWebSocket做实时通信,性能和体验都远超 SSR 方案。Nuxt.js 的价值,在于它能让你在“需要 SEO 和首屏性能”的页面(如产品介绍页、博客文章页)用 SSR,在“需要极致交互”的页面(如白板、编辑器)用 CSR,通过ssr: false配置轻松切换。

5.2 静态内容为主的网站:SSG(静态站点生成)是更优选择

如果你的网站是企业官网、文档站、博客,内容更新频率很低(每周一次),那么 SSG(Static Site Generation)比 SSR 更合适。SSG 在构建时(nuxt generate)就生成所有页面的 HTML 文件,部署时只需一个静态文件服务器(如 Nginx、Vercel、Netlify),无需运行任何 Node.js 进程。

Nuxt 3 对 SSG 的支持是无缝的。你只需要在nuxt.config.ts中添加:

export default defineNuxtConfig({ ssr: false, // 关闭 SSR target: 'static', // 指定目标为静态站点 })

然后运行npm run generate,Nuxt 会自动:

  • 遍历所有pages/路由,模拟用户访问;
  • 执行每个页面的useAsyncData,获取数据;
  • 将渲染出的 HTML 保存为dist/index.htmldist/about/index.html等文件。

生成的dist/目录,可以直接扔到任何 CDN 上,全球访问速度都是毫秒级。而且,它天然具备“无限扩展性”——你不需要担心服务器 CPU 被打满,因为根本没有服务器。

我曾负责一个技术文档站的迁移,从 Nuxt 2 SSR 迁移到 Nuxt 3 SSG。迁移后,首屏加载时间从平均 450ms 降至 80ms,服务器月度账单从 $45 降至 $0(托管在 Vercel 免费层),并且支持了 instant cache invalidation(即时缓存失效)。唯一的代价是,内容更新后需要手动触发一次构建,但这对于文档站来说,完全可接受。

5.3 优雅降级:当 SSR 失败时,如何不让用户看到一片空白

再健壮的 SSR 服务,也可能因为数据库连接失败、外部 API 超时、内存溢出等原因,在某个瞬间无法生成 HTML。这时,Nuxt 提供了error.vue这个全局错误布局。你可以在layouts/error.vue中自定义错误页面:

<template> <div class="error-container"> <h1>Oops! Something went wrong.</h1> <p>We're working to fix the issue. Please try again later.</p> <button @click="$router.go()">Refresh</button> </div> </template> <script setup> // 这个组件会在服务端渲染失败时,由客户端接管并渲染 </script>

更重要的是,Nuxt 的useAsyncData提供了initialCachegetCachedData机制,可以实现“服务端失败,客户端兜底”的策略。例如:

<script setup> const { data, error, refresh } = await useAsyncData('posts', () => $fetch('/api/posts'), { // 如果服务端获取失败,尝试从 localStorage 读取缓存 getCachedData: () => { const cached = localStorage.getItem('cached-posts') return cached ? JSON.parse(cached) : null }, // 获取成功后,存入 localStorage onResponse: ({ data }) => { localStorage.setItem('cached-posts', JSON.stringify(data)) } }) // 如果服务端没拿到数据,且客户端也没有缓存,则显示加载状态 if (!data.value && !error.value) { // 显示 skeleton loader } </script>

这种设计,让用户感知不到 SSR 的存在与否。他们看到的,永远是一个有内容、有反馈、有兜底的页面。这才是现代 Web 应用应有的韧性。

最后分享一个小技巧:在nuxt.config.ts中,你可以用runtimeConfig.public.isSSR这个布尔值,在组件里做细粒度的条件判断。比如,你想在服务端记录一个日志,但在客户端不执行:

if (process.server) { console.log('This runs only on server') } // 或者 if (import.meta.env.SSR) { // 同上 }

这个process.server是 Nuxt 注入的全局变量,它比typeof window === 'undefined'更可靠,因为它在构建时就被确定,不会因运行时环境变化而产生意外。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 22:02:29

量子计算入门

量子计算入门&#xff1a;探索未来计算的新疆界 在传统计算机逐渐逼近物理极限的今天&#xff0c;量子计算以其颠覆性的潜力吸引了全球科技界的目光。与经典计算机基于二进制比特不同&#xff0c;量子计算机利用量子比特&#xff08;qubit&#xff09;的叠加与纠缠特性&#x…

作者头像 李华
网站建设 2026/6/23 22:00:41

5分钟用AI生成Python自动化测试框架:Selenium+Pytest+Allure实战

1. 项目概述&#xff1a;当AI遇上自动化测试 最近在跟几个测试团队的朋友聊天&#xff0c;发现一个挺普遍的现象&#xff1a;大家心里都清楚自动化测试是提升效率、保证质量的利器&#xff0c;但真到要动手搭建一个框架的时候&#xff0c;往往就卡在了第一步。从零开始写一个结…

作者头像 李华
网站建设 2026/6/23 22:00:14

Python异步测试实战:pytest-asyncio从配置到高级应用

1. 项目概述&#xff1a;为什么异步测试在今天变得如此重要&#xff1f;如果你最近在维护一个Python后端服务&#xff0c;尤其是涉及大量I/O操作&#xff08;比如网络请求、数据库查询&#xff09;的项目&#xff0c;你大概率已经用上了asyncio。异步编程带来的性能提升是实实在…

作者头像 李华
网站建设 2026/6/23 21:39:37

Android逆向实战:Hook dlopen绕过Frida检测机制

1. 项目概述&#xff1a;为什么我们要关注 dlopen 的 Hook&#xff1f;在移动安全逆向和动态分析领域&#xff0c;绕过应用的保护机制是一个永恒的话题。很多应用&#xff0c;尤其是那些对安全性有较高要求的金融、游戏类应用&#xff0c;会采用各种手段来检测和阻止动态分析工…

作者头像 李华