2) Vue-Router
vue 属于单页面应用
单页面应用:就是你的整个程序就那一个HTML页面。 可能有人会疑问,我们写了这么多的视图组件,难道这些视图组件都会被用在同一个HTML页面中吗? 没错,他们就是会被用在同一个HTML页面中,只不过这个页面的内容,将来会替换成组件1、组件2、或者是组件3的内容,他的内容会变,但是页面只有一个。
而我们今天学习的这个所谓的路由,就是根据浏览器路径不同,用不同的视图组件替换这个页面内容展示。
例子:
访问根路径:主页就是1个视图组件
访问404:
可以看到,中间的视图组件发生了改变,但是最外层的HTML页面没变。
1. 配置路由
新建一个路由 js 文件,例如 src/router/example14.js,内容如下
代码语言:javascript复制 import Vue from 'vue'
import VueRouter from 'vue-router'
// @ 绝对路径,代表src
import ContainerView from '@/views/example14/ContainerView.vue'
import LoginView from '@/views/example14/LoginView.vue'
import NotFountView from '@/views/example14/NotFountView.vue'
Vue.use(VueRouter)
const routes = [
{
path: "/",
component: ContainerView,
},
{
path: "/login",
component: LoginView,
},
{
path: "/404",
component: NotFountView,
},
]
const router = new VueRouter({
routes
})
export default router
- 最重要的就是建立了【路径】与【视图组件】之间的映射关系
- 本例中映射了 3 个路径与对应的视图组件
在 main.js 中采用我们的路由 js
代码语言:javascript复制import Vue from 'vue'
import e14 from './views/Example14View.vue'
import router from './router/example14' // 修改这里
import store from './store'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
Vue.use(Element)
new Vue({
router,
store,
render: h => h(e14)
}).$mount('#app')
根组件是 Example14View.vue,内容为:
代码语言:javascript复制<template>
<div class="out">
最外层
<!-- 起到占位作用,改变路径后,这个路径对应的视图组件就会占据 `<router-view>` 的位置,替换掉它之前的内容 -->
<router-view></router-view>
</div>
</template>
<style scoped>
...
</style>
- 样式略
- 其中
<router-view>
起到占位作用,改变路径后,这个路径对应的视图组件就会占据<router-view>
的位置,替换掉它之前的内容
2. 动态导入
之前都是使用import
这个关键字导入了 我们的vue组件,这种叫做静态导入;除此之外还有动态导入,首先说一下为什么要使用静态导入呢?
将来我们vue的应用程序发布的时候,要打个包,打包的时候,他会将所有组件的JavaScript代码打包到一起,JavaScript包会变得越来越大,影响页面加载速度。所以我们最好的一种解决方式就是不要把所有代码打包到一起,让它按需加载,比如我们用到LoginView.vue的代码时候,这时候才把这个组件的JavaScript代码加载出来,这样就可以大大减少你代码的体积,分层不同的代码块,更为高效。
代码语言:javascript复制原文; https://router.vuejs.org/zh/guide/advanced/lazy-loading.html 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把 不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: "/",
// 箭头函数 ()=>import(路径)
component: () => import('@/views/example14/ContainerView.vue'),
},
{
path: "/login",
component: () => import('@/views/example14/LoginView.vue'),
},
{
path: "/404",
component: () => import('@/views/example14/NotFoundView.vue'),
},
]
const router = new VueRouter({
routes
})
export default router
- 静态导入是将所有组件的 js 代码打包到一起,如果组件非常多,打包后的 js 文件会很大,影响页面加载速度
- 动态导入是将组件的 js 代码放入独立的文件,用到时才加载
验证动态路由的好处: 静态路由:
并且打开F12可以看到,静态加载将三个的组件的js代码打包到了app.js
文件里:
动态路由:
打开F12,可以看到,是生成了一个对应的.js文件,加载访问组件的js代码。
3. 嵌套路由
组件内再要切换内容,就需要用到嵌套路由(子路由),下面的例子是在【ContainerView 组件】内定义了 3 个子路由
代码语言:javascript复制const routes = [
{
path:'/',
component: () => import('@/views/example14/ContainerView.vue'),
redirect: '/c/p1',
children: [
{
path:'c/p1',
component: () => import('@/views/example14/container/P1View.vue')
},
{
path:'c/p2',
component: () => import('@/views/example14/container/P2View.vue')
},
{
path:'c/p3',
component: () => import('@/views/example14/container/P3View.vue')
}
]
},
{
path:'/login',
component: () => import('@/views/example14/LoginView.vue')
},
{
path:'/404',
component: () => import('@/views/example14/NotFoundView.vue')
},
{
path:'*', // path 的取值为 * 表示匹配不到其它 path 时,就会匹配它
redirect: '/404' // redirect 可以用来重定向(跳转)到一个新的地址
}
]
子路由变化,切换的是【ContainerView 组件】中 <router-view></router-view>
部分的内容
<template>
<div class="container">
<router-view></router-view>
</div>
</template>
- redirect 可以用来重定向(跳转)到一个新的地址。
- path 的取值为 * 表示匹配不到其它 path 时,就会匹配它。
4. ElementUI 布局
通常主页要做布局,下面的代码是 ElementUI 提供的【上-【左-右】】布局:
https://element.eleme.cn/#/zh-CN/component/container
代码语言:javascript复制<template>
<div class="container">
<el-container>
<el-header></el-header>
<el-container>
<el-aside width="200px"></el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
5. 路由跳转
标签式
代码语言:javascript复制<el-aside width="200px">
<router-link to="/c1/p1">P1</router-link>
<router-link to="/c1/p2">P2</router-link>
<router-link to="/c1/p3">P3</router-link>
</el-aside>
编程式
代码语言:javascript复制<el-header>
<el-button type="primary" icon="el-icon-edit"
circle size="mini" @click="jump('/c1/p1')"></el-button>
<el-button type="success" icon="el-icon-check"
circle size="mini" @click="jump('/c1/p2')"></el-button>
<el-button type="warning" icon="el-icon-star-off"
circle size="mini" @click="jump('/c1/p3')"></el-button>
</el-header>
jump 方法
代码语言:javascript复制<script>
const options = {
methods : {
jump(url) {
this.$router.push(url);
}
}
}
export default options;
</script>
- 其中 this.$router 是拿到路由对象
- push 方法根据 url 进行跳转
导航菜单
代码语言:javascript复制<el-menu router background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<el-submenu index="/c1">
<span slot="title">
<!--小图标-->
<i class="el-icon-platform-eleme"></i>
菜单1
</span>
<el-menu-item index="/c1/p1">子项1</el-menu-item>
<el-menu-item index="/c1/p2">子项2</el-menu-item>
<el-menu-item index="/c1/p3">子项3</el-menu-item>
</el-submenu>
<el-menu-item index="/c2">
<span slot="title">
<i class="el-icon-phone"></i>
菜单2
</span>
</el-menu-item>
<el-menu-item index="/c3">
<span slot="title">
<i class="el-icon-star-on"></i>
菜单3
</span>
</el-menu-item>
</el-menu>
- 关于小图标:https://element.eleme.cn/#/zh-CN/component/icon
- 图标和菜单项文字 建议用
<span slot='title'></span>
包裹起来 el-menu-item
替换成el-submenu
就可以添加子项了el-menu
标签上加上router
属性,表示结合导航菜单与路由对象,此时,就可以利用菜单项的index
属性来路由跳转(表示你要跳转到哪里去)
6. 动态路由与菜单
https://www.bilibili.com/video/BV1Tt4y1772f
我们实际应用中,不同的用户,根据身份不一样,看到的菜单和跳转的路由可能是不一样的。
将菜单、路由信息(仅主页的)存入数据库中
代码语言:javascript复制 CREATE TABLE `menu`(
id INT,
name VARCHAR(10),
icon VARCHAR(30),
path VARCHAR(30),
pid INT,
component VARCHAR(20),
PRIMARY KEY (id)
) COMMENT '菜单表';
INSERT INTO menu(id, name, pid, path, component, icon) VALUES
(101, '菜单1', 0, '/m1', null, 'el-icon-platform-eleme'),
(105, '子项1', 101, '/m1/c1', 'C1View.vue', 'el-icon-s-goods'),
(106, '子项2', 101, '/m1/c2', 'C2View.vue', 'el-icon-menu'),
(102, '菜单2', 0, '/m2', null, 'el-icon-delete-solid'),
(107, '子项3', 102, '/m2/c3', 'C3View.vue', 'el-icon-s-marketing'),
(108, '子项4', 102, '/m2/c4', 'C4View.vue', 'el-icon-s-platform'),
(109, '子项5', 102, '/m2/c5', 'C5 View.vue', 'el-icon-picture'),
(103, '菜单3', 0, '/m3', null, 'el-icon-s-tools'),
(110, '子项6', 103, '/m3/c6', 'C6View.vue', 'el-icon-upload'),
(111, '子项7', 103, '/m3/c7', 'C7View.vue', 'el-icon-s-promotion'),
(104, '菜单4', 0, '/m4', 'M4View.vue', 'el-icon-user-solid');
不同的用户查询的的菜单、路由信息是不一样的
例如:访问 /api/menu/admin
返回所有的数据,访问 /api/menu/zhang
,是个普通用户,返回部分数据 ,类似的路由跳转也一样,如果你是普通用户,你可以跳转的路由也是有限的,我们想要实现这样的功能,就需要将主页的路由和菜单用后台的数据库给他管理起来。前端根据他们身份不同,动态添加路由和显示菜单。
后端代码自己实现:
/api/menu/admin
返回所有菜单,/api/menu/zhang
:返回菜单2及其子项,/api/menu/wang
:返回菜单3及其子项 前端:
1. 动态路由
src/router/example15.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: "/",
// 箭头函数 ()=>import(路径)
component: () => import('@/views/example15/ContainerView.vue'),
},
{
path: "/login",
component: () => import('@/views/example15/LoginView.vue'),
},
{
path: "/404",
component: () => import('@/views/example15/NotFoundView.vue'),
},
{
path: '*', // path 的取值为 * 表示匹配不到其它 path 时,就会匹配它
redirect: '/404' // redirect 可以用来重定向(跳转)到一个新的地址
},
]
const router = new VueRouter({
routes
})
export default router
- js 这边只保留几个固定路由,如主页、404 和 login,其他路由由后端获得,然后动态加入前端路由里去
src/views/example15/LoginView.vue
<!-- 登录 -->
<template>
<div class="login">
<el-input v-model="username" placeholder="请输入用户名"></el-input>
<el-button type="primary" v-on:click="login()">登录</el-button>
</div>
</template>
<script>
import axios from 'axios';
export default {
data: function () {
return {
username: "admin",
}
},
methods: {
async login() {
const resp = await axios.get(`/api/menu/${this.username}`)
const array = resp.data.data
// 打印变化前的路由表的所有路由
console.log(this.$router.getRoutes());
for (const { Id, Path, Component } of array) {
// 如果视图组件不为空
if (Component !== "") {
// 动态添加路由
// this.$router 路由对象
// addRouter(参数1,参数2)
// 参数1:父路由名字
// 参数2:要添加的路由信息对象
this.$router.addRoute('c', {
path: Path,
name: Id,
component: () => import(`@/views/example15/container/${Component}`),
})
}
}
// 打印变化后的路由表的所有路由
console.log(this.$router.getRoutes());
},
},
}
</script>
<style>
.login {
height: 100%;
background-color: #e4efbe;
}
</style>
- 以上方法执行时,将服务器返回的路由信息加入到名为 c 的父路由中去
- 这里要注意组件路径,前面 @/views 是必须在 js 这边完成拼接的,否则 import 函数会失效
此时如果你直接访问/m1/c1
,由于还没有添加到路由中,会直接跳转到404页面。
先登录,打开F12可以看到路由表里路由的变化:
此时我们再访问/m1/c1
,可以看到已经被注册到路由表里了:
2. 重置路由
但是现在还有一个问题,我们登录zhang之后,再登录wang,会发现,他是直接在上一个路由的基础上新增了2个,这是不对的,我们需要在登录wang之前,将路由重置到初始状态:
现在使用的vue2,配合使用的Vue Router的版本是3.x版本,以后使用vue3的时候会使用vue4.x版本,但是v3.x的版本里,他的API里只有新增路由的方法,没有删除路由的方法,所以本例中用一些”外门邪道“来实现:
在/src/router/example15.js
里加入:
// 重置路由
export function resetRouter(){
// router.matcher的matcher属性 包含了这些路由信息,
// 用最初的路由的matcher替换当前的路由的match达到重置路由的目的
router.matcher = new VueRouter({routes}).matcher
}
然后再/src/views/example15/LoginView.vue
里加入:
import { resetRouter } from '@/router/example15';
...
//在登录的时候重置路由(正常是在注销时调用)
async login() {
resetRouter();// 重置路由
此时再登录zhang和wang可以看到路由正常了:
3. 页面刷新
我们上面说的动态路由会遭遇页面刷新的问题(vue属于单页面程序,一刷新页面就意味着页面所有内容都重置了),页面刷新后,会导致动态添加的路由失效。
代码调整:
src/router/example15.js
新增: // addServerRouters 添加服务器返回的路由信息 // 这一部分重用次数多,封装成方法 export function addServerRouters(array) { // 打印变化前的路由表的所有路由 console.log(router.getRoutes()); for (const { Id, Path, Component } of array) { // 如果视图组件不为空 if (Component !== "") { // 动态添加路由 // this.$router 路由对象 // addRouter(参数1,参数2) // 参数1:父路由名字 // 参数2:要添加的路由信息对象 router.addRoute('c', { path: Path, name: Id, component: () => import(`@/views/example15/container/${Component}`), }) } } // 打印变化后的路由表的所有路由 console.log(router.getRoutes()); }src/views/example15/LoginView.vue
: import { resetRouter, addServerRouters } from '@/router/example15'; ... async login() { resetRouter();// 重置路由 const resp = await axios.get(`/api/menu/${this.username}`) const array = resp.data.data // 将原本请求服务器数据封装成了方法 addServerRouters(array) ...
思路就是将后端服务器返回的路由数据先存入浏览器,页面刷新后可以将上次存入浏览器的路由数据再取出来重新调用addServerRouter()
,将路由信息进行恢复。
那么我们往浏览器里存,存哪呢?
浏览器提供了2个对象,二者的区别是范围不一样:
localStorage
:即使你浏览器关了,存储数据仍然还在;sessionStorage
:以标签页为单位,标签不关,数据就在,但是关闭标签页时,数据会被清除。
我们的路由信息并不需要永久保存,所以这里使用sessionStorage
比较合适,解决方法是将路由数据存入 sessionStorage
<script>
import axios from '@/util/myaxios'
import {resetRouter, addServerRoutes} from '@/router/example15'
const options = {
data() {
return {
username: 'admin'
}
},
methods: {
async login() {
resetRouter(); // 重置路由
const resp = await axios.get(`/api/menu/${this.username}`)
const array = resp.data.data;
// localStorage 即使浏览器关闭,存储的数据仍在
// sessionStorage 以标签页为单位,关闭标签页时,数据被清除
sessionStorage.setItem('serverRouters', JSON.stringify(array));
addServerRoutes(array); // 动态添加路由
this.$router.push('/');
}
}
}
export default options;
</script>
sessionStorage.setItem(key,value)
: 参数1:key string 参数2:value string 可以看到并不可以直接存储数组,所以这里需要JSON.stringify
将数组转成json字符串存储。- 然后[F12]->[应用程序]->[会话存储] 查看存储的信息:
现在我们把后端给的路由数据存储到浏览器了,那么我们在哪里读取呢?
页面刷新,重新创建路由对象时,从 sessionStorage 里恢复路由数据:
代码语言:javascript复制const router = new VueRouter({
routes
})
// 从 sessionStorage 中恢复路由数据
const serverRoutes = sessionStorage.getItem('serverRouters');
if (serverRoutes) {
const array = JSON.parse(serverRoutes);
addServerRouters(array) // 动态添加路由
}
4. 动态菜单
我们现在实现一个功能,在登录之后跳转到首页,主页里我们再看如何制作动态菜单:
之前学习过的路由跳转方式:
- 通过
<router-link to=''>
来跳转 - 通过编程,写代码来跳转
- 通过ElementUI的导航栏来跳转
methods: {
async login() {
...
this.$router.push("/"); // 跳转到主页
},
},
思路:从sessionStorage中获取路由数据,通过array的两次遍历将一位的map变成有父子关系的map,再去赋值给data返回对象里的的top,然后使用v-for
循环这个top,如果有子元素就使用el-submenu
,如果没有更深层子元素了就使用el-menu-item
。
<!-- 主页 -->
<template>
<div class="container">
<!-- 容器 -->
<el-container>
<!-- 头部 -->
<el-header>
</el-header>
<!-- 主体容器 -->
<el-container>
<el-aside width="200px">
<!-- 侧边导航栏 -->
<!-- v-bind:unique-opened="true" 同时只能打开一个导航-->
<el-menu router background-color="#545c64" text-color="#fff" active-text-color="#ffd04b"
v-bind:unique-opened="true">
<!-- 在v-for循环里,生成的标签上必须绑定一个key,否则会报错 v-bind:key="遍历对象的唯一标识" -->
<template v-for="m of top">
<!-- 一级菜单 -->
<!-- submenu的index推荐也加上,虽然没有真正跳转,但是不加后台会打印错误信息 -->
<el-submenu v-if="m.children" v-bind:key="m.Id" v-bind:index="m.Path">
<span slot="title">
<i :class="m.Icon"></i>{{m.Name}}
</span>
<!-- 二级菜单 -->
<!-- 这里默认只有二级菜单 -->
<el-menu-item v-for="mchild of m.children" v-bind:key="mchild.Id"
v-bind:index="mchild.Path">
<span slot="title">
<i :class="mchild.Icon"></i>{{mchild.Name}}
</span>
</el-menu-item>
</el-submenu>
<!--
这里可能还会报错:v-if/else branches must use unique keys.
不用管,这是vsCode的问题,运行不会报错
-->
<el-menu-item v-else v-bind:key="m.Id" v-bind:index="m.Path">
<span slot="title">
<i :class="m.Icon"></i>{{m.Name}}
</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<!-- 主要内容 -->
<el-main>
<!-- 占位,组件加载替换 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
data: function () {
return {
top: [],
}
},
methods: {},
mounted: function () {
// 页面加载的时候从sessionStorage里取出后端给的路由数据
const routerStr = sessionStorage.getItem("serverRouters");
const array = JSON.parse(routerStr);
// 一维数组转导航栏的格式
const map = new Map();
for (const obj of array) {
map.set(obj.Id, obj);
}
// 寻找顶层元素
const top = [];
for (const obj of array) {
// 如果这个parent存在,就创建对应父元素的子元素数组,并将当前元素作为子数组加入其中,
// 否则就是顶层元素
const parent = map.get(obj.Pid);
if (parent) {
// ?? 如果左侧不存在,右侧赋值给左侧
parent.children ??= [];
parent.children.push(obj);
} else {
top.push(obj);
}
}
// console.log(top)
this.top = top;
},
}
</script>
<style>
.container {
width: 1000px;
height: 100%;
background-color: #7bbfec;
padding: 20px;
box-sizing: border-box;
}
</style>
- 没有考虑递归菜单问题,认为菜单只有两级
- 重点掌握
v-for
、v-if
这些指令。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!