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。