Vue 路由(Vue Router

Vue 提供了官方的路由库 Vue Router,用于构建单页面应用(SPA,Single Page Application)

Vue Router 可以让应用在不刷新的情况下根据不同的 URL 显示不同的页面

Vue Router 核心功能:

  • 在单页面应用中实现页面导航
  • 根据 URL 动态加载对应的组件
  • 支持浏览器前进、后退等功能
  • 提供路由守卫用于权限控制和数据预加载

基本使用

  1. 在 Vue 项目中安装 Vue Router
1
npm install vue-router@4 # 如果是 Vue2 请使用 vue-router@3
  1. 创建路由配置

创建一个 router/index.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* router/index.js */
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

// 定义路由 routes
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]

// 创建路由器 router
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 history 模式
routes
})

// 导出路由器
export default router
  1. 在主程序( main.js )中使用 router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* main.js */
import { createApp } from 'vue'
import App from './App.vue'
// 导入路由器
import router from './router'

const app = createApp(App)

// 在 Vue 应用中使用路由
app.use(router)
app.mount('#app')

/*
app.use(router) 会执行 router.install(app) 方法,它会注册 2 个全局组件 Router-Link 和 Router-View,并将路由实例($router) 和 路由信息对象($route) 注入应用,后续我们可以在组件中使用
*/
  1. 在模板中使用 <router-link><router-view> 组件
1
2
3
4
5
6
7
8
9
10
11
12
<!-- App.vue -->
<template>
<div>
<h1>我的网站</h1>
<nav>
<!-- 导航链接 -->
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
</nav>
<router-view /> <!-- 当前路由对应的组件会显示在这里 -->
</div>
</template>

基本概念

路由(Route

路由 Route 定义了 URL 与组件之间的映射关系

语法:

1
2
3
4
5
6
7
8
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
/*
path 对应浏览器地址栏的路径
component 对应 Vue 的组件
*/
]

导航(Navigation

“导航”指的是在不同路由之间进行跳转。在 Vue 中使用 <router-link> 组件或编程方式(如 router.push)进行页面跳转

声明式导航:

1
2
3
4
<template>
<router-link to="/home">Home</router-link> <!-- 导航链接组件, 点击时会改变浏览器地址栏 URL, 从而触发 <router-view> 的改变(显示路由对应的组件) -->
<router-view /> <!-- 视图占位组件, 它会渲染当前路由匹配到的组件 -->
</template>

编程式导航:

编程式导航是在组件 script 中通过路由器实例 API 进行页面跳转的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<button @click="toHome">Home</button>
<router-view />
</template>

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

const router = useRouter()

const toHome = () => {
router.push("/home")
}
</script>

路由器(Router

路由器 Router 是路由管理的核心对象,用于配置路由并控制导航

创建路由器:

1
2
3
4
5
6
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
history: createWebHistory(), // 使用 History 模式
routes, // 路由规则
})

历史模式(History Mode

Web 前端路由主要基于 History API 和 Location API 实现,一般包括两种模式:History 模式和 Hash 模式

Vue 中是用 createWebHistory()createWebHashHistory() 来创建对应的历史模式,它会返回一个 RouterHistory 对象,Vue Router 内部会使用这个对象,用于跳转和监听浏览器地址栏 URL 的变化

Hash 模式

Vue 中 Hash 模式是用 createWebHashHistory() 创建:

1
2
3
4
5
6
7
8
9
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
// 使用 Hash 历史模式
history: createWebHashHistory(),
routes: [
// 路由规则...
],
})
  • 浏览器地址栏中 # 号后面的内容都是 hash 值,它不会包含在 HTTP 请求的路径中。其兼容性较好,无需后端特地去处理路径问题

History 模式

Vue 中 History 模式是用 createWebHistory() 创建:

