导读

Vue Router 是 Vue.js 官方的路由管理器。在 Vue.js 应用程序中,Vue Router 为用户提供了灵活的路由配置方式,可以通过编写路由组件来实现页面跳转、参数传递、路由守卫等功能,为单页应用提供了完整的解决方案。

Vue Router@4 与之前的版本有所不同,它使用了新的 API 和语法糖,更加简洁易用。下面是 Vue Router@4 的一些新特性:

  1. 支持完整的 TypeScript 类型声明。
  2. 使用 ES modules 和 webpack tree-shaking 以实现更小的 bundle size。
  3. 支持动态路由和嵌套路由的新的声明方式。
  4. 新增了一个 createRouter() 工厂函数来创建 router 实例,让使用更加灵活。
  5. 支持自定义路由匹配规则和路由模式(hash、history、abstract)。

安装

注意版本,Vue3对应的是4

npm install vue-router@4

创建 router

首先创建一个 /src/router/a5router.ts 文件,在其中定义路由

import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
// 路由 => 路径和组件之间的对应关系
const routes = [
  {
    path: '/a1',
    component: A51
  },
  {
    path: '/a2', 
    component: A52
  }
]

const router = createRouter({ 
  history: createWebHashHistory(), // 路径格式
  routes: routes // 路由数组
})

export default router
  • createWebHashHistory 是用 # 符号作为【单页面】跳转技术,上面两个路由访问时路径格式为

    • http://localhost:7070/#/a1
    • http://localhost:7070/#/a2
  • 每个路由都有两个必须属性

    • path:路径

    • component:组件

  • createRouter 用来创建 router 对象,作为默认导出

需要在 main.ts 中导入 router 对象:

// ...
import A5 from './views/A5.vue'  // vue-router
import router from './router/a5router'
createApp(A5).use(antdv).use(router).mount('#app')

A5 是根组件,不必在 router 中定义,但需要在其中定义 router-view,用来控制路由跳转后,A51、A52 这些组件的显示位置,内容如下

<template>
  <div class="a5">
    <router-view></router-view>
  </div>
</template>

效果如下

image-20220926145812121

image-20220926145959690

动态导入【推荐】

import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
  // ...
  {
    path: '/a3',
    component: () => import('../views/A53.vue')
  }
]
  • 用 import 关键字导入,效果是打包时会将组件的 js 代码都打包成一个大的 js 文件,如果组件非常多,会影响页面加载速度
  • 而 import 函数导入(动态导入),则是按需加载,即
    • 当路由跳转到 /a3 路径时,才会去加载 A53 组件对应的 js 代码
    • vue-router 官方推荐采用动态导入

编程式导航和路由传参(待补充)

编程式导航:通过route对象调用一些属性和方法实现页面的前进和后退

以及如何避免页面内组件切换产生页面刷新跳转和产生浏览器历史记录的问题

  • to

  • go

  • pre

  • replace

  • push(不刷新跳转~)

路有传参:传递参数给另一个组件然后通过参数执行不同的页面渲染

  • query,路径后拼接
  • params,在内存中传递

路由元信息(待补充)

其实很早之前在不少开源博客上也看到了“元信息”的概念,其实无非就是一些附带的属性,。

像Route每个路由我们都可以在定义的时候专门使用meta属性去记录和附加路由的元信息,把meta属性的值设置成JSON格式,里边包含title等各种键值对,像发生路由跳转的时候我们可以使用document.title修改跳转路由的页面对应名称(名称就是从元信息中title读取到的)

路由过渡动态效果(待补充)

参考:

其实就是在两个路由页面切换的时候加上一些动画效果,对应的在Router-view标签中使用一个叫做transition的标签

至于具体的过渡动画效果可以安装和引入使用一些第三方的库来完成

路由的滚动行为(待补充)

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,
就像重新加载页面那样。vue-router 可以自定义路由切换时页面如何滚动。
当创建一个Router 实例,你可以提供一个 scro11Behavior 方法来实现,支持异步

嵌套路由

如果希望再嵌套更深层次的路由跳转,例如:希望在 A53 组件内再进行路由跳转

image-20220926150819624

首先,修改 A53.vue

<template>
  <div class="a53">
    <router-view></router-view>
  </div>
</template>

其次,再修改 /src/router/a5router.ts 文件 内容

