什么是路由(详细讲解)

270 阅读8分钟

一、vue之beforeEach路由鉴权

一般我们会相应的把路由表角色菜单配置在后端,当用户未通过页面菜单,直接从地址栏访问非权限范围内的url时,拦截用户访问并重定向到首页。

vue的初期是可以通过动态路由的方式,按照权限加载对应的路由表AddRouter,但是由于权限交叉,导致权限路由表要做判断结合,想想还是挺麻烦的,所以采用的是在beforeEach里面直判断用非动态路由的方式

在使用 Vue的时候,框架提供了路由守卫功能,用来在进入某个路有前进行一些校验工作,如果校验失败,就跳转到 404 或者登陆页面,比如 Vue 中的 beforeEnter 函数:

...
router.beforeEach(async(to, from, next) => {
    const toPath = to.path;
    const fromPath = from.path;
})
...
复制代码

1、路由概览

// index.js
import Vue from 'vue'
import Router from 'vue-router'

import LabelMarket from './modules/label-market'
import PersonalCenter from './modules/personal-center'
import SystemSetting from './modules/system-setting'

import API from '@/utils/api'

Vue.use(Router)

const routes = [
  {
    path: '/label',
    component: () => import(/* webpackChunkName: "index" */ '@/views/index.vue'),
    redirect: { name: 'LabelMarket' },
    children: [
      { // 基础公共页面
        path: 'label-market',
        name: 'LabelMarket',
        component: () => import(/* webpackChunkName: "label-market" */ '@/components/page-layout/OneColLayout.vue'),
        redirect: { name: 'LabelMarketIndex' },
        children: LabelMarket
      },
      { // 个人中心
        path: 'personal-center',
        name: 'PersonalCenter',
        redirect: '/label/personal-center/my-apply',
        component: () => import(/* webpackChunkName: "personal-center" */ '@/components/page-layout/TwoColLayout.vue'),
        children: PersonalCenter
      },
      { // 系统设置
        path: 'system-setting',
        name: 'SystemSetting',
        redirect: '/label/system-setting/theme',
        component: () => import(/* webpackChunkName: "system-setting" */ '@/components/page-layout/TwoColLayout.vue'),
        children: SystemSetting
      }]
  },
  {
    path: '*',
    redirect: '/label'
  }
]

const router = new Router({ mode: 'history', routes })
// personal-center.js
export default [
    ...
  { // 我的审批
    path: 'my-approve',
    name: 'PersonalCenterMyApprove',
    component: () => import(/* webpackChunkName: "personal-center" */ '@/views/personal-center/index.vue'),
    children: [
      { // 数据服务审批
        path: 'api',
        name: 'PersonalCenterMyApproveApi',
        meta: {
          requireAuth: true,
          authRole: 'dataServiceAdmin'
        },
        component: () => import(/* webpackChunkName: "personal-center" */ '@/views/personal-center/api-approve/index.vue')
      },
      ...
    ]
  }
]
复制代码
export default [
    ...
  { // 数据服务设置
    path: 'api',
    name: 'SystemSettingApi',
    meta: {
      requireAuth: true,
      authRole: 'dataServiceAdmin'
    },
    component: () => import(/* webpackChunkName: "system-setting" */ '@/views/system-setting/api/index.vue')
  },
  { // 主题设置
    path: 'theme',
    name: 'SystemSettingTheme',
    meta: {
      requireAuth: true,
      authRole: 'topicAdmin'
    },
    component: () => import(/* webpackChunkName: "system-setting" */ '@/views/system-setting/theme/index.vue')
  },
    ...
]
复制代码

2、鉴权判断

用户登陆信息请求后端接口,返回菜单、权限、版权信息等公共信息,存入vuex。此处用到权限字段如下:

_userInfo: {
    admin:false, // 是否超级管理员
    dataServiceAdmin:true, // 是否数据服务管理员
    topicAdmin:false // 是否主题管理员
}
复制代码
  1. 判断当前路由是否需要鉴权(router中meta字段下requireAuth是否为true),让公共页面直接放行;
  2. 判断角色是超级管理员,直接放行;
  3. (本系统特殊逻辑)判断跳转路径是主题设置但角色不为主题管理员,继续判断角色是否为数据服务管理员,跳转数据服务设置页or重定向(‘系统设置’菜单'/label/system-setting'默认重定向到'/label/system-setting/theme',其他菜单默认重定向的都是基础公共页面,故需要对这里的重定向鉴权。系统设置的权限不是主题管理员就一定是数据服务管理员,所以能这样做);
  4. 判断路由需求权限是否符合,若不符合直接重定向。