1
2
3
4
5
6
7
8
9
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
// 使用 History 历史模式
history: createWebHistory(),
routes: [
// 路由规则...
]
})
  • History 模式中没有 # 号,比较符合正常的 URL,但在项目上线时,需要后端开发人员去分辨前端路由和后端路由,返回不同的资源,从而解决 Vue 单页应用在切换路由后,刷新页面,服务器返回 404 的问题

详细用法

命名路由

1
2
3
4
5
6
{
// 命名路由, 多个路由 name 值不能重复, 否则路由会匹配失败
name: 'Message',
path: '/message',
component: Message
}
1
2
3
<router-link :to="{
name: 'Message'
}">显示消息</router-link> <!-- 点击 URL = /message -->
1
2
3
import { useRouter } from 'vue-router'
const router = useRouter()
router.push({ name: 'Message' })

路由参数

参数类型:

类型说明示例 URL特点
params 参数路径参数(出现在路径中)/user/123需要先在路由配置中定义参数名(占位符)
query 参数查询参数(?key=value 形式,多个 key=value 用 & 连接)/user?id=123不需在路由中预先定义参数名

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
/*
params 参数
如果不传参数, 路由会匹配失败
:id 为参数名
*/
path: '/list1/:id',
component: List
},
{
/*
query 参数
无需显示声明参数名, 而是在定义导航时指定参数名(key)
也可以不传参, 因此参数是可选的
*/
path: '/list2',
component: List
}
]

在对应页面组件中可以

  • 通过 $route.query.<key> 获取 query 参数
  • 通过 $route.params.<name> 来获取 params 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!--
URL 写法
-->
<router-link to="/list1/123">
显示列表1, 并传入 params 参数
</router-link> <!-- URL = /list1/123 -->
<router-link to="/list2?id=123">
显示列表2, 并传入 query 参数
</router-link> <!-- URL = /list2?id=123 -->

<!--
对象写法,推荐
-->
<router-link :to="{
path: '/list1',
params: {
id: 123
}
}">
显示列表1, 并传入 params 参数
</router-link> <!-- URL = /list1/123 -->
<router-link :to="{
path: '/list2',
query: {
id: 123
}
}">
显示列表2, 并传入 query 参数
</router-link> <!-- URL = /list2?id=123 -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 显示列表1, 并传入 params 参数
router.push({
path: '/list1',
params: {
id: 123
}
}) // URL = /list1/123

// 显示列表2, 并传入 query 参数
router.push({
path: '/list2',
query: {
id: 123
}
}) // URL = /list2?id=123

使用正则表达式动态匹配 params 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
// ? 代表 :id 为可选参数, 不传参也可以匹配到路由
path: '/user/:id?'
},
{
// (\\d+) 代表 :id 只能为数字
path: '/user/:id(\\d+)'
},
{
// (\\d+)? 代表 :id 为可选参数, 且只能为数字
path: '/user/:id(\\d+)?'
},
{
// ([A-Z0-9]{6}) 代表 :id 可以是数字也可以是字母且长度不能超过6位
path: '/user/:id([A-Za-z0-9]{6})'
},
{
// /:pathMatch(.*)* 代表匹配任何 URL 路径, 通配符匹配优先级最低, 通常用于其它所有路由都没有匹配成功时, 显示找不到页面(404)
path: '/:pathMatch(.*)*'
}
]

子路由(嵌套路由)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
path: '/user/:id?',
component: User,
children: [
/*
子路由的路径不要以 / 开头, 因为这样就变成绝对路径了, 无法在 URL 路径中体现出父子关系
*/
{
path: 'detail', // 如果子路由也需要 params 参数, 则不能与父级 params 参数名相同
name: 'Detail',
component: Detail
}
// 如果还有子级路由, 还可以继续嵌套 children
]
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2>我是 User 组件</h2>
<!-- active-class 为导航激活时应用的 class -->
<router-link to="/user/1/detail" active-class="active">前往用户1详情页面</router-link> <!-- URL = /user/1/detail -->
<router-link to="/user/detail" active-class="active">前往用户详情页面</router-link> <!-- URL = /user/detail -->
<router-link :to="{ name: 'Detail' }" active-class="active">前往用户详情页面</router-link> <!-- URL = /user/detail or /user/:id/detail -->

