React路由鑒權(quán)的實現(xiàn)方法

前言
上一篇文章中有同學提到路由鑒權(quán),由于時間關(guān)系沒有寫,本文將針對這一特性對 vue 和 react 做專門說明,希望同學看了以后能夠受益匪淺,對你的項目能夠有所幫助,本文借鑒了很多大佬的文章篇幅也是比較長的。
背景
單獨項目中是希望根據(jù)登錄人來看下這個人是不是有權(quán)限進入當前頁面。雖然服務(wù)端做了進行接口的權(quán)限,但是每一個路由加載的時候都要去請求這個接口太浪費了。有時候是通過SESSIONID來校驗登陸權(quán)限的。
在正式開始 react 路由鑒權(quán)之前我們先看一下vue的路由鑒權(quán)是如何工作的:
一、vue之beforeEach路由鑒權(quán)
一般我們會相應(yīng)的把路由表角色菜單配置在后端,當用戶未通過頁面菜單,直接從地址欄訪問非權(quán)限范圍內(nèi)的url時,攔截用戶訪問并重定向到首頁。
vue 的初期是可以通過動態(tài)路由的方式,按照權(quán)限加載對應(yīng)的路由表 AddRouter ,但是由于權(quán)限交叉,導致權(quán)限路由表要做判斷結(jié)合,想想還是挺麻煩的,所以采用的是在 beforeEach 里面直判斷用非動態(tài)路由的方式
在使用 Vue的時候,框架提供了路由守衛(wèi)功能,用來在進入某個路有前進行一些校驗工作,如果校驗失敗,就跳轉(zhuǎn)到 404 或者登陸頁面,比如 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ǔ)公共頁面
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
},
{ // 系統(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)判斷
用戶登陸信息請求后端接口,返回菜單、權(quán)限、版權(quán)信息等公共信息,存入vuex。此處用到權(quán)限字段如下:
_userInfo: {
admin:false, // 是否超級管理員
dataServiceAdmin:true, // 是否數(shù)據(jù)服務(wù)管理員
topicAdmin:false // 是否主題管理員
}
- 判斷當前路由是否需要鑒權(quán)(router中meta字段下requireAuth是否為true),讓公共頁面直接放行;
- 判斷角色是超級管理員,直接放行;
- (本系統(tǒng)特殊邏輯)判斷跳轉(zhuǎn)路徑是主題設(shè)置但角色不為主題管理員,繼續(xù)判斷角色是否為數(shù)據(jù)服務(wù)管理員,跳轉(zhuǎn)數(shù)據(jù)服務(wù)設(shè)置頁or重定向(‘系統(tǒng)設(shè)置'菜單'/label/system-setting'默認重定向到'/label/system-setting/theme',其他菜單默認重定向的都是基礎(chǔ)公共頁面,故需要對這里的重定向鑒權(quán)。系統(tǒng)設(shè)置的權(quán)限不是主題管理員就一定是數(shù)據(jù)服務(wù)管理員,所以能這樣做);
- 判斷路由需求權(quán)限是否符合,若不符合直接重定向。
// 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、路由簡介
路由是干什么的?
根據(jù)不同的 url 地址展示不同的內(nèi)容或頁面。
單頁面應(yīng)用最大的特點就是只有一個 web 頁面。因而所有的頁面跳轉(zhuǎn)都需要通過javascript實現(xiàn)。當需要根據(jù)用戶操作展示不同的頁面時,我們就需要根據(jù)訪問路徑使用js控制頁面展示內(nèi)容。
2、React-router 簡介
React Router 是專為 React 設(shè)計的路由解決方案。它利用HTML5 的history API,來操作瀏覽器的 session history (會話歷史)。
3、使用
React Router被拆分成四個包:react-router,react-router-dom,react-router-native和react-router-config。react-router提供核心的路由組件與函數(shù)。react-router-config用來配置靜態(tài)路由(還在開發(fā)中),其余兩個則提供了運行環(huán)境(瀏覽器與react-native)所需的特定組件。
進行網(wǎng)站(將會運行在瀏覽器環(huán)境中)構(gòu)建,我們應(yīng)當安裝react-router-dom。因為react-router-dom已經(jīng)暴露出react-router中暴露的對象與方法,因此你只需要安裝并引用react-router-dom即可。
4、相關(guān)組件
4-1、
使用了 HTML5 的 history API (pushState, replaceState and the popstate event) 用于保證你的地址欄信息與界面保持一致。
主要屬性:
basename:設(shè)置根路徑
getUserConfirmation:獲取用戶確認的函數(shù)
forceRefresh:是否刷新整個頁面
keyLength:location.key的長度
children:子節(jié)點(單個)
4-2、
為舊版本瀏覽器開發(fā)的組件,通常簡易使用BrowserRouter。
4-3、
為項目提供聲明性的、可訪問的導航
主要屬性:
to:可以是一個字符串表示目標路徑,也可以是一個對象,包含四個屬性:
- pathname:表示指向的目標路徑
- search: 傳遞的搜索參數(shù)
- hash:路徑的hash值
- state: 地址狀態(tài)
replace:是否替換整個歷史棧
innerRef:訪問部件的底層引用
同時支持所有a標簽的屬性例如className,title等等
4-4、
React-router 中最重要的組件,最主要的職責就是根據(jù)匹配的路徑渲染指定的組件
主要屬性:
path:需要匹配的路徑
component:需要渲染的組件
render:渲染組件的函數(shù)
children :渲染組件的函數(shù),常用在path無法匹配時呈現(xiàn)的'空'狀態(tài)即所謂的默認顯示狀態(tài)
4-5、
重定向組件
主要屬性: to:指向的路徑
<Switch>
嵌套組件:唯一的渲染匹配路徑的第一個子 <Route> 或者 <Redirect>
三、react-router-config之路由鑒權(quán)
引言
在之前的版本中,React Router 也提供了類似的 onEnter 鉤子,但在 React Router 4.0 版本中,取消了這個方法。React Router 4.0 采用了聲明式的組件,路由即組件,要實現(xiàn)路由守衛(wèi)功能,就得我們自己去寫了。
1、react-router-config 是一個幫助我們配置靜態(tài)路由的小助手。其源碼就是一個高階函數(shù) 利用一個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è)這是我們設(shè)置的路由數(shù)組(這種寫法和vue很相似是不是?)
const routes = [
{ path: '/',
exact: true,
component: Home,
},
{
path: '/login',
component: Login,
},
{
path: '/user',
component: User,
},
{
path: '*',
component: NotFound
}
]
//app.js 那么我們在app.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
用過vue的小朋友都知道,vue的router.js 里面添加 meta: { requiresAuth: true }
然后利用 導航守衛(wèi)
router.beforeEach((to, from, next) => {
// 在每次路由進入之前判斷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
修改后的源碼增加了兩個參數(shù) authed 、 authPath 和一個屬性 route.requiresAuth
然后再來看一下最關(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 } }} />
很簡單 如果 route.requiresAuth = false 或者 authed = true 或者 route.path === authPath(參數(shù)默認值'/login')則渲染我們頁面,否則就渲染我們設(shè)置的 authPath 頁面,并記錄從哪個頁面跳轉(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)的頁面
},
{
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' // 默認未登錄的時候返回的頁面,可以自行設(shè)置
const App = () => (
<main>
<Switch>
{renderRoutes(routes, authed, authPath)}
</Switch>
</main>
)
export default App
//登陸之后返回原先要去的頁面login函數(shù)
login(){
const { from } = this.props.location.state || { from: { pathname: '/' } }
// authed = true // 這部分邏輯自己寫吧。。。
this.props.history.push(from.pathname)
}
到此 react-router-config 就結(jié)束了并完成了我們想要的效果
3、注意:
很多人會發(fā)現(xiàn),有時候達不到我們想要的效果,那么怎么辦呢,接著往下看
1、設(shè)計全局組建來管理是否登陸
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>
很多時候我們是可以通過監(jiān)聽路由變化實現(xiàn)的比如 getUserConfirmation 鉤子就是做這件事情的
const getConfirmation = (message, callback) => {
if (!isLogin) {
message.push('/login')
} else {
message.push(message.location.pathname)
}
接下來我們看一下 react-acl-router 又是怎么實現(xiàn)的
四、權(quá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)中非常核心的一個部分,一直以來因為業(yè)務(wù)方很多時候無法使用準確的術(shù)語來描述需求成為了困擾開發(fā)者們的一大難題。這里我們先來介紹兩種常見的權(quán)限管理設(shè)計模式,即基于角色的訪問控制以及訪問控制列表。
1、布局與路由
在討論具體的布局組件設(shè)計前,我們首先要解決一個更為基礎(chǔ)的問題,那就是如何將布局組件與應(yīng)用路由結(jié)合起來。
下面的這個例子是 react-router 官方提供的側(cè)邊欄菜單與路由結(jié)合的例子,筆者這里做了一些簡化:
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>
);
抽象為布局的思想,寫成簡單的偽代碼就是:
<Router>
<BasicLayout> // with sidebar
{routes.map(route => (
<Route {...route} />
))}
</BasicLayout>
</Router>
這樣的確是一種非常優(yōu)雅的解決方案,但它的局限性在于無法支持多種不同的布局。受限于一個 Router 只能包含一個子組件,即使我們將多個布局組件包裹在一個容器組件中,如:
<Router>
<div>
<BasicLayout> // with sidebar
{routes.map(route => (
<Route {...route} />
)}
</BasicLayout>
<FlexLayout> // with footer
{routes.map(route => (
<Route {...route} />
)}
</FlexLayout>
</div>
</Router>
路由在匹配到 FlexLayout 下的頁面時, BasicLayout 中的 sidebar 也會同時顯示出來,這顯然不是我們想要的結(jié)果。換個思路,我們可不可以將布局組件當做 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>
這里我們將不同的布局組件當做高階組件,相應(yīng)地包裹在了不同的頁面組件上,這樣就實現(xiàn)了對多種不同布局的支持。還有一點需要注意的是, react-router 默認會將 match 、 location 、 history 等路由信息傳遞給 Route 的下一級組件,由于在上述方案中, Route 的下一級組件并不是真正的頁面組件而是布局組件,因而我們需要在布局組件中手動將這些路由信息傳遞給頁面組件,或者統(tǒng)一改寫 Route 的 render 方法為:
<Route
render={props => ( // props contains match, location, history
<BasicLayout {...props}>
<PageComponent {...props} />
</BasicLayout>
)}
/>
另外一個可能會遇到的問題是, connected-react-router 并不會將路由中非常重要的 match 對象(包含當前路由的 params 等數(shù)據(jù) )同步到 redux store 中,所以我們一定要保證布局及頁面組件在路由部分就可以接收到 match 對象,否則在后續(xù)處理頁面頁眉等與當前路由參數(shù)相關(guān)的需求時就會變得非常麻煩。
2、頁眉 & 頁腳
解決了與應(yīng)用路由相結(jié)合的問題,具體到布局組件內(nèi)部,其中最重要的兩部分就是頁面的頁眉和頁腳部分,而頁眉又可以分為應(yīng)用頁眉與頁面頁眉兩部分。

應(yīng)用頁眉指的是整個應(yīng)用層面的頁眉,與具體的頁面無關(guān),一般來說會包含用戶頭像、通知欄、搜索框、多語言切換等這些應(yīng)用級別的信息與操作。頁面頁眉則一般來講會包含頁面標題、面包屑導航、頁面通用操作等與具體頁面相關(guān)的內(nèi)容。
在以往的項目中,尤其是在項目初期許多開發(fā)者因為對項目本身還沒有一個整體的認識,很多時候會傾向于將應(yīng)用頁眉做成一個展示型組件并在不同的頁面中直接調(diào)用。這樣做當然有其方便之處,比如說頁面與布局之間的數(shù)據(jù)同步環(huán)節(jié)就被省略掉了,每個頁面都可以直接向頁眉傳遞自己內(nèi)部的數(shù)據(jù)。
但從理想的項目架構(gòu)角度來講這樣做卻是一個 反模式(anti-pattern) 。因為應(yīng)用頁眉實際是一個應(yīng)用級別的組件,但按照上述做法的話卻變成了一個頁面級別的組件,偽代碼如下:
<App> <BasicLayout> <PageA> <AppHeader title="Page A" /> </PageA> </BasicLayout> <BasicLayout> <PageB> <AppHeader title="Page B" /> </PageB> </BasicLayout> </App>
從應(yīng)用數(shù)據(jù)流的角度來講也存在著同樣的問題,那就是應(yīng)用頁眉應(yīng)該是向不同的頁面去傳遞數(shù)據(jù)的,而不是反過來去接收來自頁面的數(shù)據(jù)。這導致應(yīng)用頁眉喪失了控制自己何時 rerender(重繪) 的機會,作為一個純展示型組件,一旦接收到的 props 發(fā)生變化頁眉就需要進行一次重繪。
另一方面,除了通用的應(yīng)用頁眉外,頁面頁眉與頁面路由之間是有著嚴格的一一對應(yīng)的關(guān)系的,那么我們能不能將頁面頁眉部分的配置也做到路由配置中去,以達到新增加一個頁面時只需要在 config/routes.js 中多配置一個路由對象就可以完成頁面頁眉部分的創(chuàng)建呢?理想情況下的偽代碼如下:
<App> <BasicLayout> // with app & page header already <PageA /> </BasicLayout> <BasicLayout> <PageB /> </BasicLayout> </App>
1、配置優(yōu)于代碼
在過去關(guān)于組件庫的討論中我們曾經(jīng)得出過代碼優(yōu)于配置的結(jié)論,即需要使用者自定義的部分,應(yīng)該盡量拋出回調(diào)函數(shù)讓使用者可以使用代碼去控制自定義的需求。這是因為組件作為極細粒度上的抽象,配置式的使用模式往往很難滿足使用者多變的需求。但在企業(yè)管理系統(tǒng)中,作為一個應(yīng)用級別的解決方案,能使用配置項解決的問題我們都應(yīng)該盡量避免讓使用者編寫代碼。
配置項(配置文件)天然就是一種集中式的管理模式,可以極大地降低應(yīng)用復雜度。以頁眉為例來說,如果我們每個頁面文件中都調(diào)用了頁眉組件,那么一旦頁眉組件出現(xiàn)問題我們就需要修改所有用到頁眉組件頁面的代碼。除去 debug 的情況外,哪怕只是修改一個頁面標題這樣簡單的需求,開發(fā)者也需要先找到這個頁面相對應(yīng)的文件,并在其 render 函數(shù)中進行修改。這些隱性成本都是我們在設(shè)計企業(yè)管理系統(tǒng)解決方案時需要注意的,因為就是這樣一個個的小細節(jié)造成了本身并不復雜的企業(yè)管理系統(tǒng)在維護、迭代了一段時間后應(yīng)用復雜度陡增。理想情況下,一個優(yōu)秀的企業(yè)管理系統(tǒng)解決方案應(yīng)該可以做到 80% 以上非功能性需求變更都可以使用修改配置文件的方式解決。
2、配置式頁眉

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ù)當前頁面的 pathname 使用 react-router-config 提供的 matchRoutes 方法來獲取到當前頁面 route 對象的所有配置項,也就意味著我們可以對所有的這些配置項做統(tǒng)一的處理。這不僅為處理通用邏輯帶來了方便,同時對于編寫頁面代碼的同事來說也是一種約束,能夠讓不同開發(fā)者寫出的代碼帶有更少的個人色彩,方便對于代碼庫的整體管理。
3、頁面標題
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、面包屑導航
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è)計策略
1、基于角色的訪問控制
基于角色的訪問控制不直接將系統(tǒng)操作的各種權(quán)限賦予具體用戶,而是在用戶與權(quán)限之間建立起角色集合,將權(quán)限賦予角色再將角色賦予用戶。這樣就實現(xiàn)了對于權(quán)限和角色的集中管理,避免用戶與權(quán)限之間直接產(chǎn)生復雜的多對多關(guān)系。
2、訪問控制列表
具體到角色與權(quán)限之間,訪問控制列表指代的是某個角色所擁有的系統(tǒng)權(quán)限列表。在傳統(tǒng)計算機科學中,權(quán)限一般指的是對于文件系統(tǒng)進行增刪改查的權(quán)力。而在 Web 應(yīng)用中,大部分系統(tǒng)只需要做到頁面級別的權(quán)限控制即可,簡單來說就是根據(jù)當前用戶的角色來決定其是否擁有查看當前頁面的權(quán)利。
下面就讓我們按照這樣的思路實現(xiàn)一個基礎(chǔ)版的包含權(quán)限管理功能的應(yīng)用路由。
4、實戰(zhàn)代碼
1、路由容器
在編寫權(quán)限管理相關(guān)的代碼前,我們需要先為所有的頁面路由找到一個合適的容器,即 react-router 中的 Switch 組件。與多個獨立路由不同的是,包裹在 Switch 中的路由每次只會渲染路徑匹配成功的第一個,而不是所有符合路徑匹配條件的路由。
<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>
以上面兩段代碼為例,如果當前頁面路徑是 /about 的話,因為 <About /> 、 <User /> 及 <NoMatch /> 這三個路由的路徑都符合 /about ,所以它們會同時被渲染在當前頁面。而將它們包裹在 Switch 中后, react-router 在找到第一個符合條件的 <About /> 路由后就會停止查找直接渲染 <About /> 組件。
在企業(yè)管理系統(tǒng)中因為頁面與頁面之間一般都是平行且排他的關(guān)系,所以利用好 Switch 這個特性對于我們簡化頁面渲染邏輯有著極大的幫助。
另外值得一提的是,在 react-router 作者 Ryan Florence 的新作@reach/router 中, Switch 的這一特性被默認包含了進去,而且 @reach/router 會自動匹配最符合當前路徑的路由。這就使得使用者不必再去擔心路由的書寫順序,感興趣的朋友可以關(guān)注一下。
2、權(quán)限管理
現(xiàn)在我們的路由已經(jīng)有了一個大體的框架,下面就讓我們?yōu)槠涮砑泳唧w的權(quán)限判斷邏輯。
對于一個應(yīng)用來說,除去需要鑒權(quán)的頁面外,一定還存在著不需要鑒權(quán)的頁面,讓我們先將這些頁面添加到我們的路由中,如登錄頁。
<Router>
<Switch>
<Route path="/login" component={Login}/>
</Switch>
</Router>
對于需要鑒權(quán)的路由,我們需要先抽象出一個判斷當前用戶是否有權(quán)限的函數(shù)來作為判斷依據(jù),而根據(jù)具體的需求,用戶可以擁有單個角色或多個角色,抑或更復雜的一個鑒權(quán)函數(shù)。這里筆者提供一個最基礎(chǔ)的版本,即我們將用戶的角色以字符串的形式存儲在后臺,如一個用戶的角色是 admin,另一個用戶的角色是 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)槊恳粋€需要鑒權(quán)的路由再添加一個屬性 permissions ,即哪些角色可以訪問該頁面。
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 都可以訪問門店列表頁面,但只有 admin 才可以訪問門店詳情頁面。

