Vue 3 服务端渲染(SSR)实战指南

Vue 3 服务端渲染(SSR)实战指南

前言

服务端渲染(Server-Side Rendering, SSR)是现代前端应用的重要技术之一,它解决了单页应用(SPA)在首屏加载速度慢、SEO不友好等问题。Vue 3 的服务端渲染相比 Vue 2 有了显著改进,本文将详细介绍 Vue 3 服务端渲染的原理、实现方式以及最佳实践。

一、SSR 基础概念

1. 什么是服务端渲染

服务端渲染是指在服务器端生成HTML内容并发送给客户端的技术。与传统的单页应用不同,SSR应用在服务器端就已经渲染好了初始HTML,客户端接收到后只需要激活交互功能即可。

2. SSR 的优势

  • 更好的首屏加载性能:服务器直接返回渲染好的HTML,减少了客户端的渲染时间
  • 更好的SEO友好性:搜索引擎可以直接爬取到完整的页面内容
  • 更好的内容可达性:对于不支持JavaScript的环境也能展示基本内容
  • 更好的用户体验:用户可以更快地看到页面内容,减少等待感

3. SSR 与 SPA 的区别

特性 服务端渲染(SSR) 单页应用(SPA)
初始HTML 完整渲染的HTML 空HTML模板
渲染时机 服务器端 客户端
首屏加载 更快 较慢
SEO友好度 低(需额外处理)
服务器压力
开发复杂度
构建过程 更复杂 简单

二、Vue 3 SSR 架构设计

1. 架构概览

Vue 3 SSR 架构主要包含以下几个部分:

  • 服务器入口(server entry):负责创建Vue应用实例并渲染为HTML
  • 客户端入口(client entry):负责激活服务端渲染的HTML,添加交互功能
  • 通用代码:应用的主要代码,在客户端和服务器端共享
  • 构建配置:用于生成服务端和客户端的bundle文件

2. 渲染流程

  1. 客户端发送请求到服务器
  2. 服务器创建Vue应用实例
  3. 服务器执行路由匹配和数据预取
  4. 服务器渲染Vue应用为HTML字符串
  5. 服务器将HTML字符串和预取的数据一起发送给客户端
  6. 客户端接收HTML并激活,复用服务端渲染的DOM结构
  7. 客户端接管应用,提供完整的交互体验

三、Vue 3 SSR 从零实现

1. 项目初始化

首先,让我们创建一个基本的Vue 3 SSR项目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 创建项目目录
mkdir vue3-ssr-demo
cd vue3-ssr-demo

# 初始化package.json
npm init -y

# 安装依赖
npm install vue@next vue-router@4 vuex@4 express
npm install -D @vitejs/plugin-vue vite rollup @rollup/plugin-commonjs @rollup/plugin-node-resolve typescript ts-node @types/node @types/express

2. 项目结构

创建如下项目结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
vue3-ssr-demo/
├── src/
│   ├── entry-client.js     # 客户端入口
│   ├── entry-server.js     # 服务器入口
│   ├── app.js              # Vue应用创建
│   ├── router.js           # 路由配置
│   ├── store.js            # 状态管理
│   ├── components/         # 组件
│   │   └── HelloWorld.vue
│   └── views/              # 页面组件
│       ├── Home.vue
│       └── About.vue
├── public/                 # 静态资源
├── server.js               # Express服务器
├── vite.config.js          # Vite配置
├── tsconfig.json           # TypeScript配置
└── package.json            # 项目配置

3. 创建Vue应用

创建应用实例,确保在服务器端每次请求都创建新的实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/app.js
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

// 导出一个工厂函数,用于创建新的应用实例
export function createApp() {
  // 创建应用实例
  const app = createSSRApp(App)
  
  // 创建路由实例
  const router = createRouter()
  
  // 创建状态管理实例
  const store = createStore()
  
  // 注册路由和状态管理
  app.use(router)
  app.use(store)
  
  // 返回应用实例和路由实例
  return { app, router, store }
}

4. 配置路由

创建路由配置,使用createMemoryHistory用于服务器端渲染:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/router.js
import { createRouter as _createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'

// 导出工厂函数
export function createRouter() {
  // 根据环境选择history模式
  const history = import.meta.env.SSR 
    ? createMemoryHistory() 
    : createWebHistory()
  
  const routes = [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      // 路由懒加载
      component: () => import('./views/About.vue')
    }
  ]
  
  return _createRouter({
    history,
    routes
  })
}

5. 配置状态管理

创建状态管理配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/store.js
import { createStore as _createStore } from 'vuex'

