vue

Vue 3 SSR 与 Nuxt 3

By AI-Writer 18 min read

Vue 3 SSR 与 Nuxt 3

服务端渲染(SSR)是现代前端架构的重要话题,它可以提升首屏加载性能、改善 SEO、并提供更好的用户体验。Nuxt 3 是 Vue 生态中最成熟的 SSR 框架,本文详细介绍其核心概念和使用方法。

为什么需要 SSR

SPA 的问题

传统 SPA(单页面应用)存在以下问题:

  • 首屏白屏:浏览器需要下载并执行 JS 后才能渲染内容
  • SEO 不友好:搜索引擎爬虫可能无法正确索引动态内容
  • 首屏加载慢:所有资源打包在一个大文件中

SSR 的优势

  • 更快的内容呈现:服务器返回完整的 HTML,用户立即看到内容
  • 更好的 SEO:搜索引擎可以直接抓取 HTML 内容
  • 更好的性能感知:减少首屏等待时间

SSR 的挑战

  • 更高的服务器负载:每个请求都需要服务端渲染
  • 复杂的状态管理:需要区分服务端和客户端的数据
  • 更多的调试复杂度:需要在服务端和客户端都进行调试

Vue 3 SSR 基础

使用 Vite SSR

bash
npm install vite-ssr
javascript
// server.js
import express from 'express'
import { renderToString } from 'vue/server'
import { createSSRApp } from 'vue'
import App from './src/App.vue'

const server = express()

