成都創(chuàng)新互聯(lián)專注于企業(yè)營(yíng)銷型網(wǎng)站、網(wǎng)站重做改版、自貢網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5場(chǎng)景定制、商城網(wǎng)站建設(shè)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)公司、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為自貢等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
上一篇文章中有同學(xué)提到路由鑒權(quán),由于時(shí)間關(guān)系沒(méi)有寫,本文將針對(duì)這一特性對(duì) vue
和 react
做專門說(shuō)明,希望同學(xué)看了以后能夠受益匪淺,對(duì)你的項(xiàng)目能夠有所幫助,本文借鑒了很多大佬的文章篇幅也是比較長(zhǎng)的。
單獨(dú)項(xiàng)目中是希望根據(jù)登錄人來(lái)看下這個(gè)人是不是有權(quán)限進(jìn)入當(dāng)前頁(yè)面。雖然服務(wù)端做了進(jìn)行接口的權(quán)限,但是每一個(gè)路由加載的時(shí)候都要去請(qǐng)求這個(gè)接口太浪費(fèi)了。有時(shí)候是通過(guò)SESSIONID來(lái)校驗(yàn)登陸權(quán)限的。
在正式開始 react
路由鑒權(quán)之前我們先看一下vue的路由鑒權(quán)是如何工作的:
一般我們會(huì)相應(yīng)的把路由表角色菜單配置在后端,當(dāng)用戶未通過(guò)頁(yè)面菜單,直接從地址欄訪問(wèn)非權(quán)限范圍內(nèi)的url時(shí),攔截用戶訪問(wèn)并重定向到首頁(yè)。
vue
的初期是可以通過(guò)動(dòng)態(tài)路由的方式,按照權(quán)限加載對(duì)應(yīng)的路由表 AddRouter
,但是由于權(quán)限交叉,導(dǎo)致權(quán)限路由表要做判斷結(jié)合,想想還是挺麻煩的,所以采用的是在 beforeEach
里面直判斷用非動(dòng)態(tài)路由的方式
在使用 Vue的時(shí)候,框架提供了路由守衛(wèi)功能,用來(lái)在進(jìn)入某個(gè)路有前進(jìn)行一些校驗(yàn)工作,如果校驗(yàn)失敗,就跳轉(zhuǎn)到 404 或者登陸頁(yè)面,比如 Vue 中的 beforeEnter
函數(shù):
... 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: [ { // 基礎(chǔ)公共頁(yè)面 path: 'label-market', name: 'LabelMarket', component: () => import(/* webpackChunkName: "label-market" */ '@/components/page-layout/OneColLayout.vue'), redirect: { name: 'LabelMarketIndex' }, children: LabelMarket }, { // 個(gè)人中心 path: 'personal-center', name: 'PersonalCenter', redirect: '/label/personal-center/my-apply', component: () => import(/* webpackChunkName: "personal-center" */ '@/components/page-layout/TwoColLayout.vue'), children: PersonalCenter }, { // 系統(tǒng)設(shè)置 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: [ { // 數(shù)據(jù)服務(wù)審批 path: 'api', name: 'PersonalCenterMyApproveApi', meta: { requireAuth: true, authRole: 'dataServiceAdmin' }, component: () => import(/* webpackChunkName: "personal-center" */ '@/views/personal-center/api-approve/index.vue') }, ... ] } ]
export default [ ... { // 數(shù)據(jù)服務(wù)設(shè)置 path: 'api', name: 'SystemSettingApi', meta: { requireAuth: true, authRole: 'dataServiceAdmin' }, component: () => import(/* webpackChunkName: "system-setting" */ '@/views/system-setting/api/index.vue') }, { // 主題設(shè)置 path: 'theme', name: 'SystemSettingTheme', meta: { requireAuth: true, authRole: 'topicAdmin' }, component: () => import(/* webpackChunkName: "system-setting" */ '@/views/system-setting/theme/index.vue') }, ... ]
2、鑒權(quán)判斷
用戶登陸信息請(qǐng)求后端接口,返回菜單、權(quán)限、版權(quán)信息等公共信息,存入vuex。此處用到權(quán)限字段如下:
_userInfo: { admin:false, // 是否超級(jí)管理員 dataServiceAdmin:true, // 是否數(shù)據(jù)服務(wù)管理員 topicAdmin:false // 是否主題管理員 }
// 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、路由簡(jiǎn)介
路由是干什么的?
根據(jù)不同的 url 地址展示不同的內(nèi)容或頁(yè)面。
單頁(yè)面應(yīng)用最大的特點(diǎn)就是只有一個(gè) web 頁(yè)面。因而所有的頁(yè)面跳轉(zhuǎn)都需要通過(guò)javascript實(shí)現(xiàn)。當(dāng)需要根據(jù)用戶操作展示不同的頁(yè)面時(shí),我們就需要根據(jù)訪問(wèn)路徑使用js控制頁(yè)面展示內(nèi)容。
2、React-router 簡(jiǎn)介
React Router 是專為 React 設(shè)計(jì)的路由解決方案。它利用HTML5 的history API,來(lái)操作瀏覽器的 session history (會(huì)話歷史)。
3、使用
React Router被拆分成四個(gè)包:react-router,react-router-dom,react-router-native和react-router-config。react-router提供核心的路由組件與函數(shù)。react-router-config用來(lái)配置靜態(tài)路由(還在開發(fā)中),其余兩個(gè)則提供了運(yùn)行環(huán)境(瀏覽器與react-native)所需的特定組件。
進(jìn)行網(wǎng)站(將會(huì)運(yùn)行在瀏覽器環(huán)境中)構(gòu)建,我們應(yīng)當(dāng)安裝react-router-dom。因?yàn)閞eact-router-dom已經(jīng)暴露出react-router中暴露的對(duì)象與方法,因此你只需要安裝并引用react-router-dom即可。
4、相關(guān)組件
4-1、
使用了 HTML5 的 history API (pushState, replaceState and the popstate event) 用于保證你的地址欄信息與界面保持一致。
主要屬性:
basename:設(shè)置根路徑
getUserConfirmation:獲取用戶確認(rèn)的函數(shù)
forceRefresh:是否刷新整個(gè)頁(yè)面
keyLength:location.key的長(zhǎng)度
children:子節(jié)點(diǎn)(單個(gè))
4-2、
為舊版本瀏覽器開發(fā)的組件,通常簡(jiǎn)易使用BrowserRouter。
4-3、
為項(xiàng)目提供聲明性的、可訪問(wèn)的導(dǎo)航
主要屬性:
to:可以是一個(gè)字符串表示目標(biāo)路徑,也可以是一個(gè)對(duì)象,包含四個(gè)屬性:
replace:是否替換整個(gè)歷史棧
innerRef:訪問(wèn)部件的底層引用
同時(shí)支持所有a標(biāo)簽的屬性例如className,title等等
4-4、
React-router 中最重要的組件,最主要的職責(zé)就是根據(jù)匹配的路徑渲染指定的組件
主要屬性:
path:需要匹配的路徑
component:需要渲染的組件
render:渲染組件的函數(shù)
children :渲染組件的函數(shù),常用在path無(wú)法匹配時(shí)呈現(xiàn)的'空'狀態(tài)即所謂的默認(rèn)顯示狀態(tài)
4-5、
重定向組件
主要屬性: to:指向的路徑
<Switch>
嵌套組件:唯一的渲染匹配路徑的第一個(gè)子 <Route> 或者 <Redirect>
引言
在之前的版本中,React Router 也提供了類似的 onEnter
鉤子,但在 React Router 4.0 版本中,取消了這個(gè)方法。React Router 4.0 采用了聲明式的組件,路由即組件,要實(shí)現(xiàn)路由守衛(wèi)功能,就得我們自己去寫了。
1、react-router-config 是一個(gè)幫助我們配置靜態(tài)路由的小助手。其源碼就是一個(gè)高階函數(shù) 利用一個(gè)map函數(shù)生成靜態(tài)路由
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 假設(shè)這是我們?cè)O(shè)置的路由數(shù)組(這種寫法和vue很相似是不是?)
const routes = [ { path: '/', exact: true, component: Home, }, { path: '/login', component: Login, }, { path: '/user', component: User, }, { path: '*', component: NotFound } ]
//app.js 那么我們?cè)赼pp.js里這么使用就能幫我生成靜態(tài)的路由了
import { renderRoutes } from 'react-router-config' import routes from './router.js' const App = () => ( <main> <Switch> {renderRoutes(routes)} </Switch> </main> ) export default App
用過(guò)vue的小朋友都知道,vue的router.js 里面添加 meta: { requiresAuth: true }
然后利用 導(dǎo)航守衛(wèi)
router.beforeEach((to, from, next) => { // 在每次路由進(jìn)入之前判斷requiresAuth的值,如果是true的話呢就先判斷是否已登陸 })
2、基于類似vue的路由鑒權(quán)想法,我們稍稍改造一下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
修改后的源碼增加了兩個(gè)參數(shù) authed 、 authPath 和一個(gè)屬性 route.requiresAuth
然后再來(lái)看一下最關(guān)鍵的一段代碼
if (!route.requiresAuth || authed || route.path === authPath) { return <route.component {...props} {...extraProps} route={route} /> } return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />
很簡(jiǎn)單 如果 route.requiresAuth = false 或者 authed = true 或者 route.path === authPath(參數(shù)默認(rèn)值'/login')則渲染我們頁(yè)面,否則就渲染我們?cè)O(shè)置的 authPath 頁(yè)面,并記錄從哪個(gè)頁(yè)面跳轉(zhuǎn)。
相應(yīng)的router.js也要稍微修改一下
const routes = [ { path: '/', exact: true, component: Home, requiresAuth: false, }, { path: '/login', component: Login, requiresAuth: false, }, { path: '/user', component: User, requiresAuth: true, //需要登陸后才能跳轉(zhuǎn)的頁(yè)面 }, { 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修改該值(關(guān)于redux不在我們這篇文章的討論范圍之內(nèi)) const authPath = '/login' // 默認(rèn)未登錄的時(shí)候返回的頁(yè)面,可以自行設(shè)置 const App = () => ( <main> <Switch> {renderRoutes(routes, authed, authPath)} </Switch> </main> ) export default App
//登陸之后返回原先要去的頁(yè)面login函數(shù) login(){ const { from } = this.props.location.state || { from: { pathname: '/' } } // authed = true // 這部分邏輯自己寫吧。。。 this.props.history.push(from.pathname) }
到此 react-router-config
就結(jié)束了并完成了我們想要的效果
3、注意:
很多人會(huì)發(fā)現(xiàn),有時(shí)候達(dá)不到我們想要的效果,那么怎么辦呢,接著往下看
1、設(shè)計(jì)全局組建來(lái)管理是否登陸
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)
通過(guò)在主路由模塊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>
很多時(shí)候我們是可以通過(guò)監(jiān)聽路由變化實(shí)現(xiàn)的比如 getUserConfirmation
鉤子就是做這件事情的
const getConfirmation = (message, callback) => { if (!isLogin) { message.push('/login') } else { message.push(message.location.pathname) }
接下來(lái)我們看一下 react-acl-router
又是怎么實(shí)現(xiàn)的
本節(jié)參考代碼:
react-acl-router
react-boilerplate-pro/src/app/init/router.js
react-boilerplate-pro/src/app/config/routes.js
權(quán)限管理作為企業(yè)管理系統(tǒng)中非常核心的一個(gè)部分,一直以來(lái)因?yàn)闃I(yè)務(wù)方很多時(shí)候無(wú)法使用準(zhǔn)確的術(shù)語(yǔ)來(lái)描述需求成為了困擾開發(fā)者們的一大難題。這里我們先來(lái)介紹兩種常見的權(quán)限管理設(shè)計(jì)模式,即基于角色的訪問(wèn)控制以及訪問(wèn)控制列表。
1、布局與路由
在討論具體的布局組件設(shè)計(jì)前,我們首先要解決一個(gè)更為基礎(chǔ)的問(wèn)題,那就是如何將布局組件與應(yīng)用路由結(jié)合起來(lái)。
下面的這個(gè)例子是 react-router
官方提供的側(cè)邊欄菜單與路由結(jié)合的例子,筆者這里做了一些簡(jiǎn)化:
const SidebarExample = () => ( <Router> <div style={{ display: "flex" }}> <div style={{ padding: "10px", width: "40%", background: "#f0f0f0" }} > <ul style={{ listStyleType: "none", padding: 0 }}> <li> <Link to="/">Home</Link> </li> <li> <Link to="/bubblegum">Bubblegum</Link> </li> <li> <Link to="/shoelaces">Shoelaces</Link> </li> </ul> </div> <div style={{ flex: 1, padding: "10px" }}> {routes.map((route, index) => ( <Route key={index} path={route.path} exact={route.exact} component={route.main} /> ))} </div> </div> </Router> );
抽象為布局的思想,寫成簡(jiǎn)單的偽代碼就是:
<Router> <BasicLayout> // with sidebar {routes.map(route => ( <Route {...route} /> ))} </BasicLayout> </Router>
這樣的確是一種非常優(yōu)雅的解決方案,但它的局限性在于無(wú)法支持多種不同的布局。受限于一個(gè) Router
只能包含一個(gè)子組件,即使我們將多個(gè)布局組件包裹在一個(gè)容器組件中,如:
<Router> <div> <BasicLayout> // with sidebar {routes.map(route => ( <Route {...route} /> )} </BasicLayout> <FlexLayout> // with footer {routes.map(route => ( <Route {...route} /> )} </FlexLayout> </div> </Router>
路由在匹配到 FlexLayout
下的頁(yè)面時(shí), BasicLayout
中的 sidebar
也會(huì)同時(shí)顯示出來(lái),這顯然不是我們想要的結(jié)果。換個(gè)思路,我們可不可以將布局組件當(dāng)做 children
直接傳給更底層的 Route
組件呢?代碼如下:
<Router> <div> {basicLayoutRoutes.map(route => ( <Route {...route}> <BasicLayout component={route.component} /> </Route> ))} {flexLayoutRoutes.map(route => ( <Route {...route}> <FlexLayout component={route.component} /> </Route> ))} </div> </Router>
這里我們將不同的布局組件當(dāng)做高階組件,相應(yīng)地包裹在了不同的頁(yè)面組件上,這樣就實(shí)現(xiàn)了對(duì)多種不同布局的支持。還有一點(diǎn)需要注意的是, react-router
默認(rèn)會(huì)將 match
、 location
、 history
等路由信息傳遞給 Route
的下一級(jí)組件,由于在上述方案中, Route
的下一級(jí)組件并不是真正的頁(yè)面組件而是布局組件,因而我們需要在布局組件中手動(dòng)將這些路由信息傳遞給頁(yè)面組件,或者統(tǒng)一改寫 Route
的 render
方法為:
<Route render={props => ( // props contains match, location, history <BasicLayout {...props}> <PageComponent {...props} /> </BasicLayout> )} />
另外一個(gè)可能會(huì)遇到的問(wèn)題是, connected-react-router
并不會(huì)將路由中非常重要的 match
對(duì)象(包含當(dāng)前路由的 params
等數(shù)據(jù) )同步到 redux store 中,所以我們一定要保證布局及頁(yè)面組件在路由部分就可以接收到 match
對(duì)象,否則在后續(xù)處理頁(yè)面頁(yè)眉等與當(dāng)前路由參數(shù)相關(guān)的需求時(shí)就會(huì)變得非常麻煩。
2、頁(yè)眉 & 頁(yè)腳
解決了與應(yīng)用路由相結(jié)合的問(wèn)題,具體到布局組件內(nèi)部,其中最重要的兩部分就是頁(yè)面的頁(yè)眉和頁(yè)腳部分,而頁(yè)眉又可以分為應(yīng)用頁(yè)眉與頁(yè)面頁(yè)眉兩部分。
應(yīng)用頁(yè)眉指的是整個(gè)應(yīng)用層面的頁(yè)眉,與具體的頁(yè)面無(wú)關(guān),一般來(lái)說(shuō)會(huì)包含用戶頭像、通知欄、搜索框、多語(yǔ)言切換等這些應(yīng)用級(jí)別的信息與操作。頁(yè)面頁(yè)眉則一般來(lái)講會(huì)包含頁(yè)面標(biāo)題、面包屑導(dǎo)航、頁(yè)面通用操作等與具體頁(yè)面相關(guān)的內(nèi)容。
在以往的項(xiàng)目中,尤其是在項(xiàng)目初期許多開發(fā)者因?yàn)閷?duì)項(xiàng)目本身還沒(méi)有一個(gè)整體的認(rèn)識(shí),很多時(shí)候會(huì)傾向于將應(yīng)用頁(yè)眉做成一個(gè)展示型組件并在不同的頁(yè)面中直接調(diào)用。這樣做當(dāng)然有其方便之處,比如說(shuō)頁(yè)面與布局之間的數(shù)據(jù)同步環(huán)節(jié)就被省略掉了,每個(gè)頁(yè)面都可以直接向頁(yè)眉傳遞自己內(nèi)部的數(shù)據(jù)。
但從理想的項(xiàng)目架構(gòu)角度來(lái)講這樣做卻是一個(gè) 反模式(anti-pattern) 。因?yàn)閼?yīng)用頁(yè)眉實(shí)際是一個(gè)應(yīng)用級(jí)別的組件,但按照上述做法的話卻變成了一個(gè)頁(yè)面級(jí)別的組件,偽代碼如下:
<App> <BasicLayout> <PageA> <AppHeader title="Page A" /> </PageA> </BasicLayout> <BasicLayout> <PageB> <AppHeader title="Page B" /> </PageB> </BasicLayout> </App>
從應(yīng)用數(shù)據(jù)流的角度來(lái)講也存在著同樣的問(wèn)題,那就是應(yīng)用頁(yè)眉應(yīng)該是向不同的頁(yè)面去傳遞數(shù)據(jù)的,而不是反過(guò)來(lái)去接收來(lái)自頁(yè)面的數(shù)據(jù)。這導(dǎo)致應(yīng)用頁(yè)眉喪失了控制自己何時(shí) rerender(重繪) 的機(jī)會(huì),作為一個(gè)純展示型組件,一旦接收到的 props 發(fā)生變化頁(yè)眉就需要進(jìn)行一次重繪。
另一方面,除了通用的應(yīng)用頁(yè)眉外,頁(yè)面頁(yè)眉與頁(yè)面路由之間是有著嚴(yán)格的一一對(duì)應(yīng)的關(guān)系的,那么我們能不能將頁(yè)面頁(yè)眉部分的配置也做到路由配置中去,以達(dá)到新增加一個(gè)頁(yè)面時(shí)只需要在 config/routes.js
中多配置一個(gè)路由對(duì)象就可以完成頁(yè)面頁(yè)眉部分的創(chuàng)建呢?理想情況下的偽代碼如下:
<App> <BasicLayout> // with app & page header already <PageA /> </BasicLayout> <BasicLayout> <PageB /> </BasicLayout> </App>
1、配置優(yōu)于代碼
在過(guò)去關(guān)于組件庫(kù)的討論中我們?cè)?jīng)得出過(guò)代碼優(yōu)于配置的結(jié)論,即需要使用者自定義的部分,應(yīng)該盡量拋出回調(diào)函數(shù)讓使用者可以使用代碼去控制自定義的需求。這是因?yàn)榻M件作為極細(xì)粒度上的抽象,配置式的使用模式往往很難滿足使用者多變的需求。但在企業(yè)管理系統(tǒng)中,作為一個(gè)應(yīng)用級(jí)別的解決方案,能使用配置項(xiàng)解決的問(wèn)題我們都應(yīng)該盡量避免讓使用者編寫代碼。
配置項(xiàng)(配置文件)天然就是一種集中式的管理模式,可以極大地降低應(yīng)用復(fù)雜度。以頁(yè)眉為例來(lái)說(shuō),如果我們每個(gè)頁(yè)面文件中都調(diào)用了頁(yè)眉組件,那么一旦頁(yè)眉組件出現(xiàn)問(wèn)題我們就需要修改所有用到頁(yè)眉組件頁(yè)面的代碼。除去 debug 的情況外,哪怕只是修改一個(gè)頁(yè)面標(biāo)題這樣簡(jiǎn)單的需求,開發(fā)者也需要先找到這個(gè)頁(yè)面相對(duì)應(yīng)的文件,并在其 render
函數(shù)中進(jìn)行修改。這些隱性成本都是我們?cè)谠O(shè)計(jì)企業(yè)管理系統(tǒng)解決方案時(shí)需要注意的,因?yàn)榫褪沁@樣一個(gè)個(gè)的小細(xì)節(jié)造成了本身并不復(fù)雜的企業(yè)管理系統(tǒng)在維護(hù)、迭代了一段時(shí)間后應(yīng)用復(fù)雜度陡增。理想情況下,一個(gè)優(yōu)秀的企業(yè)管理系統(tǒng)解決方案應(yīng)該可以做到 80% 以上非功能性需求變更都可以使用修改配置文件的方式解決。
2、配置式頁(yè)眉
import { matchRoutes } from 'react-router-config'; // routes config const routes = [{ path: '/outlets', exact: true, permissions: ['admin', 'user'], component: Outlets, unauthorized: Unauthorized, pageTitle: '門店管理', breadcrumb: ['/outlets'], }, { path: '/outlets/:id', exact: true, permissions: ['admin', 'user'], component: OutletDetail, unauthorized: Unauthorized, pageTitle: '門店詳情', breadcrumb: ['/outlets', '/outlets/:id'], }]; // find current route object const pathname = get(state, 'router.location.pathname', ''); const { route } = head((matchRoutes(routes, pathname)));
基于這樣一種思路,我們可以在通用的布局組件中根據(jù)當(dāng)前頁(yè)面的 pathname
使用 react-router-config
提供的 matchRoutes
方法來(lái)獲取到當(dāng)前頁(yè)面 route
對(duì)象的所有配置項(xiàng),也就意味著我們可以對(duì)所有的這些配置項(xiàng)做統(tǒng)一的處理。這不僅為處理通用邏輯帶來(lái)了方便,同時(shí)對(duì)于編寫頁(yè)面代碼的同事來(lái)說(shuō)也是一種約束,能夠讓不同開發(fā)者寫出的代碼帶有更少的個(gè)人色彩,方便對(duì)于代碼庫(kù)的整體管理。
3、頁(yè)面標(biāo)題
renderPageHeader = () => { const { prefixCls, route: { pageTitle }, intl } = this.props; if (isEmpty(pageTitle)) { return null; } const pageTitleStr = intl.formatMessage({ id: pageTitle }); return ( <div className={`${prefixCls}-pageHeader`}> {this.renderBreadcrumb()} <div className={`${prefixCls}-pageTitle`}>{pageTitleStr}</div> </div> ); }
4、面包屑導(dǎo)航
renderBreadcrumb = () => { const { route: { breadcrumb }, intl, prefixCls } = this.props; const breadcrumbData = generateBreadcrumb(breadcrumb); return ( <Breadcrumb className={`${prefixCls}-breadcrumb`}> {map(breadcrumbData, (item, idx) => ( idx === breadcrumbData.length - 1 ? <Breadcrumb.Item key={item.href}> {intl.formatMessage({ id: item.text })} </Breadcrumb.Item> : <Breadcrumb.Item key={item.href}> <Link href={item.href} to={item.href}> {intl.formatMessage({ id: item.text })} </Link> </Breadcrumb.Item> ))} </Breadcrumb> ); }
3、設(shè)計(jì)策略
1、基于角色的訪問(wèn)控制
基于角色的訪問(wèn)控制不直接將系統(tǒng)操作的各種權(quán)限賦予具體用戶,而是在用戶與權(quán)限之間建立起角色集合,將權(quán)限賦予角色再將角色賦予用戶。這樣就實(shí)現(xiàn)了對(duì)于權(quán)限和角色的集中管理,避免用戶與權(quán)限之間直接產(chǎn)生復(fù)雜的多對(duì)多關(guān)系。
2、訪問(wèn)控制列表
具體到角色與權(quán)限之間,訪問(wèn)控制列表指代的是某個(gè)角色所擁有的系統(tǒng)權(quán)限列表。在傳統(tǒng)計(jì)算機(jī)科學(xué)中,權(quán)限一般指的是對(duì)于文件系統(tǒng)進(jìn)行增刪改查的權(quán)力。而在 Web 應(yīng)用中,大部分系統(tǒng)只需要做到頁(yè)面級(jí)別的權(quán)限控制即可,簡(jiǎn)單來(lái)說(shuō)就是根據(jù)當(dāng)前用戶的角色來(lái)決定其是否擁有查看當(dāng)前頁(yè)面的權(quán)利。
下面就讓我們按照這樣的思路實(shí)現(xiàn)一個(gè)基礎(chǔ)版的包含權(quán)限管理功能的應(yīng)用路由。
4、實(shí)戰(zhàn)代碼
1、路由容器
在編寫權(quán)限管理相關(guān)的代碼前,我們需要先為所有的頁(yè)面路由找到一個(gè)合適的容器,即 react-router
中的 Switch
組件。與多個(gè)獨(dú)立路由不同的是,包裹在 Switch
中的路由每次只會(huì)渲染路徑匹配成功的第一個(gè),而不是所有符合路徑匹配條件的路由。
<Router> <Route path="/about" component={About}/> <Route path="/:user" component={User}/> <Route component={NoMatch}/> </Router>
<Router> <Switch> <Route path="/about" component={About}/> <Route path="/:user" component={User}/> <Route component={NoMatch}/> </Switch> </Router>
以上面兩段代碼為例,如果當(dāng)前頁(yè)面路徑是 /about
的話,因?yàn)?<About />
、 <User />
及 <NoMatch />
這三個(gè)路由的路徑都符合 /about
,所以它們會(huì)同時(shí)被渲染在當(dāng)前頁(yè)面。而將它們包裹在 Switch
中后, react-router
在找到第一個(gè)符合條件的 <About />
路由后就會(huì)停止查找直接渲染 <About />
組件。
在企業(yè)管理系統(tǒng)中因?yàn)轫?yè)面與頁(yè)面之間一般都是平行且排他的關(guān)系,所以利用好 Switch
這個(gè)特性對(duì)于我們簡(jiǎn)化頁(yè)面渲染邏輯有著極大的幫助。
另外值得一提的是,在 react-router
作者 Ryan Florence 的新作@reach/router 中, Switch
的這一特性被默認(rèn)包含了進(jìn)去,而且 @reach/router
會(huì)自動(dòng)匹配最符合當(dāng)前路徑的路由。這就使得使用者不必再去擔(dān)心路由的書寫順序,感興趣的朋友可以關(guān)注一下。
2、權(quán)限管理
現(xiàn)在我們的路由已經(jīng)有了一個(gè)大體的框架,下面就讓我們?yōu)槠涮砑泳唧w的權(quán)限判斷邏輯。
對(duì)于一個(gè)應(yīng)用來(lái)說(shuō),除去需要鑒權(quán)的頁(yè)面外,一定還存在著不需要鑒權(quán)的頁(yè)面,讓我們先將這些頁(yè)面添加到我們的路由中,如登錄頁(yè)。
<Router> <Switch> <Route path="/login" component={Login}/> </Switch> </Router>
對(duì)于需要鑒權(quán)的路由,我們需要先抽象出一個(gè)判斷當(dāng)前用戶是否有權(quán)限的函數(shù)來(lái)作為判斷依據(jù),而根據(jù)具體的需求,用戶可以擁有單個(gè)角色或多個(gè)角色,抑或更復(fù)雜的一個(gè)鑒權(quán)函數(shù)。這里筆者提供一個(gè)最基礎(chǔ)的版本,即我們將用戶的角色以字符串的形式存儲(chǔ)在后臺(tái),如一個(gè)用戶的角色是 admin,另一個(gè)用戶的角色是 user。
import isEmpty from 'lodash/isEmpty'; import isArray from 'lodash/isArray'; import isString from 'lodash/isString'; import isFunction from 'lodash/isFunction'; import indexOf from 'lodash/indexOf'; const checkPermissions = (authorities, permissions) => { if (isEmpty(permissions)) { return true; } if (isArray(authorities)) { for (let i = 0; i < authorities.length; i += 1) { if (indexOf(permissions, authorities[i]) !== -1) { return true; } } return false; } if (isString(authorities)) { return indexOf(permissions, authorities) !== -1; } if (isFunction(authorities)) { return authorities(permissions); } throw new Error('[react-acl-router]: Unsupport type of authorities.'); }; export default checkPermissions;
在上面我們提到了路由的配置文件,這里我們?yōu)槊恳粋€(gè)需要鑒權(quán)的路由再添加一個(gè)屬性 permissions
,即哪些角色可以訪問(wèn)該頁(yè)面。
const routes = [{ path: '/outlets', exact: true, permissions: ['admin', 'user'], component: Outlets, unauthorized: Unauthorized, pageTitle: 'Outlet Management', breadcrumb: ['/outlets'], }, { path: '/outlets/:id', exact: true, permissions: ['admin'], component: OutletDetail, redirect: '/', pageTitle: 'Outlet Detail', breadcrumb: ['/outlets', '/outlets/:id'], }];
在上面的配置中,admin 和 user 都可以訪問(wèn)門店列表頁(yè)面,但只有 admin 才可以訪問(wèn)門店詳情頁(yè)面。
對(duì)于沒(méi)有權(quán)限查看當(dāng)前頁(yè)面的情況,一般來(lái)講有兩種處理方式,一是直接重定向到另一個(gè)頁(yè)面(如首頁(yè)),二是渲染一個(gè)無(wú)權(quán)限頁(yè)面,提示用戶因?yàn)闆](méi)有當(dāng)前頁(yè)面的權(quán)限所以無(wú)法查看。二者是排他的,即每個(gè)頁(yè)面只需要使用其中一種即可,于是我們?cè)诼酚膳渲弥锌梢愿鶕?jù)需要去配置 redirect
或 unauthorized
屬性,分別對(duì)應(yīng) 無(wú)權(quán)限重定向 及 無(wú)權(quán)限顯示無(wú)權(quán)限頁(yè)面 兩種處理方式。具體代碼大家可以參考示例項(xiàng)目 react-acl-router 中的實(shí)現(xiàn),這里摘錄一小段核心部分。
renderRedirectRoute = route => ( <Route key={route.path} {...omitRouteRenderProperties(route)} render={() => <Redirect to={route.redirect} />} /> ); renderAuthorizedRoute = (route) => { const { authorizedLayout: AuthorizedLayout } = this.props; const { authorities } = this.state; const { permissions, path, component: RouteComponent, unauthorized: Unauthorized, } = route; const hasPermission = checkPermissions(authorities, permissions); if (!hasPermission && route.unauthorized) { return ( <Route key={path} {...omitRouteRenderProperties(route)} render={props => ( <AuthorizedLayout {...props}> <Unauthorized {...props} /> </AuthorizedLayout> )} /> ); } if (!hasPermission && route.redirect) { return this.renderRedirectRoute(route); } return ( <Route key={path} {...omitRouteRenderProperties(route)} render={props => ( <AuthorizedLayout {...props}> <RouteComponent {...props} /> </AuthorizedLayout> )} /> ); }
于是,在最終的路由中,我們會(huì)優(yōu)先匹配無(wú)需鑒權(quán)的頁(yè)面路徑,保證所有用戶在訪問(wèn)無(wú)需鑒權(quán)的頁(yè)面時(shí),第一時(shí)間就可以看到頁(yè)面。然后再去匹配需要鑒權(quán)的頁(yè)面路徑,最終如果所有的路徑都匹配不到的話,再渲染 404 頁(yè)面告知用戶當(dāng)前頁(yè)面路徑不存在。
需要鑒權(quán)的路由和不需要鑒權(quán)的路由作為兩種不同的頁(yè)面,一般而言它們的頁(yè)面布局也是不同的。如登錄頁(yè)面使用的就是普通頁(yè)面布局:
在這里我們可以將不同的頁(yè)面布局與鑒權(quán)邏輯相結(jié)合以達(dá)到只需要在路由配置中配置相應(yīng)的屬性,新增加的頁(yè)面就可以同時(shí)獲得鑒權(quán)邏輯和基礎(chǔ)布局的效果。這將極大地提升開發(fā)者們的工作效率,尤其是對(duì)于項(xiàng)目組的新成員來(lái)說(shuō)純配置的上手方式是最友好的。
5、應(yīng)用集成
至此一個(gè)包含基礎(chǔ)權(quán)限管理的應(yīng)用路由就大功告成了,我們可以將它抽象為一個(gè)獨(dú)立的路由組件,使用時(shí)只需要配置需要鑒權(quán)的路由和不需要鑒權(quán)的路由兩部分即可。
const authorizedRoutes = [{ path: '/outlets', exact: true, permissions: ['admin', 'user'], component: Outlets, unauthorized: Unauthorized, pageTitle: 'pageTitle_outlets', breadcrumb: ['/outlets'], }, { path: '/outlets/:id', exact: true, permissions: ['admin', 'user'], component: OutletDetail, unauthorized: Unauthorized, pageTitle: 'pageTitle_outletDetail', breadcrumb: ['/outlets', '/outlets/:id'], }, { path: '/exception/403', exact: true, permissions: ['god'], component: WorkInProgress, unauthorized: Unauthorized, }]; const normalRoutes = [{ path: '/', exact: true, redirect: '/outlets', }, { path: '/login', exact: true, component: Login, }]; const Router = props => ( <ConnectedRouter history={props.history}> <MultiIntlProvider defaultLocale={locale} messageMap={messages} > // the router component <AclRouter authorities={props.user.authorities} authorizedRoutes={authorizedRoutes} authorizedLayout={BasicLayout} normalRoutes={normalRoutes} normalLayout={NormalLayout} notFound={NotFound} /> </MultiIntlProvider> </ConnectedRouter> ); const mapStateToProps = state => ({ user: state.app.user, }); Router.propTypes = propTypes; export default connect(mapStateToProps)(Router);
在實(shí)際項(xiàng)目中,我們可以使用 react-redux
提供的 connect
組件將應(yīng)用路由 connect 至 redux store,以方便我們直接讀取當(dāng)前用戶的角色信息。一旦登錄用戶的角色發(fā)生變化,客戶端路由就可以進(jìn)行相應(yīng)的判斷與響應(yīng)。
6、組合式開發(fā):權(quán)限管理
對(duì)于頁(yè)面級(jí)別的權(quán)限管理來(lái)說(shuō),權(quán)限管理部分的邏輯是獨(dú)立于頁(yè)面的,是與頁(yè)面中的具體內(nèi)容無(wú)關(guān)的。也就是說(shuō),權(quán)限管理部分的代碼并不應(yīng)該成為頁(yè)面中的一部分,而是應(yīng)該在拿到用戶權(quán)限后創(chuàng)建應(yīng)用路由時(shí)就將沒(méi)有權(quán)限的頁(yè)面替換為重定向或無(wú)權(quán)限頁(yè)面。
這樣一來(lái),頁(yè)面部分的代碼就可以實(shí)現(xiàn)與權(quán)限管理邏輯的徹底解耦,以至于如果抽掉權(quán)限管理這一層后,頁(yè)面就變成了一個(gè)無(wú)需權(quán)限判斷的頁(yè)面依然可以獨(dú)立運(yùn)行。而通用部分的權(quán)限管理代碼也可以在根據(jù)業(yè)務(wù)需求微調(diào)后服務(wù)于更多的項(xiàng)目。
7、小結(jié)
文中我們從權(quán)限管理的基礎(chǔ)設(shè)計(jì)思想講起,實(shí)現(xiàn)了一套基于角色的頁(yè)面級(jí)別的應(yīng)用權(quán)限管理系統(tǒng)并分別討論了無(wú)權(quán)限重定向及無(wú)權(quán)限顯示無(wú)權(quán)限頁(yè)面兩種無(wú)權(quán)限查看時(shí)的處理方法。
接下來(lái)我們來(lái)看一下多級(jí)菜單是如何實(shí)現(xiàn)的
本節(jié)參考代碼:
react-sider
在大部分企業(yè)管理系統(tǒng)中,頁(yè)面的基礎(chǔ)布局所采取的一般都是側(cè)邊欄菜單加頁(yè)面內(nèi)容這樣的組織形式。在成熟的組件庫(kù)支持下,UI 層面想要做出一個(gè)漂亮的側(cè)邊欄菜單并不困難,但因?yàn)樵谄髽I(yè)管理系統(tǒng)中菜單還承擔(dān)著頁(yè)面導(dǎo)航的功能,于是就導(dǎo)致了兩大難題,一是多級(jí)菜單如何處理,二是菜單項(xiàng)的子頁(yè)面(如點(diǎn)擊門店管理中的某一個(gè)門店進(jìn)入的門店詳情頁(yè)在菜單中并沒(méi)有對(duì)應(yīng)的菜單項(xiàng))如何高亮其隸屬于的父級(jí)菜單。
1、多級(jí)菜單
為了增強(qiáng)系統(tǒng)的可擴(kuò)展性,企業(yè)管理系統(tǒng)中的菜單一般都需要提供多級(jí)支持,對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)就是在每一個(gè)菜單項(xiàng)中都要有 children 屬性來(lái)配置下一級(jí)菜單項(xiàng)。
const menuData = [{ name: '儀表盤', icon: 'dashboard', path: 'dashboard', children: [{ name: '分析頁(yè)', path: 'analysis', children: [{ name: '實(shí)時(shí)數(shù)據(jù)', path: 'realtime', }, { name: '離線數(shù)據(jù)', path: 'offline', }], }], }];
遞歸渲染父菜單及子菜單
想要支持多級(jí)菜單,首先要解決的問(wèn)題就是如何統(tǒng)一不同級(jí)別菜單項(xiàng)的交互。
在大多數(shù)的情況下,每一個(gè)菜單項(xiàng)都代表著一個(gè)不同的頁(yè)面路徑,點(diǎn)擊后會(huì)觸發(fā) url 的變化并跳轉(zhuǎn)至相應(yīng)頁(yè)面,也就是上面配置中的 path 字段。
但對(duì)于一個(gè)父菜單來(lái)說(shuō),點(diǎn)擊還意味著打開或關(guān)閉相應(yīng)的子菜單,這就與點(diǎn)擊跳轉(zhuǎn)頁(yè)面發(fā)生了沖突。為了簡(jiǎn)化這個(gè)問(wèn)題,我們先統(tǒng)一菜單的交互為點(diǎn)擊父菜單(包含 children 屬性的菜單項(xiàng))為打開或關(guān)閉子菜單,點(diǎn)擊子菜單(不包含 children 屬性的菜單項(xiàng))為跳轉(zhuǎn)至相應(yīng)頁(yè)面。
首先,為了成功地渲染多級(jí)菜單,菜單的渲染函數(shù)是需要支持遞歸的,即如果當(dāng)前菜單項(xiàng)含有 children 屬性就將其渲染為父菜單并優(yōu)先渲染其 children 字段下的子菜單,這在算法上被叫做深度優(yōu)先遍歷。
renderMenu = data => ( map(data, (item) => { if (item.children) { return ( <SubMenu key={item.path} title={ <span> <Icon type={item.icon} /> <span>{item.name}</span> </span> } > {this.renderMenu(item.children)} </SubMenu> ); } return ( <Menu.Item key={item.path}> <Link to={item.path} href={item.path}> <Icon type={item.icon} /> <span>{item.name}</span> </Link> </Menu.Item> ); }) )
這樣我們就擁有了一個(gè)支持多級(jí)展開、子菜單分別對(duì)應(yīng)頁(yè)面路由的側(cè)邊欄菜單。細(xì)心的朋友可能還發(fā)現(xiàn)了,雖然父菜單并不對(duì)應(yīng)一個(gè)具體的路由但在配置項(xiàng)中依然還有 path 這個(gè)屬性,這是為什么呢?
2、處理菜單高亮
在傳統(tǒng)的企業(yè)管理系統(tǒng)中,為不同的頁(yè)面配置頁(yè)面路徑是一件非常痛苦的事情,對(duì)于頁(yè)面路徑,許多開發(fā)者唯一的要求就是不重復(fù)即可,如上面的例子中,我們把菜單數(shù)據(jù)配置成這樣也是可以的。
const menuData = [{ name: '儀表盤', icon: 'dashboard', children: [{ name: '分析頁(yè)', children: [{ name: '實(shí)時(shí)數(shù)據(jù)', path: '/realtime', }, { name: '離線數(shù)據(jù)', path: '/offline', }], }], }]; <Router> <Route path="/realtime" render={() => <div />} <Route path="/offline" render={() => <div />} </Router>
用戶在點(diǎn)擊菜單項(xiàng)時(shí)一樣可以正確地跳轉(zhuǎn)到相應(yīng)頁(yè)面。但這樣做的一個(gè)致命缺陷就是,對(duì)于 /realtime
這樣一個(gè)路由,如果只根據(jù)當(dāng)前的 pathname
去匹配菜單項(xiàng)中 path
屬性的話,要怎樣才能同時(shí)也匹配到「分析頁(yè)」與「儀表盤」呢?因?yàn)槿绻ヅ洳坏降脑?,「分析?yè)」和「儀表盤」就不會(huì)被高亮了。我們能不能在頁(yè)面的路徑中直接體現(xiàn)出菜單項(xiàng)之間的繼承關(guān)系呢?來(lái)看下面這個(gè)工具函數(shù)。
import map from 'lodash/map'; const formatMenuPath = (data, parentPath = '/') => ( map(data, (item) => { const result = { ...item, path: `${parentPath}${item.path}`, }; if (item.children) { result.children = formatMenuPath(item.children, `${parentPath}${item.path}/`); } return result; }) );
這個(gè)工具函數(shù)把菜單項(xiàng)中可能有的 children
字段考慮了進(jìn)去,將一開始的菜單數(shù)據(jù)傳入就可以得到如下完整的菜單數(shù)據(jù)。
[{ name: '儀表盤', icon: 'dashboard', path: '/dashboard', // before is 'dashboard' children: [{ name: '分析頁(yè)', path: '/dashboard/analysis', // before is 'analysis' children: [{ name: '實(shí)時(shí)數(shù)據(jù)', path: '/dashboard/analysis/realtime', // before is 'realtime' }, { name: '離線數(shù)據(jù)', path: '/dashboard/analysis/offline', // before is 'offline' }], }], }];
然后讓我們?cè)賹?duì)當(dāng)前頁(yè)面的路由做一下逆向推導(dǎo),即假設(shè)當(dāng)前頁(yè)面的路由為 /dashboard/analysis/realtime
,我們希望可以同時(shí)匹配到 ['/dashboard', '/dashboard/analysis', '/dashboard/analysis/realtime']
,方法如下:
import map from 'lodash/map'; const urlToList = (url) => { if (url) { const urlList = url.split('/').filter(i => i); return map(urlList, (urlItem, index) => `/${urlList.slice(0, index + 1).join('/')}`); } return []; };
上面的這個(gè)數(shù)組代表著不同級(jí)別的菜單項(xiàng),將這三個(gè)值分別與菜單數(shù)據(jù)中的 path
屬性進(jìn)行匹配就可以一次性地匹配到所有當(dāng)前頁(yè)面應(yīng)當(dāng)被高亮的菜單項(xiàng)了。
這里需要注意的是,雖然菜單項(xiàng)中的 path
一般都是普通字符串,但有些特殊的路由也可能是正則的形式,如 /outlets/:id
。所以我們?cè)趯?duì)二者進(jìn)行匹配時(shí),還需要引入 path-to-regexp
這個(gè)庫(kù)來(lái)處理類似 /outlets/1
和 /outlets/:id
這樣的路徑。又因?yàn)槌跏紩r(shí)菜單數(shù)據(jù)是樹形結(jié)構(gòu)的,不利于進(jìn)行 path
屬性的匹配,所以我們還需要先將樹形結(jié)構(gòu)的菜單數(shù)據(jù)扁平化,然后再傳入 getMeunMatchKeys
中。
import pathToRegexp from 'path-to-regexp'; import reduce from 'lodash/reduce'; import filter from 'lodash/filter'; const getFlatMenuKeys = menuData => ( reduce(menuData, (keys, item) => { keys.push(item.path); if (item.children) { return keys.concat(getFlatMenuKeys(item.children)); } return keys; }, []) ); const getMeunMatchKeys = (flatMenuKeys, paths) => reduce(paths, (matchKeys, path) => ( matchKeys.concat(filter(flatMenuKeys, item => pathToRegexp(item).test(path))) ), []);
在這些工具函數(shù)的幫助下,多級(jí)菜單的高亮也不再是問(wèn)題了。
3、知識(shí)點(diǎn):記憶化(Memoization)
在側(cè)邊欄菜單中,有兩個(gè)重要的狀態(tài):一個(gè)是 selectedKeys
,即當(dāng)前選定的菜單項(xiàng);另一個(gè)是 openKeys
,即多個(gè)多級(jí)菜單的打開狀態(tài)。這二者的含義是不同的,因?yàn)樵?selectedKeys
不變的情況下,用戶在打開或關(guān)閉其他多級(jí)菜單后, openKeys
是會(huì)發(fā)生變化的,如下面二圖所示, selectedKeys
相同但 openKeys
不同。
對(duì)于 selectedKeys
來(lái)說(shuō),由于它是由頁(yè)面路徑( pathname
)決定的,所以每一次 pathname
發(fā)生變化都需要重新計(jì)算 selectedKeys
的值。又因?yàn)橥ㄟ^(guò) pathname
以及最基礎(chǔ)的菜單數(shù)據(jù) menuData
去計(jì)算 selectedKeys
是一件非常昂貴的事情(要做許多數(shù)據(jù)格式處理和計(jì)算),有沒(méi)有什么辦法可以優(yōu)化一下這個(gè)過(guò)程呢?
Memoization 可以賦予普通函數(shù)記憶輸出結(jié)果的功能,它會(huì)在每次調(diào)用函數(shù)之前檢查傳入的參數(shù)是否與之前執(zhí)行過(guò)的參數(shù)完全相同,如果完全相同則直接返回上次計(jì)算過(guò)的結(jié)果,就像常用的緩存一樣。
import memoize from 'memoize-one'; constructor(props) { super(props); this.fullPathMenuData = memoize(menuData => formatMenuPath(menuData)); this.selectedKeys = memoize((pathname, fullPathMenu) => ( getMeunMatchKeys(getFlatMenuKeys(fullPathMenu), urlToList(pathname)) )); const { pathname, menuData } = props; this.state = { openKeys: this.selectedKeys(pathname, this.fullPathMenuData(menuData)), }; }
在組件的構(gòu)造器中我們可以根據(jù)當(dāng)前 props 傳來(lái)的 pathname
及 menuData
計(jì)算出當(dāng)前的 selectedKeys
并將其當(dāng)做 openKeys
的初始值初始化組件內(nèi)部 state。因?yàn)?openKeys
是由用戶所控制的,所以對(duì)于后續(xù) openKeys
值的更新我們只需要配置相應(yīng)的回調(diào)將其交給 Menu
組件控制即可。
import Menu from 'antd/lib/menu'; handleOpenChange = (openKeys) => { this.setState({ openKeys, }); }; <Menu style={{ padding: '16px 0', width: '100%' }} mode="inline" theme="dark" openKeys={openKeys} selectedKeys={this.selectedKeys(pathname, this.fullPathMenuData(menuData))} onOpenChange={this.handleOpenChange} > {this.renderMenu(this.fullPathMenuData(menuData))} </Menu>
這樣我們就實(shí)現(xiàn)了對(duì)于 selectedKeys
及 openKeys
的分別管理,開發(fā)者在使用側(cè)邊欄組件時(shí)只需要將應(yīng)用當(dāng)前的頁(yè)面路徑同步到側(cè)邊欄組件中的 pathname
屬性即可,側(cè)邊欄組件會(huì)自動(dòng)處理相應(yīng)的菜單高亮( selectedKeys
)和多級(jí)菜單的打開與關(guān)閉( openKeys
)。
4、知識(shí)點(diǎn):正確區(qū)分 prop 與 state
上述這個(gè)場(chǎng)景也是一個(gè)非常經(jīng)典的關(guān)于如何正確區(qū)分 prop 與 state 的例子。
selectedKeys
由傳入的 pathname
決定,于是我們就可以將 selectedKeys
與 pathname
之間的轉(zhuǎn)換關(guān)系封裝在組件中,使用者只需要傳入正確的 pathname
就可以獲得相應(yīng)的 selectedKeys
而不需要關(guān)心它們之間的轉(zhuǎn)換是如何完成的。而 pathname
作為組件渲染所需的基礎(chǔ)數(shù)據(jù),組件無(wú)法從自身內(nèi)部獲得,所以就需要使用者通過(guò) props 將其傳入進(jìn)來(lái)。
另一方面, openKeys
作為組件內(nèi)部的 state,初始值可以由 pathname
計(jì)算而來(lái),后續(xù)的更新則與組件外部的數(shù)據(jù)無(wú)關(guān)而是會(huì)根據(jù)用戶的操作在組件內(nèi)部完成,那么它就是一個(gè) state,與其相關(guān)的所有邏輯都可以徹底地被封裝在組件內(nèi)部而不需要暴露給使用者。
簡(jiǎn)而言之,一個(gè)數(shù)據(jù)如果想成為 prop 就必須是組件內(nèi)部無(wú)法獲得的,而且在它成為了 prop 之后,所有可以根據(jù)它的值推導(dǎo)出來(lái)的數(shù)據(jù)都不再需要成為另外的 props,否則將違背 React 單一數(shù)據(jù)源的原則。對(duì)于 state 來(lái)說(shuō)也是同樣,如果一個(gè)數(shù)據(jù)想成為 state,那么它就不應(yīng)該再能夠被組件外部的值所改變,否則也會(huì)違背單一數(shù)據(jù)源的原則而導(dǎo)致組件的表現(xiàn)不可預(yù)測(cè),產(chǎn)生難解的 bug。
5、組合式開發(fā):應(yīng)用菜單
嚴(yán)格來(lái)說(shuō),在這一小節(jié)中著重探討的應(yīng)用菜單部分的思路并不屬于組合式開發(fā)思想的范疇,更多地是如何寫出一個(gè)支持無(wú)限級(jí)子菜單及自動(dòng)匹配當(dāng)前路由的菜單組件。組件當(dāng)然是可以隨意插拔的,但前提是應(yīng)用該組件的父級(jí)部分不依賴于組件所提供的信息。這也是我們?cè)诰帉懡M件時(shí)所應(yīng)當(dāng)遵循的一個(gè)規(guī)范,即組件可以從外界獲取信息并在此基礎(chǔ)上進(jìn)行組件內(nèi)部的邏輯判斷。但當(dāng)組件向其外界拋出信息時(shí),更多的時(shí)候應(yīng)該是以回調(diào)的形式讓調(diào)用者去主動(dòng)觸發(fā),然后更新外部的數(shù)據(jù)再以 props 的形式傳遞給組件以達(dá)到更新組件的目的,而不是強(qiáng)制需要在外部再配置一個(gè)回調(diào)的接收函數(shù)去直接改變組件的內(nèi)部狀態(tài)。
從這點(diǎn)上來(lái)說(shuō),組合式開發(fā)與組件封裝其實(shí)是有著異曲同工之妙的,關(guān)鍵都在于對(duì)內(nèi)部狀態(tài)的嚴(yán)格控制。不論一個(gè)模塊或一個(gè)組件需要向外暴露多少接口,在它的內(nèi)部都應(yīng)該是解決了某一個(gè)或某幾個(gè)具體問(wèn)題的。就像工廠產(chǎn)品生產(chǎn)流水線上的一個(gè)環(huán)節(jié),在經(jīng)過(guò)了這一環(huán)節(jié)后產(chǎn)品相較于進(jìn)入前一定產(chǎn)生了某種區(qū)別,不論是增加了某些功能還是被打上某些標(biāo)簽,產(chǎn)品一定會(huì)變得更利于下游合作者使用。更理想的情況則是即使刪除掉了這一環(huán)節(jié),原來(lái)這一環(huán)節(jié)的上下游依然可以無(wú)縫地銜接在一起繼續(xù)工作,這就是我們所說(shuō)的模塊或者說(shuō)組件的可插拔性。
在前后端分離架構(gòu)的背景下,前端已經(jīng)逐漸代替后端接管了所有固定路由的判斷與處理,但在動(dòng)態(tài)路由這樣一個(gè)場(chǎng)景下,我們會(huì)發(fā)現(xiàn)單純前端路由服務(wù)的靈活度是遠(yuǎn)遠(yuǎn)不夠的。在用戶到達(dá)某個(gè)頁(yè)面后,可供下一步邏輯判斷的依據(jù)就只有當(dāng)前頁(yè)面的 url,而根據(jù) url 后端的路由服務(wù)是可以返回非常豐富的數(shù)據(jù)的。
常見的例子如頁(yè)面的類型。假設(shè)應(yīng)用中營(yíng)銷頁(yè)和互動(dòng)頁(yè)的渲染邏輯并不相同,那么在頁(yè)面的 DSL 數(shù)據(jù)之外,我們就還需要獲取到頁(yè)面的類型以進(jìn)行相應(yīng)的渲染。再比如頁(yè)面的 SEO 數(shù)據(jù),創(chuàng)建和更新時(shí)間等等,這些數(shù)據(jù)都對(duì)應(yīng)用能夠在前端靈活地展示頁(yè)面,處理業(yè)務(wù)邏輯有著巨大的幫助。
甚至我們還可以推而廣之,徹底拋棄掉由 react-router 等提供的前端路由服務(wù),轉(zhuǎn)而寫一套自己的路由分發(fā)器,即根據(jù)頁(yè)面類型的不同分別調(diào)用不同的頁(yè)面渲染服務(wù),以多種類型頁(yè)面的方式來(lái)組成一個(gè)完整的前端應(yīng)用。
為了解決大而全的方案在實(shí)踐中不夠靈活的問(wèn)題,我們是不是可以將其中包含的各個(gè)模塊解耦后,獨(dú)立發(fā)布出來(lái)供開發(fā)者們按需取用呢?讓我們先來(lái)看一段理想中完整的企業(yè)管理系統(tǒng)應(yīng)用架構(gòu)部分的偽代碼:
const App = props => ( <Provider> // react-redux bind <ConnectedRouter> // react-router-redux bind <MultiIntlProvider> // intl support <AclRouter> // router with access control list <Route path="/login"> // route that doesn't need authentication <NormalLayout> // layout component <View /> // page content (view component) </NormalLayout> <Route path="/login"> ... // more routes that don't need authentication <Route path="/analysis"> // route that needs authentication <LoginChecker> // hoc for user login check <BasicLayout> // layout component <SiderMenu /> // sider menu <Content> <PageHeader /> // page header <View /> // page content (view component) <PageFooter /> // page footer </Content> </BasicLayout> </LoginChecker> </Route> ... // more routes that need authentication <Route render={() => <div>404</div>} /> // 404 page </AclRouter> </MultiIntlProvider> </ConnectedRouter> </Provider> );
在上面的這段偽代碼中,我們抽象出了多語(yǔ)言支持、基于路由的權(quán)限管理、登錄鑒權(quán)、基礎(chǔ)布局、側(cè)邊欄菜單等多個(gè)獨(dú)立模塊,可以根據(jù)需求添加或刪除任意一個(gè)模塊,而且添加或刪除任意一個(gè)模塊都不會(huì)對(duì)應(yīng)用的其他部分產(chǎn)生不可接受的副作用。這讓我們對(duì)接下來(lái)要做的事情有了一個(gè)大體的認(rèn)識(shí),但在具體的實(shí)踐中,如 props 如何傳遞、模塊之間如何共享數(shù)據(jù)、如何靈活地讓用戶自定義某些特殊邏輯等都仍然面臨著巨大的挑戰(zhàn)。我們需要時(shí)刻注意,在處理一個(gè)具體問(wèn)題時(shí)哪些部分應(yīng)當(dāng)放在某個(gè)獨(dú)立模塊內(nèi)部去處理,哪些部分應(yīng)當(dāng)暴露出接口供使用者自定義,模塊與模塊之間如何做到零耦合以至于使用者可以隨意插拔任意一個(gè)模塊去適應(yīng)當(dāng)前項(xiàng)目的需要。
從一個(gè)具體的前端應(yīng)用直接切入開發(fā)技巧與理念的講解,所以對(duì)于剛?cè)腴T React 的朋友來(lái)說(shuō)可能存在著一定的基礎(chǔ)知識(shí)部分梳理的缺失,這里為大家提供一份較為詳細(xì)的 React 開發(fā)者學(xué)習(xí)路線圖,希望能夠?yàn)閯側(cè)腴T React 的朋友提供一條規(guī)范且便捷的學(xué)習(xí)之路。
到此react的路由鑒權(quán)映梳理完了歡迎大家轉(zhuǎn)發(fā)交流分享 轉(zhuǎn)載請(qǐng)注明出處 ,附帶一個(gè)近期相關(guān)項(xiàng)目案例代碼給大家一個(gè)思路:
react-router-config
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。
網(wǎng)站題目:React路由鑒權(quán)的實(shí)現(xiàn)方法
文章網(wǎng)址:http://jinyejixie.com/article46/ppiohg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站收錄、云服務(wù)器、企業(yè)網(wǎng)站制作、網(wǎng)站導(dǎo)航、網(wǎng)站設(shè)計(jì)公司、網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)