// 导出工厂函数
export function createStore() {
  return _createStore({
    state: () => ({
      count: 0,
      items: []
    }),
    mutations: {
      increment(state) {
        state.count++
      },
      setItems(state, items) {
        state.items = items
      }
    },
    actions: {
      // 数据预取示例
      async fetchItems({ commit }) {
        // 模拟API请求
        // const response = await api.get('/api/items')
        // commit('setItems', response.data)
        
        // 模拟数据
        return new Promise(resolve => {
          setTimeout(() => {
            commit('setItems', [
              { id: 1, title: 'Item 1' },
              { id: 2, title: 'Item 2' },
              { id: 3, title: 'Item 3' }
            ])
            resolve()
          }, 100)
        })
      }
    }
  })
}

6. 服务器入口

创建服务器入口,负责渲染Vue应用为HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// src/entry-server.js
import { createApp } from './app.js'
import { renderToString } from '@vue/server-renderer'

export async function render(url, manifest) {
  // 创建应用实例
  const { app, router, store } = createApp()
  
  // 设置路由
  router.push(url)
  await router.isReady()
  
  // 获取路由匹配的组件
  const matchedComponents = router.currentRoute.value.matched.flatMap(record => 
    Object.values(record.components)
  )
  
  // 如果没有匹配的组件,返回404
  if (!matchedComponents.length) {
    return {
      notFound: true
    }
  }
  
  // 预取数据
  try {
    await Promise.all(
      matchedComponents.map(component => {
        if (component.asyncData) {
          return component.asyncData({ store, route: router.currentRoute.value })
        }
      })
    )
  } catch (error) {
    console.error('Data prefetch error:', error)
  }
  
  // 序列化状态,以便客户端可以接管
  const state = JSON.stringify(store.state)
  
  // 渲染应用为HTML
  const html = await renderToString(app)
  
  // 获取需要预加载的资源
  const preloadLinks = renderPreloadLinks(manifest, matchedComponents)
  
  return {
    html,
    state,
    preloadLinks
  }
}

// 生成预加载链接
function renderPreloadLinks(manifest, components) {
  const links = []
  const seen = new Set()
  
  if (!manifest) return links
  
  components.forEach(component => {
    const files = manifest[component.__file] || []
    if (files) {
      files.forEach(file => {
        if (!seen.has(file)) {
          seen.add(file)
          if (file.endsWith('.js')) {
            links.push(`<link rel="modulepreload" href="${file}">`)
          } else if (file.endsWith('.css')) {
            links.push(`<link rel="stylesheet" href="${file}">`)
          }
        }
      })
    }
  })
  
  return links.join('')
}

7. 客户端入口

创建客户端入口,负责激活服务端渲染的HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/entry-client.js
import { createApp } from './app.js'

// 创建应用实例
const { app, router, store } = createApp()

// 从window中恢复服务器端预取的状态
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

// 路由准备就绪后挂载应用
router.isReady().then(() => {
  // 挂载应用
  app.mount('#app', true) // 第二个参数设为true,启用 hydration
})

// 注册prefetch钩子
router.beforeResolve(async (to, from, next) => {
  try {
    // 获取路由匹配的组件
    const matchedComponents = to.matched.flatMap(record => 
      Object.values(record.components)
    )
    
    // 预加载组件
    await Promise.all(
      matchedComponents.map(component => {
        if (component.asyncData && !component.__prefetched) {
          component.__prefetched = true
          return component.asyncData({ store, route: to })
        }
      })
    )
    
    next()
  } catch (error) {
    console.error('Client prefetch error:', error)
    next(error)
  }
})

8. 创建App.vue

创建应用的根组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<template>
  <div id="app">
    <header>
      <nav>
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
      </nav>
    </header>
    <main>
      <router-view v-slot="{ Component }">
        <transition name="fade" mode="out-in">
          <component :is="Component" />
        </transition>
      </router-view>
    </main>
    <footer>
      <p>Vue 3 SSR Demo</p>
    </footer>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: Arial, sans-serif;
  line-height: 1.6;
  color: #333;
}

header {
  background: #333;
  color: #fff;
  padding: 1rem;
}

nav {
  max-width: 1200px;
  margin: 0 auto;
}

nav a {
  color: #fff;
  text-decoration: none;
  margin-right: 1rem;
}

main {
  max-width: 1200px;
  margin: 2rem auto;
  padding: 0 1rem;
}

