本系列文章計劃撰文3篇:javascript
項目地址:github react-adminjava
系統預覽:react-admin systemreact
上一篇咱們介紹了系統架構,這一篇將繼續介紹:webpack
側邊導航欄(Sidebar)咱們實現了根據配置渲染菜單項,如今咱們要繼續完善它的功能:導航高亮與鑑權。咱們經過redux
管理咱們Sidebar的狀態,想要redux
的store
可用,咱們必須使用它的<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()
的做用是將store
和component
鏈接起來。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
}
}
複製代碼
經處理後的路由配置數據新增了active
和key
兩個屬性: 後端
渲染側邊導航欄過程須要檢測高亮狀態:根據當前路由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
包裹後才能在組件內獲取路由相關信息。
這裏設定的場景是:用戶的登陸數據在當前會話期內有效(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') // 登陸重定向
}
}
複製代碼
上一篇中咱們實現了頁面級路由,如今咱們須要根據路由配置文件,註冊應用級路由。回顧下咱們的路由配置:
export default [
{
title: '個人事務', // 頁面標題&一級nav標題
icon: 'icon-home',
routes: [{
name: '待審批',
path: '/front/approval/undo',
component: 'ApprovalUndo'
}, {
name: '已處理',
path: '/front/approval/done',
auth: 'add',
component: 'ApprovalDone'
}]
}
]
複製代碼
咱們根據path
和component
信息註冊路由,根據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