<router-view></router-view> <!-- 子路由对应的组件显示位置 -->
</template>

<style scoped>
.active {
color: red
}
</style>

重定向

1
2
3
4
5
6
7
8
9
10
11
[
{
path: '/',
// 当 URL 为 / 时, 重定向到 /home 对应的路由
redirect: '/home'
},
{
path: '/home',
component: Home
}
]

路由守卫

缓存路由

Vue Router API

route 路由信息对象

route 路由信息对象,包含当前路由的:

  • fullPath:完整 URL 路径,带参数
  • name:名称
  • params:params 参数
  • path:基础路径
  • query:查询字符串参数

可以通过 useRoute 来获取 route 对象

例:

1
2
3
4
5
6
7
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

console.log(route)
</script>

router 路由器对象

router 路由器对象用于控制路由跳转(导航)

router.push()

用于跳转到新路由,会添加历史记录

例:

  • router.push('/home')
  • router.push({ name: 'User', params: { id: 1 } })

router.replace()

跳转路由但不添加历史记录(替换当前历史记录)

例:

  • router.replace('/login')

router.go()

前进/后退 n 个页面

例:

  • router.go(-2) 回退 2 个页面(正数为前进)

router.back()

回退到上一个路由

router.forward()

前进到下一个路由

router.addRoute()

添加路由

例:

  • router.addRoute({ path: '/about', component: About }) 添加根路由
  • router.addRoute('Root', { path: '/user', component: User }) 向路由 name 为 Root 的路由添加子路由

router.removeRoute()

移除路由

例:

  • router.removeRoute('Other') 移除路由 name 为 Other 的路由

router.hasRoute()

判断是否存在某路由(true / false)

例:

  • router.hasRoute('Ahout') 检测路由 name 为 Ahout 的路由是否存在

router.getRoutes()

获取所有已经注册的路由记录

Vue 状态管理(Vue State Management

状态(state) 就是程序中那些会影响 UI 显示的数据,一旦这些数据变化,页面显示也要跟着变化。状态管理的核心目的,就是 让这些数据的修改与使用更加可控、有序。

在 Vue 中,简单的项目可以用 props 和 events 进行组件通信,但当项目规模扩大后,就会遇到以下痛点:

  • 跨层级通信麻烦(props drilling)
  • 兄弟组件通信不方便
  • 状态分散(不同组件各自维护,难以管理)

这时候就需要引入一种可以在多个组件之间共享状态的方案

Pinia

Pinia 是 Vue 官方推荐的新一代状态管理库,它的设计目标是 更轻量、更直观、更易用

Pinia 其名字来源 “菠萝”(西班牙语 pina)

Pinia 作为集中式状态管理库,可以帮助我们解决 Vue 中多组件间共享和管理全局状态混乱的问题,让状态管理更统一、更规范、更可维护

Pinia 的特点

  • 更轻量:API 简单易学
  • 更直观:和 Vue3 的 Composition API 风格一致
  • 完整的 TypeScript 支持:类型推导更有好
  • 插件系统:支持持久化存储(如 localStroage)、中间件扩展等

Pinia 基本使用

  1. 安装 pinia
1
npm install pinia
  1. 在主程序( main.js )中,创建并使用 Pinia
1
2
3
4
5
6
7
8
9
10
11
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
// 创建 Pinia 实例
const pinia = createPinia();
// 在 Vue 应用中使用 Pinia
app.use(pinia);
app.mount('#app');
  1. 定义一个 Store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// stores/counter.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useCounterStore = defineStore("counter", () => {
  const count = ref(0);
  const doubleCount = computed(() => (state) => state.count * 2);
  const increment = () => count.value++;

  return {
    count,
    doubleCount,
    increment,
  };
});
  1. 在组件中使用 Store
