【React系列】手把手帶你擼後臺系統(Redux與路由鑑權)

1、前言

本系列文章計劃撰文3篇:javascript

項目地址:github react-adminjava

系統預覽:react-admin systemreact

上一篇咱們介紹了系統架構,這一篇將繼續介紹:webpack

  • Redux的應用
  • 登陸受權
  • 路由鑑權

2、Redux應用

側邊導航欄(Sidebar)咱們實現了根據配置渲染菜單項,如今咱們要繼續完善它的功能:導航高亮與鑑權。咱們經過redux管理咱們Sidebar的狀態,想要reduxstore可用,咱們必須使用它的<Provider />connect()git

// Page.js
import { createStore } from  'redux'
import store from 'store' // 新建store目錄
ReactDOM.render(<Provider store={ createStore(store) }> <Page /> </Provider>, document.getElementById('root'))
複製代碼

<Provider />的做用是讓store在整個App中可用,connect()的做用是將storecomponent鏈接起來。github

// Sidebar.js
import { connect } from 'react-redux'
import { updateSidebarState } from 'store/action'
class Sidebar extends Component {
    // 省略代碼...
}
const mapStateToProps = (state, owns) => ({
    sidebar: prop('sidebarInfo', state), // 從store中取出sidebarInfo數據
    ...owns
})
const mapDispatchToProps = dispatch => ({
    dispatchSideBar: sidebar => dispatch(updateSidebarState(sidebar)) // 更新數據狀態
})
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(SideBar)
複製代碼

2.1 初始化Sidebar數據web

初始化Sidebar數據主要作兩點:redux

  • 默認的路由配置數據裏面沒有標識高亮狀態,初始化過程增長active標誌位;
  • 爲父級導航項增長key值標誌位,用於檢測收合狀態;
// store/reducer.js
function initSidebar(arr) {
  return map(each => {
      if(each.routes) {
          each.active = false
          each.key = generateKey(each.routes) // 生產惟一key值,與路由path關聯
          each.routes = initSidebar(each.routes)
      }
      return each
  }, arr)
}

// 更新sidebar狀態
function updateSidebar(arr, key='') {
  return map(each => {
      if(key === each.key) {
          each.active = !!!each.active
      } else if(each.routes) {
          each.routes = updateSidebar(each.routes, key)
      }
      return each
  }, arr)
}

export const sidebarInfo = (state=initSidebar(routes), action) => {
  switch (action.type) {
      case 'UPDATE':
          return updateSidebar(state, action.key)
      default:
          return state
  }
}
複製代碼

經處理後的路由配置數據新增了activekey兩個屬性: 後端

2.2 檢測高亮

渲染側邊導航欄過程須要檢測高亮狀態:根據當前路由path與導航項的key值相比較:數組

