第6章 6.4 服务端渲染 SSR:Nuxt 3 入门

⏱️ 学习节奏:90 分钟
🎯 难度定位:进阶(需要懂 Vue 3 基础)


🎯 开场 3 分钟:为什么要学这个?

你有没有遇到过这种情况?

场景还原:你做了一个很棒的个人博客,用 Vue 写的,放到网上分享给朋友。结果朋友搜了半天搜不到,或者搜到了点进去,发现标题是空的、描述是乱的——百度/谷歌根本看不懂你写的啥

更惨的是,首屏加载慢得要命,用户还没看到内容就跑了。

这就是 SPA(单页面应用) 的原罪:搜索引擎看不到内容,加载要等 JS 跑完才显示。

那怎么办?

答案是 SSR(Server-Side Rendering,服务端渲染)——让网页在服务器上就渲染好,搜索引擎能抓到、用户打开就能看。

这一章,我们来学 Nuxt 3——Vue 生态里最成熟的 SSR 框架。学完你能:做 SEO 友好的网站、首屏秒开、用户体验 up up。


🧱 基础 25 分钟:核心概念(小白视角)

6.4\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n.1 Nuxt 3 是什么?

生活类比:想象你去餐厅吃饭。

  • 普通 Vue(SPA):你进了餐厅,菜单是空白的,等服务员拿来菜单才能点菜(慢)
  • Nuxt 3(SSR):你进了餐厅,菜单已经摆在桌上了,直接点就行(快)

Nuxt 3 就是那个「提前把菜单摆好」的服务员——在服务器上把网页渲染好,用户打开就能用。

为什么用它?

痛点 Nuxt 3 解决方式
SEO 差 页面在服务器渲染好,搜索引擎直接抓取
首屏慢 服务器返回完整 HTML,不用等 JS 下载执行
配置烦 约定式路由、自动导入、零配置启动

6.4.2 快速安装:3 分钟跑起来

打开终端,一行命令搞定:

npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev

然后浏览器打开 http://localhost:3000,你就能看到欢迎页面。

小提示:安装时选「No」跳过 TypeScript(新手先别给自己加难度)

6.4.3 Nuxt 3 目录结构:每个文件夹干嘛的

Nuxt 3 最大的特点就是「约定式路由」——你放什么文件,就自动有什么路由,不用手动配置。

my-nuxt-app/
├── pages/          # 👉 页面文件夹,放路由页面
│   ├── index.vue   # → / 首页
│   └── about.vue   # → /about 关于页
├── components/     # 👉 组件文件夹,自动导入
│   └── Hello.vue   # 直接用,不用 import
├── server/         # 👉 服务端代码(API 接口)
│   └── api/
│       └── hello.ts
├── nuxt.config.ts  # 👉 配置文件
└── app.vue          # 👉 根组件

6.4.4 写第一个页面:pages/index.vue

Nuxt 3 的页面就是一个普通的 Vue 组件,但有一些特殊能力。

<script setup>
// Nuxt 3 的 script setup,自动在服务端运行
const message = ref('你好,世界!')
const count = ref(0)

function addCount() {
count.value++
}
</script>

<template>
<div>
<h1>{{ message }}</h1>
<p>点击次数:{{ count }}</p>
<button @click="addCount">点我 +1</button>
</div>
</template>

这 5 行关键点

代码 解释
<script setup> Nuxt 3 语法糖,自动导入+自动在服务端运行
ref('你好') 创建响应式变量
{{ message }} 双括号输出,类似 Vue
@click 点击事件,类似 Vue
count.value++ ref 变量要 .value 才能修改

6.4.5 约定式路由:文件结构 = 路由表

这是 Nuxt 3 最爽的地方——不用 router-link 配置,文件名就是路由

pages/
├── index.vue          → /
├── about.vue          → /about
├── user/
│   ├── index.vue      → /user
│   └── profile.vue    → /user/profile
└── blog/
└── [id].vue       → /blog/:id  动态路由

创建一个 pages/blog/[id].vue

<script setup>
// 动态路由参数,通过 route.params 获取
const route = useRoute()
const { id } = route.params
</script>

<template>
<div>
<h1>博客文章 ID:{{ id }}</h1>
<p>访问 /blog/123 就显示「123」</p>
</div>
</template>

6.4.6 useFetch:数据获取的利器

Nuxt 3 提供了一个超好用的 useFetch,它在服务端运行一次,客户端复用,省流量又快。

<script setup>
// useFetch 自动在服务端执行,客户端直接用结果
const { data, pending, error } = await useFetch('/api/hello', {
// 懒加载(客户端才请求)
lazy: false
})
</script>

<template>
<div>
<!-- pending:加载中状态 -->
<p v-if="pending">加载中...</p>