footer {
  background: #333;
  color: #fff;
  text-align: center;
  padding: 1rem;
  margin-top: 2rem;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

9. 创建页面组件

创建首页组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<template>
  <div class="home">
    <h1>Home Page</h1>
    <p>Welcome to Vue 3 SSR Demo</p>
    
    <div class="counter">
      <p>Count: {{ count }}</p>
      <button @click="increment">Increment</button>
    </div>
    
    <div class="items">
      <h2>Items</h2>
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.title }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Home',
  
  // 数据预取方法,将在服务器端调用
  asyncData({ store, route }) {
    // 预取数据
    return store.dispatch('fetchItems')
  },
  
  computed: {
    count() {
      return this.$store.state.count
    },
    items() {
      return this.$store.state.items
    }
  },
  
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}
</script>

<style scoped>
.home {
  text-align: center;
}

.counter {
  margin: 2rem 0;
}

.items {
  margin-top: 2rem;
  text-align: left;
}

ul {
  list-style-position: inside;
}

li {
  margin: 0.5rem 0;
}
</style>

创建关于页面组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template>
  <div class="about">
    <h1>About Page</h1>
    <p>This is a Vue 3 SSR demo application.</p>
    <p>Server-Side Rendering with Vue 3, Vite and Express.</p>
  </div>
</template>

<script>
export default {
  name: 'About'
}
</script>

<style scoped>
.about {
  text-align: center;
}
</style>

10. 配置Vite

创建Vite配置,用于构建客户端和服务器端bundle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig(({ command, mode }) => {
  const isServer = command === 'serve' ? false : mode === 'ssr'
  
  return {
    plugins: [vue()],
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src')
      }
    },
    build: {
      minify: false, // 开发环境可以设置为false,方便调试
      outDir: isServer ? 'dist/server' : 'dist/client',
      manifest: !isServer,
      rollupOptions: {
        input: isServer 
          ? './src/entry-server.js' 
          : './src/entry-client.js',
        output: {
          format: isServer ? 'cjs' : 'esm'
        }
      }
    }
  }
})

11. 创建Express服务器

创建Express服务器,用于处理请求并返回渲染好的HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// server.js
const express = require('express')
const fs = require('fs')
const path = require('path')
const app = express()

// 静态资源服务
app.use('/assets', express.static(path.join(__dirname, 'dist/client/assets')))

// 读取客户端构建清单
let manifest
let render

async function setupRenderer() {
  // 动态导入服务端渲染函数
  const { render: _render } = await import('./dist/server/entry-server.js')
  render = _render
  
  // 读取构建清单
  const manifestPath = path.join(__dirname, 'dist/client/manifest.json')
  manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
}

// 渲染HTML模板
function renderHtml(html, state, preloadLinks = '') {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Vue 3 SSR Demo</title>
      ${preloadLinks}
      <link rel="stylesheet" href="/assets/index.css">
    </head>
    <body>
      <div id="app">${html}</div>
      <script>
        window.__INITIAL_STATE__ = ${state}
      </script>
      <script type="module" src="/assets/index.js"></script>
    </body>
    </html>
  `
}

// 处理所有路由请求
app.get('*', async (req, res) => {
  try {
    // 确保渲染器已设置
    if (!render) {
      await setupRenderer()
    }
    
    // 渲染应用
    const result = await render(req.url, manifest)
    
    // 处理404
    if (result.notFound) {
      res.status(404).send('Not Found')
      return
    }
    
    // 发送渲染后的HTML
    res.send(renderHtml(result.html, result.state, result.preloadLinks))
  } catch (error) {
    console.error('Render error:', error)
    res.status(500).send('Internal Server Error')
  }
})

// 启动服务器
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`)
})

12. 配置构建脚本

在package.json中添加构建和启动脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "name": "vue3-ssr-demo",
  "version": "1.0.0",
  "description": "Vue 3 SSR Demo",
  "main": "server.js",
  "scripts": {
    "dev": "npm run build && node server.js",
    "build:client": "vite build",
    "build:server": "vite build --mode ssr",
    "build": "npm run build:client && npm run build:server"
  },
  "dependencies": {
    "express": "^4.17.1",
    "vue": "^3.2.0",
    "vue-router": "^4.0.0",
    "vuex": "^4.0.0"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^21.0.0",
    "@rollup/plugin-node-resolve": "^13.0.0",
    "@types/express": "^4.17.13",
    "@types/node": "^16.0.0",
    "@vitejs/plugin-vue": "^2.0.0",
    "rollup": "^2.60.0",
    "ts-node": "^10.0.0",
    "typescript": "^4.5.0",
    "vite": "^2.0.0"
  }
}

四、Nuxt.js 实现 Vue 3 SSR

