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. 渲染流程
- 客户端发送请求到服务器
- 服务器创建Vue应用实例
- 服务器执行路由匹配和数据预取
- 服务器渲染Vue应用为HTML字符串
- 服务器将HTML字符串和预取的数据一起发送给客户端
- 客户端接收HTML并激活,复用服务端渲染的DOM结构
- 客户端接管应用,提供完整的交互体验
三、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. 启动开发服务器
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框架。同时,我们还讨论了性能优化策略、常见问题的解决方案以及部署运维的最佳实践。
主要内容回顾:
- SSR基础概念:服务端渲染的基本原理和优势
- Vue 3 SSR架构设计:整体架构和渲染流程
- 从零实现Vue 3 SSR:详细的代码实现和配置
- 使用Nuxt.js实现SSR:利用框架简化开发流程
- 性能优化:代码分割、资源预加载、缓存策略和数据预取优化
- 常见问题与解决方案:客户端激活不匹配、内存泄漏、CSS问题和异步数据获取
- 部署与运维:Node.js服务器部署、Docker部署和与CDN配合
通过掌握这些技术和最佳实践,开发者可以构建高性能、SEO友好的Vue 3应用,为用户提供更好的体验。无论是从零开始构建还是使用框架,Vue 3 SSR都为现代前端开发提供了强大的支持。