// Sidebar.js
class Sidebar extends Component {
  constructor(props) {
    // ...
    this.state = {
      routeName: path(['locaotion', 'pathname'], this.props), // 獲取當前路由信息
      routes: compose(this.checkActive.bind(this), prop('sidebar'))(this.props) // 返回檢測後的路由數據
    }
  }
  // 省略代碼...
  checkActive (arr, routeName='') {
        const rName = routeName || path(['location', 'pathname'], this.props)
        if(!rName) return arr
        return map((each) => {
            const reg = new RegExp(rName)
            if(reg.test(each.key)) {
                each.active = true
            } else if (each.routes) {
                each.routes = this.checkActive(each.routes, rName)
            }
            return each
        }, arr)
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(withRouter(SideBar))
複製代碼

特別注意: Sidebar組件須要經由withRouter包裹後才能在組件內獲取路由相關信息。

3、登陸受權

這裏設定的場景是:用戶的登陸數據在當前會話期內有效(sessionStorage存儲用戶信息),用戶信息全局可用(redux管理)。假定咱們存儲的用戶數據有:

{
    username: '',  // 賬號
    permission: [],  // 用戶權限列表
    isAdmin: false  // 管理員標識
}
複製代碼

3.1 初始化用戶信息

// store/reducer.js
const userInfo = getSessionStore('user') || { // 首先從sessionStorage中獲取數據
    username: '',
    permission: [],
    isAdmin: false
}
export const user = (state=userInfo, action) => {
    switch (action.type) {
        case 'LOGIN': 
            return pick(keys(state), action.user)
        default:
            return state
    }
}
複製代碼

3.2 實現登陸

首先將store的state和action注入login組件:

import { connect } from 'react-redux'
import { doLogin } from 'store/action'
import Login from './login'
const mapStateToProps = (state, owns) => ({
    user: state,
    ...owns
})
const mapDispatchToProps = dispatch => ({
    dispatchLogin: user => dispatch(doLogin(user))
})
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Login)
複製代碼

繼而在login.js中實現登陸邏輯:

class Login extends Component {
  login () {
      const { dispatchLogin, history } = this.props
      const { form } = this.state
      const user = {
          username: '安歌',
          permission: ['add'],
          isAdmin: false
      }
      dispatchLogin(user) // 更新store存儲的用戶數據
      setSessionStore('user', user) // 將用戶數據存儲在sessionStorage
      // login success
      history.push('/front/approval/undo') // 登陸重定向
  } 
}
複製代碼

4、路由鑑權

上一篇中咱們實現了頁面級路由,如今咱們須要根據路由配置文件,註冊應用級路由。回顧下咱們的路由配置:

export default [
    {
        title: '個人事務', // 頁面標題&一級nav標題
        icon: 'icon-home',
        routes: [{
            name: '待審批',
            path: '/front/approval/undo',
            component: 'ApprovalUndo'
        }, {
            name: '已處理',
            path: '/front/approval/done',
            auth: 'add',
            component: 'ApprovalDone'
        }]
    }
]
複製代碼

咱們根據pathcomponent信息註冊路由,根據auth信息進行路由鑑權。看下咱們如何實現各個路由對應的組件。

views目錄存放了咱們全部的頁面應用組件, index.js則做爲組件的入口文件:

// views/index.js
import AsyncComponent from 'components/AsyncComponent'
const ApprovalUndo = AsyncComponent(() => import(/* webpackChunkName: "approvalundo" */ 'views/approval/undo'))
const ApprovalDone = AsyncComponent(() => import(/* webpackChunkName: "approvaldone" */ 'views/approval/done'))
export default {
    ApprovalUndo, ApprovalDone
}
複製代碼

說明: 關於AsyncComponent的說明已在上篇有所介紹。

4.1 註冊路由

// router/index.js 
import routesConf from './config'
import views from 'views'
class CRouter extends Component {
    render () {
        return (
            <Switch>
                { pipe(map(each => {
                    const routes = each.routes
                    return this.generateRoute(routes, each.title)
                }), flatten)(routesConf) }
                <Route render={ () => <Redirect to="/front/404" /> } />
            </Switch>
        )
    }
    generateRoute (routes=[], title='React Admin') { // 遞歸註冊路由
        return map(each => each.component ? (
            <Route 
                key={ each.path }
                exact  // 精確匹配路由
                path={ each.path }
                render={ props => {
                    const reg = /\?\S*g/
                    const queryParams = window.location.hash.match(reg)  // 匹配路由參數
                    const { params } = props.match
                    Object.keys(params).forEach(key => { // 去除?參數
                        params[key] = params[key] && params[key].replace(reg, '')
                    })
                    props.match.params = { ...params }
                    const merge = { ...props, query: queryParams ? queryString.parse(queryParams[0]) : {} }
                    const View = views[each.component]
                    const wrapperView = ( // 包裝組件設置標籤頁標題
                        <DocumentTitle title={ title }>
                            <View { ...merge } />
                        </DocumentTitle>
                    )
                    return wrapperView
                } }
            />
        ) : this.generateRoute(each.routes, title), routes)
    }
}
const mapStateToProps = (state, owns) => ({
    user: prop('user', state),
    ...owns
})
export default connect(
    mapStateToProps
)(CRouter)
複製代碼

咱們的路由配置文件支持多級嵌套,遞歸注返回的Route路由也是嵌套的數組,最後須要藉助flatten將整個路由數組打平。

4.2 權限管理

權限管理分爲登陸校驗和權限校驗,默認咱們的應用路由都是須要登陸校驗的。

class CRouter extends Component {
  generateRoute (routes=[], title='React Admin') {
    // ...
    // 在上一個版本中直接將wrapperView返回,這個版本包裹了一層登陸校驗
    return this.requireLogin(wrapperView, each.auth)
  }
  requireLogin (component, permission) {
    const { user } = this.props
    const isLogin = user.username || false  // 登陸標識, 從redux取
    if(!isLogin) { // 判斷是否登陸
        return <Redirect to={'/front/login'} />
    }
    // 若是當前路由存在權限要求,則再進入全權限校驗
    return permission ? this.requirePermission(component, permission) : component
  }
  requirePermission (component, permission) {
    const permissions = path(['user', 'permission'], this.props) // 用戶權限, 從redux取
    if(!permissions || !this.checkPermission(permission, permissions)) return <Redirect to="/front/autherror" />
    return component
  }
  checkPermission (requirePers, userPers) {
    const isAdmin = path(['user', 'isAdmin'], this.props) // // 超管標識, 從redux取
    if(isAdmin) return true
    if(typeof userPers === 'undefined') return false
    if(Array.isArray(requirePers)) { // 路由權限爲數組
        return requirePers.every(each => userPers.includes(each))
    } else if(requirePers instanceof RegExp) { // 路由權限設置爲正則
        return userPers.some(each => requirePers.test(each))
    }
    return userPers.includes(requirePers)  // 路由權限設置爲字符串
  }
}
複製代碼

checkPermission函數中實現了字符串、數組和正則類型的校驗,所以,在咱們路由配置文件中的auth能夠支持字符串、數組和正則三種方式去設置。

如上,結合redux的應用,咱們輕鬆實現可配置、高可複用的路由鑑權功能。系統會根據當前用戶的權限列表(通常經過接口由後端返回)與配置文件中定義的權限要求進行校驗,若是無權限,則重定向至Permission Error頁面。

最後,本系列文章:

項目地址:github react-admin

系統預覽:react-admin system

相關文章
相關標籤/搜索