1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { useCounterStore } from '@/stores/counter';

const counter = useCounterStore();
</script>

<template>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">增加</button>
</template>

Pinia 核心概念

Pinia 的核心就是 Store(仓库),它里面包含了 state、getters、actions

Store(仓库)

  • Store 就是一个全局的 状态容器
  • 每个 Store 都有唯一的 id
  • 可以有多个 Store,互相独立,也可以组合使用

定义一个 Store:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineStore } from 'pinia';

/*
defineStore
第一个参数为 Store 的唯一 id
第二个参数为配置对象或一个 setup 函数
返回一个 Store 实例
*/
export const useCountStore = defineStore('count', {
state: () => ({
count: 0
})
})

使用 Store:

1
const countStore = useCountStore();

State(状态)

  • 存放数据,类似 Vue 组件的 data
  • state 是响应式的,修改后会触发视图更新

例:

1
2
3
4
5
6
7
8
9
10
// stores/counter.js
import { defineStore } from 'pinia';

export const useCountStore = defineStore('count', {
// 定义 state
state: () => ({
count: 0,
user: { name: 'Tom', age: 18 }
})
})
1
2
3
4
5
6
7
8
9
10
11
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, reactive } from "vue";

export const useCountStore = defineStore('count', () => {
// 定义 state
const count = ref(0);
const user = reactive({ name: 'Tom', age: 18 })

return { count, user }
});

在组件中访问 state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 在模板中访问 countStore -->
<p>{{ countStore.count }}</p>
<p>{{ countStore.user.name }}</p>
<p>{{ countStore.user.age }}</p>
</template>

<script setup>
import { useCountStore } from '~/stores/counter.js';

const countStore = useCountStore();

// 修改 countStore 会自动更新视图
countStore.count++
countStore.user.name = 'Steve'
</script>

Getters(计算属性)

  • 类似 Vue 的 computed,会缓存使用过的响应式数据,只有在这些响应式数据发生变化时才会重新计算并更新视图
  • 用来派生出新数据,不会修改 state,只是基于 state 计算

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// stores/counter.js
import { defineStore } from 'pinia';

