路由的简介
路由(route):就是根据特定的规则将数据包或请求从源地址传输到目标地址的过程。
在前端或者vue3项目中路由主要用于构建单页面应用程序(SPA),其中所有的页面都在同一个HTML文件中加载,通过JavaScript动态地切换显示不同的内容。
路由器(Router):路由器是负责管理路由的组件或模块。它定义了路由的规则、路径和对应的处理函数或组件。
请着重注意,路由器(Router)与路由(Route)的区别。路由器是用来管理路由的,你也可以理解为路由的集合。如果将一个一个的路由比作学生,那么路由器就是老师,负责管理所有的路由。在vue3项目中,路由是由路径和对应的组件构成的。我们通过切换不用的路径来展示不同的组件效果,路由器就是负责管理这些路径和路由对应的一些属性,来控制和管理路由的。
vue3中路由的官网为:Vue Router | Vue.js 的官方路由
路由的创建
导入路由对应的依赖(也可以选择在创建vue项目时直接导入)
1,下载vue-router的依赖
npm install vue-router@4
2,在src目录下新建一个router目录,在router目录下新建一个index.ts文件,在这个ts文件中实现路由的定义的管理。
import {
createRouter, // 路由hook,用来创建路由实例
type RouteRecordRaw, // 路由类型,直接导出vue-router中定义的路由类型来约自定义的路由类型
createWebHashHistory, // 路由模式,
} from "vue-router";
// 直接导入组件,一般像登录页、错误页直接导入,并且配置固定路由信息,以便在项目启动之后就展示
import ParentComponent from "../passvalue/ParentComponent.vue";
// 定义路由信息是一个数组
const routes: Array<RouteRecordRaw> = [
{
path: "/", // 路由访问路劲
name: "Parent", // 路由名称
component: ParentComponent, // 路由组件,具体的组件,根据path或者name跳转的具体组件。
children: [], // 子路由数组,如果该路由下有子路由的话
},
{
//path: '/tab/:foo/:too',
path: "/tab",
name: "Tab",
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import("../dynamiccomponent/TabComponent.vue"), // 动态导入具体的组件,相当于是懒加载
},
];
// 调用路由hook,用来创建路由实例
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), // 路由模式
routes: routes, // 路由信息
});
// 暴漏路由实例,在全局中使用
export default router;
3,在main.ts中引入路由
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
路由的模式
1,createWedHashHistory:vue3中hash模式,在url中的表现为后面会拼接一个 #
hash模式下,hash部分指的是 # 以及 # 后边的那一部分,在页面进行导航时,改变hash部分不会引起页面刷新。一般通过hashchange事件监听hash的改变,改变hash部分的方式有一下几种方式:
1.1通过浏览器上的前进后退改变URL
// 当点击浏览器上的前进后退时,就会触发hashchange事件,并将前后的路由信息回传到回调函数中
window.addEventListener('hashchange', (e) => console.log(e))
1.2通过《a》改变URL
1.3通过window.location改变URL
// 直接通过改变location.hash进行路由跳转
location.hash = '/cc'
2,createWebHistory:vue3中的history模式,基于H5的history模式实现
history模式时基于H5的history模式实现的,改变URL有以下几种方式:
2.1通过浏览器上的前进后退改变URL
// 当点击浏览器上的前进后退时,就会触发popstate事件,并将前后的路由信息回传到回调函数中
window.addEventListener('popstate', (e) => console.log(e))
2.2通过history.pushState()改变URL
// 第一个参数是一个对象,可以存一些参数信息,第二个参数是title,第三个参数是要跳转的路由
history.pushState({},'','/xxx');
路由的跳转方式
1,使用<router-link to='/'>根据path跳转或者<router-link :to="Partent">根据name跳转。
<template>
<router-link to="/">
<!-- replace 加上replace就会销毁历史记录,浏览器中就不能前进后退-->
<div class="box">
你好 vue3
</div>
</router-link>
<router-view></router-view>
</template>
2,使用<a href="/">Parent</a>跳转
<a href="/home">Parent</a>
3,通过button点击事件进行跳转,多 应用于类似登录页面这样的场景下,点击按钮之后,登录验证通过之后跳转到首页。
<template>
<button @click="xz('/')"></button>
<button @click="sc">上一页</button>
<button @click="fs">下一页</button>
</template>
<script setup lang='ts'>
import router from "@/router"
const xz = (url:string) =>{
// router.push(url) // 根据path跳转,这里的url是path
// router.replace({ // 使用replace就会销毁历史记录,浏览器中不能前进后退
// name:url
// })
router .push({
name:url
})
}
const sc = () =>{
//使用router.go(1)进行自定义跳转,1代表前进一页
router.go(1)
}
const fs = () =>{
//使用router.go(-1)进行自定义跳转,-1代表后退一页
//router.go(-1)
router.back() //后退一页
}
</script>
路由传参:params/query
1,name+params方式:
// 路由定义
{
path: '/tab/:foo/:too', // 新版的vue-router要传递的参数必须在path中以该形式定义才可以获取到值,否则获取不到值
name: 'Tab',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../dynamiccomponent/TabComponent.vue')
}
// 路由跳转
<button @click="toPage('Tab')" style="width: 200px; background-color: red; height: 30px">Tab</button>
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = (url: string) => {
router.push({
name: url,
params: {
foo: "foo",
too: "too",
},
});
};
// 接收的时候,也要用params去接收
import { useRoute } from 'vue-router'
const route = useRoute()
<div>{{ route.params.foo }}</div>
<div>{{ route.params.too }}</div>
2,path+query方式:
// 路由定义
{
path: '/tab',
name: 'Tab',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../dynamiccomponent/TabComponent.vue')
}
// 路由跳转
<button @click="toPage('/tab')" style="width: 200px; background-color: red; height: 30px">Tab</button>
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = (url: string) => {
router.push({
path: url,
query: {
foo: "foo",
too: "too",
},
});
};
// 接收的时候,也要用query去接收
import { useRoute } from 'vue-router'
const route = useRoute()
<div>{{ route.query.foo }}</div>
<div>{{ route.query.too }}</div>
嵌套路由:children
嵌套路由就是指,父路由下还有子路由,子路由下还有子路由,理论下,可以无限制嵌套子路由,子路由一般会展示在父路由下的<router-view></router-view>中,在跳转子路由的时候,path中必须是 父路由+子路由path 才可以跳转,即必须添加父路由前缀,先当于必须告诉vue具体的子路由路径才可以找到对应的子路由组件。
{
path: "/opine",
name: "opine",
component: () => import('../views/opine.vue'),
children: [
{
path: "/opine/home",
name: "Home",
component: () => import('../views/opine/Home.vue'),
children:[
{
path:"/opine/home/index",
name:"Home/index",
component: () => import('../views/opine/Home/index.vue')
}
]
},
{
path: "/opine/index",
name: "Index",
component: () => import('../views/opine/index.vue')
}
]
}
命名视图:components
命名视图指的是一个路由下可以有多个别名的路由组件信息,比如我们有一个tab页签,通过登录人的不同,再该页面下展示不同的信息,可以考虑使用该方式的组件。
{
path: "/", // 路由访问路劲
name: "Parent", // 路由名称
component: ParentComponent
children: [
path: "/", // 路由访问路劲
name: "Parent", // 路由名称
components: { // 路由组件,具体的组件,根据path或者name跳转的具体组件,如果有多个就用components,单个就用component。
default: () => import('../dynamiccomponent/TabComponent.vue'), // 默认路由组件,跳转到Parent组件之后,这样写<router-view></router-view>,就会展示default命名的组件
Tab11: () => import('../dynamiccomponent/TabComponent.vue'), // Tab11命名的路由组件,跳转到Parent组件之后,这样写<router-view name="Tab11"></router-view>,就会展示Tab11命名的组件
Tab12: () => import('../dynamiccomponent/TabComponent.vue'), // Tab12命名的路由组件,跳转到Parent组件之后,这样写<router-view name="Tab12"></router-view>,就会展示Tab12命名的组件
}
], // 子路由数组,如果该路由下有子路由的话
},
路由重定向 redirect
{
path: "/", // 路由访问路劲
alias: ["/P1", "/P2", "/P3"],
// 给Parent路由起多个别名,实际上访问"/P1", "/P2", "/P3"都指向的是Parent所对应的路由组件信息
name: "Parent", // 路由名称
// 当跳转/的时候,就会被重定向到/tab上,访问/tab下的组件信息,以下几种redirect的方式都是等价的
// redirect: "/tab", // 直接使用字符串的方式重定向
// redirect: { // 直接使用对象的的方式重定向,name或者path
// // name: 'Tab1',
// path: '/tab'
// },
redirect(to) {
// 直接使用对象的的方式重定向,该方式是一个接收一个回调函数,通过回调函数返回重定向路由信息,参数to实际上就是父路由的全部信息
console.log('to', to);
// return '/tab' // 回调函数返回字符串重定向路由信息
return {
// 回调函数返回对象重定向路由信息,对象中可以传递参数给子路由,子路由可以接收该参数
// path: '/tab',
// query: {
// n: to.name,
// }
name: 'Tab',
params:{ name: 'zs'}
}
},
component: ParentComponent
children: [
path: "/tab", // 路由访问路劲
name: "Tab", // 路由名称
components: { // 路由组件,具体的组件,根据path或者name跳转的具体组件,如果有多个就用components,单个就用component。
default: () => import('../dynamiccomponent/TabComponent.vue'), // 默认路由组件,跳转到Parent组件之后,这样写<router-view></router-view>,就会展示default命名的组件
Tab11: () => import('../dynamiccomponent/TabComponent.vue'), // Tab11命名的路由组件,跳转到Parent组件之后,这样写<router-view name="Tab11"></router-view>,就会展示Tab11命名的组件
Tab12: () => import('../dynamiccomponent/TabComponent.vue'), // Tab12命名的路由组件,跳转到Parent组件之后,这样写<router-view name="Tab12"></router-view>,就会展示Tab12命名的组件
}
], // 子路由数组,如果该路由下有子路由的话
},
路由守卫:beforeEach & afterEach
main.ts中引入路由之后,添加路由守卫逻辑之后,所有的路由跳转前都会执行beforeEach的逻辑,所有的路由跳转完成之后都会执行afterEach的逻辑。
1,前置守卫:beforeEach
当路由跳转前都会走这个方法,一般应用于权限控制场景。
/**
* 前置守卫: 当路由跳转前都会走这个方法,一般应用于权限控制场景。
* to:要跳转到哪个路由
* from:从哪个路由跳转过来的
* next:允许跳转,是一个函数,如果不执行next()函数,那么路由就不会跳转,相当于是一个放行信号
*/
router.beforeEach((to, from, next) => {
if (to.path == "/") {
// 如果是跳转的登录页,直接放行
next();
return;
}
if (window.sessionStorage.getItem("orkasgb_satoken")) {
// 这里可以做权限判断,比如:token如果存在的时候才允许跳转到首页
if (!user) {
// 如果用户不存在,就去初始化用户信息
// 初始化用户信息
}
next();
return;
}
next("/?redirect" + to.path);
// 其他异常情况下,直接重定向到登录页面
});
2,后置守卫:afterEach
当路由跳转完成之后都会走这个方法
/**
* 后置守卫: 当路由跳转完成之后都会走这个方法
* to:要跳转到哪个路由
* from:从哪个路由跳转过来的
*/
router.afterEach((to, from) => {
console.log("from: ", from);
console.log("to: ", to);
});
3,例:用前置守卫和后置守卫实现一个全局的组件加载进度条功能
// ProcessBarComponent.vue组件封装
<template>
<div class="wraps">
<div ref="bar" class="bar"></div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
let speed = ref(1)
const bar = ref<HTMLElement>() // 获取进度条dom
const timer = ref<number>(0)
// 开始处理进度条
const startProcess = () => {
const dom = bar.value as HTMLElement
// 实际上类似于setTimeOut定时器
timer.value = window.requestAnimationFrame(function handleBar() {
if (speed.value < 90) {
speed.value += 1
dom.style.width = speed.value + '%' // 设置进度条
timer.value = window.requestAnimationFrame(handleBar) // 递归调用
} else {
speed.value = 1
window.cancelAnimationFrame(timer.value) // 清除定时器
}
})
}
// 结束进度条
const stopProcess = () => {
const dom = bar.value as HTMLElement
speed.value = 100
dom.style.width = speed.value + '%'
}
// 暴漏方法
defineExpose({
startProcess,
stopProcess
})
</script>
// main.ts中将进度条注册为全局组件,左右的组件加载的时候都会有进度条加载效果
import { createApp, createVNode, render } from 'vue'
import ProcessBarComponent from './processbar/ProcessBarComponent.vue' // 导入进度条注册
const processBar = createVNode(ProcessBarComponent) // createVNode将导入进度条注册转化为VNode,为了直接将其挂载到body上,并且转化为VNode之后,才能在mian.ts中使用其暴漏出来的方法
render(processBar, document.body) // render函数将VNode挂载到body上
router.beforeEach((to, from, next) => {
processBar.component?.exposed?.startProcess(); // 前置路由守卫调用startProcess开始跑进度条
next();
});
router.afterEach((to, from) => {
processBar.component?.exposed?.stopProcess(); // 后置路由守卫调用stopProcess处理进度条
});
动态路由
动态路由实际就是在登录验证之后由后台根据用户权限返回路由信息,
1,路由表可以分为静态的和动态的
静态路由表:不需要作权限控制的路由,每个用户都可以正常访问
动态路由表:需要做权限控制的路由,用户如果权限不一致访问的路由也不一样
原理
思路
实现
流程:
- 登录的时候,根据登录用户返回此角色可以访问的页面的路由和token
- 前端将路由存储到sessionStorage和vuex中(因为vuex存储的数据一刷就没了,所以需要配合sessionStorage)
- 在路由前置守卫处动态添加拿到的路由,对页面进行渲染。
代码
1,/views/Login.vue:登录(onSubmit方法)成功时,根据用户名密码区分角色,根据角色将不同路由和token写入vuex,dynamicRoutes.js中有两个路由模板,分别是user和admin,然后调用generateRoutes()方法向vue-router中动态添加路由
import {admin,user} from '@/router/dynamicRoutes'
onSubmit(formName) {
//为表单绑定验证功能
this.$refs [formName].validate((valid) => {
if (valid) {
if(this.form.username==="user"&&this.form.password==="user"){
store.commit('SET_MENULIST', user);
store.commit('SET_TOKEN', 'user');
}else if(this.form.username==="admin"&&this.form.password==="admin"){
store.commit('SET_MENULIST', admin);
store.commit('SET_TOKEN', 'admin');
}else{
this.$message.error("登录失败")
return
}
this.$message.success("登录成功")
generateRoutes()
this.$router.replace('/')
} else {
this.dialogVisible = true;
return false;
}
});
},
2,/store/index.js:vuex状态管理(配合sessionStorage)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token:'',
menuList:[],
isLoadRouters:false
},
getters: {
GET_TOKEN:state => {
state.token = sessionStorage.getItem("token")
return state.token
},
GET_MENULIST:state => {
state.menuList = JSON.parse(sessionStorage.getItem("menuList")||'[]')
return state.menuList
},
GET_ISLOADROUTERS:state=>{
return state.isLoadRouters
}
},
mutations: {
SET_TOKEN:(state,token)=>{
state.token = token
sessionStorage.setItem("token",token);
},
SET_MENULIST:(state,menuList)=>{
state.menuList = menuList
sessionStorage.setItem("menuList",JSON.stringify(menuList));
},
SET_ISLOADROUTERS:(state,isLoadRouters)=>{
state.isLoadRouters = isLoadRouters
}
},
})
3,/util/index.js:方法动态添加路由工具
import router from '../router'
import store from '@/store'
//动态添加路由
export function generateRoutes() {
const _asyncRoutes = store.getters.GET_MENULIST
if(_asyncRoutes==null)
return
_asyncRoutes.forEach(menu => {
if (menu.children) {
menu.children.forEach(m => {
let route = menuToRoute(m, menu.name);
if (route) {
router.addRoute("Home",route)
}
})
}
})
}
//将菜单转换成router可以识别的路由
const menuToRoute = (menu, parentName) => {
if (!menu.component) {
return null;
} else {
let route = {
name: menu.name,
path: menu.path,
meta: {
parentName: parentName
}
}
route.component = () => import('@/views/' + menu.component + '.vue');
return route;
}
}
4,/router/permission.js:前置路由守卫,判断是否登录和是否已经添加过路由
import router from "@/router/index"
import store from "@/store"
import {generateRoutes} from "@/util";
// 检查是否存在于免登陆白名单
function inWhiteList(toPath) {
const whiteList = ['/login', '/404']
const path = whiteList.find((value) => {
// 使用正则匹配
const reg = new RegExp('^' + value)
return reg.test(toPath)
})
return !!path
}
router.beforeEach((to, from, next) => {
console.group('%c%s', 'color:blue', `${new Date().getTime()} ${to.path} 的全局前置守卫----------`)
console.log('所有活跃的路由记录列表', router.getRoutes())
console.groupEnd()
const token = store.getters.GET_TOKEN
let isLoadRouters = store.state.isLoadRouters
if (inWhiteList(to.path)) {
next()
} else {
//用户已登录
if (token && JSON.stringify(store.getters.GET_MENULIST) !== '[]') {
if (isLoadRouters) {
// console.log('路由已添加,直接跳转到目标页面');
next()
} else {
//解决刷新页面空白
//console.log('重新加载路由,并跳转到目标页');
let menuList = store.getters.GET_MENULIST
store.commit('SET_ISLOADROUTERS', true)
//添加动态路由
generateRoutes(menuList)
next({...to, replace: true})
}
} else {
// console.log('无登录信息,跳转到登录页');
store.commit('SET_ISLOADROUTERS', false)
next(`/login`)
}
}
})
5, 退出时清空用户信息,清空角色,清空路由
methods: {
// 退出登录
handleLogout() {
window.localStorage.removeItem("token")
// 清除用户信息
this.$store.commit("User/removeUserInfo")
// 清除角色权限列表
this.$store.commit("permission/setRoles", [])
// 清除角色权限数组
this.$store.commit("permission/SET_ROUTES", [])
Message({
type: "success",
message: "退出登录",
showClose: true,
duration: 3000,
})
this.$router.push({ path: "/login" })
},
}
总结:本人才疏学浅,水平有限,请大家多多包涵和指点。多谢🤝🫡 👍。 加油✊🫶