Skip to content

Vue.js 服务端渲染(SSR)

什么是 SSR?

服务端渲染(Server-Side Rendering,SSR)是指在服务器上将 Vue 组件渲染成 HTML 字符串,然后发送给客户端。这与客户端渲染(CSR)不同,后者在浏览器中渲染组件。

SSR 的优势

1. 更好的 SEO

搜索引擎爬虫可以直接看到完整的页面内容。

2. 更快的首屏加载

用户可以更快地看到页面内容,无需等待 JavaScript 下载和执行。

3. 更好的性能(在某些情况下)

对于内容密集型应用,SSR 可以提供更好的性能。

SSR 的劣势

  • 服务器负载更高
  • 开发复杂度增加
  • 部署和维护成本更高
  • 某些浏览器 API 不可用

使用 Nuxt.js(推荐)

Nuxt.js 是基于 Vue 的 SSR 框架,提供了开箱即用的 SSR 支持。

安装 Nuxt

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

Nuxt 项目结构

my-app/
├── pages/          # 页面(自动路由)
├── components/     # 组件
├── layouts/        # 布局
├── composables/    # 组合式函数
├── plugins/        # 插件
├── middleware/     # 中间件
├── public/         # 静态资源
├── server/         # 服务端代码
└── nuxt.config.ts  # 配置文件

创建页面

vue
<!-- pages/index.vue -->
<script setup>
const title = 'Welcome to Nuxt!'
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>This page is server-rendered!</p>
  </div>
</template>

数据获取

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

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

手动实现 SSR

如果不使用 Nuxt,可以手动实现 SSR:

1. 安装依赖

bash
npm install express vue @vue/server-renderer

2. 创建服务器

javascript
// server.js
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'

const server = express()

server.get('*', async (req, res) => {
  const app = createSSRApp({
    data() {
      return {
        message: 'Hello from SSR!'
      }
    },
    template: `<div>{{ message }}</div>`
  })
  
  const html = await renderToString(app)
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR</title>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
  `)
})

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000')
})

3. 客户端激活(Hydration)

javascript
// client.js
import { createSSRApp } from 'vue'

const app = createSSRApp({
  data() {
    return {
      message: 'Hello from SSR!'
    }
  },
  template: `<div>{{ message }}</div>`
})

app.mount('#app')

SSR 数据预取

vue
<!-- pages/users.vue -->
<script setup>
// 服务端和客户端都会执行
const { data: users, pending, error } = await useFetch('/api/users')
</script>

<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else>
      <h1>Users</h1>
      <ul>
        <li v-for="user in users" :key="user.id">
          {{ user.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

SSR 生命周期

在 SSR 中,只有 beforeCreatecreated 钩子会在服务端执行:

vue
<script setup>
import { onMounted, onServerPrefetch } from 'vue'

// 只在服务端执行
onServerPrefetch(async () => {
  console.log('Server prefetch')
  // 预取数据
})

// 只在客户端执行
onMounted(() => {
  console.log('Client mounted')
  // 客户端逻辑
})
</script>

处理浏览器 API

某些浏览器 API 在服务端不可用:

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

const windowWidth = ref(0)

// ❌ 错误:window 在服务端不存在
// const windowWidth = ref(window.innerWidth)

// ✅ 正确:在客户端钩子中使用
onMounted(() => {
  windowWidth.value = window.innerWidth
})

// 或者使用条件判断
if (typeof window !== 'undefined') {
  windowWidth.value = window.innerWidth
}
</script>

状态管理与 SSR

Pinia with SSR

javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null
  }),
  actions: {
    async fetchUser() {
      // 服务端和客户端都会执行
      const response = await fetch('/api/user')
      this.user = await response.json()
    }
  }
})
vue
<!-- pages/profile.vue -->
<script setup>
const userStore = useUserStore()

// 服务端预取数据
await userStore.fetchUser()
</script>

<template>
  <div>
    <h1>{{ userStore.user?.name }}</h1>
  </div>
</template>

缓存策略

页面级缓存

javascript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },  // 预渲染
    '/api/**': { cache: { maxAge: 60 } },  // API 缓存
    '/admin/**': { ssr: false }  // 禁用 SSR
  }
})

组件级缓存

vue
<script setup>
// 缓存组件 60 秒
defineOptions({
  cache: {
    maxAge: 60
  }
})
</script>

静态站点生成(SSG)

Nuxt 支持静态站点生成:

bash
# 生成静态站点
npm run generate
javascript
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  target: 'static'
})

混合渲染

可以为不同路由选择不同的渲染模式:

javascript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },           // SSG
    '/blog/**': { swr: 3600 },          // ISR (增量静态再生)
    '/admin/**': { ssr: false },        // CSR
    '/api/**': { cors: true }           // API
  }
})

SEO 优化

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

// 设置 meta 标签
useHead({
  title: post.value.title,
  meta: [
    { name: 'description', content: post.value.excerpt },
    { property: 'og:title', content: post.value.title },
    { property: 'og:description', content: post.value.excerpt },
    { property: 'og:image', content: post.value.image }
  ]
})
</script>

<template>
  <article>
    <h1>{{ post.title }}</h1>
    <div v-html="post.content"></div>
  </article>
</template>

性能优化

1. 代码分割

vue
<script setup>
// 懒加载组件
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

2. 预取关键资源

vue
<script setup>
useHead({
  link: [
    { rel: 'preload', href: '/fonts/main.woff2', as: 'font' },
    { rel: 'prefetch', href: '/api/data' }
  ]
})
</script>

3. 使用 CDN

javascript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    cdnURL: 'https://cdn.example.com'
  }
})

部署

Vercel

bash
# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

Netlify

bash
# 构建命令
npm run build

# 输出目录
.output/public

Node.js 服务器

bash
# 构建
npm run build

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

总结

  • SSR 提供更好的 SEO 和首屏性能
  • Nuxt.js 是 Vue SSR 的最佳选择
  • 注意服务端和客户端的差异
  • 合理使用缓存策略
  • 可以混合使用 SSR、SSG 和 CSR
  • 针对不同场景选择合适的渲染模式

下一步

接下来学习 工具链和生态,了解 Vue 生态系统中的工具和库。