虽然我们可以手动实现Vue 3 SSR,但在实际项目中,使用Nuxt.js这样的框架可以大大简化开发过程。

1. Nuxt.js 简介

Nuxt.js是一个基于Vue.js的服务端渲染框架,它提供了开箱即用的SSR功能,同时支持静态站点生成(SSG)。Nuxt 3完全基于Vue 3构建,提供了更好的性能和开发体验。

2. 创建Nuxt 3项目

1
2
3
4
5
6
# 创建Nuxt 3项目
npx nuxi init nuxt3-demo
cd nuxt3-demo

# 安装依赖
npm install

3. Nuxt 3 项目结构

Nuxt 3使用约定优于配置的原则,具有以下默认项目结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
nuxt3-demo/
├── pages/          # 页面组件,自动生成路由
├── components/     # 组件
├── composables/    # 组合式函数
├── stores/         # 状态管理(Pinia)
├── server/         # 服务器端代码
├── public/         # 静态资源
├── assets/         # 资源文件
├── app.vue         # 根组件
├── nuxt.config.ts  # 配置文件
└── package.json    # 项目配置

4. 创建页面组件

在pages目录下创建页面组件,Nuxt会自动生成路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- pages/index.vue -->
<template>
  <div class="home">
    <h1>Home Page</h1>
    <p>Welcome to Nuxt 3 SSR Demo</p>
    <NuxtLink to="/about">About</NuxtLink>
    
    <div class="items">
      <h2>Items</h2>
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.title }}</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
// 数据预取 - 服务器端渲染
const { data } = await useFetch('/api/items')
const items = data.value || []
</script>
1
2
3
4
5
6
7
8
<!-- pages/about.vue -->
<template>
  <div class="about">
    <h1>About Page</h1>
    <p>This is a Nuxt 3 SSR demo application.</p>
    <NuxtLink to="/">Home</NuxtLink>
  </div>
</template>

5. 创建API端点

在server/api目录下创建API端点:

1
2
3
4
5
6
7
8
// server/api/items.js
export default defineEventHandler(() => {
  return [
    { id: 1, title: 'Item 1' },
    { id: 2, title: 'Item 2' },
    { id: 3, title: 'Item 3' }
  ]
})

6. 配置Nuxt

创建或修改nuxt.config.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// nuxt.config.ts
export default defineNuxtConfig({
  // 渲染模式:'universal' (SSR) 或 'static' (SSG)
  ssr: true,
  
  // 应用配置
  app: {
    head: {
      title: 'Nuxt 3 SSR Demo',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'description', name: 'description', content: 'Nuxt 3 SSR Demo' }
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
      ]
    }
  },
  
  // 构建配置
  build: {
    // 构建选项
  },
  
  // 模块配置
  modules: [
    // 可以添加Nuxt模块
  ]
})

7. 启动开发服务器

1
npm run dev

8. 构建生产版本

1
2
3
4
5
# 构建SSR版本
npm run build

# 构建静态站点(SSG)
npm run generate

五、性能优化

1. 代码分割

代码分割可以减小初始加载的bundle大小,提高加载速度:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 使用动态导入进行代码分割
const About = () => import('./views/About.vue')

