Vue3 SSR / Nuxt 入门
大约 8 分钟约 2500 字
Vue3 SSR / Nuxt 入门
简介
服务端渲染(SSR)在服务器生成 HTML 后发送给客户端,提升首屏加载速度和 SEO。Nuxt 是基于 Vue3 的全栈框架,提供了开箱即用的 SSR、路由、数据获取和部署方案。
特点
Nuxt 项目
创建项目
# 创建 Nuxt 项目
npx nuxi@latest init my-app
cd my-app
npm install
npm run dev目录结构
my-app/
├── nuxt.config.ts # Nuxt 配置
├── app.vue # 根组件
├── pages/ # 路由页面(自动路由)
│ ├── index.vue
│ ├── about.vue
│ └── users/
│ ├── index.vue
│ └── [id].vue # 动态路由
├── components/ # 自动导入组件
│ ├── AppHeader.vue
│ └── UserCard.vue
├── composables/ # 自动导入组合式函数
│ └── useAuth.ts
├── server/ # 服务端 API
│ └── api/
│ └── users.ts
├── public/ # 静态资源
└── layouts/ # 布局组件
└── default.vue页面和路由
<!-- pages/index.vue -->
<template>
<div>
<h1>首页</h1>
<p>{{ greeting }}</p>
</div>
</template>
<script setup lang="ts">
const greeting = ref('欢迎使用 Nuxt')
</script><!-- pages/users/[id].vue -->
<template>
<div>
<h1>用户详情: {{ user?.name }}</h1>
<p>邮箱: {{ user?.email }}</p>
<NuxtLink to="/users">返回列表</NuxtLink>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: user } = await useFetch(`/api/users/${route.params.id}`)
</script>数据获取
useFetch
<template>
<div>
<div v-if="pending">加载中...</div>
<div v-else-if="error">加载失败: {{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
</li>
</ul>
<button @click="refresh">刷新</button>
</div>
</template>
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const { data: users, pending, error, refresh } = await useFetch<User[]>('/api/users', {
lazy: false, // 等待数据加载完成
server: true, // 服务端也执行
default: () => [], // 默认值
headers: {
'Accept-Language': 'zh-CN'
}
})
</script>服务端 API
// server/api/users.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const size = Number(query.size) || 20
// 模拟数据库查询
const users = [
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' },
]
return {
items: users,
total: users.length,
page,
size
}
})// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// 查询数据库
const user = { id: Number(id), name: '张三', email: 'zhang@example.com' }
if (!user) {
throw createError({
statusCode: 404,
statusMessage: '用户不存在'
})
}
return user
})布局和中间件
布局系统
<!-- layouts/default.vue -->
<template>
<div class="app">
<AppHeader />
<main>
<slot />
</main>
<AppFooter />
</div>
</template><!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<AdminSidebar />
<main>
<slot />
</main>
</div>
</template><!-- pages/admin/dashboard.vue -->
<template>
<div>管理面板</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
</script>路由中间件
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const token = useCookie('auth-token')
if (!token.value) {
return navigateTo('/login')
}
})<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
</script>Nuxt 配置
nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
app: {
head: {
title: 'My Nuxt App',
meta: [
{ name: 'description', content: '基于 Nuxt 3 的应用' }
]
}
},
modules: [
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
],
runtimeConfig: {
// 服务端私有
dbUrl: process.env.DATABASE_URL,
// 公共(暴露给客户端)
public: {
apiBase: process.env.API_BASE || '/api'
}
},
nitro: {
preset: 'node-cluster' // 部署模式
}
})渲染模式对比
混合渲染与路由规则
// nuxt.config.ts — 混合渲染配置
export default defineNuxtConfig({
routeRules: {
// 首页 — SSR,缓存 60 秒(ISR 模式)
'/': { prerender: false, swr: 60 },
// 博客文章 — 预渲染为静态 HTML(SSG)
'/blog/**': { prerender: true },
// API 文档 — 预渲染
'/docs/**': { prerender: true },
// 管理后台 — 纯客户端渲染(SPA)
'/admin/**': { ssr: false },
// 产品列表 — ISR,缓存 1 小时
'/products': { swr: 3600 },
// 产品详情 — ISR,缓存 10 分钟
'/products/**': { swr: 600 },
}
})状态管理与 SSR
// composables/useAuth.ts — SSR 兼容的状态管理
export function useAuth() {
// useCookie — 服务端和客户端都能访问的 Cookie
const token = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 7, // 7 天
path: '/',
})
const user = useState<User | null>('auth-user', () => null)
const isAuthenticated = computed(() => !!token.value && !!user.value)
async function login(credentials: { email: string; password: string }) {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials,
})
token.value = response.token
user.value = response.user
}
function logout() {
token.value = null
user.value = null
}
// SSR 时自动获取用户信息
if (token.value && !user.value) {
const data = await useFetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token.value}` }
})
if (data.data.value) {
user.value = data.data.value
}
}
return { token, user, isAuthenticated, login, logout }
}SEO 优化
// pages/blog/[slug].vue — SEO 优化示例
<template>
<div>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</article>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
// useHead — 动态设置页面 meta
useHead({
title: computed(() => `${post.value?.title || '加载中...'} - 我的博客`),
meta: [
{ name: 'description', content: computed(() => post.value?.excerpt || '') },
{ property: 'og:title', content: computed(() => post.value?.title || '') },
{ property: 'og:description', content: computed(() => post.value?.excerpt || '') },
{ property: 'og:image', content: computed(() => post.value?.coverImage || '') },
{ property: 'og:type', content: 'article' },
{ name: 'twitter:card', content: 'summary_large_image' },
],
})
// useSeoMeta — 更简洁的 SEO 设置
useSeoMeta({
title: computed(() => post.value?.title || ''),
ogTitle: computed(() => post.value?.title || ''),
description: computed(() => post.value?.excerpt || ''),
ogDescription: computed(() => post.value?.excerpt || ''),
ogImage: computed(() => post.value?.coverImage || ''),
})
const { data: post } = await useFetch(`/api/blog/${route.params.slug}`)
// JSON-LD 结构化数据
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.value?.title,
datePublished: post.value?.createdAt,
author: {
'@type': 'Person',
name: post.value?.author.name,
},
}),
},
],
})
</script>错误处理与错误页面
<!-- error.vue — 全局错误页面 -->
<template>
<div class="error-page">
<h1>{{ error.statusCode }}</h1>
<h2>{{ error.message }}</h2>
<p v-if="error.statusCode === 404">页面不存在</p>
<p v-else>出了点问题</p>
<button @click="handleError">返回首页</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
error: {
statusCode: number
message: string
data?: any
}
}>()
const handleError = () => clearError({ redirect: '/' })
useHead({
title: computed(() => `错误 ${props.error.statusCode}`)
})
</script><!-- pages/users/[id].vue — 页面级错误处理 -->
<script setup lang="ts">
const route = useRoute()
const { data: user, error, pending } = await useFetch(`/api/users/${route.params.id}`, {
// 404 时自动跳转到错误页面
onResponseError({ response }) {
if (response.status === 404) {
throw createError({
statusCode: 404,
statusMessage: '用户不存在',
})
}
}
})
</script>Nitro 服务端工具
// server/utils/db.ts — 数据库连接
import { defineNitroPlugin } from 'nitropack/runtime'
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
const { Pool } = pg
let _pool: pg.Pool | null = null
export function useDb() {
if (!_pool) {
const config = useRuntimeConfig()
_pool = new Pool({
connectionString: config.dbUrl,
})
}
return drizzle(_pool)
}
// server/middleware/logger.ts — 服务端日志中间件
export default defineEventHandler((event) => {
const start = Date.now()
const method = event.method
const path = getRequestURL(event).pathname
event.node.res.on('finish', () => {
const duration = Date.now() - start
const status = event.node.res.statusCode
console.log(`[${method}] ${path} ${status} - ${duration}ms`)
})
})部署配置
// nuxt.config.ts — 不同部署环境
export default defineNuxtConfig({
// Node.js 服务器
nitro: {
preset: 'node-cluster',
},
// 或者部署到 Vercel
// nitro: {
// preset: 'vercel',
// },
// 或者部署到静态托管(SSG 模式)
// nitro: {
// preset: 'static',
// },
// 或者部署到 Docker
// Dockerfile:
// FROM node:20-alpine
// WORKDIR /app
// COPY . .
// RUN npm ci
// RUN npm run build
// EXPOSE 3000
// CMD ["node", ".output/server/index.mjs"]
})SSR 性能优化
// 1. 组件懒加载 — 客户端专用组件
// nuxt.config.ts
export default defineNuxtConfig({
components: [
{
path: '~/components/global', // 服务端渲染
},
{
path: '~/components/client-only',
prefix: 'Client',
global: true,
lazy: true, // 客户端懒加载
},
],
})
// 2. ClientOnly 组件 — 仅客户端渲染
<template>
<ClientOnly>
<HeavyChart :data="chartData" />
<template #fallback>
<div class="chart-skeleton">加载图表...</div>
</template>
</ClientOnly>
</template>
// 3. 数据缓存策略
const { data: config } = await useFetch('/api/config', {
// getCachedData — 使用 payload 缓存
getCachedData(key, nuxtApp) {
return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
},
})
// 4. 页面预加载
<NuxtLink to="/about" prefetch>关于我们</NuxtLink>| 模式 | 说明 | 适用场景 |
|---|---|---|
| SSR | 服务端渲染 | SEO 重要、首屏要求高 |
| SSG | 静态生成 | 内容不常变化 |
| SPA | 客户端渲染 | 管理后台、内部系统 |
| Hybrid | 混合模式 | 部分页面 SSR、部分 SPA |
优点
缺点
总结
Nuxt 核心:基于文件的路由(pages/)、自动导入组件(components/)、服务端 API(server/api/)。数据获取用 useFetch 统一处理 SSR 和客户端。布局系统用 layouts/ 和 definePageMeta。路由中间件做权限控制。渲染模式按需选择:SEO 场景用 SSR、内容站用 SSG、管理后台用 SPA。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《Vue3 SSR / Nuxt 入门》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Vue3 SSR / Nuxt 入门》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Vue3 SSR / Nuxt 入门》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Vue3 SSR / Nuxt 入门》最大的收益和代价分别是什么?