// index.js
router.beforeEach(async (to, from, next) => {
  try {
    // get user login info
    const _userInfo = await API.get('/common/query/menu', {}, false)
    router.app.$store.dispatch('setLoginUser', _userInfo)

    if (_userInfo && Object.keys(_userInfo).length > 0 &&
      to.matched.some(record => record.meta.requireAuth)) {
      if (_userInfo.admin) { // super admin can pass
        next()
      } else if (to.fullPath === '/label/system-setting/theme' &&
        !_userInfo.topicAdmin) {
        if (_userInfo.dataServiceAdmin) {
          next({ path: '/label/system-setting/api' })
        } else {
          next({ path: '/label' })
        }
      } else if (!(_userInfo[to.meta.authRole])) {
        next({ path: '/label' })
      }
    }
  } catch (e) {
    router.app.$message.error('获取用户登陆信息失败!')
  }
  next()
})
复制代码

二、简介

1、路由简介

路由是干什么的?

根据不同的 url 地址展示不同的内容或页面。

单页面应用最大的特点就是只有一个 web 页面。因而所有的页面跳转都需要通过javascript实现。当需要根据用户操作展示不同的页面时,我们就需要根据访问路径使用js控制页面展示内容。

2、React-router 简介

React Router 是专为 React 设计的路由解决方案。它利用HTML5 的history API,来操作浏览器的 session history (会话历史)。

3、使用

React Router被拆分成四个包:react-router,react-router-dom,react-router-native和react-router-config。react-router提供核心的路由组件与函数。react-router-config用来配置静态路由(还在开发中),其余两个则提供了运行环境(浏览器与react-native)所需的特定组件。

进行网站(将会运行在浏览器环境中)构建,我们应当安装react-router-dom。因为react-router-dom已经暴露出react-router中暴露的对象与方法,因此你只需要安装并引用react-router-dom即可。

4、相关组件

4-1、

使用了 HTML5 的 history API (pushState, replaceState and the popstate event) 用于保证你的地址栏信息与界面保持一致。

主要属性:

basename:设置根路径

getUserConfirmation:获取用户确认的函数

forceRefresh:是否刷新整个页面

keyLength:location.key的长度

children:子节点(单个)

4-2、

为旧版本浏览器开发的组件,通常简易使用BrowserRouter。

4-3、

为项目提供声明性的、可访问的导航

主要属性:

to:可以是一个字符串表示目标路径,也可以是一个对象,包含四个属性:

pathname:表示指向的目标路径

search: 传递的搜索参数

hash:路径的hash值

state: 地址状态

replace:是否替换整个历史栈

innerRef:访问部件的底层引用

同时支持所有a标签的属性例如className,title等等

4-4、

React-router 中最重要的组件,最主要的职责就是根据匹配的路径渲染指定的组件

主要属性:

path:需要匹配的路径

component:需要渲染的组件

render:渲染组件的函数

children :渲染组件的函数,常用在path无法匹配时呈现的’空’状态即所谓的默认显示状态

4-5、

重定向组件

主要属性: to:指向的路径

嵌套组件:唯一的渲染匹配路径的第一个子 或者

三、react-router-config之路由鉴权

引言

在之前的版本中,React Router 也提供了类似的 onEnter 钩子,但在 React Router 4.0 版本中,取消了这个方法。React Router 4.0 采用了声明式的组件,路由即组件,要实现路由守卫功能,就得我们自己去写了。

1、react-router-config 是一个帮助我们配置静态路由的小助手。其源码就是一个高阶函数 利用一个map函数生成静态路由

import React from "react";
import Switch from "react-router/Switch";
import Route from "react-router/Route";
const renderRoutes = (routes, extraProps = {}, switchProps = {}) =>
routes ? (
    <Switch {...switchProps}>
        {routes.map((route, i) => ( 
        <Route
          key={route.key || i}
          path={route.path}
          exact={route.exact}
          strict={route.strict}
          render={props => (
            <route.component {...props} {...extraProps} route={route} />
          )}
        />
      ))}
    </Switch>
  ) : null;
 export default renderRoutes;
复制代码

//router.js 假设这是我们设置的路由数组(这种写法和vue很相似是不是?)

const routes = [
    { path: '/',
        exact: true,
        component: Home,
    },
    {
        path: '/login',
        component: Login,
    },
    {
        path: '/user',
        component: User,
    },
    {
        path: '*',
        component: NotFound
    }
]
复制代码

//app.js 那么我们在app.js里这么使用就能帮我生成静态的路由了

import { renderRoutes } from 'react-router-config'
import routes from './router.js'
const App = () => (
   <main>
      <Switch>
         {renderRoutes(routes)}
      </Switch>
   </main>
)

export default App
复制代码