對于沒有權(quán)限查看當前頁面的情況,一般來講有兩種處理方式,一是直接重定向到另一個頁面(如首頁),二是渲染一個無權(quán)限頁面,提示用戶因為沒有當前頁面的權(quán)限所以無法查看。二者是排他的,即每個頁面只需要使用其中一種即可,于是我們在路由配置中可以根據(jù)需要去配置 redirect 或 unauthorized 屬性,分別對應(yīng) 無權(quán)限重定向 及 無權(quán)限顯示無權(quán)限頁面 兩種處理方式。具體代碼大家可以參考示例項目 react-acl-router 中的實現(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>
)}
/>
);
}
于是,在最終的路由中,我們會優(yōu)先匹配無需鑒權(quán)的頁面路徑,保證所有用戶在訪問無需鑒權(quán)的頁面時,第一時間就可以看到頁面。然后再去匹配需要鑒權(quán)的頁面路徑,最終如果所有的路徑都匹配不到的話,再渲染 404 頁面告知用戶當前頁面路徑不存在。

需要鑒權(quán)的路由和不需要鑒權(quán)的路由作為兩種不同的頁面,一般而言它們的頁面布局也是不同的。如登錄頁面使用的就是普通頁面布局:

在這里我們可以將不同的頁面布局與鑒權(quán)邏輯相結(jié)合以達到只需要在路由配置中配置相應(yīng)的屬性,新增加的頁面就可以同時獲得鑒權(quán)邏輯和基礎(chǔ)布局的效果。這將極大地提升開發(fā)者們的工作效率,尤其是對于項目組的新成員來說純配置的上手方式是最友好的。
5、應(yīng)用集成
至此一個包含基礎(chǔ)權(quán)限管理的應(yīng)用路由就大功告成了,我們可以將它抽象為一個獨立的路由組件,使用時只需要配置需要鑒權(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);
在實際項目中,我們可以使用 react-redux 提供的 connect 組件將應(yīng)用路由 connect 至 redux store,以方便我們直接讀取當前用戶的角色信息。一旦登錄用戶的角色發(fā)生變化,客戶端路由就可以進行相應(yīng)的判斷與響應(yīng)。
6、組合式開發(fā):權(quán)限管理
對于頁面級別的權(quán)限管理來說,權(quán)限管理部分的邏輯是獨立于頁面的,是與頁面中的具體內(nèi)容無關(guān)的。也就是說,權(quán)限管理部分的代碼并不應(yīng)該成為頁面中的一部分,而是應(yīng)該在拿到用戶權(quán)限后創(chuàng)建應(yīng)用路由時就將沒有權(quán)限的頁面替換為重定向或無權(quán)限頁面。
這樣一來,頁面部分的代碼就可以實現(xiàn)與權(quán)限管理邏輯的徹底解耦,以至于如果抽掉權(quán)限管理這一層后,頁面就變成了一個無需權(quán)限判斷的頁面依然可以獨立運行。而通用部分的權(quán)限管理代碼也可以在根據(jù)業(yè)務(wù)需求微調(diào)后服務(wù)于更多的項目。
7、小結(jié)
文中我們從權(quán)限管理的基礎(chǔ)設(shè)計思想講起,實現(xiàn)了一套基于角色的頁面級別的應(yīng)用權(quán)限管理系統(tǒng)并分別討論了無權(quán)限重定向及無權(quán)限顯示無權(quán)限頁面兩種無權(quán)限查看時的處理方法。
接下來我們來看一下多級菜單是如何實現(xiàn)的
五、菜單匹配邏輯
本節(jié)參考代碼:

在大部分企業(yè)管理系統(tǒng)中,頁面的基礎(chǔ)布局所采取的一般都是側(cè)邊欄菜單加頁面內(nèi)容這樣的組織形式。在成熟的組件庫支持下,UI 層面想要做出一個漂亮的側(cè)邊欄菜單并不困難,但因為在企業(yè)管理系統(tǒng)中菜單還承擔著頁面導航的功能,于是就導致了兩大難題,一是多級菜單如何處理,二是菜單項的子頁面(如點擊門店管理中的某一個門店進入的門店詳情頁在菜單中并沒有對應(yīng)的菜單項)如何高亮其隸屬于的父級菜單。
1、多級菜單
為了增強系統(tǒng)的可擴展性,企業(yè)管理系統(tǒng)中的菜單一般都需要提供多級支持,對應(yīng)的數(shù)據(jù)結(jié)構(gòu)就是在每一個菜單項中都要有 children 屬性來配置下一級菜單項。
const menuData = [{
name: '儀表盤',
icon: 'dashboard',
path: 'dashboard',
children: [{
name: '分析頁',
path: 'analysis',
children: [{
name: '實時數(shù)據(jù)',
path: 'realtime',
}, {
name: '離線數(shù)據(jù)',
path: 'offline',
}],
}],
}];
遞歸渲染父菜單及子菜單
想要支持多級菜單,首先要解決的問題就是如何統(tǒng)一不同級別菜單項的交互。
在大多數(shù)的情況下,每一個菜單項都代表著一個不同的頁面路徑,點擊后會觸發(fā) url 的變化并跳轉(zhuǎn)至相應(yīng)頁面,也就是上面配置中的 path 字段。

