Vue Router SSR配置
基础路由配置
// src/router/index.js
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import About from '@/pages/About.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/pages/User.vue'),
props: true
}
]
export function createRouter() {
return createRouter({
// 服务端使用内存历史,客户端使用Web历史
history: import.meta.env.SSR
? createMemoryHistory()
: createWebHistory(),
routes
})
}
环境判断
// 更详细的环境判断
export function createRouter() {
const isServer = typeof window === 'undefined'
return createRouter({
history: isServer
? createMemoryHistory()
: createWebHistory(),
routes
})
}
路由组件
基础页面组件
<!-- src/pages/Home.vue -->
<template>
<div class="home">
<h1>{{ title }}</h1>
<p>欢迎来到Vue SSR首页</p>
<div class="posts" v-if="posts.length">
<h2>最新文章</h2>
<div v-for="post in posts" :key="post.id" class="post-item">
<h3>{{ post.title }}</h3>
<p>{{ post.excerpt }}</p>
<router-link :to="`/post/${post.id}`">阅读更多</router-link>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { useStore } from 'vuex'
export default {
name: 'Home',
// SSR数据预取
async asyncData({ store }) {
await store.dispatch('fetchPosts')
},
setup() {
const store = useStore()
const title = ref('Vue SSR 首页')
const posts = computed(() => store.state.posts)
// 客户端数据获取
onMounted(async () => {
if (!posts.value.length) {
await store.dispatch('fetchPosts')
}
})
return {
title,
posts
}
}
}
</script>
动态路由组件
<!-- src/pages/User.vue -->
<template>
<div class="user">
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else-if="user">
<h1>{{ user.name }}</h1>
<p>邮箱: {{ user.email }}</p>
<p>注册时间: {{ formatDate(user.createdAt) }}</p>
</div>
<div v-else>
用户不存在
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
export default {
name: 'User',
props: ['id'],
// SSR数据预取
async asyncData({ store, route }) {
const userId = route.params.id
await store.dispatch('fetchUser', userId)
},
setup(props) {
const store = useStore()
const route = useRoute()
const loading = ref(false)
const error = ref(null)
const user = computed(() => store.getters.getUserById(props.id))
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN')
}
// 客户端数据获取
onMounted(async () => {
if (!user.value) {
loading.value = true
try {
await store.dispatch('fetchUser', props.id)
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
})
return {
user,
loading,
error,
formatDate
}
}
}
</script>
路由守卫
全局前置守卫
// src/router/index.js
export function createRouter() {
const router = createRouter({
history: import.meta.env.SSR
? createMemoryHistory()
: createWebHistory(),
routes
})
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查路由是否需要认证
if (to.meta.requiresAuth) {
// 在SSR中检查认证状态
const isAuthenticated = checkAuth()
if (!isAuthenticated) {
next('/login')
return
}
}
next()
})
return router
}
function checkAuth() {
// 服务端和客户端的认证检查
if (typeof window === 'undefined') {
// 服务端认证检查
return false // 或从请求头中获取认证信息
} else {
// 客户端认证检查
return localStorage.getItem('token') !== null
}
}
路由级别守卫
const routes = [
{
path: '/admin',
name: 'Admin',
component: () => import('@/pages/Admin.vue'),
meta: { requiresAuth: true },
beforeEnter: (to, from, next) => {
// 路由级别的守卫
if (checkAdminPermission()) {
next()
} else {
next('/403')
}
}
}
]
异步组件与代码分割
路由级代码分割
const routes = [
{
path: '/',
name: 'Home',
component: Home // 同步加载首页
},
{
path: '/about',
name: 'About',
// 异步加载,代码分割
component: () => import('@/pages/About.vue')
},
{
path: '/blog',
name: 'Blog',
// 带有webpack魔法注释的代码分割
component: () => import(
/* webpackChunkName: "blog" */
'@/pages/Blog.vue'
)
}
]
组件级代码分割
<template>
<div>
<h1>博客页面</h1>
<Suspense>
<template #default>
<AsyncBlogList />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AsyncBlogList: defineAsyncComponent(() =>
import('@/components/BlogList.vue')
)
}
}
</script>
数据预取策略
组件级数据预取
// 在路由组件中定义asyncData方法
export default {
async asyncData({ store, route, context }) {
// 服务端和客户端都会执行
const { id } = route.params
try {
await Promise.all([
store.dispatch('fetchPost', id),
store.dispatch('fetchComments', id)
])
} catch (error) {
// 错误处理
if (context) {
context.statusCode = 404
}
throw error
}
}
}
路由级数据预取
// src/router/index.js
router.beforeResolve(async (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()
}
// 显示加载状态
const loadingComponent = document.querySelector('.loading')
if (loadingComponent) {
loadingComponent.style.display = 'block'
}
try {
// 并行加载所有需要的数据
await Promise.all(activated.map(c => {
if (c.components.default.asyncData) {
return c.components.default.asyncData({
store,
route: to
})
}
}))
next()
} catch (error) {
next(error)
} finally {
// 隐藏加载状态
if (loadingComponent) {
loadingComponent.style.display = 'none'
}
}
})
错误处理
404页面处理
const routes = [
// ... 其他路由
{
path: '/404',
name: 'NotFound',
component: () => import('@/pages/404.vue')
},
{
path: '/:pathMatch(.*)*',
redirect: '/404'
}
]
错误页面组件
<!-- src/pages/404.vue -->
<template>
<div class="error-page">
<h1>404</h1>
<p>页面未找到</p>
<router-link to="/">返回首页</router-link>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>
路由元信息
SEO优化
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: {
title: '首页 - Vue SSR Demo',
description: 'Vue SSR示例应用首页',
keywords: 'vue,ssr,首页'
}
},
{
path: '/about',
name: 'About',
component: About,
meta: {
title: '关于我们 - Vue SSR Demo',
description: '了解更多关于Vue SSR的信息',
keywords: 'vue,ssr,关于'
}
}
]
动态元信息
// 在组件中动态设置元信息
export default {
async asyncData({ store, route }) {
const post = await store.dispatch('fetchPost', route.params.id)
// 设置动态元信息
return {
meta: {
title: `${post.title} - 博客`,
description: post.excerpt,
keywords: post.tags.join(',')
}
}
}
}
下一步
在下一章节中,我们将学习如何配置状态管理(Vuex/Pinia)以支持SSR。