用户登录【上】

登录流程分析

界面及逻辑简化

template 修改

  • 修改标题 <h3 class="title">小慕读书</h3>
  • 删除提示区 <div style="position:relative">...</div>
  • 修改 inputplaceholder 为中文,修改 el-button 中的文字为中文
  • 删除 el-dialog 代码

script 修改

  • 删除 SocialSign 的引用和注册,删除 login 组件的 components 文件夹
  • 删除 showDialog 变量
  • 修改 data 中的表单校验文字为中文
  • 删除 createddestroyed 钩子函数和被注释的 afterQRScan 代码

Webstorm 开发过程中可能会碰到 script 标签中源码的 eslint 关于 indent 的报错,解决方法如下:ctrl + alt + L 格式化代码后,script 可能会出现 indent 的警告,解决方案有两种:

  • 关闭 eslint 中的 indent 检查

  • 修改 Webstormindent 设置

    Webstorm => Preferences => Editor => Code Style => HTML => Other

    do not indent of children 中增加 script 即可

VSCode 里的 prettier 插件格式化不存在这个问题可以忽略

路由实例处理

开始修改文件:

  1. 创建组件 src/views/book/create.vue ,内容随便写点

  2. 配置路由,修改 src/router/index.jsasyncRoutes

    children 的路由需要带有 name 属性,有 name 属性才会在 tags-view 中展示出来

    meta 中的属性

    • roles 设置该路由进入的权限,支持多个权限叠加
    • title 设置该路由在侧边栏和面包屑展示的文字
    • icon 对应 src/icons/svg,也支持 element-ui 的 icon

    其他配置项可以参考:vue-element-admin 路由和侧边栏 配置项

    export const asyncRoutes = [
      {
        path: '/book',
        component: Layout,
        redirect: '/book/create',
        meta: { title: '图书管理', icon: 'documentation', roles: ['admin'] },
        children: [
          {
            path: '/book/create',
            component: () => import('@/views/book/create'),
            name: 'book',
            meta: { title: '上传图书', icon: 'edit', roles: ['admin'] }
          }
        ]
      },
      // 404 page must be placed at the end !!!
      { path: '*', redirect: '/404', hidden: true }
    ]
    

预备知识

路由和权限校验

中后台路由分析如下:

  • 已获取 Token:
    • 访问 /login :重定向到 /
    • 访问 /login?redirect=/xxx :重定向到 /xxx
    • 访问 /login 以外的路由:直接访问 /xxx
  • 未获取 Token:
    • 访问 /login :直接访问 /login
    • 访问 /login 以外的路由:如访问 /dashboard,实际访问路径为 /login?redirect=%2Fdashboard ,登陆后会直接重定向 /dashboard