但對于一個父菜單來說,點擊還意味著打開或關(guān)閉相應(yīng)的子菜單,這就與點擊跳轉(zhuǎn)頁面發(fā)生了沖突。為了簡化這個問題,我們先統(tǒng)一菜單的交互為點擊父菜單(包含 children 屬性的菜單項)為打開或關(guān)閉子菜單,點擊子菜單(不包含 children 屬性的菜單項)為跳轉(zhuǎn)至相應(yīng)頁面。
首先,為了成功地渲染多級菜單,菜單的渲染函數(shù)是需要支持遞歸的,即如果當前菜單項含有 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>
);
})
)
這樣我們就擁有了一個支持多級展開、子菜單分別對應(yīng)頁面路由的側(cè)邊欄菜單。細心的朋友可能還發(fā)現(xiàn)了,雖然父菜單并不對應(yīng)一個具體的路由但在配置項中依然還有 path 這個屬性,這是為什么呢?
2、處理菜單高亮
在傳統(tǒng)的企業(yè)管理系統(tǒng)中,為不同的頁面配置頁面路徑是一件非常痛苦的事情,對于頁面路徑,許多開發(fā)者唯一的要求就是不重復即可,如上面的例子中,我們把菜單數(shù)據(jù)配置成這樣也是可以的。
const menuData = [{
name: '儀表盤',
icon: 'dashboard',
children: [{
name: '分析頁',
children: [{
name: '實時數(shù)據(jù)',
path: '/realtime',
}, {
name: '離線數(shù)據(jù)',
path: '/offline',
}],
}],
}];
<Router>
<Route path="/realtime" render={() => <div />}
<Route path="/offline" render={() => <div />}
</Router>
用戶在點擊菜單項時一樣可以正確地跳轉(zhuǎn)到相應(yīng)頁面。但這樣做的一個致命缺陷就是,對于 /realtime 這樣一個路由,如果只根據(jù)當前的 pathname 去匹配菜單項中 path 屬性的話,要怎樣才能同時也匹配到「分析頁」與「儀表盤」呢?因為如果匹配不到的話,「分析頁」和「儀表盤」就不會被高亮了。我們能不能在頁面的路徑中直接體現(xiàn)出菜單項之間的繼承關(guān)系呢?來看下面這個工具函數(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;
})
);
這個工具函數(shù)把菜單項中可能有的 children 字段考慮了進去,將一開始的菜單數(shù)據(jù)傳入就可以得到如下完整的菜單數(shù)據(jù)。
[{
name: '儀表盤',
icon: 'dashboard',
path: '/dashboard', // before is 'dashboard'
children: [{
name: '分析頁',
path: '/dashboard/analysis', // before is 'analysis'
children: [{
name: '實時數(shù)據(jù)',
path: '/dashboard/analysis/realtime', // before is 'realtime'
}, {
name: '離線數(shù)據(jù)',
path: '/dashboard/analysis/offline', // before is 'offline'
}],
}],
}];
然后讓我們再對當前頁面的路由做一下逆向推導,即假設(shè)當前頁面的路由為 /dashboard/analysis/realtime ,我們希望可以同時匹配到 ['/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 [];
};
上面的這個數(shù)組代表著不同級別的菜單項,將這三個值分別與菜單數(shù)據(jù)中的 path 屬性進行匹配就可以一次性地匹配到所有當前頁面應(yīng)當被高亮的菜單項了。
這里需要注意的是,雖然菜單項中的 path 一般都是普通字符串,但有些特殊的路由也可能是正則的形式,如 /outlets/:id 。所以我們在對二者進行匹配時,還需要引入 path-to-regexp 這個庫來處理類似 /outlets/1 和 /outlets/:id 這樣的路徑。又因為初始時菜單數(shù)據(jù)是樹形結(jié)構(gòu)的,不利于進行 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ù)的幫助下,多級菜單的高亮也不再是問題了。
3、知識點:記憶化(Memoization)
在側(cè)邊欄菜單中,有兩個重要的狀態(tài):一個是 selectedKeys ,即當前選定的菜單項;另一個是 openKeys ,即多個多級菜單的打開狀態(tài)。這二者的含義是不同的,因為在 selectedKeys 不變的情況下,用戶在打開或關(guān)閉其他多級菜單后, openKeys 是會發(fā)生變化的,如下面二圖所示, selectedKeys 相同但 openKeys 不同。

