路由 router

# 路由 router

路由模块

Router modules
- router
 - modules
  - components.ts
  - table.ts
 - index.ts

// 导入模块
import tableRouter from "./modules/table";
import chartsRouter from './modules/charts';

// RouteConfig 路由配置

// path: 路径
// component: 组件变量 或者 懒加载路由
// redirect: 重定向地址
// name: 名字
// meta: 元信息
// children: 子路径

const tableRoutes: RouteConfig = {}
export default tableRoutes


/*
  redirect:                      如果设置为'noredirect',则单击breadcrumb时不会触发重定向操作
  meta: {
    title: 'title'               显示在subMenu和breadcrumb中的名称(建议设置)
    icon: 'svg-name'             边栏中显示的图标
    breadcrumb: false            如果为false,项目将隐藏在breadcrumb中(默认为true)
    hidden: true                 如果为true,该路由将不会显示在侧边栏中(默认为false)
  }
*/
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
30
31
32
33

asyncRoutes: 根据用户角色需要 动态加载 的路由。

export const asyncRoutes: RouteConfig[] = [
 {
  path: '/test',
  component: Layout,
  meta: { title: 'test', icon: 'test', roles: ['admin', 'editor'] },
  children: [
   {
    path: 'test1',
    component: () => import('@/views/testPage/index.vue'),
    name: 'test',
    meta: { title: 'test', icon: 'test', noCache: true }
   }
  ]
 },
 {
  path: '*',
  redirect: '/404',
  meta: { hidden: true }
 }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# resetRouter

我的应用程序对不同的用户角色有不同的路由(有些被替换,有些应该无法访问或根本不存在)。虽然已经可以使用钩子和基本权限定义来实现这一点,但我对在登录/注销时完全替换路由的想法很感兴趣。然后,可用的路由集对于每个用户角色和公共/非身份验证用户可能是唯一的。

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

const createRouter = () => new Router({
  mode: 'history',
  routes: []
})

const router = createRouter()

export function resetRouter () {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // the relevant part
}

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 路由和侧边栏

路由和侧边栏是组织起一个后台应用的关键骨架。

侧边栏和路由是绑定在一起的,所以你只有在 @/router/index.js 下面配置对应的路由,侧边栏就能动态的生成了。

大大减轻了手动重复编辑侧边栏的工作量。当然这样就需要在配置路由的时候遵循一些约定的规则。

# 配置项

了解一下提供了哪些配置项

// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
hidden: true // (默认 false)

//当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: 'noRedirect'

// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: true
1
2
3
4
5
6
7
8
9
10
11
name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
meta: {
  roles: ['admin', 'editor'] // 设置该路由进入的权限,支持多个权限叠加
  title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
  icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon
  noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
  breadcrumb: false //  如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
  affix: true // 如果设置为true,它则会固定在tags-view中(默认 false)

  // 当路由设置了该属性,则会高亮相对应的侧边栏。
  // 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list
  // 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,就可以进行如下设置
  activeMenu: '/article/list'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如:

{
  path: '/permission',
  component: Layout,
  redirect: '/permission/index', //重定向地址,在面包屑中点击会重定向去的地址
  hidden: true, // 不在侧边栏显示
  alwaysShow: true, //一直显示根路由
  meta: { roles: ['admin','editor'] }, //你可以在根路由设置权限,这样它下面所有的子路由都继承了这个权限
  children: [{
    path: 'index',
    component: ()=>import('permission/index'),
    name: 'permission',
    meta: {
      title: 'permission',
      icon: 'lock', //图标
      roles: ['admin','editor'], //或者你可以给每一个子路由设置自己的权限
      noCache: true // 不会被 <keep-alive> 缓存
    }
  }]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 路由

这里的路由分为两种, constantRoutesasyncRoutes。

constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404、等通用页面。

asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。

路由可能涉及到权限验证问题,路由-路由懒加载 historyhash

browserHistoryhashHistory

(构建发布问题)

如果这里有一个需要非常注意的地方就是 404 页面一定要最后加载,如果放在 constantRoutes 一同声明了 404 ,后面的所有页面都会被拦截到 404

# 侧边栏

侧边栏主要基于 element-uiel-menu 改造。

前面也介绍了,侧边栏是通过读取路由并结合权限判断而动态生成的,而且还需要支持路由无限嵌套,所以这里还使用到了递归组件。

一般侧边栏有两种形式即: submenu 和 直接 el-menu-item。 一个是嵌套子菜单,另一个则是直接一个链接。

Sidebar 中已经帮你做了判断,当你一个路由下面的 children 声明的路由大于>1 个时,自动会变成嵌套的模式。如果子路由正好等于一个就会默认将子路由作为根路由显示在侧边栏中,若不想这样,可以通过设置在根路由中设置 alwaysShow: true 来取消这一特性。

// No submenu, because children.length===1
{
  path: '/icon',
  component: Layout,
  children: [{
    path: 'index',
    component: ()=>import('svg-icons/index'),
    name: 'icons',
    meta: { title: 'icons', icon: 'icon' }
  }]
},

// Has submenu, because children.length>=1
{
  path: '/components',
  component: Layout,
  name: 'component-demo',
  meta: {
    title: 'components',
    icon: 'component'
  },
  children: [
    { path: 'tinymce', component: ()=>import('components-demo/tinymce'), name: 'tinymce-demo', meta: { title: 'tinymce' }},
    { path: 'markdown', component: ()=>import('components-demo/markdown'), name: 'markdown-demo', meta: { title: 'markdown' }},
  ]
}
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

设置 unique-opened 来控制侧边栏,是否只保持一个子菜单的展开。

Sidebar/index.vue

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 多级目录(嵌套路由)

如果你的路由是多级目录, 有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 <router-view>

 <!-- 父级路由组件  -->
<template>
  <div>
    <!-- xxx html 内容  -->
    <router-view />
  </div>
</template>
1
2
3
4
5
6
7

原则上有多少级路由嵌套就需要多少个<router-view>

# 点击侧边栏 刷新当前路由

在用 spa(单页面应用) 这种开发模式之前,用户每次点击侧边栏都会重新请求这个页面,用户渐渐养成了点击侧边栏当前路由来刷新 view 的习惯。

现在 spa 就不一样了,用户点击当前高亮的路由并不会刷新 view ,因为 vue-router 会拦截你的路由,它判断你的 url 并没有任何变化,所以它不会触发任何钩子或者是 view 的变化。

不改变 current URL 就不会触发任何东西,那我可不可以强行触发你的 hook 呢?

方法也很简单,通过不断改变 urlquery 来触发 view 的变化。我们监听侧边栏每个 linkclick 事件,每次点击都给 router push 一个不一样的 query 来确保会重新刷新 view。

clickLink(path) {
  this.$router.push({
    path,
    query: {
      t: +new Date() //保证每次点击路由的query项都是不一样的,确保会重新刷新view
    }
  })
}
1
2
3
4
5
6
7
8

ps: 不要忘了在 router-view 加上一个特定唯一的 key ,如 <router-view :key="$route.path"></router-view>, 但这也有一个弊端就是 url 后面有一个很难看的 query 后缀如 xxx.com/article/list?t=1496832345025

判断当前点击的菜单路由和当前的路由是否一致,若一致的时候,会先跳转到一个专门 Redirect 的页面,它会将路由重定向到我想去的页面,这样就起到了刷新的效果了。

全局 size 大小切换按钮,你会发现 页面 app-main 区域进行了刷新。它就是运用了重定向到 Redirect 页面之后再重定向回原始页面的方法。

点击的时候重定向页面至 /redirect

const { fullPath } = this.$route
this.$router.replace({
  path: '/redirect' + fullPath
})
1
2
3
4

redirect 页面在重定向回原始页面

// redirect.vue
// https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/redirect/index.vue
export default {
  beforeCreate() {
    const { params, query } = this.$route
    const { path } = params
    this.$router.replace({ path: '/' + path, query })
  },
  render: function(h) {
    return h() // avoid warning message
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 面包屑

封装了一个面包屑导航,它也是通过 watch $route 变化动态生成的。它和 menu 也一样,也可以通过之前那些配置项控制一些路由在面包屑中的展现。

# 侧边栏滚动问题

之前版本的滚动都是用 css 来做处理的

overflow-y: scroll;

::-webkit-scrollbar {
  display: none;
}
1
2
3
4
5

首先这样写会有兼容性问题,在火狐或者其它低版本浏览器中都会比较不美观。其次在侧边栏收起的情况下,受限于 element-uimenu 组件的实现方式,不能使用该方式来处理。

所以现版本中使用了 el-scrollbar 来处理侧边栏滚动问题。

# 侧边栏 外链

{
  "path": "external-link",
  "component": Layout,
  "children": [
    {
      "path": "https://github.com/PanJiaChen/vue-element-admin",
      "meta": { "title": "externalLink", "icon": "link" }
    }
  ]
}
1
2
3
4
5
6
7
8
9
10

# 侧边栏默认展开

某些场景下,用户需要默认展开侧边栏的某些sub-menu

可以通过 default-openeds 来进行设置

注意: default-openeds="['/example','/nested']" 里面填写的是 submenuroute-path

# ts-template

{
  path: '/example',
  component: Layout,
  redirect: '/example/tree',
  meta: {
    title: 'Example',
    icon: 'example'
  },
  children: [
    {
      path: 'tree',
      component: () => import(/* webpackChunkName: "tree" */ '@/views/tree/index.vue'),
      meta: {
        title: 'Tree',
        icon: 'tree'
      }
    },
    {
      path: 'table',
      component: () => import(/* webpackChunkName: "table" */ '@/views/table/index.vue'),
      meta: {
        title: 'Table',
        icon: 'table'
      }
    }
  ]
},
{
  path: '/form',
  component: Layout,
  children: [
    {
      path: 'index',
      component: () => import(/* webpackChunkName: "form" */ '@/views/form/index.vue'),
      meta: {
        title: 'Form',
        icon: 'form'
      }
    }
  ]
},
{
  path: '/nested',
  component: Layout,
  redirect: '/nested/menu1',
  meta: {
    title: 'Nested',
    icon: 'nested'
  },
  children: [
    {
      path: 'menu1',
      component: () => import(/* webpackChunkName: "menu1" */ '@/views/nested/menu1/index.vue'),
      redirect: '/nested/menu1/menu1-1',
      meta: { title: 'Menu1' },
      children: [
        {
          path: 'menu1-1',
          component: () => import(/* webpackChunkName: "menu1-1" */ '@/views/nested/menu1/menu1-1/index.vue'),
          meta: { title: 'Menu1-1' }
        },
        {
          path: 'menu1-2',
          component: () => import(/* webpackChunkName: "menu1-2" */ '@/views/nested/menu1/menu1-2/index.vue'),
          redirect: '/nested/menu1/menu1-2/menu1-2-1',
          meta: { title: 'Menu1-2' },
          children: [
            {
              path: 'menu1-2-1',
              component: () => import(/* webpackChunkName: "menu1-2-1" */ '@/views/nested/menu1/menu1-2/menu1-2-1/index.vue'),
              meta: { title: 'Menu1-2-1' }
            },
            {
              path: 'menu1-2-2',
              component: () => import(/* webpackChunkName: "menu1-2-2" */ '@/views/nested/menu1/menu1-2/menu1-2-2/index.vue'),
              meta: { title: 'Menu1-2-2' }
            }
          ]
        },
        {
          path: 'menu1-3',
          component: () => import(/* webpackChunkName: "menu1-3" */ '@/views/nested/menu1/menu1-3/index.vue'),
          meta: { title: 'Menu1-3' }
        }
      ]
    },
    {
      path: 'menu2',
      component: () => import(/* webpackChunkName: "menu2" */ '@/views/nested/menu2/index.vue'),
      meta: { title: 'Menu2' }
    }
  ]
},
{
  path: 'external-link',
  component: Layout,
  children: [
    {
      path: '',
      meta: {
        title: 'External Link',
        icon: 'link'
      }
    }
  ]
},
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
上次更新: 2022/6/23 下午5:55:04