应用架构概述
Vue SSR应用需要同时支持服务端和客户端渲染,因此需要创建三个主要入口文件:
- app.js
- 通用应用工厂函数
- entry-client.js
- 客户端入口
- entry-server.js
- 服务端入口
通用应用入口(app.js)
基础应用工厂
// src/app.js
import { createSSRApp } from 'vue'
import { createRouter } from './router'
import { createStore } from './store'
import App from './App.vue'
// 导出应用工厂函数
export function createApp() {
const app = createSSRApp(App)
const router = createRouter()
const store = createStore()
app.use(router)
app.use(store)
return { app, router, store }
}
为什么使用工厂函数?
- 避免状态污染:每个请求都创建新的应用实例
- 内存泄漏防护:确保服务端不会在请求间共享状态
- 并发安全:支持多个并发请求
客户端入口(entry-client.js)
基础客户端入口
// src/entry-client.js
import { createApp } from './app'
const { app, router } = createApp()
// 等待路由准备就绪
router.isReady().then(() => {
// 挂载应用到DOM
app.mount('#app')
})
客户端激活(Hydration)
// src/entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp()
// 恢复服务端状态
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.isReady().then(() => {
app.mount('#app', true) // true 表示激活模式
})
客户端路由处理
// 处理客户端路由跳转
router.beforeResolve((to, from, next) => {
const matched = router.resolve(to).matched
const prevMatched = router.resolve(from).matched
// 只加载新增的组件
const activated = matched.filter((c, i) => {
return prevMatched[i] !== c
})
if (!activated.length) {
return next()
}
// 加载异步数据
Promise.all(activated.map(c => {
if (c.components.default.asyncData) {
return c.components.default.asyncData({ store, route: to })
}
})).then(() => {
next()
}).catch(next)
})
服务端入口(entry-server.js)
基础服务端入口
// src/entry-server.js
import { createApp } from './app'
export async function render(url, context = {}) {
const { app, router, store } = createApp()
// 设置路由位置
await router.push(url)
await router.isReady()
// 检查路由是否匹配
const matchedComponents = router.currentRoute.value.matched
if (!matchedComponents.length) {
throw new Error(`No matched components for route: ${url}`)
}
// 预取数据
await Promise.all(
matchedComponents.map(component => {
if (component.components.default.asyncData) {
return component.components.default.asyncData({
store,
route: router.currentRoute.value
})
}
})
)
// 将状态附加到上下文
context.state = store.state
return app
}
错误处理
// src/entry-server.js
export async function render(url, context = {}) {
try {
const { app, router, store } = createApp()
await router.push(url)
await router.isReady()
const matchedComponents = router.currentRoute.value.matched
if (!matchedComponents.length) {
context.statusCode = 404
throw new Error(`404: Page not found - ${url}`)
}
// 数据预取
const asyncDataPromises = matchedComponents
.filter(component => component.components.default.asyncData)
.map(component =>
component.components.default.asyncData({
store,
route: router.currentRoute.value,
context
})
)
await Promise.all(asyncDataPromises)
context.state = store.state
context.statusCode = 200
return app
} catch (error) {
context.statusCode = context.statusCode || 500
context.error = error
throw error
}
}
根组件(App.vue)
基础App组件
<!-- src/App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
</nav>
<main>
<router-view />
</main>
<footer>
<p>© 2024 Vue SSR Demo</p>
</footer>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
nav {
padding: 30px;
background: #f0f0f0;
}
nav a {
font-weight: bold;
color: #2c3e50;
margin-right: 20px;
text-decoration: none;
}
nav a.router-link-exact-active {
color: #42b983;
}
main {
padding: 20px;
min-height: 400px;
}
footer {
text-align: center;
padding: 20px;
background: #f0f0f0;
margin-top: 40px;
}
</style>
HTML模板
基础HTML模板
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
{{{meta}}}
{{{preloadLinks}}}
</head>
<body>
<div id="app">{{{app}}}</div>
<script>
window.__INITIAL_STATE__ = {{{state}}}
</script>
{{{scripts}}}
</body>
</html>
应用生命周期
SSR渲染流程
1. 服务器接收请求
↓
2. 创建应用实例
↓
3. 路由匹配
↓
4. 数据预取
↓
5. 渲染为HTML字符串
↓
6. 发送HTML到客户端
↓
7. 客户端激活(Hydration)
↓
8. 应用变为可交互状态
最佳实践
1. 状态管理
- 使用工厂函数避免状态污染
- 正确处理服务端状态序列化
- 客户端正确恢复状态
2. 错误处理
- 服务端渲染错误处理
- 客户端激活错误处理
- 404和500错误页面
3. 性能优化
- 避免在服务端执行副作用
- 合理使用缓存
- 优化数据预取策略
下一步
在下一章节中,我们将学习如何配置Vue Router以支持SSR。