Vue 3 路由配置与多页面应用(十七)

在前面的章节中,我们已经实现了任务管理的核心功能,并通过 Pinia 实现了数据的持久化。接下来,我们将为应用添加路由系统,使其能够支持多页面导航,同时增强安全性和用户体验。本文将详细介绍如何在 Vue 3 项目中实现路由配置、页面切换效果、登录验证以及性能优化。

安装与基础配置

路由系统是现代前端应用不可或缺的一部分,它不仅负责页面之间的切换,还能处理权限控制和动态标题等高级功能。首先,我们需要安装 Vue Router:

npm install vue-router

接着,在 src 目录下创建 router 文件夹,并在其中创建 index.js 文件。我们将定义两个页面:工作台 (Dashboard) 和 登录页 (Login)。

创建页面结构

在 src 目录下创建 views 文件夹,并在其中分别创建 Dashboard.vue 和 Login.vue 文件。以下是各个文件的用途:

  • src/App.vue:根容器,只包含 <router-view /> 和全局动画。
  • src/views/Dashboard.vue:任务管理的核心功能页面。
  • src/views/Login.vue:用户登录页面。

修改 App.vue

为了让 App.vue 成为一个干净的容器,我们将原本的逻辑迁移到 Dashboard.vue 中,并在 App.vue 中添加页面切换的动画效果。


<template>
  <div class="min-h-screen bg-slate-50">
    <router-view v-slot="{ Component }">
      <transition name="page" mode="out-in">
        <component :is="Component" />
      </transition>
    </router-view>
  </div>
</template>

<style>
/* 页面切换的淡入淡出效果 */
.page-enter-active, .page-leave-active {
  transition: opacity 0.2s ease;
}
.page-enter-from, .page-leave-to {
  opacity: 0;
}
</style>

迁移逻辑到 Dashboard.vue

将之前在 App.vue 中的代码(引入 Store、引入组件、模板布局)全部剪切到 src/views/Dashboard.vue 中。

<script setup>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import { useRouter } from 'vue-router'
import TaskHeader from '@/components/TaskHeader.vue'
import TaskInput from '@/components/TaskInput.vue'
import TaskFilter from '@/components/TaskFilter.vue'
import TaskItem from '@/components/TaskItem.vue'

const taskStore = useTaskStore()
const router = useRouter()
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore

// 退出登录方法
const handleLogout = () => {
  localStorage.removeItem('isLoggedIn')
  router.push('/login')
}
</script>

<template>
  <div class="py-12 px-4">
    <div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
      <TaskHeader />
      <main class="p-6">
        <TaskInput @add-task="addTask" />
        <TaskFilter v-model="filter" />
        <ul class="space-y-3">
          <TransitionGroup name="list">
            <TaskItem
              v-for="task in filteredTasks"
              :key="task.id"
              :task="task"
              @toggle="toggleTask"
              @remove="removeTask"
            />
          </TransitionGroup>
        </ul>
        <button
          @click="handleLogout"
          class="mt-8 w-full py-2 text-xs text-slate-400 hover:text-red-500 transition-colors"
        >
          退出当前账号
        </button>
      </main>
    </div>
  </div>
</template>

实现登录逻辑 (Login.vue)

利用 Tailwind CSS 快速构建一个极简登录页,并模拟登录行为。

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const isLoading = ref(false)

const handleLogin = () => {
  isLoading.value = true
  // 模拟异步请求
  setTimeout(() => {
    localStorage.setItem('isLoggedIn', 'true')

    router.push('/') // 登录成功跳转首页
    isLoading.value = false
  }, 1000)
}
</script>

<template>
  <div class="min-h-screen flex items-center justify-center bg-slate-50">
    <div class="p-8 bg-white rounded-3xl shadow-xl w-full max-w-sm border border-slate-100">
      <h2 class="text-2xl font-black mb-6 text-slate-800">欢迎回来</h2>
      <button
        @click="handleLogin"
        :disabled="isLoading"
        class="w-full bg-linear-to-r from-blue-600 to-indigo-600 text-white py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all disabled:opacity-50"
      >
        {{ isLoading ? '登录中...' : '一键进入系统' }}
      </button>
      <p class="mt-4 text-center text-xs text-slate-400">测试环境:点击即可登录</p>
    </div>
  </div>
</template>

完善路由配置

确保你的路由配置中已经开启了权限控制和动态标题。

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: {
      title: '登录 - TaskHub',
      requiresAuth: false
    }
  },
  {
    path: '/',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: {
      title: '我的工作台',
      requiresAuth: true,
      breadcrumb: '首页'
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true'
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else if (to.path === '/login' && isAuthenticated) {
    next('/')
  } else {
    next()
  }
})

export default router

在 main.js 中完成最后拼图

确保你的 main.js 引入并挂载了路由。

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './style.css'

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

最终效果

  1. 访问 /:如果你未登录,页面会自动跳转到 /login。
  2. 访问 /login:点击登录按钮,本地存储 isLoggedIn 变为 true,然后跳转到工作台。
  3. 刷新页面:Pinia 会从 LocalStorage 读取任务,路由守卫会检查登录状态,确保数据不会丢失。

