第六章:SSR 与服务端集成
在前几章中,我们深入探讨了 Vue Router 的核心功能和生产级实现。现在,让我们关注 Vue Router 在服务端渲染(SSR)环境中的应用,包括内存历史记录、状态同步和路由脱水注水等关键概念。
SSR 支持:服务端路由管理的挑战
在服务端渲染环境中,Vue Router 面临着与客户端不同的挑战。浏览器环境中有 History API 和 DOM,而服务端没有这些 API,需要特殊的处理方式。
问题:服务端渲染时,如何避免路由状态跨请求共享?
在服务端环境中,Node.js 是一个长期运行的进程,如果多个请求共享同一个路由器实例,会导致路由状态污染:
javascript
// 错误示例:共享路由器实例
let router // 全局路由器实例
// 每个请求都使用同一个路由器实例
app.use('*', async (req, res) => {
// 请求 A 修改了路由状态
await router.push('/user/123')
// 请求 B 可能会看到请求 A 的路由状态
const app = createApp()
app.use(router) // 使用共享的路由器
const html = await renderToString(app)
res.send(html)
})
// 正确做法:为每个请求创建独立的路由器实例
app.use('*', async (req, res) => {
// 为每个请求创建独立的路由器实例
const router = createRouter({
history: createMemoryHistory(),
routes
})
// 初始化路由
await router.push(req.url)
await router.isReady()
const app = createApp()
app.use(router)
const html = await renderToString(app)
res.send(html)
})createMemoryHistory():在 Node.js 中创建内存路由
createMemoryHistory 是 Vue Router 专门为 SSR 环境设计的历史记录管理器:
javascript
// MemoryHistory 实现
function createMemoryHistory(base = '') {
const normalizedBase = normalizeBase(base)
// 内存中的历史记录栈
let stack = [normalizedBase + '/']
let index = 0
let listeners = []
function setupListeners() {
// 内存历史记录不需要监听器
}
function push(to, data) {
// 移除当前位置之后的历史记录
stack = stack.slice(0, index + 1)
// 添加新的历史记录
stack.push(normalizedBase + to)
index++
// 通知监听器
listeners.forEach(listener => {
listener(normalizedBase + to, data)
})
}
function replace(to, data) {
// 替换当前位置
stack[index] = normalizedBase + to
// 通知监听器
listeners.forEach(listener => {
listener(normalizedBase + to, data)
})
}
function go(delta) {
const newIndex = index + delta
if (newIndex < 0 || newIndex >= stack.length) {
return
}
index = newIndex
// 通知监听器
listeners.forEach(listener => {
listener(stack[index])
})
}
function current() {
return stack[index]
}
function back() {
go(-1)
}
function forward() {
go(1)
}
function listen(callback) {
listeners.push(callback)
return () => {
const index = listeners.indexOf(callback)
if (index > -1) {
listeners.splice(index, 1)
}
}
}
function destroy() {
listeners = []
stack = []
}
// 返回 MemoryHistory 对象
return {
base: normalizedBase,
location: current,
push,
replace,
go,
back,
forward,
listen,
destroy,
state: null // MemoryHistory 不需要状态
}
}从 req.url 初始化
在 SSR 环境中,路由器需要从 HTTP 请求的 URL 初始化:
javascript
// SSR 路由器创建函数
function createSSRRouter(req) {
// 创建内存历史记录
const history = createMemoryHistory()
// 从请求 URL 初始化路由
const url = new URL(req.url, `http://${req.headers.host}`)
history.push(url.pathname + url.search + url.hash)
// 创建路由器
const router = createRouter({
history,
routes: [
// 路由配置
]
})
return router
}
// 在 Express 中使用
app.use('*', async (req, res) => {
try {
// 为每个请求创建独立的路由器实例
const router = createSSRRouter(req)
// 等待路由器准备就绪
await router.push(req.url)
await router.isReady()
// 创建应用实例
const app = createApp()
app.use(router)
// 渲染应用
const html = await renderToString(app)
// 发送响应
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
} catch (error) {
console.error('SSR Error:', error)
res.status(500).send('Internal Server Error')
}
})状态同步:客户端如何接管服务端渲染的路由
在 SSR 应用中,服务端生成的 HTML 需要客户端 JavaScript 来"激活"(hydrate),其中包括路由状态的同步。
状态脱水(Dehydration):服务端序列化路由状态
javascript
// 服务端路由状态脱水
app.use('*', async (req, res) => {
try {
// 创建 SSR 路由器
const router = createSSRRouter(req)
await router.push(req.url)
await router.isReady()
// 创建应用
const app = createApp()
app.use(router)
// 渲染应用
const html = await renderToString(app)
// 获取初始路由状态
const initialState = {
route: router.currentRoute.value
}
// 序列化状态(防止 XSS)
const serializedState = JSON.stringify(initialState)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/"/g, '\\u0022')
// 注入到 HTML 中
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="app">${html}</div>
<script>
window.__INITIAL_STATE__ = ${serializedState}
</script>
<script src="/client.js"></script>
</body>
</html>
`)
} catch (error) {
console.error('SSR Error:', error)
res.status(500).send('Internal Server Error')
}
})状态注水(Rehydration):客户端恢复路由状态
javascript
// 客户端路由状态注水
async function hydrateApp() {
// 获取初始状态
const initialState = window.__INITIAL_STATE__
// 创建客户端路由器
const router = createRouter({
history: createWebHistory(),
routes
})
// 恢复路由状态
if (initialState && initialState.route) {
// 注意:这里不能直接设置 currentRoute
// 需要通过导航来同步状态
await router.push(initialState.route)
}
// 创建应用
const app = createApp()
app.use(router)
// 激活应用
app.mount('#app')
}
// 启动应用
hydrateApp()完整的 SSR 实现示例
让我们看一个完整的 SSR 实现示例:
javascript
// router/index.js - 通用路由器配置
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
export const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/user/:id',
name: 'User',
component: () => import('../views/User.vue'),
meta: { requiresAuth: true }
}
]
export function createRouterInstance(initialState) {
// 根据环境选择历史记录模式
const history = import.meta.env.SSR
? createMemoryHistory()
: createWebHistory()
const router = createRouter({
history,
routes
})
// 如果有初始状态,同步路由
if (initialState && initialState.route) {
// 注意:在实际应用中,可能需要更复杂的同步逻辑
}
return router
}
// server.js - 服务端入口
import { createApp } from './main'
import { renderToString } from '@vue/server-renderer'
import { routes } from './router'
export async function render(url, manifest) {
// 创建 SSR 路由器
const router = createRouter({
history: createMemoryHistory(),
routes
})
// 导航到请求的 URL
await router.push(url)
await router.isReady()
// 创建应用
const { app } = createApp()
app.use(router)
// 执行路由守卫中的数据预取
const matchedComponents = router.currentRoute.value.matched
const asyncDataHooks = matchedComponents
.map(component => component.asyncData)
.filter(hook => hook)
await Promise.all(asyncDataHooks.map(hook => hook({ router })))
// 渲染应用
const appHtml = await renderToString(app)
// 获取初始状态
const initialState = {
route: router.currentRoute.value
}
// 生成 HTML
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR App</title>
</head>
<body>
<div id="app">${appHtml}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')}
</script>
<script type="module" src="/src/entry-client.js"></script>
</body>
</html>
`
return html
}
// client.js - 客户端入口
import { createApp } from './main'
// 获取初始状态
const initialState = window.__INITIAL_STATE__ || {}
// 创建应用
const { app, router } = createApp(initialState)
// 等待路由器准备就绪
router.isReady().then(() => {
// 挂载应用
app.mount('#app')
})SSR 中的路由守卫处理
在 SSR 环境中,路由守卫的处理方式与客户端有所不同:
javascript
// SSR 中的路由守卫处理
export async function render(url) {
const router = createRouter({
history: createMemoryHistory(),
routes
})
// 导航到目标 URL
await router.push(url)
await router.isReady()
// 执行全局前置守卫
const guards = router.beforeEachGuards || []
const to = router.currentRoute.value
const from = { path: '', name: null, params: {}, query: {}, hash: '' }
for (const guard of guards) {
try {
const result = await new Promise((resolve, reject) => {
const next = (result) => {
if (result === false) {
reject(new Error('Navigation aborted'))
} else if (typeof result === 'string' ||
(typeof result === 'object' && result.path)) {
reject(new NavigationRedirected(result))
} else {
resolve(result)
}
}
const guardResult = guard(to, from, next)
if (guardResult instanceof Promise) {
guardResult.then(next).catch(reject)
} else if (guardResult !== undefined) {
next(guardResult)
}
})
} catch (error) {
if (error instanceof NavigationRedirected) {
// 处理重定向
return render(error.location)
} else {
throw error
}
}
}
// 渲染应用
// ...
}内存管理与性能优化
在 SSR 环境中,内存管理尤为重要,需要确保每次请求后正确清理资源:
javascript
// 内存安全的 SSR 实现
export async function render(url) {
let router
let app
try {
// 创建路由器和应用
router = createRouter({
history: createMemoryHistory(),
routes
})
await router.push(url)
await router.isReady()
const appContext = createApp()
app = appContext.app
app.use(router)
// 渲染应用
const html = await renderToString(app)
// 返回结果
return html
} finally {
// 清理资源
if (router && router.history.destroy) {
router.history.destroy()
}
if (app) {
// 清理应用资源
app.unmount && app.unmount()
}
}
}小结
在本章中,我们深入探讨了 Vue Router 在 SSR 环境中的应用:
- 服务端路由管理 - 使用
createMemoryHistory创建内存路由 - 避免状态污染 - 为每个请求创建独立的路由器实例
- 状态同步 - 服务端脱水和客户端注水机制
- 路由守卫处理 - SSR 环境中的守卫执行
- 内存管理 - 确保资源正确清理
这些机制使得 Vue Router 能够在 SSR 环境中正常工作,提供一致的路由体验。
在实际项目中,SSR 的实现可能会更加复杂,需要考虑缓存、错误处理、性能优化等多个方面。
思考题:
- 你在 SSR 项目中如何处理路由守卫中的异步操作?遇到了哪些挑战?
- 对于大型 SSR 应用,你通常如何优化路由状态的序列化和传输?