用过vue的小朋友都知道,vue的router.js 里面添加 meta: { requiresAuth: true }

然后利用导航守卫

router.beforeEach((to, from, next) => {
  // 在每次路由进入之前判断requiresAuth的值,如果是true的话呢就先判断是否已登陆
})
复制代码

2、基于类似vue的路由鉴权想法,我们稍稍改造一下react-router-config

// utils/renderRoutes.js

import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom'
const renderRoutes = (routes, authed, authPath = '/login', extraProps = {}, switchProps = {}) => routes ? (
  <Switch {...switchProps}>
    {routes.map((route, i) => (
      <Route
        key={route.key || i}
        path={route.path}
        exact={route.exact}
        strict={route.strict}
        render={(props) => {
          if (!route.requiresAuth || authed || route.path === authPath) {
            return <route.component {...props} {...extraProps} route={route} />
          }
          return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />
        }}
      />
    ))}
  </Switch>
) : null
export default renderRoutes
复制代码

修改后的源码增加了两个参数 authed 、 authPath 和一个属性 route.requiresAuth

然后再来看一下最关键的一段代码

if (!route.requiresAuth || authed || route.path === authPath) {
    return <route.component {...props} {...extraProps} route={route} />
    }
    return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />
复制代码

很简单 如果 route.requiresAuth = false 或者 authed = true 或者 route.path === authPath(参数默认值'/login')则渲染我们页面,否则就渲染我们设置的authPath页面,并记录从哪个页面跳转。

相应的router.js也要稍微修改一下

const routes = [
    { path: '/',
        exact: true,
        component: Home,
        requiresAuth: false,
    },
    {
        path: '/login',
        component: Login,
        requiresAuth: false,
    },
    {
        path: '/user',
        component: User,
        requiresAuth: true, //需要登陆后才能跳转的页面
    },
    {
        path: '*',
        component: NotFound,
        requiresAuth: false,
    }
]
复制代码

//app.js

import React from 'react'
import { Switch } from 'react-router-dom'
//import { renderRoutes } from 'react-router-config'
import renderRoutes from './utils/renderRoutes'
import routes from './router.js'
const authed = false // 如果登陆之后可以利用redux修改该值(关于redux不在我们这篇文章的讨论范围之内)
const authPath = '/login' // 默认未登录的时候返回的页面,可以自行设置
const App = () => (
   <main>
      <Switch>
         {renderRoutes(routes, authed, authPath)}
      </Switch>
   </main>
)
export default App
复制代码
//登陆之后返回原先要去的页面login函数
login(){
    const { from } = this.props.location.state || { from: { pathname: '/' } }
     // authed = true // 这部分逻辑自己写吧。。。
    this.props.history.push(from.pathname)
}
复制代码

到此react-router-config就结束了并完成了我们想要的效果

3、注意

很多人会发现,有时候达不到我们想要的效果,那么怎么办呢,接着往下看

1、设计全局组建来管理是否登陆

configLogin.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'

class App extends Component {
  static propTypes = {
    children: PropTypes.object,
    location: PropTypes.object,
    isLogin: PropTypes.bool,
    history: PropTypes.object
  };
  componentDidMount () {
    if (!this.props.isLogin) {
      setTimeout(() => {
        this.props.history.push('/login')
      }, 300)
    }
    if (this.props.isLogin && this.props.location.pathname === '/login') {
      setTimeout(() => {
        this.props.history.push('/')
      }, 300)
    }
  }

  componentDidUpdate () {
    if (!this.props.isLogin) {
      setTimeout(() => {
        this.props.history.push('/login')
      }, 300)
    }
  }
  render () {
    return this.props.children
  }
}

export default withRouter(App)
复制代码

通过在主路由模块index.js中引入

import {
  BrowserRouter as Router,
  Redirect,
  Route,
  Switch
} from 'react-router-dom'

<Router
   history={ history }
   basename="/"
   getUserConfirmation={ getConfirmation(history, 'yourCallBack') }
   forceRefresh={ !supportsHistory }
 >
  <App isLogin={ isLogin ? true : false }>
    <Switch>
     <Route
     exact
     path="/"
     render={ () => <Redirect to="/layout/dashboard" push /> }
     />
     <Route path="/login" component={ Login } />
     <Route path="/layout" component={ RootLayout } />
     <Route component={ NotFound } />
   </Switch>
  </App>
 </Router>
复制代码

很多时候我们是可以通过监听路由变化实现的比如getUserConfirmation钩子就是做这件事情的

const getConfirmation = (message, callback) => {
  if (!isLogin) {
    message.push('/login')
  } else {
    message.push(message.location.pathname)
  }
复制代码