server.get('*', async (req, res) => {
  const app = createSSRApp(App)

  // 服务端数据预取
  app.provide('serverData', { url: req.url })

  const html = await renderToString(app)

  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>SSR App</title></head>
      <body>
        <div id="app">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `)
})

server.listen(3000)

服务端与客户端差异

vue
<script setup>
// 在 setup 中访问 window 会导致 SSR 失败
// 错误:
// const width = window.innerWidth

// 正确:使用 import.meta.env.SSR 判断
import { ref, onMounted, onServerPrefetch } from 'vue'

const data = ref(null)

// 服务端预取数据
onServerPrefetch(async () => {
  // 仅在服务端执行
  const response = await fetch('https://api.example.com/data')
  data.value = await response.json()
})

// 客户端挂载后的操作
onMounted(() => {
  // 仅在客户端执行
  console.log('Client mounted')
})

// 通用逻辑(服务端和客户端都执行)
const config = { apiUrl: '/api' }
</script>

useFetch 组合函数

vue
<script setup>
import { ref } from 'vue'

// Nuxt 3 的 useFetch 自动处理 SSR/CSR 差异
const { data: posts, pending, error } = await useFetch('/api/posts', {
  // 服务端预取
  server: true,
  // 懒加载(客户端执行)
  lazy: false,
  // 缓存键
  key: 'posts',
  // 转换响应
  transform: (response) => response.data
})
</script>

Nuxt 3 入门

Nuxt 3 是基于 Vue 3 的全栈框架,内置 SSR、路由、状态管理、自动导入等功能。

创建项目

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

目录结构

plaintext
nuxt-app/
├── pages/           # 页面目录(自动生成路由)
│   ├── index.vue    # 首页 -> /
│   ├── about.vue    # 关于页 -> /about
│   └── blog/
│       ├── index.vue    # 博客列表 -> /blog
│       └── [id].vue     # 博客详情 -> /blog/:id
├── components/      # 组件(自动导入)
├── layouts/         # 布局
├── composables/     # 组合式函数(自动导入)
├── server/         # 服务端 API
│   ├── api/
│   │   └── posts.get.ts  # GET /api/posts
│   └── routes/
├── nuxt.config.ts  # 配置文件
└── app.vue         # 应用入口

基础页面

vue
<!-- pages/index.vue -->
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>

    <nav>
      <NuxtLink to="/about">关于</NuxtLink>
      <NuxtLink to="/blog">博客</NuxtLink>
    </nav>

    <Counter />

    <slot />
  </div>
</template>

<script setup>
// 页面 meta
useHead({
  title: '首页',
  meta: [
    { name: 'description', content: 'Nuxt 3 博客' }
  ]
})

const title = ref('欢迎来到我的博客')
const description = '使用 Nuxt 3 构建的现代化博客'
</script>

自动导入机制

Nuxt 3 最大的特点之一是自动导入,显著减少样板代码:

自动导入的内容

  • ref, reactive, computed 等 Vue 组合式 API
  • useHead, useSeoMeta 等 Nuxt 组合式函数
  • components/ 目录下的所有组件
  • composables/ 目录下的所有函数
  • utils/ 目录下的工具函数

组件自动导入

vue
<!-- components/Header.vue -->
<template>
  <header class="site-header">
    <slot />
  </header>
</template>
vue
<!-- pages/index.vue -->
<!-- 无需 import,Header 会被自动引入 -->
<template>
  <Header>Logo</Header>
</template>

Composables 自动导入

javascript
// composables/useCounter.js
export function useCounter(initial = 0) {
  const count = useState('count', () => initial)

  function increment() {
    count.value++
  }

  return { count, increment }
}
vue
<!-- pages/index.vue -->
<!-- 无需 import -->
<script setup>
const { count, increment } = useCounter()
</script>

数据获取

useFetch

vue
<script setup>
// 基本用法
const { data, pending, error, refresh } = await useFetch('/api/posts')

// 带参数
const { data: user } = await useFetch(`/api/users/${id}`, {
  method: 'GET',
  headers: { 'Authorization': 'Bearer token' }
})

// 转换数据
const { data: formattedPosts } = await useFetch('/api/posts', {
  transform: (posts) => posts.map(p => ({
    ...p,
    formattedDate: new Date(p.date).toLocaleDateString()
  }))
})

// 刷新数据
async function refreshData() {
  await refresh()
}
</script>

useAsyncData

vue
<script setup>
// 更精细的控制
const { data, pending, error, execute } = await useAsyncData(
  'user-profile',  // 缓存键
  () => $fetch('/api/user'),
  {
    server: true,           // 服务端执行
    lazy: false,           // 等待数据
    default: () => null,   // 默认值
    transform: (data) => {
      // 数据转换
      return data
    }
  }
)
</script>

服务端 API 路由

typescript
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const page = parseInt(query.page as string) || 1
  const limit = parseInt(query.limit as string) || 10

  // 模拟数据库查询
  const posts = await db.posts.findMany({
    skip: (page - 1) * limit,
    take: limit,
    orderBy: { createdAt: 'desc' }
  })

  const total = await db.posts.count()

  return {
    posts,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  }
})
typescript
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = parseInt(getRouterParam(event, 'id'))

  const post = await db.posts.findUnique({
    where: { id }
  })

  if (!post) {
    throw createError({
      statusCode: 404,
      message: 'Post not found'
    })
  }

  return post
})

混合渲染模式

Nuxt 3 支持在不同路由使用不同的渲染策略:

渲染模式类型

  • SSR:每个请求在服务端渲染
  • SSG:构建时预渲染静态 HTML
  • CSR:纯客户端渲染
  • ISR:增量静态再生成

路由规则配置

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 首页:SSR(实时数据)
    '/': { ssr: true },

    // 博客列表:ISR(每小时重新生成)
    '/blog': { ssr: true, prerender: false, cache: { maxAge: 3600 } },

    // 博客详情:SSG(预渲染所有页面)
    '/blog/**': { prerender: true },

    // 管理后台:CSR(客户端渲染)
    '/admin/**': { ssr: false },

    // API 路由:配置缓存
    '/api/posts': { cache: { maxAge: 60, staleMaxAge: 600 } }
  }
})

动态渲染模式

vue
<script setup>
// 运行时切换渲染模式
definePageMeta({
  ssr: true  // 或 false
})

// 或者在 setup 中控制
if (process.server) {
  // 服务端代码
}
</script>

状态管理

useState

typescript
// composables/useAuth.ts
export const useAuth = () => {
  const user = useState('user', () => null)

  const login = async (credentials) => {
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials
    })
    user.value = response.user
  }

  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
  }

  return {
    user: readonly(user),
    isAuthenticated: computed(() => !!user.value),
    login,
    logout
  }
}

Pinia 集成

bash
npm install @pinia/nuxt
typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
})
typescript
// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

SEO 与 Meta

useHead

vue
<script setup>
useHead({
  title: '博客文章',
  meta: [
    { name: 'description', content: '文章描述' },
    { property: 'og:title', content: '社交分享标题' },
    { property: 'og:description', content: '社交分享描述' }
  ],
  link: [
    { rel: 'canonical', href: 'https://example.com/current-page' }
  ]
})
</script>

useSeoMeta

vue
<script setup>
useSeoMeta({
  title: '博客文章',
  ogTitle: '社交分享标题',
  description: '文章描述',
  ogDescription: '社交分享描述',
  ogImage: '/images/og-image.jpg',
  twitterCard: 'summary_large_image'
})
</script>

动态 SEO

vue
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)

// 使用 post 数据设置 SEO
useSeoMeta({
  title: () => `${post.value?.title} - 我的博客`,
  description: () => post.value?.description
})
</script>

部署

Node.js 服务器

bash
# 构建
npm run build

# 启动
node .output/server/index.mjs

Dockerfile

dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

Vercel / Netlify

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'vercel'
  }
})

性能优化

组件懒加载

vue
<script setup>
// 自动按需加载
const LazyHeavyChart = defineAsyncComponent(() =>
  import('./HeavyChart.vue')
)
</script>

<template>
  <LazyHeavyChart v-if="showChart" />
</template>

图片优化

vue
<template>
  <!-- Nuxt Image 自动优化 -->
  <NuxtImg
    src="/images/photo.jpg"
    width="800"
    height="600"
    format="webp"
    loading="lazy"
    alt="Description"
  />
</template>

总结

Nuxt 3 为 Vue 3 开发者提供了完整的 SSR 解决方案:

  • 框架优势:开箱即用、自动导入、文件系统路由
  • 渲染模式:SSR / SSG / CSR / ISR 按需选择
  • 数据获取useFetchuseAsyncData 自动处理服务端和客户端
  • SEO 支持useHeaduseSeoMeta 轻松管理 meta 信息
  • API 路由:在 server/api/ 目录创建服务端 API
  • 状态管理useState 和 Pinia 双重支持
  • 部署灵活:支持 Node.js、Docker、Vercel 等多种部署方式

掌握 Nuxt 3,你就能构建出性能优秀、SEO 友好、功能完整的 Vue 3 全栈应用。

#vue #nuxt #ssr #server-side-rendering #全栈

评论

A

Written by

AI-Writer

Related Articles

vue
#3

条件渲染与列表渲染

深入讲解 Vue 3 中 v-if、v-show、v-for 的使用场景与性能差异,以及 key 属性的作用和常见渲染陷阱的规避方法

Read More
vue
#7

Pinia 状态管理

深入讲解 Vue 3 官方推荐的状态管理库 Pinia,包括 store 定义、 getters、actions、插件机制以及模块化设计模式

Read More
vue
#1

Vue 实例与模板语法

深入讲解 Vue 3 的应用创建方式(createApp)、模板语法核心规则(插值、指令、双向绑定),以及响应式数据(ref、reactive)和计算属性的使用

Read More