浅学前端:Vue篇(三)

2023-11-13 23:12:22 浏览数 (1)

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代码加载出来,这样就可以大大减少你代码的体积,分层不同的代码块,更为高效。

原文; https://router.vuejs.org/zh/guide/advanced/lazy-loading.html 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把 不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

代码语言: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> 部分的内容

代码语言:javascript复制
<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

代码语言:javascript复制
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

代码语言:javascript复制
<!-- 登录 -->
<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里加入:

代码语言:javascript复制
// 重置路由
export function resetRouter(){
    // router.matcher的matcher属性 包含了这些路由信息,
    // 用最初的路由的matcher替换当前的路由的match达到重置路由的目的
    router.matcher = new VueRouter({routes}).matcher
}

然后再/src/views/example15/LoginView.vue里加入:

代码语言:javascript复制
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

代码语言:javascript复制
<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. 动态菜单

我们现在实现一个功能,在登录之后跳转到首页,主页里我们再看如何制作动态菜单:

之前学习过的路由跳转方式:

  1. 通过<router-link to=''>来跳转
  2. 通过编程,写代码来跳转
  3. 通过ElementUI的导航栏来跳转
代码语言:javascript复制
 methods: {
     async login() {
         ...
         this.$router.push("/"); // 跳转到主页
     },
 },

思路:从sessionStorage中获取路由数据,通过array的两次遍历将一位的map变成有父子关系的map,再去赋值给data返回对象里的的top,然后使用v-for循环这个top,如果有子元素就使用el-submenu,如果没有更深层子元素了就使用el-menu-item

代码语言:javascript复制
 <!-- 主页 -->
 <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-forv-if这些指令。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