05. Vue3生态实战:Router、Pinia与完整项目

Vue3生态实战:Router、Pinia与完整项目

Vue Ecosystem

恭喜你走到最后一期!前四期我们学习了ES6、Vite、Vue3基础和组合式API。现在,让我们把这些知识整合起来,构建一个完整的Vue3应用。

这一期,我们学习Vue Router(路由)和Pinia(状态管理),然后做一个实战项目。

Vue Router

安装

pnpm add vue-router

基础配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/users/:id',
    name: 'User',
    component: () => import('@/views/User.vue') // 懒加载
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
]

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

export default router
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')
<!-- App.vue -->
<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
  </nav>
  <router-view />
</template>

路由参数

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

const route = useRoute()

// 获取参数
console.log(route.params.id) // /users/123 → 123
console.log(route.query.page) // /users?page=2 → 2
</script>

编程式导航

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

const router = useRouter()

function goToUser(id) {
  router.push({ name: 'User', params: { id } })
}

function goBack() {
  router.back()
}

function replace() {
  router.replace({ name: 'Home' }) // 不留历史记录
}
</script>

嵌套路由

const routes = [
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue'),
    children: [
      {
        path: '', // /settings
        component: () => import('@/views/SettingsProfile.vue')
      },
      {
        path: 'account', // /settings/account
        component: () => import('@/views/SettingsAccount.vue')
      },
      {
        path: 'security', // /settings/security
        component: () => import('@/views/SettingsSecurity.vue')
      }
    ]
  }
]
<!-- Settings.vue -->
<template>
  <div>
    <h1>Settings</h1>
    <nav>
      <router-link to="/settings">Profile</router-link>
      <router-link to="/settings/account">Account</router-link>
      <router-link to="/settings/security">Security</router-link>
    </nav>
    <router-view />
  </div>
</template>

导航守卫

// 全局前置守卫
router.beforeEach((to, from) => {
  // 需要登录
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// 全局后置守卫
router.afterEach((to) => {
  document.title = to.meta.title || 'My App'
})
// 路由元信息
const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: 'Admin Dashboard'
    }
  }
]

Pinia

Pinia是Vue3官方推荐的状态管理库,比Vuex更简单、更强大。

安装

pnpm add pinia

基础配置

// src/stores/index.js
import { createPinia } from 'pinia'

const pinia = createPinia()
export default pinia
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'

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

定义Store

// src/stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 组合式API风格(推荐)
export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isLoggedIn,
    userName,
    login,
    logout
  }
})

使用Store

<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构state和getters(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

// actions直接解构
const { login, logout } = userStore

// 直接访问
console.log(userStore.user)
</script>

<template>
  <div v-if="isLoggedIn">
    Welcome, {{ userName }}
    <button @click="logout">Logout</button>
  </div>
  <div v-else>
    <button @click="login">Login</button>
  </div>
</template>

选项式API风格

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),

  getters: {
    doubleCount: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    },

    async fetchCount() {
      const response = await fetch('/api/count')
      this.count = await response.json()
    }
  }
})

持久化

// 使用pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: true // 启用持久化
})

实战项目:任务管理应用

让我们把学到的知识整合起来,做一个任务管理应用。

项目结构

src/
├── main.js
├── App.vue
├── router/
│   └── index.js
├── stores/
│   ├── index.js
│   ├── tasks.js
│   └── user.js
├── composables/
│   └── useLocalStorage.js
├── views/
│   ├── Home.vue
│   ├── Login.vue
│   └── Tasks.vue
└── components/
    ├── TaskItem.vue
    └── TaskForm.vue

任务Store

