第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\n
\n\n
\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 点
- Nuxt 3 是 Vue 的 SSR 框架——约定式路由、零配置启动、自动服务端渲染
useFetch是数据获取核心——服务端执行一次、客户端复用、支持响应式参数- SEO 靠
useHead/useSeoMeta——让搜索引擎能看懂你的页面
延伸学习资源
- 📖 Nuxt 3 官方文档(最权威,有中文)
- 📖 Vue 3 核心知识(基础不扎实 Nuxt 也学不深)
- 📖 《Vue.js 设计与实现》(好未来前端团队著,深入理解 Vue 原理)
互动钩子:你在做网站的时候遇到过「SEO 怎么都做不上去」的困扰吗?或者首屏加载慢被用户吐槽过?评论区聊聊你是怎么解决的,老粉优先回复!
📢 下期预告:学会了 Nuxt 3 基础,下一章我们要做一个「完整后台管理系统」——组合所有学过的知识(组件、路由、API、SSR),一个真实可用的后台系统等你来挑战!

评论(0)