import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
  // ...
  {
    path: '/a3',
    component: () => import('../views/A53.vue'),
    children: [
      {
        path: 'student',
        component: () => import('../views/A531.vue')
      },
      {
        path: 'teacher',
        component: () => import('../views/A532.vue')
      }
    ]
  }
]

// ...

将来访问 /a3/student 时,效果为

image-20220926151216217

访问 /a3/teacher 时,效果为

image-20220926151249403

重定向

用法1

import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
  // ...
  {
    path: '/a3',
    component: () => import('../views/A53.vue'),
    redirect: '/a3/student', // 重定向到另外路径
    children: [
      {
        path: 'student',
        component: () => import('../views/A531.vue')
      },
      {
        path: 'teacher',
        component: () => import('../views/A532.vue')
      }
    ]
  }
]
// ...

效果是,页面输入 /a3,紧接着会重定向跳转到 /a3/student

用法2

import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
  {
    path: '/a1',
    component: A51
  },
  {
    path: '/a2', 
    component: A52
  },
  // ...
  {
    path: '/:pathMatcher(.*)*', // 可以匹配剩余的路径
    redirect: '/a2'
  }
]
// ...

效果是,当页面输入一个不存在路径 /aaa 时,会被 path: '/:pathMatcher(.*)*' 匹配到,然后重定向跳转到 A52 组件去

动态路由与菜单

动态路由的意思就是路由文件不是一成不变的,举个例子就是说不同的用户可以看到一样的界面的内容就是静态路由,不同权限用户对应不同的路由就是要使用动态路由,应该存储在数据库访问后端获取之后拼接到静态路由取得完整的路由,同时为了保证页面刷新动态路由不丢失可以使用vueuse库的useStorage存储会话信息

动态菜单就是根据动态路由返回的路由和菜单信息动态生成菜单项而已,同样的也需要使用useStorage存储会话信息

路由文件

a6router.js

import { createRouter, createWebHashHistory } from 'vue-router'
import { useStorage } from '@vueuse/core'
import { Route, Menu } from '../model/Model8080'
const clientRoutes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/A6Login.vue')
  },
  {
    path: '/404',
    name: '404',
    component: () => import('../views/A6NotFound.vue')
  },
  {
    path: '/',
    name: 'main',
    component: () => import('../views/A6Main.vue')
  },
  {
    path: '/:pathMatcher(.*)*',
    name: 'remaining',
    redirect: '/404'
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes: clientRoutes
})

export const serverMenus = useStorage<Menu[]>('serverMenus', [])
const serverRoutes = useStorage<Route[]>('serverRoutes', [])
addServerRoutes(serverRoutes.value)

export function addServerRoutes(routeList: Route[]) {
  for (const r of routeList) {
    if (r.parentName) {
      router.addRoute(r.parentName, {
        path: r.path,
        component: () => import(r.component),
        name: r.name
      })
    }
  }
  serverRoutes.value = routeList
}

export function resetRoutes() {
  for (const r of clientRoutes) {
    router.addRoute(r)
  }
  serverRoutes.value = null
  serverMenus.value = null
}

export default router

本文件重要的函数及变量

  • addServerRoutes 函数向路由表中添加由服务器提供的路由,路由分成两部分
    • clientRoutes 这是客户端固定的路由
    • serverRoutes 这是服务器变化的路由,存储于 localStorage
  • resetRoutes 函数用来将路由重置为 clientRoutes
    • vue-router@4 中的 addRoute 方法会【覆盖】同名路由,这是这种实现的关键
    • 因此,服务器返回的路由最好是 main 的子路由,这样重置时就会比较简单,用之前的 main 一覆盖就完事了
  • serverMenus 变量记录服务器变化的菜单,存储于 localStorage

登录组件

动态路由应当在登录时生成,A6Login.vue

<template>
  <div class="login">
    <a-form :label-col="{ span: 6 }" autocomplete="off">
      <a-form-item label="用户名" v-bind="validateInfos.username">
        <a-input v-model:value="dto.username" />
      </a-form-item>
      <a-form-item label="密码" v-bind="validateInfos.password">
        <a-input-password v-model:value="dto.password" />
      </a-form-item>
      <a-form-item :wrapper-col="{ offset: 6, span: 16 }">
        <a-button type="primary" @click="onClick">Submit</a-button>
      </a-form-item>      
    </a-form>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Form } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import axios from '../api/request'