<!-- error:错误状态 -->
<p v-else-if="error">出错了:{{ error.message }}</p>

<!-- data:数据 -->
<p v-else>服务端返回:{{ data }}</p>
</div>
</template>

useFetch 的优势

  • ✅ 服务端先请求数据,HTML 里就有内容(SEO 友好)
  • ✅ 客户端复用缓存,不重复请求
  • ✅ 自动类型提示(TypeScript)

6.4.7 创建 API 接口:server/api

Nuxt 3 可以轻松写服务端 API,在 server/api/ 目录下放文件就行。

创建 server/api/users.ts

// 服务端 API,返回用户列表
export default defineEventHandler((event) => {
const users = [
{ id: 1, name: '张三', city: '北京' },
{ id: 2, name: '李四', city: '上海' },
{ id: 3, name: '王五', city: '广州' }
]

return {
success: true,
data: users
}
})

然后在页面里用 useFetch 调用:

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

<template>
<div>
<h2>用户列表</h2>
<ul>
  <li v-for="user in data?.data" :key="user.id">
    {{ user.name }} - {{ user.city }}
  </li>
</ul>
</div>
</template>

🔥 实战 35 分钟:3 个递进的小项目

📦 项目 1(5 分钟):个人介绍页

目标:创建一个带动态数据的个人介绍页,理解 Nuxt 3 基础用法。

完整代码 - pages/me.vue

<script setup>
// 页面数据
const profile = {
name: '小明',
age: 25,
skills: ['Vue', 'Node.js', 'Python'],
hobby: '写代码、健身、撸猫'
}

// 当前时间(展示服务端渲染)
const now = new Date().toLocaleString('zh-CN')
</script>

<template>
<main class="container">
<h1>👋 你好,我叫 {{ profile.name }}</h1>

<section>
  <h2>基本信息</h2>
  <p>年龄:{{ profile.age }} 岁</p>
  <p>当前时间(服务端渲染):{{ now }}</p>
</section>

<section>
  <h2>🛠️ 技术栈</h2>
  <ul>
    <li v-for="skill in profile.skills" :key="skill">
      {{ skill }}
    </li>
  </ul>
</section>

<section>
  <h2>💡 爱好</h2>
  <p>{{ profile.hobby }}</p>
</section>
</main>
</template>