// src/stores/tasks.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTaskStore = defineStore('tasks', () => {
  const tasks = ref([
    { id: 1, title: 'Learn Vue3', completed: false },
    { id: 2, title: 'Build a project', completed: false }
  ])

  const filter = ref('all') // all, active, completed

  const filteredTasks = computed(() => {
    if (filter.value === 'active') {
      return tasks.value.filter(t => !t.completed)
    }
    if (filter.value === 'completed') {
      return tasks.value.filter(t => t.completed)
    }
    return tasks.value
  })

  const remainingCount = computed(() =>
    tasks.value.filter(t => !t.completed).length
  )

  function addTask(title) {
    tasks.value.push({
      id: Date.now(),
      title,
      completed: false
    })
  }

  function removeTask(id) {
    const index = tasks.value.findIndex(t => t.id === id)
    if (index > -1) {
      tasks.value.splice(index, 1)
    }
  }

  function toggleTask(id) {
    const task = tasks.value.find(t => t.id === id)
    if (task) {
      task.completed = !task.completed
    }
  }

  function clearCompleted() {
    tasks.value = tasks.value.filter(t => !t.completed)
  }

  return {
    tasks,
    filter,
    filteredTasks,
    remainingCount,
    addTask,
    removeTask,
    toggleTask,
    clearCompleted
  }
}, {
  persist: true
})

任务列表组件

<!-- src/views/Tasks.vue -->
<script setup>
import { useTaskStore } from '@/stores/tasks'
import { storeToRefs } from 'pinia'
import TaskItem from '@/components/TaskItem.vue'
import TaskForm from '@/components/TaskForm.vue'

const taskStore = useTaskStore()
const { filteredTasks, filter, remainingCount } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask, clearCompleted } = taskStore
</script>

<template>
  <div class="tasks">
    <h1>Tasks</h1>
    <TaskForm @add="addTask" />

    <ul class="task-list">
      <TaskItem
        v-for="task in filteredTasks"
        :key="task.id"
        :task="task"
        @toggle="toggleTask(task.id)"
        @remove="removeTask(task.id)"
      />
    </ul>

    <div class="filters">
      <button
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
      >
        All
      </button>
      <button
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
      >
        Active
      </button>
      <button
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
      >
        Completed
      </button>
    </div>

    <div class="footer">
      {{ remainingCount }} items left
      <button @click="clearCompleted">Clear completed</button>
    </div>
  </div>
</template>

<style scoped>
.tasks {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.filters button.active {
  font-weight: bold;
}

.task-list {
  list-style: none;
  padding: 0;
}
</style>

任务项组件

<!-- src/components/TaskItem.vue -->
<script setup>
defineProps({
  task: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['toggle', 'remove'])
</script>

<template>
  <li class="task-item" :class="{ completed: task.completed }">
    <input
      type="checkbox"
      :checked="task.completed"
      @change="emit('toggle')"
    />
    <span>{{ task.title }}</span>
    <button @click="emit('remove')">×</button>
  </li>
</template>

<style scoped>
.task-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-item.completed span {
  text-decoration: line-through;
  color: #999;
}
</style>

任务表单组件

<!-- src/components/TaskForm.vue -->
<script setup>
import { ref } from 'vue'

const title = ref('')

const emit = defineEmits(['add'])

function handleSubmit() {
  if (title.value.trim()) {
    emit('add', title.value.trim())
    title.value = ''
  }
}
</script>

<template>
  <form class="task-form" @submit.prevent="handleSubmit">
    <input
      v-model="title"
      type="text"
      placeholder="What needs to be done?"
    />
    <button type="submit">Add</button>
  </form>
</template>

<style scoped>
.task-form {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.task-form input {
  flex: 1;
  padding: 10px;
  font-size: 16px;
}
</style>

项目优化

代码分割

// router/index.js
const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue')
  }
]

懒加载组件

<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('@/components/HeavyComponent.vue')
)
</script>

构建优化

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
})

系列总结

恭喜你完成了现代前端开发到Vue3的完整学习之旅!

第一期:ES6+、Node.js、npm基础
第二期:Vite构建工具
第三期:Vue3响应式和组件
第四期:组合式API和逻辑复用
第五期:Router、Pinia和实战项目

你现在掌握了:

  • 现代JavaScript语法
  • Vite项目构建
  • Vue3核心概念
  • 组合式API和组合式函数
  • Vue Router路由管理
  • Pinia状态管理
  • 完整项目开发流程

下一步

  • 学习TypeScript(Vue3的最佳伴侣)
  • 探索UI框架(Element Plus、Naive UI)
  • 学习服务端渲染(Nuxt 3)
  • 阅读官方文档(https://vuejs.org/

进阶资源


恭喜!你已经掌握了现代前端开发到Vue3的核心技能。去构建你的下一个项目吧!

Views: 2