import { useRequest } from 'vue-request'
import { AxiosRespToken, LoginDto, AxiosRespMenuAndRoute } from '../model/Model8080'
import { resetRoutes, addServerRoutes, serverMenus } from '../router/a6router'
const dto = ref({username:'', password:''})
const rules = ref({
  username: [
    {required: true, message:'用户名必填'}
  ],
  password:[
    {required: true, message:'密码必填'}
  ]
})
const { validateInfos, validate } = Form.useForm(dto, rules)
const router = useRouter()
const { runAsync:login } = useRequest<AxiosRespToken, LoginDto[]>((dto)=> axios.post('/api/loginJwt', dto), {manual:true})
const { runAsync:menu } = useRequest<AxiosRespMenuAndRoute, string[]>((username)=> axios.get(`/api/menu/${username}`), {manual:true})
async function onClick() {
  try {
    await validate()
    const loginResp = await login(dto.value
    if(loginResp.data.code === 200) { // 登录成功
      const token = loginResp.data.data.token
      const menuResp = await menu(dto.value.username)
      const routeList = menuResp.data.data.routeList
      addServerRoutes(routeList)
      serverMenus.value = menuResp.data.data.menuTree
      router.push('/')
    })
  } catch (e) {
    console.error(e)
  }
}
onMounted(()=>{
  resetRoutes()
})
</script>
<style scoped>
.login{
  margin: 200px auto;
  width: 25%;
  padding: 20px;
  height: 180px;
  background-color: antiquewhite;
}
</style>
  • 登录成功后去请求 /api/menu/{username} 获取该用户的菜单和路由
  • router.push 方法用来以编程方式跳转至主页路由

主页组件

A6Main.vue

<template>
  <div class="a6main">
    <a-layout>
      <a-layout-header>
      </a-layout-header>
      <a-layout>
        <a-layout-sider>
          <a-menu mode="inline" theme="dark">
            <template v-for="m1 of serverMenus">
              <a-sub-menu v-if="m1.children" :key="m1.id" :title="m1.title">
                <template #icon><a-icon :icon="m1.icon"></a-icon></template>
                <a-menu-item v-for="m2 of m1.children" :key="m2.id">
                  <template #icon><a-icon :icon="m2.icon"></a-icon></template>
                  <router-link v-if="m2.routePath" :to="m2.routePath">{{m2.title}}</router-link>
                  <span v-else>{{m2.title}}</span>
                </a-menu-item>
              </a-sub-menu>
              <a-menu-item v-else :key="m1.id">
                <template #icon><a-icon :icon="m1.icon"></a-icon></template>
                <router-link v-if="m1.routePath" :to="m1.routePath">{{m1.title}}</router-link>
                <span v-else>{{m1.title}}</span>
              </a-menu-item>
            </template>            
          </a-menu>
        </a-layout-sider>
        <a-layout-content>
          <router-view></router-view>
        </a-layout-content>
      </a-layout>
    </a-layout>
  </div>
</template>
<script setup lang="ts">
import AIcon from '../components/AIcon3' // jsx icon 组件
import { serverMenus } from '../router/a6router'
</script>
<style scoped>
.a6main {
  height: 100%;
  background-color: rgb(220, 225, 255);
  box-sizing: border-box;
}
.ant-layout-header {
  height: 50px;
  background-color:darkseagreen;
}

.ant-layout-sider {
  background-color:lightsalmon;
}

.ant-layout-content {
  background-color: aliceblue;
}

.ant-layout-footer {
  background-color:darkslateblue;
  height: 30px;
}

.ant-layout {
  height: 100%;
}

.ant-layout-has-sider {
  height: calc(100% - 50px);
}

</style>

token 使用

  1. 获取用户信息,例如服务器端可以把用户名、该用户的路由、菜单信息都统一从 token 返回
    • atob()函数,解码base64获取token前两部分存储公共信息的部分、Json.parse()解析成JSON对象
  2. 前端路由跳转依据,例如跳转前检查 token,如果不存在,表示未登录,就避免跳转至某些路由
    • router.beforeeach(to,from):在路由跳转前通过token进行校验
    • router.aftereach(to,from):跳转后修改页面标题(因为Vue3的所有组件其实只有一个index.html)
    • 都有两个相同的参数to:目标路由对象;from:来源路有对象
  3. 后端 api 访问依据,例如每次发请求携带 token,后端需要身份校验的 api 需要用到
    • 拦截器中获取token、判断、放行