export const useCountStore = defineStore('count', {
// 定义 state
state: () => ({
count: 0,
user: { name: 'Tom', age: 18 }
}),
// 定义 getters
getters: {
doubleCount: (state) => state.count * 2,
userInfo: (state) => `${state.user.name} (${state.user.age})`
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, reactive, computed } from "vue";

export const useCountStore = defineStore('count', () => {
// 定义 state
const count = ref(0);
const user = reactive({ name: 'Tom', age: 18 })

// 定义 getters
const doubleCount = computed(() => count.value * 2)
const userInfo = computed(() => `${user.name} (${user.age})`)

return { count, user, doubleCount, userInfo }
})

在组件中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<p>{{ countStore.doubleCount }}</p>
<p>{{ countStore.userInfo }}</p>
</template>

<script setup>
import { useCountStore } from '~/stores/counter.js';

const countStore = useCountStore();

// 修改 countStore 中的 state 会使 getters 重新计算并更新视图
countStore.count++
countStore.user.name = 'Steve'
</script>

Actions(方法)

  • 类似 Vue 组件的 methods
  • 用来修改状态处理异步逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// stores/counter.js
import { defineStore } from 'pinia';

export const useCountStore = defineStore('count', {
// 定义 state
state: () => ({
count: 0,
user: { name: 'Tom', age: 18 }
}),
// 定义 actions
actions: {
increment() {
this.count++;
},
async fetchUser() {
// fetch 获取 User Info
const res = await fetch('/api/user');
// 将 User Info 存储到 state.user 中
this.user = await res.json();
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// stores/counter.js
import { defineStore } from 'pinia';
import { ref } from "vue";

export const useCountStore = defineStore('count', () => {
// 定义 state
const count = ref(0);
const user = ref({ name: 'Tom', age: 18 });

// 定义 actions
const increment = () => count.value++;
const fetchUser = async () => {
// fetch 获取 User Info
const res = await fetch('/api/user');
// 将 User Info 存储到 state.user 中
user.value = await res.json();
};

return { count, increment, fetchUser }
});

在组件中使用:

1
2
3
4
5
6
7
8
9
10
<template>
<button @click="countStore.increment">+1</button>
<button @click="countStore.fetchUser">获取用户</button>
</template>

<script setup>
import { useCountStore } from '~/stores/counter.js';

const countStore = useCountStore();
</script>

Pinia 常用 API

$reset()

将 store 恢复到初始状态(定义时的初始值),但使用组合式 setup 函数定义的 Store 不能使用该方法(需要手动实现 $reset 方法)

$patch()

一次性批量修改多个 state 属性
避免多次触发响应式更新

1
2
3
4
5
6
7
8
9
10
11
// 语法1
userStore.$patch({
name: 'Tom',
age: 25
})

// 语法2
userStore.$patch((state) => {
state.age++
state.name = 'Jerry'
})

$subscribe()

监听整个 state 的变化

1
2
3
4
5
6
7
8
/*
mutation: 包含状态变化的相关信息,如 新旧值、storeId、type(修改方式) 等
state: 变化后的 state
return stop: 清除监听器函数, 默认会随组件卸载时自动清除, 在第2个参数中配置 { detached: true } 可以关闭自定清除
*/
const stop = userStore.$subscribe((mutation, state) => {
console.log('state changed:', mutation, state)
})

$onAction()

监听 action 的调用

1
2
3
4
5
6
7
8
9
10
11
12
/*
name: action 名称
store: 当前 store
args: action 调用时传递的参数
after: action 调用成功时执行的回调
onError: action 调用失败时执行的回调
*/
userStore.$onAction(({ name, store, args, after, onError }) => {
console.log(`${name} action called with`, args)
after(() => console.log(`${name} 完成`))
onError((error) => console.error(`${name} 出错`, error))
})

storeToRefs()

将 store 的响应式 state 和 getter 转为 refs,防止解构丢失响应式

1
2
3
import { storeToRefs } from 'pinia'
const user = useUserStore()
const { name, age } = storeToRefs(user)

Pinia 插件

Pinia 插件(Plugin) 是用来「扩展 Pinia 功能」的一种机制。你可以在不修改原始 Store 代码的前提下,为所有 Store 添加新功能

1.定义一个插件

1
2
3
4
5
6
7
8
9
10
11
/*
plugins/myPlugin.js
*/
function myPlugin({ store, options, pinia }) {
// store:当前 Store 实例
// options:defineStore 时定义的配置选项(state、actions 等)
// pinia:Pinia 实例

// 为所有 Store 实例添加 createdAt 属性, 即 Store 实例创建时间
store.createdAt = new Date()
}

2.使用插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
main.js
*/
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// 导入插件
import { myPlugin } from './plugins/myPlugin'

const app = createApp()
// 穿件 Pinia 实例
const pinia = createPinia()
// 使用 myPlugin 插件, pinia 会在 Store 实例创建时执行 myPlugin
pinia.use(myPlugin)
app.use(pinia)

例(定义状态持久化插件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
plugins/persist.js
*/
export function persistPlugin({ store }) {
const key = `pinia-${store.$id}`

// 1️. 初始化时,从 localStorage 读取数据
const fromStorage = localStorage.getItem(key)
if (fromStorage) {
store.$patch(JSON.parse(fromStorage))
}

// 2️. 监听 store 的变化,自动保存到 storage 中
store.$subscribe((mutation, state) => {
localStorage.setItem(key, JSON.stringify(state))
})
}

使用状态持久化插件:

1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// 导入插件
import { persistPlugin } from './plugins/persist.js'

const app = createApp()
const pinia = createPinia()
// 使用 persistPlugin 插件
pinia.use(persistPlugin)
app.use(pinia)