__# Vue Router与Pinia状态管理
Vue Router 核心概念与应用
Vue Router 是 Vue.js 的官方路由管理器。它能帮助我们构建单页面应用(SPA),将组件映射到不同的 URL 路径。
首先,我们定义一个路由表 routes,它是一个由多个路由配置对象组成的数组,每个对象描述一个页面路径与对应组件的映射关系。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
// 1. 定义路由表
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
// 2. 创建路由实例
const router = createRouter({
// 3. 配置路由模式
history: createWebHistory(), // 使用 HTML5 History 模式
routes // 等价于 routes: routes
});
export default router;这里有几个关键点值得注意:
history: createWebHistory()
表示使用 HTML5 的 History 模式,URL 是普通路径形式,例如(https://example.com/about), 没有 #
这种模式的好处是更美观、更贴近真实网站结构,但需要服务器做一些额外配置——无论访问哪个路径,都应该返回 index.html,否则刷新页面会 404。
另一种方式:createWebHashHistory()
如果不想配置服务器,可以使用 hash 模式,路径中会带 #,例如:https://example.com/#/about。这种方式兼容性更好,但 URL 略显“丑陋”。
在 Vue 组件中使用
使用 <router-link> 生成导航链接,使用 <router-view> 渲染当前路由匹配的组件。
<!-- App.vue -->
<template>
<header>
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
</nav>
</header>
<!-- 路由出口:当前路由匹配的组件将在这里渲染 -->
<main>
<router-view />
</main>
</template>高级路由特性
动态路由匹配
当需要将具有给定模式的路由映射到同一个组件时,可以使用动态路由。例如,一个 User 组件需要根据不同的用户 ID 显示不同内容。
// router/index.js
import User from '@/views/User.vue';
const routes = [
// `:id` 是一个动态段,可以匹配任意字符串
{
path: '/user/:id',
name: 'User',
component: User,
props: true // 将路由参数(如 id)作为 props 传递给组件
}
];在 User.vue 组件中,可以通过 props 接收 id:
嵌套路由
对于复杂的布局,可以使用嵌套路由。父路由拥有自己的 <router-view> 来渲染子路由组件。
// router/index.js
// ... 假设已导入 User, UserProfile, UserSettings 组件
{
path: '/user/:id',
name: 'User',
component: User, // User.vue 是父路由组件
props: true,
children: [ // 子路由
{
// 当 URL 是 /user/:id 时,UserProfile 会在 User 的 <router-view> 中渲染
path: '',
component: UserProfile
},
{
// 当 URL 是 /user/:id/settings 时,UserSettings 会在 User 的 <router-view> 中渲染
path: 'settings',
component: UserSettings
}
]
}User.vue 组件需要包含 <router-view> 来显示子组件:
<!-- User.vue -->
<template>
<div>
<h1>用户 {{ id }} 的主页</h1>
<!-- 子路由组件的渲染出口 -->
<router-view />
</div>
</template>路由懒加载
当应用规模变大时,可以将不同路由的组件分割成不同的代码块(chunk),然后在访问路由时才加载它们。这可以显著提升首屏加载速度。
// router/index.js
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
// 只有在访问 /dashboard 时,才会下载并执行 Dashboard.vue 的代码
component: () => import('@/views/Dashboard.vue')
}
];重定向与别名
重定向: 当用户访问
/home时,URL会被替换成/,然后匹配/的路由。别名: 访问
/people的效果与访问/users完全一样,但URL保持为/people。
// ... 假设已导入 UserList 组件
const routes = [
// 重定向
{ path: '/home', redirect: '/' },
// 别名
{ path: '/users', component: UserList, alias: '/people' }
];编程式导航
除了使用 <router-link>,我们还可以在 JavaScript 代码中控制路由跳转。
// 在组件的 <script setup> 中
import { useRouter } from 'vue-router';
const router = useRouter();
// 跳转到指定路径
const goToDashboard = () => {
router.push('/dashboard');
};
// 带参数跳转
const goToUser = (userId) => {
router.push({ name: 'User', params: { id: userId } });
};
// 替换当前历史记录,用户无法通过后退按钮返回
const replaceToLogin = () => {
router.replace('/login');
};
// 前进或后退
const goBack = () => {
router.go(-1); // 或 router.back()
};路由守卫
路由守卫(Navigation Guards)提供了在路由跳转过程中执行逻辑的机会,常用于权限验证、数据预取等场景。
全局前置守卫 (beforeEach)
beforeEach 是最常用的守卫,它在任何路由跳转发生之前被调用。
// router/index.js
// import { useUserStore } from '@/stores/user' // 示例:从 Pinia store 获取用户状态
router.beforeEach((to, from, next) => {
const isAuthenticated = false; // 示例:实际应从 store 或 cookie 获取
// 检查路由是否需要认证
if (to.meta.requiresAuth && !isAuthenticated) {
// 用户未登录,重定向到登录页
next({ name: 'Login', query: { redirect: to.fullPath } });
} else {
// 允许导航
next();
}
});to: 即将进入的目标路由对象。from: 当前导航正要离开的路由对象。next: 必须调用的函数,以解析这个钩子。next(): 继续导航。next(false): 中断当前导航。next('/path')或next({ name: '...' }): 重定向到新的地址。
Pinia 状态管理深入
Pinia 是 Vue 官方推荐的状态管理库。它以更简洁的 API、出色的 TypeScript 支持和对 Composition API 的友好性,成为了 Vuex 的现代替代品。
1. Store 定义与使用
一个 Store(仓库)是使用 defineStore 定义的,它包含三部分核心内容:state、getters 和 actions。
State: 响应式的数据源,类似于组件的
data。Getters: 计算属性,类似于组件的
computed,用于派生state。Actions: 方法,类似于组件的
methods,用于修改state,可以包含异步操作。
基础 Store 示例 (stores/user.js)
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// --- State ---
const user = ref(null);
const token = ref('');
// --- Getters ---
const isAuthenticated = computed(() => !!user.value && !!token.value);
const userName = computed(() => user.value?.name || '游客');
// --- Actions ---
async function login(credentials) {
// 假设 authAPI 是一个处理网络请求的模块
// const response = await authAPI.login(credentials);
// user.value = response.user;
// token.value = response.token;
// 模拟登录成功
user.value = { name: '张三', email: 'zhangsan@example.com' };
token.value = 'fake-jwt-token-string';
}
function logout() {
user.value = null;
token.value = '';
}
return {
// State
user,
token,
// Getters
isAuthenticated,
userName,
// Actions
login,
logout
};
});注意:在 Composition API 风格的 store 中,ref() 定义 state,computed() 定义 getters,普通 function 定义 actions。
在组件中使用 Store
<script setup>
import { useUserStore } from '@/stores/user';
// 获取 store 实例
const userStore = useUserStore();
// 访问 state 和 getters (具有响应性)
console.log(userStore.userName);
// 调用 actions
const handleLogin = async () => {
try {
await userStore.login({ username: 'test', password: '123' });
console.log('登录成功!');
} catch (error) {
console.error('登录失败:', error);
}
};
</script>
<template>
<div>
<p>用户: {{ userStore.userName }}</p>
<p v-if="userStore.isAuthenticated">状态: 已登录</p>
<button v-if="!userStore.isAuthenticated" @click="handleLogin">登录</button>
<button v-else @click="userStore.logout()">登出</button>
</div>
</template>Store 组合
Pinia 的一大优势是其模块化的设计,Store 之间可以轻松地相互调用。
// stores/cart.js
import { defineStore } from 'pinia';
import { useUserStore } from './user'; // 导入用户 store
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore(); // 在另一个 store 中获取实例
async function checkout() {
if (userStore.isAuthenticated) {
console.log(`用户 ${userStore.userName} 正在结算...`);
// 执行结算逻辑...
} else {
console.log('请先登录再结算。');
}
}
return { checkout };
});面试常见追问及应对
"Pinia 相比 Vuex 有什么优势?"
答题思路:从设计理念、API 简洁性和开发体验三个方面进行对比,并用表格清晰展示。
| 特性 | Pinia | Vuex (4.x) |
|---|---|---|
| 核心概念 | State, Getters, Actions | State, Getters, Mutations, Actions |
| State 修改 | 在 Actions 中直接修改 | 必须通过 Mutations,Actions commit Mutation |
| API 风格 | 更贴近 Vue 3 Composition API,直观 | 概念较多,有一定模板代码 |
| TypeScript | 完美的类型推断,无需额外类型定义 | 需要复杂的类型体操来获得良好支持 |
| 模块化 | 天然的模块化,每个 store 都是一个独立的模块 | 通过 modules 配置,有命名空间概念 |
| 代码体积 | 非常轻量,仅约 1KB | 体积相对较大 |
总结:Pinia 的主要优势在于其简洁直观的 API、移除了 Mutations的心智负担、出色的 TypeScript 支持以及更自然的模块化方式。它让状态管理代码更易于编写和维护。
如何设计一个大型应用的路由结构?"
答题思路:从模块化、权限控制、性能和可维护性四个角度阐述。
一个健壮的路由结构应该具备以下特点:
模块化:按业务功能或页面区域(如 后台管理、用户中心)将路由配置拆分到不同的文件中,再由主路由文件统一导入整合。这样可以避免单个路由文件过于庞大。
权限控制:
在路由的
meta字段中定义权限信息,如meta: { roles: ['admin'] }。使用全局路由守卫
router.beforeEach来检查用户角色和meta字段,实现页面访问控制。对于需要根据用户权限动态生成的菜单,可以后端返回路由数据,前端进行动态添加 (
router.addRoute())。
- 性能优化:
全量使用懒加载:对所有页面级组件使用
() => import(...)进行懒加载,这是最关键的性能优化手段。预加载(Prefetching):对于用户很可能访问的下一个页面,可以考虑使用
webpack的魔法注释/* webpackPrefetch: true */进行资源预加载。
- 可维护性:
统一命名:为路由
name属性制定清晰、唯一的命名规范,方便编程式导航和缓存控制(<keep-alive>)。目录结构清晰:将路由配置文件、视图组件、路由相关的工具函数等分门别类存放。
"如何处理 Pinia 中的异步操作和错误?"
答题思路:展示一个包含 loading 和 error 状态管理的标准异步 action 模式。
在 Pinia 中,异步操作通常在 actions 中使用 async/await 完成。一个健壮的实践是同时管理加载状态(loading)和错误状态(error)。
// stores/data.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useDataStore = defineStore('data', () => {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
async function fetchData() {
loading.value = true;
error.value = null; // 重置之前的错误
try {
// 假设 myApi.get() 是一个返回 Promise 的 API 请求函数
const response = await myApi.get('/some-data');
data.value = response.data;
} catch (e) {
// 捕获错误并存储
error.value = e;
// 可以选择将错误再次抛出,让调用方处理 UI 反馈,如弹窗提示
throw e;
} finally {
// 确保 loading 状态总是被重置
loading.value = false;
}
}
return { data, loading, error, fetchData };
});最佳实践:
分离状态:用
loading和error两个独立的state来追踪异步流程。UI 绑定:在组件中,可以直接使用
v-if="store.loading"显示加载指示器,或v-if="store.error"显示错误信息。错误抛出:在
catch块中再次throw错误,可以让组件层捕获到具体的失败,从而执行如“消息提示”、“跳转页面”等交互。finally清理:使用finally确保无论成功还是失败,loading状态都会被正确地设置为false。