對于 selectedKeys 來說,由于它是由頁面路徑( pathname )決定的,所以每一次 pathname 發(fā)生變化都需要重新計算 selectedKeys 的值。又因為通過 pathname 以及最基礎(chǔ)的菜單數(shù)據(jù) menuData 去計算 selectedKeys 是一件非常昂貴的事情(要做許多數(shù)據(jù)格式處理和計算),有沒有什么辦法可以優(yōu)化一下這個過程呢?
Memoization 可以賦予普通函數(shù)記憶輸出結(jié)果的功能,它會在每次調(diào)用函數(shù)之前檢查傳入的參數(shù)是否與之前執(zhí)行過的參數(shù)完全相同,如果完全相同則直接返回上次計算過的結(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ù)當前 props 傳來的 pathname 及 menuData 計算出當前的 selectedKeys 并將其當做 openKeys 的初始值初始化組件內(nèi)部 state。因為 openKeys 是由用戶所控制的,所以對于后續(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>
這樣我們就實現(xiàn)了對于 selectedKeys 及 openKeys 的分別管理,開發(fā)者在使用側(cè)邊欄組件時只需要將應(yīng)用當前的頁面路徑同步到側(cè)邊欄組件中的 pathname 屬性即可,側(cè)邊欄組件會自動處理相應(yīng)的菜單高亮( selectedKeys )和多級菜單的打開與關(guān)閉( openKeys )。
4、知識點:正確區(qū)分 prop 與 state
上述這個場景也是一個非常經(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ù),組件無法從自身內(nèi)部獲得,所以就需要使用者通過 props 將其傳入進來。
另一方面, openKeys 作為組件內(nèi)部的 state,初始值可以由 pathname 計算而來,后續(xù)的更新則與組件外部的數(shù)據(jù)無關(guān)而是會根據(jù)用戶的操作在組件內(nèi)部完成,那么它就是一個 state,與其相關(guān)的所有邏輯都可以徹底地被封裝在組件內(nèi)部而不需要暴露給使用者。
簡而言之,一個數(shù)據(jù)如果想成為 prop 就必須是組件內(nèi)部無法獲得的,而且在它成為了 prop 之后,所有可以根據(jù)它的值推導出來的數(shù)據(jù)都不再需要成為另外的 props,否則將違背 React 單一數(shù)據(jù)源的原則。對于 state 來說也是同樣,如果一個數(shù)據(jù)想成為 state,那么它就不應(yīng)該再能夠被組件外部的值所改變,否則也會違背單一數(shù)據(jù)源的原則而導致組件的表現(xiàn)不可預測,產(chǎn)生難解的 bug。
5、組合式開發(fā):應(yīng)用菜單
嚴格來說,在這一小節(jié)中著重探討的應(yīng)用菜單部分的思路并不屬于組合式開發(fā)思想的范疇,更多地是如何寫出一個支持無限級子菜單及自動匹配當前路由的菜單組件。組件當然是可以隨意插拔的,但前提是應(yīng)用該組件的父級部分不依賴于組件所提供的信息。這也是我們在編寫組件時所應(yīng)當遵循的一個規(guī)范,即組件可以從外界獲取信息并在此基礎(chǔ)上進行組件內(nèi)部的邏輯判斷。但當組件向其外界拋出信息時,更多的時候應(yīng)該是以回調(diào)的形式讓調(diào)用者去主動觸發(fā),然后更新外部的數(shù)據(jù)再以 props 的形式傳遞給組件以達到更新組件的目的,而不是強制需要在外部再配置一個回調(diào)的接收函數(shù)去直接改變組件的內(nèi)部狀態(tài)。
從這點上來說,組合式開發(fā)與組件封裝其實是有著異曲同工之妙的,關(guān)鍵都在于對內(nèi)部狀態(tài)的嚴格控制。不論一個模塊或一個組件需要向外暴露多少接口,在它的內(nèi)部都應(yīng)該是解決了某一個或某幾個具體問題的。就像工廠產(chǎn)品生產(chǎn)流水線上的一個環(huán)節(jié),在經(jīng)過了這一環(huán)節(jié)后產(chǎn)品相較于進入前一定產(chǎn)生了某種區(qū)別,不論是增加了某些功能還是被打上某些標簽,產(chǎn)品一定會變得更利于下游合作者使用。更理想的情況則是即使刪除掉了這一環(huán)節(jié),原來這一環(huán)節(jié)的上下游依然可以無縫地銜接在一起繼續(xù)工作,這就是我們所說的模塊或者說組件的可插拔性。
六、后端路由服務(wù)的意義
在前后端分離架構(gòu)的背景下,前端已經(jīng)逐漸代替后端接管了所有固定路由的判斷與處理,但在動態(tài)路由這樣一個場景下,我們會發(fā)現(xiàn)單純前端路由服務(wù)的靈活度是遠遠不夠的。在用戶到達某個頁面后,可供下一步邏輯判斷的依據(jù)就只有當前頁面的 url,而根據(jù) url 后端的路由服務(wù)是可以返回非常豐富的數(shù)據(jù)的。
常見的例子如頁面的類型。假設(shè)應(yīng)用中營銷頁和互動頁的渲染邏輯并不相同,那么在頁面的 DSL 數(shù)據(jù)之外,我們就還需要獲取到頁面的類型以進行相應(yīng)的渲染。再比如頁面的 SEO 數(shù)據(jù),創(chuàng)建和更新時間等等,這些數(shù)據(jù)都對應(yīng)用能夠在前端靈活地展示頁面,處理業(yè)務(wù)邏輯有著巨大的幫助。
甚至我們還可以推而廣之,徹底拋棄掉由 react-router 等提供的前端路由服務(wù),轉(zhuǎn)而寫一套自己的路由分發(fā)器,即根據(jù)頁面類型的不同分別調(diào)用不同的頁面渲染服務(wù),以多種類型頁面的方式來組成一個完整的前端應(yīng)用。
七、組合式開發(fā)
為了解決大而全的方案在實踐中不夠靈活的問題,我們是不是可以將其中包含的各個模塊解耦后,獨立發(fā)布出來供開發(fā)者們按需取用呢?讓我們先來看一段理想中完整的企業(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>
);
在上面的這段偽代碼中,我們抽象出了多語言支持、基于路由的權(quán)限管理、登錄鑒權(quán)、基礎(chǔ)布局、側(cè)邊欄菜單等多個獨立模塊,可以根據(jù)需求添加或刪除任意一個模塊,而且添加或刪除任意一個模塊都不會對應(yīng)用的其他部分產(chǎn)生不可接受的副作用。這讓我們對接下來要做的事情有了一個大體的認識,但在具體的實踐中,如 props 如何傳遞、模塊之間如何共享數(shù)據(jù)、如何靈活地讓用戶自定義某些特殊邏輯等都仍然面臨著巨大的挑戰(zhàn)。我們需要時刻注意,在處理一個具體問題時哪些部分應(yīng)當放在某個獨立模塊內(nèi)部去處理,哪些部分應(yīng)當暴露出接口供使用者自定義,模塊與模塊之間如何做到零耦合以至于使用者可以隨意插拔任意一個模塊去適應(yīng)當前項目的需要。
八、學習路線
從一個具體的前端應(yīng)用直接切入開發(fā)技巧與理念的講解,所以對于剛?cè)腴T React 的朋友來說可能存在著一定的基礎(chǔ)知識部分梳理的缺失,這里為大家提供一份較為詳細的 React 開發(fā)者學習路線圖,希望能夠為剛?cè)腴T React 的朋友提供一條規(guī)范且便捷的學習之路。

總結(jié)
到此react的路由鑒權(quán)映梳理完了歡迎大家轉(zhuǎn)發(fā)交流分享 轉(zhuǎn)載請注明出處 ,附帶一個近期相關(guān)項目案例代碼給大家一個思路:
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
React利用路由實現(xiàn)登錄界面的跳轉(zhuǎn)
這篇文章主要介紹了React利用路由實現(xiàn)登錄界面的跳轉(zhuǎn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04
React中的useState和setState的執(zhí)行機制詳解
這篇文章主要介紹了React中的useState和setState的執(zhí)行機制,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03

