技术选型复盘:为何百万UV电商站弃用SPA选择Nuxt 3.12
去年双十一前夜,我接手了一个日活接近百万的电商前端重构项目。老项目是基于 Vue 2 的 SPA 架构,打包后的主包体积达到了 3.2MB,首屏加载时间(FCP)在 4G 网络下平均是 2.8 秒。这个数字在促销期间是致命的,因为我们的数据分析显示,加载超过 3 秒的用户流失率会增加 53%。
当时团队内部有过激烈的争论,一部分后端同事建议直接上纯静态化(SSG),但我否决了。电商站的商品库存、价格、用户个性化推荐是实时变化的,纯静态页面无法满足秒级更新的需求,如果强行用 SSG,每次价格变动都要全量重构建,这在百万级 SKU 面前是不现实的。
我最终拍板选择了 Nuxt 3.12.0(发布于 2024 年 5 月 21 日,基于 Nitro 2.9 和 Vue 3.4)。选择它的核心原因不是因为它新,而是因为它的混合渲染能力和内置的 Nitro 引擎能解决我们当时最痛的 TTFB(首字节时间)问题。
在 SPA 模式下,我们的 TTFB 虽然只有 200ms,但后续的 JS 解析和接口请求让白屏时间长达 1.5 秒。切换到 Nuxt 3 的 SSR 模式后,服务器直接吐出包含商品价格的 HTML,FCP 直接降到了 1.2 秒以内。
真实场景下的性能对比数据:
| 指标 | 旧 SPA 架构 | Nuxt 3.12 SSR 架构 | 变化 |
| :--- | :--- | :--- | :--- |
| FCP (4G) | 2800ms | 1100ms | -60.7% |
| TTI (可交互时间) | 3200ms | 1400ms | -56.2% |
| SEO 收录量 | 12,000 页 | 85,000 页 | +608% |
| 服务器内存占用 | 单实例 180MB | 单实例 210MB | +30MB (可接受) |
实际配置与代码实现:
在 nuxt.config.ts 中,我们并没有全站开启 SSR,因为购物车和订单页涉及隐私且不需要 SEO,全开 SSR 反而浪费服务器资源。我利用 Nuxt 3 的模块化和 TypeScript 原生支持,配置了按需渲染。
// nuxt.config.ts
export default defineNuxtConfig({
// 启用 TypeScript 严格模式,这在大型项目中能减少 30% 的运行时类型错误
typescript: {
strict: true
},
// 针对百万 UV 的优化:关闭全局 SSR,改为路由级配置
ssr: false,
// 针对特定路由开启 SSR
routeRules: {
// 商品详情页,必须 SSR 保证 SEO 和实时性
'/product/**': { ssr: true },
// 首页,开启 SSR 并缓存 1 分钟,减轻后端压力
'/': { ssr: true, cache: { maxAge: 60 * 1000 } },
// 用户中心,纯 SPA 即可
'/user/**': { ssr: false },
// 促销落地页,使用 ISR (增量静态再生),每 10 分钟重新生成一次
'/promo/**': { isr: 600 }
},
// 引入 Pinia 代替 Vuex,因为 Nuxt 3 对 Composition API 支持更好
modules: ['@pinia/nuxt'],
// 针对 Nitro 2.9 的配置,优化 Serverless 部署
nitro: {
preset: 'vercel', // 我们部署在 Vercel 边缘网络
prerender: {
crawlLinks: true,
routes: ['/sitemap.xml'] // 预渲染站点地图
}
}
})
我遇到的实际问题:
上线第一周,监控发现商品详情页的 Node 服务 CPU 占用率飙升到了 90%。排查后发现,我们在 useFetch 中获取数据时,没有正确处理序列化,导致服务端每次都要把完整的 Mongoose 对象(包含大量 MongoDB 内部属性)序列化后发给前端,再反序列化,白白消耗了 40% 的 CPU。
解决方案是我在 server/api 层做了数据清洗,只返回前端需要的字段,而不是整个数据库对象。这个改动让服务器响应时间从 450ms 降到了 120ms。
---
Nuxt 3混合渲染架构:同一应用内SSR与SSG的路由级配置实战
很多团队在架构 Nuxt 3 应用时容易走极端:要么全站 SSR,把服务器压垮;要么全站 SPA,丢了 SEO。我在做那个电商项目时,采用了一种更精细化的策略——路由级混合渲染。
Nuxt 3.12 的 routeRules 功能非常强大,它允许你在一个应用里,针对不同的 URL 路径,指定不同的渲染方式。这解决了我们一个很棘手的问题:商品列表页需要 SEO(SSR),但商品详情页的评论区不需要被搜索引擎抓取,且数据更新极快(SSG 或 CSR)。
场景分析:
我们的“品牌特卖”频道,平时流量平稳,但大促时会瞬间涌入平时 10 倍的流量。如果全用 SSR,数据库扛不住;如果全用 SSG,价格变动无法实时同步。
我采用的方案是:列表页 SSR + 缓存,详情页 ISR(增量静态再生)。
具体配置实战:
在 nuxt.config.ts 中,我通过 routeRules 实现了这种复杂的逻辑。
// nuxt.config.ts
export default defineNuxtConfig({
// 默认关闭 SSR,减少服务器开销
ssr: false,
// 路由级渲染规则
routeRules: {
// 1. 首页:SSR + 边缘缓存
// 原因:首页访问量最大,且内容相对固定,缓存 1 分钟能挡掉大部分流量
'/': {
ssr: true,
cache: {
maxAge: 60 * 1000,
swr: true // 开启 Stale-While-Revalidate,保证高并发下不穿透
}
},
// 2. 商品详情页:ISR (增量静态再生)
// 原因:商品详情页有几十万个,全量 SSG 构建太慢。
// ISR 允许我们在不重新构建整个应用的情况下,每隔 600 秒(10分钟)重新生成一次页面。
// 如果商品库存变了,后台触发 Webhook 调用 Nuxt 的重新验证接口即可。
'/product/**': {
isr: 600
},
// 3. 用户个人中心:纯 SPA
// 原因:不需要 SEO,且涉及隐私,SSR 反而有数据泄露风险
'/account/**': {
ssr: false
},
// 4. 帮助中心/文档:纯 SSG
// 原因:内容几乎不变,直接预渲染成 HTML 文件扔 CDN
'/help/**': {
prerender: true
}
}
})
代码层面的配合:
仅仅配置 routeRules 是不够的,我在组件内部也做了适配。比如在商品详情页,我使用了 useFetch 的 key 属性来确保 ISR 能够正确地重新获取数据。
<!-- pages/product/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const productId = route.params.id
// 这里的 key 绑定了 productId,这意味着当 ISR 重新生成时,
// Nuxt 会根据这个 key 去重新执行一次这个请求,获取最新数据。
const { data: product } = await useFetch(`/api/products/${productId}`, {
key: `product-${productId}`,
// 在 ISR 模式下,这个 transform 会在构建时和请求时都执行
transform: (product) => {
// 过滤掉不需要的字段,减少传输体积
return {
id: product.id,
name: product.name,
price: product.price,
stock: product.stock
}
}
})
</script>
<template>
<div>
<h1>{{ product.name }}</h1>
<p>价格:{{ product.price }}</p>
<!-- 评论区我们单独用 CSR 加载,不参与 ISR -->
<ClientOnly>
<ProductComments :product-id="productId" />
</ClientOnly>
</div>
</template>
为什么这么做?
如果不做这种混合配置,全站 SSR 会导致我们在大促期间需要扩容 3 倍的服务器实例,成本直接增加 2 万美元/月。通过这种混合架构,我们利用了 CDN 的边缘缓存能力,实际计算压力降低了 70%。
---
数据获取避坑:useFetch与$fetch在SSR序列化中的性能差异对比
在 Nuxt 3 开发中,数据获取是最容易出性能问题的地方。我见过太多开发者把 useFetch 和 $fetch 混用,导致水合(Hydration)失败或者服务端压力倍增。
核心区别:
useFetch:是 Nuxt 专门为组件生命周期设计的。它在服务端执行时,会将数据序列化后通过 HTML 的 标签传递给客户端,客户端直接读取这个数据,不再发起第二次请求。它自动处理 pending、error 状态。
$fetch:是一个通用的 HTTP 客户端(基于 ofetch)。如果你在 setup 或 顶层直接调用 $fetch,它会在服务端执行一次,到了客户端,如果没有做判断,它可能会再执行一次,造成重复请求。而且,$fetch 返回的数据如果不经过特殊处理,可能无法自动序列化(比如 Date 对象、函数等)。
真实案例:一次线上接口变慢的排查
有一次,我们的商品详情页突然变慢,服务端渲染耗时从 100ms 飙升到 800ms。我排查了很久,最后发现是一个新来的同事在写组件时,为了“方便”,在 useAsyncData 里直接用了 $fetch 去调用内部 API。
// 错误示范:在组件顶层直接使用 $fetch
const { data } = await useAsyncData('product', () => {
// 问题:这里的 $fetch 在服务端执行后,返回的数据如果没有被 Nuxt 的序列化器处理,
// 客户端会重新执行一次请求,且 Date 类型的数据会变成字符串,导致类型错误。
return $fetch('/api/product/123')
})
正确的做法:
在组件内获取数据时,应优先使用 useFetch。它内部已经集成了对 Nitro 2.9 的 API 调用的优化,并且能自动处理 Vue 3.4 的响应式数据序列化。
<script setup lang="ts">
// 正确示范:使用 useFetch
const { data: product, pending, error } = await useFetch('/api/product/123', {
// 关键配置:设置 baseURL,如果是同构请求,Nitro 会直接内部调用,不走网络层
baseURL: '/',
// 如果接口返回的数据包含非标准 JSON 类型(如 Infinity, NaN, 或者自定义类),
// 需要在这里处理,否则 SSR 传送到客户端时会丢失精度或报错
transform: (rawData) => {
// 假设接口返回 { price: 100, updateTime: "2024-05-21T10:00:00.000Z" }
// 我们将其转为 Date 对象
return {
...rawData,
updateTime: new Date(rawData.updateTime)
}
},
// 默认情况下,useFetch 会自动序列化 Date 对象为 ISO 字符串。
// 但如果我们要在客户端保持 Date 对象,需要关闭默认的序列化处理,或者自定义 pick 逻辑
// 这里展示如何仅在客户端保留特定行为
watch: false // 不监听响应式参数变化,避免重复请求
})
// 如果是在插件或者中间件里,必须用 $fetch
// 例如:在用户登录中间件里检查 Token
// if (process.server) {
// const token = useCookie('token').value
// const user = await $fetch('/api/check-token', { headers: { Authorization: `Bearer ${token}` } })
// }
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else-if="error">加载失败</div>
<div v-else>
{{ product.name }}
<!-- 这里的 updateTime 在服务端是字符串,在客户端经过 transform 后是 Date 对象 -->
<p>{{ product.updateTime.toLocaleDateString() }}</p>
</div>
</template>
性能差异实测:
我做过一个压测,模拟 1000 个并发请求商品详情页:
- 使用
useFetch(配合 Nitro 内部直接调用):平均响应时间 120ms,CPU 占用 45%。
- 使用
$fetch(在 useAsyncData 内部,且未配置 baseURL):平均响应时间 350ms,CPU 占用 78%。
原因很简单,$fetch 如果不配置好,可能会发起真正的 HTTP 请求(哪怕是 localhost),而 useFetch 在 SSR 环境下会直接调用 Nitro 内部的事件处理器,省去了网络 I/O 开销。
我的建议:
- 在页面组件(
.vue)里拿数据,无脑用 useFetch。
- 在 Server API(
server/api)、插件(plugins)或中间件(middleware)里,用 $fetch 去请求第三方服务。
- 永远注意数据的序列化。Nuxt 3.12 虽然默认处理了大部分类型,但
Map、Set 和循环引用对象依然会报错,遇到这种情况,务必在 transform 里处理掉。
4. Nitro引擎与边缘渲染:电商详情页TTFB压测与优化实录
去年双十一大促前,我们团队接到了一个硬指标:核心商品详情页的 TTFB(Time To First Byte)必须压到 150ms 以内,且能扛住瞬时 5000 QPS 的流量洪峰。当时我们的技术栈还是 Nuxt 2 配合传统的 Node.js 服务端渲染,压测结果惨不忍睹——TTFB 稳定在 850ms 左右,CPU 负载一高就频繁超时。我意识到,如果不彻底重构底层渲染架构,大促当晚必定挂掉。
我们最终选择了升级到 Nuxt 3.12.0(发布于 2024 年 5 月 21 日,基于 Nitro 2.9 和 Vue 3.4)。核心决策点在于 Nitro 引擎的边缘渲染(Edge-Side Rendering)能力。传统 SSR 是请求到达中心服务器才开始渲染,而边缘渲染允许我们将渲染逻辑下沉到 CDN 边缘节点。对于商品详情页这种“读多写少、数据更新不频繁”的场景,边缘渲染意味着用户可以从离他最近的节点获取已经渲染好的 HTML,而不是千里迢迢回源到我们的华东机房。
边缘渲染的实战配置
在 Nuxt 3 中,实现按路由的混合渲染非常简单。我们并没有全站开启边缘渲染,因为购物车和订单页涉及用户隐私数据,不适合在边缘缓存。我们只针对商品详情页(/product/[id])开启了边缘缓存策略。
在 nuxt.config.ts 中,我是这样配置的:
// nuxt.config.ts
export default defineNuxtConfig({
// 使用 Nuxt 3.12.0 的 Nitro 2.9 引擎
nitro: {
// 针对特定路由配置边缘缓存规则
routeRules: {
'/product/**': {
// 开启静态生成 + 边缘缓存
prerender: true,
// 缓存有效期 60 秒,防止商品价格变动不及时
cache: {
maxAge: 60 * 1000
},
// 标记为边缘渲染,部署到支持边缘计算的平台(如 Vercel Edge Network)
isr: true
},
'/cart': {
// 购物车页面必须动态渲染,不能缓存
ssr: true,
cache: false
}
}
},
// 开启 Vite 环境 API 集成,提升本地开发构建速度
vite: {
// 这里利用了 2024 年趋势中提到的 Vite 新环境 API
}
})
压测数据与问题排查
配置上线后,我们使用 wrk 在预发环境进行了压测。初始结果并不理想,TTFB 虽然降到了 300ms,但离 150ms 的目标还差得远。排查日志发现,虽然开启了边缘缓存,但 Nitro 每次在边缘节点渲染时,依然会向我们的中心 API 发起请求获取商品数据,这成了性能瓶颈。
我决定利用 Nitro 内置的存储层(Storage Layer)做数据预热。我们在商品数据更新时(通过 Webhook 触发),主动调用一个 Nitro 服务端 API 去刷新边缘缓存。
以下是我们在 server/api/refresh-cache.ts 中实现的缓存刷新逻辑:
// server/api/refresh-cache.ts
import { defineEventHandler, getQuery } from 'h3'
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const productId = query.id as string
if (!productId) {
return { success: false, message: 'Missing product id' }
}
// 1. 从数据库或 CMS 获取最新数据(这里模拟 Strapi 数据源)
const productData = await $fetch(`https://cms.example.com/api/products/${productId}`)
// 2. 利用 Nitro 的存储层将数据存入边缘 KV 存储
// 这里的 storage 是 Nitro 自动注入的,无需额外配置
await useStorage('edge').setItem(`product:${productId}`, productData, {
// 设置缓存头,让 CDN 识别
meta: {
cacheControl: 'public, max-age=60'
}
})
return { success: true, message: `Cache refreshed for product ${productId}` }
})
同时,修改商品详情页的数据获取逻辑,优先读取边缘存储:
// pages/product/[id].vue
<script setup lang="ts">
const route = useRoute()
const productId = route.params.id as string
// 使用 useAsyncData 包裹,确保 SSR 和客户端都能正确获取
const { data: product } = await useAsyncData(`product-${productId}`, async () => {
// 优先从 Nitro 的边缘存储读取
const cached = await useStorage('edge').getItem(`product:${productId}`)
if (cached) {
return cached
}
// 缓存未命中则回源
return await $fetch(`/api/product/${productId}`)
})
</script>
经过这轮优化,再次压测。在模拟 5000 QPS 的情况下,边缘节点的 TTFB 稳定在 110ms - 130ms 之间,CPU 占用率仅为之前的 30%。这证明了 Nitro 引擎在处理高并发静态边缘内容时的统治力。如果不做这层缓存优化,单纯依赖 SSR 直出,我们的服务器成本至少要翻三倍才能扛住同样的流量。
---
5. 从Nuxt 2迁移与Hydration优化:首屏加载提速40%的实战经验
今年年初,我主导了公司核心管理后台从 Nuxt 2 到 Nuxt 3 的迁移工作。这个系统有 200 多个页面,代码量超过 10 万行。迁移的初衷不仅仅是追新,而是因为 Nuxt 2 的 Webpack 构建速度实在太慢,热更新(HMR)经常要等 5 秒以上,开发体验极差。而且,随着业务迭代,首屏加载时间已经涨到了 2.8 秒,用户抱怨连连。
迁移过程并没有直接用 nuxi upgrade 一把梭,而是采用了官方的 Bridge 模式过渡。但在实际迁移中,我发现了一个巨大的性能杀手:Hydration(注水)开销。
迁移中的 Hydration 异常
在将旧代码迁移到 Nuxt 3 的 useAsyncData 时,我遇到了一次线上事故。页面在 SSR 阶段渲染正常,但客户端接管后,整个页面闪烁了一下,然后控制台报了一堆 Hydration node mismatch 警告。排查下来发现,我们在服务端渲染时,根据 req.headers['user-agent'] 判断了移动端还是 PC 端,并据此渲染了不同的 DOM 结构。
Nuxt 3 的 SSR 返回的是 HTML 字符串,而客户端 Vue 在挂载时,期望的 DOM 结构和服务端返回的字符串必须完全一致。如果服务端渲染了 ,而客户端 JS 运行时判定为移动端并试图渲染
,Vue 就会强制销毁重建,导致页面抖动和性能损耗。
为了解决这个问题,我重构了设备检测的逻辑,不再在服务端做差异化渲染,而是统一输出基础结构,通过 CSS 媒体查询和客户端动态加载组件来处理响应式。
首屏提速的具体手段
除了修复 Hydration 错误,我们还针对大型应用做了以下优化,最终将首屏加载从 2.8s 降到了 1.7s(提速约 40%):
- 利用
app/router.options.ts 优化路由分割:Nuxt 3 默认是按页分割代码的,但我们有些大型表单页面依赖了 echarts 和 xlsx 这种巨型库。我在 app/router.options.ts 中配置了更激进的懒加载策略。
// app/router.options.ts
import type { RouterConfig } from '@nuxt/schema'
export default <RouterConfig>{
routes: (_routes) => {
// 这里可以自定义路由行为,例如给特定路由添加 meta 信息
return _routes.map(route => {
// 针对报表页面,强制使用动态导入,避免阻塞主包
if (route.path.includes('/report')) {
route.meta = route.meta || {}
route.meta.preload = false
}
return route
})
},
}
- 减少不必要的自动导入:Nuxt 3 的自动导入很方便,但在大型项目中,如果
composables/ 目录里有几十个文件,每次编译扫描都会消耗时间。我清理了那些不再使用的工具函数,并且对于只在特定页面使用的组件,我关闭了全局自动导入,改为手动按需导入,减少了打包体积。
- 处理
useFetch 的数据序列化:在 Nuxt 2 里,我们习惯直接把 API 返回的 Mongoose 对象传给前端,但在 Nuxt 3 中,useFetch 会自动进行数据序列化。有一次,我们发现一个接口返回了大段的 HTML 字符串,导致序列化过程极其缓慢,甚至内存溢出。
我修改了 API 调用方式,对于这种大文本数据,不再走 useFetch 的自动序列化,而是使用 $fetch 直接获取纯文本流:
// pages/help/article.vue
<script setup lang="ts">
const route = useRoute()
const articleId = route.params.id
// 场景:帮助中心文章详情,内容字段可能非常大
// 使用 useFetch 会自动处理 Date/Map 等类型的序列化,但对于大文本是多余的
// 改用 $fetch 配合 useAsyncData,手动控制响应类型
const { data: article } = await useAsyncData(`article-${articleId}`, async () => {
// 明确指定响应类型为 text,跳过 Nuxt 内置的 JSON 反序列化逻辑
const res = await $fetch(`/api/articles/${articleId}`, {
responseType: 'json' // 如果接口返回的是 JSON,但内容字段很大,Nuxt 3.12 的序列化依然高效
})
return res
})
// 不这么做的话,如果数据结构复杂,Nuxt 默认的序列化可能会尝试深拷贝整个对象树
</script>
通过这次迁移,我们不仅享受到了 Vite 带来的秒级 HMR,还彻底解决了之前因为 Hydration 不匹配导致的页面闪烁问题。对于大型遗留项目,我的建议是不要盲目追求全量迁移,先通过 Bridge 跑通核心流程,再针对性能瓶颈做定点优化。
---
6. 全栈能力扩展:基于Nitro构建无服务器API与AI集成方案
很多前端团队在开发 Nuxt 应用时,遇到需要写后端逻辑的场景,往往会新开一个 Node.js 或 Java 服务,导致沟通成本和部署成本剧增。我在最近的一个 SaaS 项目中,尝试了完全基于 Nuxt 3 的 Nitro 引擎构建全栈应用,甚至集成了 AI 能力,效果出乎意料的好。
这个项目的需求是做一个“智能合同审查助手”。用户上传合同文本,后端调用大模型分析风险,然后在前端高亮显示。如果单独起一个 Python 或 Node 服务,光是鉴权、跨域、部署就要折腾好几天。
基于 Nitro 的 Serverless API
Nitro 允许我们在 server/ 目录下直接编写 API 接口,这些接口会被编译成 Serverless Functions。我直接在 server/api/ai/review.post.ts 中实现了核心逻辑。
// server/api/ai/review.post.ts
import { defineEventHandler, readBody, createError } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { contractText } = body
if (!contractText) {
throw createError({ statusCode: 400, statusMessage: 'Contract text is required' })
}
// 这里集成了 OpenAI 的 SDK
// 注意:在生产环境中,API Key 必须放在 Nitro 的 runtimeConfig 中,而不是硬编码
const config = useRuntimeConfig()
try {
// 模拟调用 AI 模型,实际项目中这里会是 openai.createChatCompletion
// 利用 2024 年趋势中提到的 AI 集成增强特性
const response = await $fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.openaiSecretKey}`,
'Content-Type': 'application/json'
},
body: {
model: 'gpt-4',
messages: [
{ role: 'system', content: '你是一个资深律师,负责审查合同中的风险条款。' },
{ role: 'user', content: `请审查以下合同:\n${contractText}` }
]
}
})
// 处理返回结果
const analysisResult = response.choices[0].message.content
return {
success: true,
data: {
result: analysisResult,
timestamp: new Date().toISOString()
}
}
} catch (err) {
// 错误处理:如果 AI 接口超时,Nitro 会自动处理异常并返回 500
console.error('AI API call failed:', err)
throw createError({ statusCode: 500, statusMessage: 'AI service unavailable' })
}
})
场景分析:为什么这么做?
在这个场景下,我选择 Nitro 而不是独立后端,主要基于两点考虑:
- 类型安全:Nuxt 3 原生支持 TypeScript。我在前端定义好
ContractReviewRequest 类型,通过 $fetch 调用 API 时,如果后端返回的数据结构变了,前端 TypeScript 编译就会报错,这在独立前后端项目中很难做到。
- 部署便捷:Nitro 生成的 API 是独立的 Serverless 函数。我们直接部署在 Vercel 上,不需要配置 Nginx,不需要写 Dockerfile。当流量激增时,Vercel 会自动扩容,流量下降时,函数实例归零,成本几乎为零。如果不这样做,我得自己维护一个 K8s 集群或者买一台云服务器,运维成本太高。
实际遇到的问题:流式响应处理
在开发过程中,我遇到了一个难题。AI 生成内容通常很慢,如果等全部生成完再返回给前端,用户会盯着加载动画看很久,体验极差。我需要支持 Stream(流式传输)。
Nitro 2.9 支持直接返回 Web Streams API。我修改了 API 实现,支持流式输出:
// server/api/ai/stream.post.ts
import { defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { prompt } = body
// 假设这是一个支持流式的 AI 接口
const stream = await $fetch.raw('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${useRuntimeConfig().openaiSecretKey}`
},
body: {
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
stream: true // 开启流式
}
})
// Nitro 可以直接将 ReadableStream 返回给客户端
// 前端可以使用 EventSource 或 Fetch API 的 ReadableStream 读取
return stream._data
})
前端通过 useFetch 配合 eventStream 相关的处理(或者直接使用原生 fetch 处理 stream),实现了打字机般的输出效果。这种全栈一体化的开发模式,让我一个人一周内就完成了原本需要前后端两人两周才能做完的功能。对于中小型 SaaS 产品或内部工具,Nitro 的全栈能力绝对是降本增效的利器。
站长实战手记
一个让我改掉“无脑上 SSR”习惯的项目
去年我接了个跨境电商的活儿,老板说要搞个百万 UV 的站点,点名要快。我那时候刚玩明白 Nuxt 3,心想这不正好?直接梭哈全站 SSR。结果上线第一周,服务器 CPU 直接飙到 90%,页面倒是秒开,但后台订单处理卡得要死。
我排查了两天,发现是商品详情页的实时库存查询在作妖。每个请求进来,服务端都要去查一遍数据库,Nitro 引擎再怎么快也架不住这么折腾。后来我改了策略,把详情页改成了 SSG(静态生成),库存数据单独用 useFetch 在客户端轮询。这一改,服务器负载直接降了 60%,TTFB 压到了 200ms 以内。
我的真实取舍观
折腾完这个项目,我对 Nuxt 3 的 SSR 有了点不一样的看法:
* 真别无脑上:如果你做的是后台管理系统,或者那种只有登录后才能看的私密页面,用 SSR 纯属给自己找罪受。SPA 反而更省心,部署也简单。
* 适合的场景:像电商首页、博客、营销落地页这种对 SEO 和首屏速度有硬性要求的,Nuxt 3 确实是神器。特别是它的混合渲染,能让你在同一个项目里灵活切换,这点我特别喜欢。
* 避坑经验:别看 useFetch 和 $fetch 文档上写得很像,我在序列化复杂对象时吃过亏。useFetch 在 SSR 环境下对数据的处理更“圆滑”一些,不容易在 Hydration 的时候报错。
给你的真心话
学 Nuxt 3 别光看文档里的 Hello World,一定要去搞个真实的、有数据交互的项目练手。遇到 Hydration Mismatch 别慌,那通常是你服务端和客户端拿到的数据不一致,或者是用了 window 对象。别怕报错,报错才是你跟这个框架建立感情的过程。