<style scoped>
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
section {
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
</style>

预期输出:访问 /me 看到个人信息,当前时间每次刷新会更新(因为是服务端渲染)。

一句话解释profile 对象定义在 <script setup> 里,模板直接用,Nuxt 3 自动在服务端渲染这段内容。


📦 项目 2(15 分钟):博客文章列表(带 API)

目标:做一个博客列表页,从服务端 API 获取数据,理解 useFetch 和 SEO 优化。

Step 1:创建博客数据 API

server/api/posts.ts

export default defineEventHandler(() => {
const posts = [
{
  id: 1,
  title: 'Vue3 入门完全指南',
  excerpt: '从零开始学会 Vue3,看这篇就够了',
  author: '小明',
  date: '2024-01-15',
  tags: ['Vue', '前端']
},
{
  id: 2,
  title: 'Nuxt 3 实战:SSR 入门',
  excerpt: 'SEO 优化和首屏加载,看这篇就会',
  author: '小明',
  date: '2024-01-20',
  tags: ['Nuxt', 'SSR']
},
{
  id: 3,
  title: 'TypeScript 速成指南',
  excerpt: '10 分钟学会 TypeScript 基础',
  author: '李四',
  date: '2024-01-25',
  tags: ['TS', '前端']
}
]

return {
total: posts.length,
posts: posts
}
})

Step 2:创建博客列表页

pages/blog/index.vue

<script setup>
// useFetch 在服务端执行一次,获取文章列表
const { data, pending, error } = await useFetch('/api/posts')

// SEO 元信息(Nuxt 3 用 useHead 设置)
useHead({
title: '博客列表 - 小明的技术笔记',
meta: [
{ name: 'description', content: '前端技术博客,包含 Vue、Nuxt、TypeScript 等' }
]
})
</script>

<template>
<div class="blog-list">
<h1>📝 我的博客</h1>

<!-- 加载状态 -->
<div v-if="pending" class="loading">
  加载中...
</div>

<!-- 错误状态 -->
<div v-else-if="error" class="error">
  出错了:{{ error.message }}
</div>

<!-- 文章列表 -->
<div v-else class="posts">
  <article v-for="post in data?.posts" :key="post.id" class="post-card">
    <h2>
      <NuxtLink :to="`/blog/${post.id}`">
        {{ post.title }}
      </NuxtLink>
    </h2>
    <p class="excerpt">{{ post.excerpt }}</p>
    <div class="meta">
      <span>👤 {{ post.author }}</span>
      <span>📅 {{ post.date }}</span>
      <span v-for="tag in post.tags" :key="tag" class="tag">
        {{ tag }}
      </span>
    </div>
  </article>
</div>
</div>
</template>

<style scoped>
.blog-list {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.post-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.post-card h2 a {
color: #333;
text-decoration: none;
}
.post-card h2 a:hover {
color: #42b883;
}
.excerpt {
color: #666;
margin: 10px 0;
}
.meta {
display: flex;
gap: 15px;
font-size: 14px;
color: #999;
}
.tag {
background: #e8f5e9;
padding: 2px 8px;
border-radius: 4px;
color: #2e7d32;
}
.loading, .error {
text-align: center;
padding: 40px;
color: #666;
}
</style>

预期输出:访问 /blog 看到3篇文章的列表,标题可点击,标签显示正确。

一句话解释useFetch('/api/posts') 在服务端请求数据,HTML 里直接有内容(SEO 友好),useHead 设置页面标题和描述。


📦 项目 3(15 分钟):天气查询小工具

目标:组合 Nuxt 3 的 API 能力 + SEO 优化,做一个实用的天气查询页。

Step 1:创建天气 API(模拟)

server/api/weather.ts

export default defineEventHandler((event) => {
// 获取查询参数
const query = getQuery(event)
const city = (query.city as string) || '北京'

// 模拟天气数据(实际项目用和风天气等 API)
const weatherData: Record<string, any> = {
'北京': { temp: 22, weather: '☀️ 晴', humidity: 45 },
'上海': { temp: 25, weather: '⛅ 多云', humidity: 60 },
'广州': { temp: 28, weather: '🌧️ 雨', humidity: 80 },
'深圳': { temp: 27, weather: '⛈️ 雷阵雨', humidity: 75 },
'成都': { temp: 20, weather: '🌫️ 雾', humidity: 70 }
}

const data = weatherData[city] || {
temp: 20,
weather: '❓ 未知',
humidity: 50
}

return {
city,
...data,
updateTime: new Date().toLocaleString('zh-CN')
}
})

Step 2:创建天气查询页

pages/weather.vue

<script setup {
// 默认查询城市
const cityName = ref('北京')

// 表单提交
async function searchWeather() {
// 触发 useFetch 重新请求
refresh()
}

// 带参数的数据请求
const { data, pending, error, refresh } = await useFetch('/api/weather', {
query: { city: cityName },
// 懒加载(输入完才请求)
lazy: true
})

// SEO
useHead({
title: '天气查询 - Nuxt 3 小工具',
meta: [
{ name: 'description', content: '简单实用的天气查询工具' }
]
})
</script>

<template>
<div class="weather-app">
<h1>🌤️ 天气查询</h1>

<!-- 搜索框 -->
<div class="search-box">
  <input
    v-model="cityName"
    type="text"
    placeholder="输入城市名,如:北京、上海、广州"
    @keyup.enter="searchWeather"
  />
  <button @click="searchWeather">查询</button>
</div>

<!-- 支持的城市提示 -->
<p class="tips">支持:北京、上海、广州、深圳、成都</p>

<!-- 加载状态 -->
<div v-if="pending" class="loading">查询中...</div>

<!-- 错误状态 -->
<div v-else-if="error" class="error">
  查询失败:{{ error.message }}
</div>

<!-- 天气结果 -->
<div v-else-if="data" class="weather-result">
  <div class="city-name">{{ data.city }}</div>
  <div class="weather-info">
    <span class="temp">{{ data.temp }}°C</span>
    <span class="weather">{{ data.weather }}</span>
  </div>
  <div class="detail">
    <p>湿度:{{ data.humidity }}%</p>
    <p>更新时间:{{ data.updateTime }}</p>
  </div>
</div>
</div>
</template>

<style scoped>
.weather-app {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.search-box {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.search-box input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
}
.search-box button {
padding: 10px 20px;
background: #42b883;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.tips {
font-size: 12px;
color: #999;
margin-bottom: 20px;
}
.weather-result {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 16px;
text-align: center;
}
.city-name {
font-size: 28px;
margin-bottom: 15px;
}
.weather-info {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.temp {
font-size: 48px;
font-weight: bold;
}
.weather {
font-size: 24px;
}
.detail {
font-size: 14px;
opacity: 0.9;
}
.loading, .error {
padding: 30px;
text-align: center;
}
.error {
color: #e53935;
}
</style>

预期输出:输入城市名点击查询,显示对应天气信息;默认显示北京的天气。

一句话解释useFetch 配合 query 参数动态请求,refresh() 函数可以手动刷新数据,实现交互效果。


💪 进阶 20 分钟:常见坑 + 性能小贴士

❌ 坑 1:本地存储(localStorage)在服务端不能用

// ❌ 错误:服务端没有 window 对象
const token = localStorage.getItem('token')

// ✅ 正确:判断环境
onMounted(() => {
// 客户端才执行
const token = localStorage.getItem('token')
})

❌ 坑 2:给 useFetch 传对象参数要小心

// ❌ 错误:对象参数会被转成 [object Object]
const params = { id: 1 }
useFetch('/api/user', { params })

// ✅ 正确:用 ref 响应式参数
const id = ref(1)
useFetch('/api/user', { query: { id } })

❌ 坑 3:asyncData vs useFetch 混淆

Nuxt 3 已经废弃 asyncData,统一用 useFetch

// ❌ 淘汰写法
asyncData() {
return { ... }
}

// ✅ 正确写法
const { data } = await useFetch('/api/xxx')

❌ 坑 4:动态路由 params 要用 route.params

// ❌ 错误:直接用 props
const props = defineProps(['id'])

// ✅ 正确:用 useRoute 获取 params
const route = useRoute()
const { id } = route.params

❌ 坑 5:SEO 元信息要放在顶层

// ❌ 错误:放在条件语句里
if (data.value) {
useHead({ title: data.value.title })
}

// ✅ 正确:始终调用(可以用 computed)
useHead({
title: computed(() => data.value?.title || '默认标题')
})

⚡ 性能小贴士:合理使用 lazy 选项

// 非关键数据:用户不立即需要的内容用 lazy
const { data: comments } = useFetch('/api/comments', { lazy: true })

// 关键数据:首屏需要的内容不用 lazy
const { data: userInfo } = useFetch('/api/user')

🔧 调试技巧:用 useSeoMeta 快速设置 SEO

// 简单 SEO 用这个,一行搞定
useSeoMeta({
title: '我的博客',
ogTitle: '我的博客',
description: '前端技术博客',
ogDescription: '前端技术博客'
})

✏️ 练习题 + 作业题(共 7 分钟)

练习题(5 道,10 分钟内完成)

练习 1(1 分钟):改个名字
- 输入:在项目 1 的 profile 里把 name 改成你自己的名字
- 预期输出:页面显示你的名字
- 提示:直接改字符串就行

练习 2(2 分钟):加个判断
- 输入:在项目 1 的年龄下面,加一个判断,≥18 显示「已成年」,<18 显示「未成年」
- 预期输出:年龄判断结果
- 提示:用三元运算符 condition ? a : b

练习 3(3 分钟):加一篇新文章
- 输入:在项目 2 的 server/api/posts.ts 里加一篇你自己的文章
- 预期输出:列表里多了一篇
- 提示:新对象加到 posts 数组里,id 要不同

练习 4(4 分钟):串起 API 和页面
- 输入:给项目 3 的天气 API 加一个新字段「风力」(wind),然后在页面显示它
- 预期输出:天气结果里多了风力信息
- 提示:API 返回新字段,模板里用 {{ data.wind }} 显示

练习 5(5 分钟):分析报错
- 输入:下面这段代码有什么问题?

<script setup>
const id = 1
const { data } = await useFetch('/api/user', {
params: { id }
})
</script>
  • 预期输出:指出问题并修复
  • 提示:params 要改成 query,且要用响应式 ref

💻 作业:做一个「名言警句」网站

需求描述:做一个展示名人名言的网站,每句话带作者和标签。

功能点
1. 主页显示名言列表(从 API 获取)
2. 点击名言进入详情页(动态路由 /quote/[id]
3. 每个页面有正确的 SEO 标题和描述

加分项
1. 加一个「随机名言」按钮
2. 加一个简单的搜索功能(按作者搜索)

验收标准
- 能跑起来(npm run dev
- 访问 /quotes 能看到列表
- 点击名言能进入详情页
- 页面标题显示正确

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源(5 分钟)

这一章的核心 3 点

  1. Nuxt 3 是 Vue 的 SSR 框架——约定式路由、零配置启动、自动服务端渲染
  2. useFetch 是数据获取核心——服务端执行一次、客户端复用、支持响应式参数
  3. SEO 靠 useHead/useSeoMeta——让搜索引擎能看懂你的页面

延伸学习资源

  • 📖 Nuxt 3 官方文档(最权威,有中文)
  • 📖 Vue 3 核心知识(基础不扎实 Nuxt 也学不深)
  • 📖 《Vue.js 设计与实现》(好未来前端团队著,深入理解 Vue 原理)

互动钩子:你在做网站的时候遇到过「SEO 怎么都做不上去」的困扰吗?或者首屏加载慢被用户吐槽过?评论区聊聊你是怎么解决的,老粉优先回复!

📢 下期预告:学会了 Nuxt 3 基础,下一章我们要做一个「完整后台管理系统」——组合所有学过的知识(组件、路由、API、SSR),一个真实可用的后台系统等你来挑战!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。