const routes = [
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

2. 资源预加载

预加载关键资源可以提高页面加载速度:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在服务器入口中添加预加载链接
function renderPreloadLinks(manifest, components) {
  const links = []
  const seen = new Set()
  
  components.forEach(component => {
    const files = manifest[component.__file] || []
    if (files) {
      files.forEach(file => {
        if (!seen.has(file)) {
          seen.add(file)
          if (file.endsWith('.js')) {
            links.push(`<link rel="modulepreload" href="${file}">`)
          } else if (file.endsWith('.css')) {
            links.push(`<link rel="stylesheet" href="${file}">`)
          }
        }
      })
    }
  })
  
  return links.join('')
}

3. 缓存策略

使用缓存可以减少服务器压力,提高响应速度:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Express中间件缓存
const NodeCache = require('node-cache')
const cache = new NodeCache({ stdTTL: 60 }) // 缓存60秒

app.get('*', async (req, res) => {
  // 检查缓存
  const cacheKey = `page_${req.url}`
  const cachedHtml = cache.get(cacheKey)
  
  if (cachedHtml) {
    return res.send(cachedHtml)
  }
  
  try {
    // 渲染页面
    const result = await render(req.url, manifest)
    const html = renderHtml(result.html, result.state, result.preloadLinks)
    
    // 设置缓存
    cache.set(cacheKey, html)
    
    res.send(html)
  } catch (error) {
    console.error('Render error:', error)
    res.status(500).send('Internal Server Error')
  }
})

4. 数据预取优化

优化数据预取策略,只预取必要的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 组件级数据预取
asyncData({ store, route }) {
  // 只在特定路由预取数据
  if (route.name === 'Home') {
    return store.dispatch('fetchItems')
  }
  return Promise.resolve()
}

// 条件预取
if (process.server) {
  // 只在服务器端预取大数据集
  return store.dispatch('fetchLargeDataset')
}

六、常见问题与解决方案

1. 客户端激活不匹配

问题:服务器渲染的HTML与客户端期望的DOM结构不匹配。

解决方案

  • 确保客户端和服务器使用相同的数据和状态
  • 检查是否有只在客户端执行的代码影响了DOM结构
  • 使用条件渲染时确保客户端和服务器的条件一致
1
2
3
4
5
// 正确的条件渲染方式
<div v-if="isVisible">{{ message }}</div>

// 避免使用
<div v-if="typeof window !== 'undefined'">Only on client</div>

2. 内存泄漏

问题:长时间运行的SSR服务器可能会出现内存泄漏。

解决方案

  • 确保每个请求都创建新的应用实例
  • 清理事件监听器和定时器
  • 监控内存使用情况,必要时重启服务器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 正确创建应用实例
export function createApp() {
  const app = createSSRApp(App)
  // ...
  return app
}

// 在路由导航离开时清理资源
onUnmounted(() => {
  // 清理定时器、事件监听器等
  clearInterval(timer)
  window.removeEventListener('resize', handleResize)
})

3. CSS问题

问题:服务器渲染时样式不完整。

解决方案

  • 确保关键CSS内联到HTML中
  • 使用CSS提取插件确保所有CSS都被正确处理
  • 检查是否有只在客户端执行的样式相关代码

4. 异步数据获取

问题:服务器渲染时异步数据没有完全获取。

解决方案

  • 使用asyncData或fetch钩子预取数据
  • 确保所有异步数据在渲染前都已解析
  • 处理错误和超时情况
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 使用try/catch处理数据获取错误
asyncData({ store, route }) {
  try {
    return store.dispatch('fetchItems')
  } catch (error) {
    console.error('Data fetch error:', error)
    // 返回默认数据或错误状态
    return { error: error.message }
  }
}

七、部署与运维

1. Node.js服务器部署

使用PM2管理Node.js应用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 安装PM2
npm install -g pm2

# 启动应用
pm run build
pm start

# 或使用PM2直接启动
pm run build
pm install -g pm2
pm start -- --name vue3-ssr

# 监控应用
pm list

# 配置自动重启
pm startup

2. 使用Docker部署

创建Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 构建阶段
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 运行阶段
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server.js ./
COPY --from=builder /app/package*.json ./
RUN npm install --only=production

EXPOSE 3000
CMD ["node", "server.js"]

构建和运行Docker容器:

1
2
docker build -t vue3-ssr-demo .
docker run -p 3000:3000 vue3-ssr-demo

3. 与CDN配合

将静态资源部署到CDN,减轻服务器负担:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 修改HTML模板
function renderHtml(html, state) {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Vue 3 SSR Demo</title>
      <link rel="stylesheet" href="https://cdn.example.com/assets/index.css">
    </head>
    <body>
      <div id="app">${html}</div>
      <script>
        window.__INITIAL_STATE__ = ${state}
      </script>
      <script type="module" src="https://cdn.example.com/assets/index.js"></script>
    </body>
    </html>
  `
}

总结

Vue 3 的服务端渲染技术为现代前端应用提供了更好的性能和SEO友好性。本文详细介绍了Vue 3 SSR的实现方式,包括从零开始构建和使用Nuxt.js框架。同时,我们还讨论了性能优化策略、常见问题的解决方案以及部署运维的最佳实践。

主要内容回顾:

  1. SSR基础概念:服务端渲染的基本原理和优势
  2. Vue 3 SSR架构设计:整体架构和渲染流程
  3. 从零实现Vue 3 SSR:详细的代码实现和配置
  4. 使用Nuxt.js实现SSR:利用框架简化开发流程
  5. 性能优化:代码分割、资源预加载、缓存策略和数据预取优化
  6. 常见问题与解决方案:客户端激活不匹配、内存泄漏、CSS问题和异步数据获取
  7. 部署与运维:Node.js服务器部署、Docker部署和与CDN配合

通过掌握这些技术和最佳实践,开发者可以构建高性能、SEO友好的Vue 3应用,为用户提供更好的体验。无论是从零开始构建还是使用框架,Vue 3 SSR都为现代前端开发提供了强大的支持。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计