登录后,可以点击退出登录按钮,回到登录页面。

知识点讲解

  1. 为什么 App.vue 变空了? 在单页面应用(SPA)中,App.vue 是整个应用的外壳。我们将其变为空壳,以便根据 URL 的变化动态加载不同的页面(如 Dashboard 或 Login)。

  2. useRouter 的实战 在 Dashboard.vue 中,我们通过 router.push('/login') 实现了退出功能。push 方法会在浏览器历史记录中添加一条新记录,点击浏览器的返回按钮可以回退到上一个页面。

  3. Tailwind CSS 的布局继承 由于我们在 App.vue 的全局容器中设置了 bg-slate-50 和 min-h-screen,所有子页面(如 Dashboard 和 Login)都会继承这些样式,确保视觉的一致性。

逻辑复用:自定义 Composables (Hooks)

随着项目的扩展,我们可能会发现一些逻辑(如存取本地数据、弹出通知)在多个页面中反复出现。为了提高代码的可维护性和复用性,我们可以使用 Composition API 创建自定义 Composables。

useLocalStorage:数据持久化逻辑抽离

不再在每个 Store 或组件中手动编写 localStorage.getItem。

// src/composables/useLocalStorage.js
export function useLocalStorage(key, defaultValue) {
  const value = ref(JSON.parse(localStorage.getItem(key)) || defaultValue)

  watch(value, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  })

  return value
}

useNotification:公共 UI 逻辑抽离

实现一个简单的通知提示功能。

// src/composables/useNotification.js
import { ref } from 'vue'

export function useNotification() {
  const message = ref('')
  const isVisible = ref(false)

  const notify = (msg, duration = 2000) => {
    message.value = msg
    isVisible.value = true
    setTimeout(() => {
      isVisible.value = false
    }, duration)
  }

  return { message, isVisible, notify }
}

性能调优:处理大数据量与高频渲染

当你的任务列表从管理 10 个任务增长到管理 1000 个任务时,Vue 默认的“深度响应式”会带来计算压力。为了优化性能,我们可以使用以下技术:

shallowRef 与 markRaw

Vue 默认的 ref 是递归响应式的(即对象内部的每个属性都会被代理)。

  • shallowRef:只监听 .value 的指向变化,不监听对象内部属性的变化。适用于从后端获取的大批量只读数据。
  • markRaw:标记一个对象,使其永远不会被转为响应式。适用于复杂的第三方库实例(如 ECharts 图表实例、地图实例)。
// 性能优化示例
import { shallowRef, markRaw } from 'vue'

// 假设这是一万条历史归档数据
const archiveTasks = shallowRef([])

const loadArchive = (data) => {
  // 仅在赋值时触发一次响应式更新,内部属性修改不触发
  archiveTasks.value = data
}

v-once 与 v-memo

  • v-once:静态内容只渲染一次。如果某个任务渲染后就不再变化(如任务的创建时间),可以使用 v-once。

    <span v-once>创建于: {{ task.createdAt }}</span>
  • v-memo:按需更新(Vue 3.2+)。它接受一个依赖数组,只有当数组中的值变化时,该节点及其子节点才会重新渲染。

    <li v-for="task in tasks" :key="task.id" v-memo="[task.isCompleted, task.title]">
      {{ task.title }} - {{ task.isCompleted }}
    </li>

什么时候该调优?

方案解决的问题推荐场景
Composables代码重复、逻辑散乱跨组件共享逻辑(如登录检查、主题切换)
shallowRef大对象深度代理导致的内存开销列表数据量级 > 1000 且只需整体替换时
v-memo频繁触发的长列表虚拟 DOM 比对复杂的 v-for 列表,且单项只有少数属性会变

结合到之前的项目中

你可以在 TaskItem.vue 中应用 v-memo,以优化长列表的性能。


<template>
  <li v-memo="[task.id, task.isCompleted]" class="...">
    ...
  </li>
</template>

现在你的项目不仅逻辑清晰(Composables),而且性能强劲。

经过前几个章节的打磨,我们创建的 TaskHub 项目功能已经非常完善了。接下来,我们将进一步优化项目的部署流程,确保代码在不同环境(测试、生产)中能自动切换配置,并且加载速度极快。

环境变量管理:一套代码,多处运行

在真实开发中,API 地址或一些配置在开发环境和线上环境是不一样的。Vite 默认支持 .env 文件,我们可以通过这些文件来管理不同环境下的配置。

在项目根目录下创建两个文件:

.env.development (本地开发)

VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=TaskHub (Dev)

.env.production (线上生产)

VITE_API_BASE_URL=https://api.taskhub.com
VITE_APP_TITLE=TaskHub

说明:

  • Vite 要求变量必须以 VITE_ 开头才能被暴露给客户端代码。
  • 在代码中,可以通过 import.meta.env.VITE_API_BASE_URL 访问这些变量。

通过以上步骤,我们不仅实现了多页面应用的路由管理和登录验证,还通过 Composables 和性能优化技术提升了代码的可维护性和运行效率。希望本文对你有所帮助,祝你在 Vue 3 项目开发中取得更大的进步!