路由逻辑源码

  1. main.js 中加载了全局路由守卫

    import './permission'
    
  2. permission 定义了全局路由守卫

    // 白名单
    const whiteList = ['/login', '/auth-redirect']
    
    router.beforeEach(async(to, from, next) => {
      // 启动进度条
      NProgress.start()
    
      // 修改页面标题
      document.title = getPageTitle(to.meta.title)
    
      // 从 Cookie 获取 Token
      const hasToken = getToken()
    
      // 判断 Token 是否存在
      if (hasToken) {
        // 如果当前路径为 login 则直接重定向至首页
        if (to.path === '/login') {
          next({ path: '/' })
          NProgress.done()
        } else {
          // 判断用户的角色是否存在
          const hasRoles = store.getters.roles && store.getters.roles.length > 0
          // 如果用户角色存在,则直接访问
          if (hasRoles) {
            next()
          } else {
            try {
              // 异步获取用户的角色
              const { roles } = await store.dispatch('user/getInfo')
              // 根据用户角色,动态生成路由
              const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
              // 调用 router.addRoutes 动态添加路由
              router.addRoutes(accessRoutes)
              // 使用 replace 访问路由,不会在 history 中留下记录
              next({ ...to, replace: true })
            } catch (error) {
              // 移除 Token 数据
              await store.dispatch('user/resetToken')
              // 显示错误提示
              Message.error(error || 'Has Error')
              // 重定向至登录页面
              next(`/login?redirect=${to.path}`)
              NProgress.done()
            }
          }
        }
      } else {
        // 如果访问的 URL 在白名单中,则直接访问
        if (whiteList.indexOf(to.path) !== -1) {
          next()
        } else {
          // 如果访问的 URL 不在白名单中,则直接重定向到登录页面,并将访问的 URL 添加到 redirect 参数中
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    })
    
    router.afterEach(() => {
      // 停止进度条
      NProgress.done()
    })
    

NProgress

// 启动进度条
NProgress.start()
// 结束进度条
NProgress.done()
// 控制右侧的环形进度条是否显示
NProgress.configure({ showSpinner: false })

动态生成路由或获取路由出错

  1. 派发执行 store.dispatch('user/resetToken'),清除 Token 、重置 roles
  2. 显示错误提示 Message.error(error || 'Has Error')
  3. 重定向到登录页 next('/login?redirect=${to.path}'),然后结束进度条
const actions = {
  resetToken({ commit }) {
    return new Promise(resolve => {
      commit('SET_TOKEN', '')
      commit('SET_ROLES', [])
      removeToken()
      resolve()
    })
  },
}

const TokenKey = 'Admin-Token'
export function removeToken() {
  return Cookies.remove(TokenKey)
}

动态路由分析

这里的路由分为两种:

  • constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404、等通用页面
  • asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面

用户登录系统时,会动态生成路由,其中 constantRoutes 必然包含,asyncRoutes 会进行过滤

asyncRoutes 过滤的逻辑是看路由下面是否包含 metameta.roles 属性

  • 如果没有该属性,则是一个通用路由,不需要进行权限校验

  • 如果包含 roles 属性,则会判断用户的角色是否命中路由中的任意一个权限

    如果命中,则将路由保存下来

    如果未命中,则直接舍弃该路由

asyncRoutes 处理完毕后,会和 constantRoutes 合并为一个新的路由对象,并保存到 vuexpermission/routes

  • 用户登录系统后,侧边栏会从 vuex 中获取 state.permission.routes ,根据该路由动态渲染用户菜单

获取用户权限,生成动态路由

const { roles } = await store.dispatch('user/getInfo')
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

生成动态路由源码位于 src/store/modules/permission.js 中的 generateRoutes 方法

import { asyncRoutes, constantRoutes } from '@/router'

const actions = {
  generateRoutes({ commit }, roles) {
    // 返回 Promise 对象
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        // 如果角色包含 admin,则直接跳过判断,直接将 asyncRoutes 全部返回
        accessedRoutes = asyncRoutes || []
      } else {
        // 如果角色中没有包含 admin,则调用 filterAsyncRoutes 过滤路由
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      // 将路由保存到 vuex 中
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

commit('SET_ROUTES', accessedRoutes) 将路由保存在 store

const state = {
  routes: [],
  addRoutes: []
}
const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

如果角色不为 admin,则会调用 filterAsyncRoutes 方法:

export function filterAsyncRoutes(routes, roles) {
  const res = []
  
  // 遍历全部路由
  routes.forEach(route => {
    // 对路由进行浅拷贝,注意 children 不会拷贝,因为不需要对 children 进行判断,所以可以使用浅拷贝
    const tmp = { ...route }
    // 检查用户是否具备访问路由的权限
    if (hasPermission(roles, tmp)) {
      // 当路由具有访问权限时,判断路由是否具备 children 属性
      if (tmp.children) {
        // 当路由包含 children 时,对 children 迭代调用 filterAsyncRoutes 方法
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      // 当路由具有访问权限时,将 temp 保存到 res 中
      res.push(tmp)
    }
  })

  return res
}

检查权限方法 hasPermission 源码如下:

function hasPermission(roles, route) {
  // 检查路由是否包含 meta 和 meta.roles 属性
  if (route.meta && route.meta.roles) {
    // 判断 route.meta.roles 中是否包含用户角色 roles 中的任何一个权限
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    // 如果路由没有 meta 或 meta.roles 属性,则视为该路由不需要进行权限控制(所有用户对该路由都具有访问权限)
    return true
  }
}

侧边栏

  • sidebar 引用自 layout 组件,组件位于 src/layout/index.vue
  • sidebar 组件源码位于 src/layout/components/Sidebar/index.vue

el-menu 用法解析

<template>
  <el-row class="tac">
    <el-col :span="12">
      <el-menu
        default-active="1-1"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
        mode="vertical"
        unique-opened
        :collapse="isCollapse"
        :collapse-transition="false"
        class="el-menu-vertical-demo"
        @open="handleOpen"
        @close="handleClose"
        @select="handleSelect"
      >
        <el-submenu index="1">
          <template slot="title">
            <i class="el-icon-location"></i>
            <span>导航一</span>
          </template>
          <el-menu-item-group>
            <template slot="title">分组一</template>
            <el-menu-item index="1-1">选项1</el-menu-item>
            <el-menu-item index="1-2">选项2</el-menu-item>
          </el-menu-item-group>
          <el-menu-item-group title="分组2">
            <el-menu-item index="1-3">选项3</el-menu-item>
          </el-menu-item-group>
          <el-submenu index="1-4">
            <template slot="title">选项4</template>
            <el-menu-item index="1-4-1">选项1</el-menu-item>
          </el-submenu>
        </el-submenu>
        <el-submenu index="2">
          <template slot="title">
            <i class="el-icon-menu"></i>
            <span slot="title">导航二</span>
          </template>
          <el-menu-item index="2-1">选项2-1</el-menu-item>
        </el-submenu>
        <el-menu-item index="3" disabled>
          <i class="el-icon-document"></i>
          <span slot="title">导航三</span>
        </el-menu-item>
        <el-menu-item index="4">
          <i class="el-icon-setting"></i>
          <span slot="title">导航四</span>
        </el-menu-item>
      </el-menu>
    </el-col>
    <el-col>
      <el-button @click="isCollapse = !isCollapse">折叠</el-button>
    </el-col>
  </el-row>
</template>

<script>
export default {
  data() {
    return {
      isCollapse: false
    }
  },
  methods: {
    handleSelect(index, indexPath) {
      console.log('handleSelect', index, indexPath)
    },
    handleOpen(index, indexPath) {
      console.log('handleOpen', index, indexPath)
    },
    handleClose(index, indexPath) {
      console.log('handleClose', index, indexPath)
    }
  }
}
</script>

el-menu 表示菜单容器组件:

  • default-active:当前激活菜单的 index,注意:如果存在子菜单,需要填写子菜单 ID

  • unique-opened:是否只保持一个子菜单的展开

  • collapse:是否水平折叠收起菜单(仅在 modevertical 时可用)

    可以结合按钮使用,点击一下就折叠再点一下就展开

  • collapse-transition:是否显示折叠动画

  • @select:点击菜单事件。回调函数:index:选中菜单项的 index;indexPath:选中菜单项的 index path,可以通过这个获取 1-4-1 菜单的所有父级菜单的 ID

    handleSelect 1-4-1 ['1', '1-4', '1-4-1']
    
  • @open:父菜单打开时触发事件

  • @close:父菜单关闭时触发事件

el-submenu 表示子菜单容器:el-submenuel-menu 不同,el-menu 表示整个菜单,而 el-submenu 表示一个具体菜单,el-submenu 可以通过定制 slot 的 title 来自定义菜单样式:

<el-submenu index="1">
  <template slot="title">
    <i class="el-icon-location"></i>
    <span>导航一</span>
  </template>
</el-submenu>

el-submenu 容器内 defaultslot 用来存放子菜单,可以包含三种子菜单组件:

  • el-menu-item-group:菜单分组,为一组菜单添加一个标题,el-menu-item-group 容器内容需要存放 el-menu-item 组件,支持 titleslot 来定制标题样式
  • el-submenuel-submenu 支持循环嵌套 el-submenu,这使得超过两级子组件得以实现
  • el-menu-item:子菜单组件

sidebar 源码分析

  • src/layout/components/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 (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
const state = {
  sidebar: {
    opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true
  }
}
</script>
  • :default-active="activeMenu":通过 meta.activeMenu 属性,指定路由对应的高亮菜单

    meta.activeMenu 需要提供一个合法的路由,否则将不能生效

  • :collapse="isCollapse"NavBar 中点击按钮,会修改 Cookie 中的 sidebarStatus,从 vuex 取值时会将 sidebarStatus 转为 Boolean,并判断默认是否需要收缩左侧菜单栏

  • v-if="showLogo":判断 settings.js 中的配置项是否需要展示 Logo

  • text-color="variables.menuText":从 @/styles/variables.scss 中获取 scss 对象,从而获取样式

sidebar 中通过 v-for 循环遍历 sidebar-item 实现子菜单:

<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />

sidebar-item 源码分析

  • src/layout/components/Sidebar/SidebarItem.vue

注意: 组件是可以在它们自己的模板中调用自己的,不过需要给这个组件 name 属性,会报错 For recursive components, make sure to provide the "name" option.

<template>
  <div v-if="!item.hidden">
    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
      </template>
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>
  </div>
</template>

<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
  mixins: [FixiOSBug],
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  data() {
    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
    // TODO: refactor with render function
    this.onlyOneChild = null
    return {}
  },
  methods: {
    hasOneShowingChild(children = [], parent) {
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // Temp set(will be used if only has one showing child)
          this.onlyOneChild = item
          return true
        }
      })

      // When there is only one child router, the child router is displayed by default
      if (showingChildren.length === 1) {
        return true
      }

      // Show parent if there are no child router to display
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
        return true
      }

      return false
    },
    resolvePath(routePath) {
      if (isExternal(routePath)) {
        return routePath
      }
      if (isExternal(this.basePath)) {
        return this.basePath
      }
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>

sidebar-itemprops 如下:

  • item:路由对象
  • basePath:路由路径

sidebar-item 展示逻辑分析:

  • 通过 item.hidden 控制菜单是否展示

  • 通过 hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.always Show 逻辑判断 template 菜单是否展示,template 代表单一菜单

    • hasOneShowingChild:判断是否只有一个需要展示的子路由
    • !onlyOneChild.children||onlyOneChild.noShowingChildren :判断需要展示的子菜单,是否包含 children 属性,如果包含,则说明子菜单可能存在孙菜单,此时需要再判断 noShowingChildren 属性
    • !item.alwaysShow :判断路由中是否存在 alwaysShow 属性,如果存在,则返回 false,不展示 template 菜单,也就是说只要配置了 alwaysShow 属性就会直接进入 el-submenu 组件

hasOneShowingChild 方法源码详解:

  • childrenrouter 对象的 children 属性
  • itemrouter 对象

hasOneShowingChild(children = [], parent) {
  const showingChildren = children.filter(item => {
    // 如果 children 中的路由包含 hidden 属性,则返回 false
    if (item.hidden) {
      return false
    } else {
      // 将子路由赋值给 onlyOneChild,用于只包含一个路由时展示 
      this.onlyOneChild = item
      return true
    }
  })

  // 如果过滤后,只包含展示一个路由,则返回 true
  if (showingChildren.length === 1) {
    return true
  }

  // 如果没有子路由需要展示,则将 onlyOneChild 的 path 设置空路由,并添加 noShowingChildren 属性,表示虽然有子路由,但是不需要展示子路由
  if (showingChildren.length === 0) {
    this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
    return true
  }

  // 返回 false,表示不需要展示子路由,或者超过一个需要展示的子路由
  return false
}

如果展示 template 组件,首先会展示 app-link 组件,然后是 el-menu-item,最里面嵌套的是 item 组件

item 组件需要路由 meta 中包含 titleicon 属性,否则将渲染内容为空的 vnode 对象

<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
  <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
    <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
  </el-menu-item>
</app-link>

如果 template 菜单不展示,则展示 el-submenu 菜单,el-submenu 逻辑中采用了嵌套组件的做法,将 sidebar-item 嵌套在 el-submenu 中:

<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
  <template slot="title">
    <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
  </template>
  <sidebar-item
    v-for="child in item.children"
    :key="child.path"
    :is-nest="true"
    :item="child"
    :base-path="resolvePath(child.path)"
    class="nest-menu"
  />
</el-submenu>

app-link 源码分析

app-link 是一个动态组件,通过解析 to 参数,如果包含 http 前缀则变成一个 a 标签,否则变成一个 router-link 组件

  • component 是内置的组件

    propsisinline-template

    作用:依照 is,来决定哪个组件被渲染

<template>
  <component :is="type" v-bind="linkProps(to)">
    <slot />
  </component>
</template>

<script>
import { isExternal } from '@/utils/validate'

export default {
  props: {
    to: {
      type: String,
      required: true
    }
  },
  computed: {
    isExternal() {
      return isExternal(this.to)
    },
    type() {
      if (this.isExternal) {
        return 'a'
      }
      return 'router-link'
    }
  },
  methods: {
    linkProps(to) {
      if (this.isExternal) {
        return {
          href: to,
          target: '_blank',
          rel: 'noopener'
        }
      }
      return {
        to: to
      }
    }
  }
}
</script>

isExternal 函数通过一个正则表达式匹配 http 链接:

export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

item 源码分析

item 组件通过定义 render 函数完成组件渲染

  • 如果 item 取不到 meta 中的 icon 会取父路由的 icon

render 函数参数:

  • createElement 函数:返回值是一个虚拟 DOM,即 VNode,也就是渲染的节点

    createElement 有三个参数:

    1. 要渲染的 html 标签、组件: { String | Object | Function }
    2. html 的各种属性:{ Object }
    3. 虚拟子节点 VNodes,当前 html 标签的子元素:{ String | Array }
  • context :里面包含 props 等参数

<script>
export default {
  name: 'MenuItem',
  functional: true,
  props: {
    icon: {
      type: String,
      default: ''
    },
    title: {
      type: String,
      default: ''
    }
  },
  render(h, context) {
    const { icon, title } = context.props
    const vnodes = []

    if (icon) {
      if (icon.includes('el-icon')) {
        vnodes.push(<i class={[icon, 'sub-el-icon']} />)
      } else {
        vnodes.push(<svg-icon icon-class={icon}/>)
      }
    }

    if (title) {
      vnodes.push(<span slot='title'>{(title)}</span>)
    }
    return vnodes
  }
}
</script>

总结

sidebarsidebar 主要包含 el-menu 容器组件中,el-menu 中遍历 vuex 中的 routes,生成 sidebar-item 组件。sidebar 主要配置项如下:

  • activeMenu:根据当前路由的 meta.activeMenu 属性控制侧边栏中高亮菜单
  • isCollapse:根据 CookiesidebarStatus 控制侧边栏是否折叠
  • variables:通过 @/styles/variables.scss 填充 el-menu 的基本样式

sidebar-item :主要分为两部分:

  • 第一部分是当只需要展示一个 children 或者没有 children 时进行展示,展示的组件包括:
    • app-link:动态组件,path 为链接时,显示为 a 标签,path 为路由时,显示为 router-link 组件
    • el-menu-item:菜单项,当 sidebar-item 为非 nest 组件时,el-menu-item 会增加 submenu-title-noDropdownclass
    • itemel-menu-item 里的内容,主要是 icontitle,当 title 为空是,整个菜单项将不会展示
  • 第二部分是当 children 超过两项时进行展示,展示的组件包括:
    • el-submenu:子菜单组件容器,用于嵌套子菜单组件
    • sidebar-itemel-submenu 迭代嵌套了 sidebar-item 组件,在 sidebar-item 组件中有两点变化:
      • 设置 is-nest 属性为 true
      • 根据 child.path 生成了 base-path 属性传入 sidebar-item 组件

vue elementui navmenu 多级导航菜单(水平、垂直)

前端开发高端操作

类型转换

快速转 Number

var a = '1'

console.log(typeof a)
console.log(typeof Number(a)) // 普通写法
console.log(typeof +a)        // 高端写法

快速转 Boolean

var a = 0

console.log(typeof a)
console.log(typeof Boolean(a)) // 普通写法
console.log(typeof !!a)        // 高端写法

混写

  • 先转为 Number,再转为 Boolean
var a = '0'

console.log(!!a)  // 直接转将得到 true,不符合预期
console.log(!!+a) // 先转为 Number 再转为 Boolean,符合预期

JS 和 CSS 两用样式

template 中需要动态定义样式,通常做法:

<template>
  <div :style="{ color: textColor }">Text</div>
</template>

<script>
export default {
  data() {
    return {
      textColor: '#ff5000'
    }
  }
}
</script>

定义 SCSS 文件

$menuActiveText:#409EFF;

:export {
  menuActiveText: $menuActiveText;
}

在 JS 中引用:

  • 使用 import 引用 SCSS 文件
  • 定义 computed 将 styles 对象变成响应式对象
  • 在 template 中使用 syles 对象
<template>
  <div :style="{ color: styles.menuActiveText }">Text</div>
</template>

<script>
import styles from '@/styles/variables.scss'

export default {
  computed: {
    styles() {
      return styles
    }
  }
}
</script>

连续解构

从数组第一个对象元素中提取某个属性,比如:err 对象中包含一个 errors 数组,errors 数组每一个对象都包含一个 msg 属性

err = {
  errors: [
    {
      msg: 'this is a message'
    }
  ]
}

// 快速的提取方法为
const [{ msg }] = err.errors
// 如果不用解构写法为
const msg = err.errors[0].msg

重定向

登录重定向

  • src/views/login/index.vue 中对 $route 进行监听:
watch: {
  $route: {
    handler: function(route) {
      const query = route.query
      if (query) {
        this.redirect = query.redirect
        this.otherQuery = this.getOtherQuery(query)
      }
    },
    immediate: true
  }
}

this.getOtherQuery(query) 的用途是获取除 redirect 以外的其他查询条件,登录成功后:

this.$store
  .dispatch('user/login', this.loginForm)
  .then(() => {
    this.$router.push({
      path: this.redirect || '/',
      query: this.otherQuery
    })
    this.loading = false
  })
  .catch(() => {
    this.loading = false
  })

完成重定向的代码:

this.$router.push({
  path: this.redirect || '/',
  query: this.otherQuery
})

vue-element-admin 提供了专门的重定向组件,源码如下:

<script>
export default {
  created() {
    const { params, query } = this.$route
    const { path } = params
    this.$router.replace({ path: '/' + path, query })
  },
  render: function(h) {
    return h() // avoid warning message
  }
}
</script>

重定向组件

重定向组件配置了动态路由:

  • * 表示匹配零个或多个路由,比如路由为 /redirect 时,仍然能匹配到 redirect 组件。如果将 * 去掉

    此时路由 /redirect 将只能匹配到 Layout 组件,而无法匹配到 redirect 组件

{
  path: '/redirect',
  component: Layout,
  hidden: true,
  children: [
    {
      path: '/redirect/:path(.*)',
      component: () => import('@/views/redirect/index')
    }
  ]
},

面包屑导航

el-breadcrumb-item

  • el-breadcrumb:面包屑导航容器,separator 控制面包屑导航文本分割线
  • el-breadcrumb-item:面包屑子项目,可以使用 to 属性切换路由,slot 中可以包含 a 标签来跳转到外链

使用 to 属性和 a 标签切换路由区别是:to 属性切换路由是动态替换 App.vue 中的路由内容,而 a 标签切换路由会刷新页面

<el-breadcrumb separator="/">
  <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
  <el-breadcrumb-item><a href="/">活动管理</a></el-breadcrumb-item>
  <el-breadcrumb-item>活动列表</el-breadcrumb-item>
  <el-breadcrumb-item>活动详情</el-breadcrumb-item>
</el-breadcrumb>

路由与面包屑导航映射

面包屑导航模板源码:

  • el-breadcrumb-item 内做了一个判断,如果是最后一个元素或者路由的 redirect 属性指定为 noRedirect 则不生成链接,否则将使用 a 标签事件触发,但这里使用了 @click.prevent 阻止了默认 a 标签事件触发,而使用自定义的 handleLink 方法处理路由跳转
<el-breadcrumb class="app-breadcrumb" separator="/">
  <transition-group name="breadcrumb">
    <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
      <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
      <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
    </el-breadcrumb-item>
  </transition-group>
</el-breadcrumb>

这里面比较重要是 levelList ,是通过 getBreadcrumb 方法生成的

面包屑导航实现逻辑如下:

  • 获取 this.$route.matched,并过滤其中不包含 item.meta.title 的项,生成新的面包屑导航数组 matched
  • 判断 matched 第一项是否为 dashboard,如果不是,则添加 dashboard 为面包屑导航第一项
  • 再次过滤 matcheditem.meta.title 为空的项和 item.meta.breadcrumbfalse 的项
getBreadcrumb() {
  // only show routes with meta.title
  let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
  const first = matched[0]

  if (!this.isDashboard(first)) {
    matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
  }

  this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},

isDashboard(route) {
  const name = route && route.name
  if (!name) {
    return false
  }
  return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},

当路由进行切换时,也会重新调用 getBreadcrumb 方法生成

watch: {
  $route(route) {
    // if you go to the redirect page, do not update the breadcrumbs
    if (route.path.startsWith('/redirect/')) {
      return
    }
    this.getBreadcrumb()
  }
},

handleLink 方法源码如下:

  • 这里的 pathCompile 用于解决动态路由的匹配问题
handleLink(item) {
  const { redirect, path } = item
  if (redirect) {
    this.$router.push(redirect)
    return
  }
  this.$router.push(this.pathCompile(path))
}

pathCompile(path) {
  // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
  const { params } = this.$route
  var toPath = pathToRegexp.compile(path)
  return toPath(params)
},
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值