React路由鑑權的實現方法

這篇文章主要介紹了React路由鑑權的實現方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨着小編來一起學習學習吧

前言

上一篇文章中有同學提到路由鑑權,由於時間關係沒有寫,本文將針對這一特性對 vuereact 做專門說明,希望同學看了以後能夠受益匪淺,對你的項目能夠有所幫助,本文借鑑了很多大佬的文章篇幅也是比較長的。

背景

單獨項目中是希望根據登錄人來看下這個人是不是有權限進入當前頁面。雖然服務端做了進行接口的權限,但是每一個路由加載的時候都要去請求這個接口太浪費了。有時候是通過SESSIONID來校驗登陸權限的。

在正式開始 react 路由鑑權之前我們先看一下vue的路由鑑權是如何工作的:

一、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:指向的路徑

<Switch>

嵌套組件:唯一的渲染匹配路徑的第一個子 <Route> 或者 <Redirect>

三、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)
 }

接下來我們看一下 react-acl-router 又是怎麼實現的

四、權限管理機制

本節參考代碼:

react-acl-router
react-boilerplate-pro/src/app/init/router.js
react-boilerplate-pro/src/app/config/routes.js

權限管理作爲企業管理系統中非常核心的一個部分,一直以來因爲業務方很多時候無法使用準確的術語來描述需求成爲了困擾開發者們的一大難題。這裏我們先來介紹兩種常見的權限管理設計模式,即基於角色的訪問控制以及訪問控制列表。

1、佈局與路由

在討論具體的佈局組件設計前,我們首先要解決一個更爲基礎的問題,那就是如何將佈局組件與應用路由結合起來。

下面的這個例子是 react-router 官方提供的側邊欄菜單與路由結合的例子,筆者這裏做了一些簡化:

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>

這樣的確是一種非常優雅的解決方案,但它的侷限性在於無法支持多種不同的佈局。受限於一個 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 也會同時顯示出來,這顯然不是我們想要的結果。換個思路,我們可不可以將佈局組件當做 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>

這裏我們將不同的佈局組件當做高階組件,相應地包裹在了不同的頁面組件上,這樣就實現了對多種不同佈局的支持。還有一點需要注意的是, react-router 默認會將 matchlocationhistory 等路由信息傳遞給 Route 的下一級組件,由於在上述方案中, Route 的下一級組件並不是真正的頁面組件而是佈局組件,因而我們需要在佈局組件中手動將這些路由信息傳遞給頁面組件,或者統一改寫 Routerender 方法爲:

<Route
 render={props => (     // props contains match, location, history
 <BasicLayout {...props}>   
  <PageComponent {...props} />
 </BasicLayout>
 )}
/>

另外一個可能會遇到的問題是, connected-react-router 並不會將路由中非常重要的 match 對象(包含當前路由的 params 等數據 )同步到 redux store 中,所以我們一定要保證佈局及頁面組件在路由部分就可以接收到 match 對象,否則在後續處理頁面頁眉等與當前路由參數相關的需求時就會變得非常麻煩。

2、頁眉 & 頁腳

解決了與應用路由相結合的問題,具體到佈局組件內部,其中最重要的兩部分就是頁面的頁眉和頁腳部分,而頁眉又可以分爲應用頁眉與頁面頁眉兩部分。

應用頁眉指的是整個應用層面的頁眉,與具體的頁面無關,一般來說會包含用戶頭像、通知欄、搜索框、多語言切換等這些應用級別的信息與操作。頁面頁眉則一般來講會包含頁面標題、麪包屑導航、頁面通用操作等與具體頁面相關的內容。

在以往的項目中,尤其是在項目初期許多開發者因爲對項目本身還沒有一個整體的認識,很多時候會傾向於將應用頁眉做成一個展示型組件並在不同的頁面中直接調用。這樣做當然有其方便之處,比如說頁面與佈局之間的數據同步環節就被省略掉了,每個頁面都可以直接向頁眉傳遞自己內部的數據。

但從理想的項目架構角度來講這樣做卻是一個 反模式(anti-pattern) 。因爲應用頁眉實際是一個應用級別的組件,但按照上述做法的話卻變成了一個頁面級別的組件,僞代碼如下:

<App>
 <BasicLayout>
 <PageA>
  <AppHeader title="Page A" />
 </PageA>
 </BasicLayout>
 <BasicLayout>
 <PageB>
  <AppHeader title="Page B" />
 </PageB>
 </BasicLayout>
</App>

從應用數據流的角度來講也存在着同樣的問題,那就是應用頁眉應該是向不同的頁面去傳遞數據的,而不是反過來去接收來自頁面的數據。這導致應用頁眉喪失了控制自己何時 rerender(重繪) 的機會,作爲一個純展示型組件,一旦接收到的 props 發生變化頁眉就需要進行一次重繪。

另一方面,除了通用的應用頁眉外,頁面頁眉與頁面路由之間是有着嚴格的一一對應的關係的,那麼我們能不能將頁面頁眉部分的配置也做到路由配置中去,以達到新增加一個頁面時只需要在 config/routes.js 中多配置一個路由對象就可以完成頁面頁眉部分的創建呢?理想情況下的僞代碼如下:

<App>
 <BasicLayout>     // with app & page header already
 <PageA />
 </BasicLayout>
 <BasicLayout>
 <PageB />
 </BasicLayout>
</App>

1、配置優於代碼

在過去關於組件庫的討論中我們曾經得出過代碼優於配置的結論,即需要使用者自定義的部分,應該儘量拋出回調函數讓使用者可以使用代碼去控制自定義的需求。這是因爲組件作爲極細粒度上的抽象,配置式的使用模式往往很難滿足使用者多變的需求。但在企業管理系統中,作爲一個應用級別的解決方案,能使用配置項解決的問題我們都應該儘量避免讓使用者編寫代碼。

配置項(配置文件)天然就是一種集中式的管理模式,可以極大地降低應用複雜度。以頁眉爲例來說,如果我們每個頁面文件中都調用了頁眉組件,那麼一旦頁眉組件出現問題我們就需要修改所有用到頁眉組件頁面的代碼。除去 debug 的情況外,哪怕只是修改一個頁面標題這樣簡單的需求,開發者也需要先找到這個頁面相對應的文件,並在其 render 函數中進行修改。這些隱性成本都是我們在設計企業管理系統解決方案時需要注意的,因爲就是這樣一個個的小細節造成了本身並不複雜的企業管理系統在維護、迭代了一段時間後應用複雜度陡增。理想情況下,一個優秀的企業管理系統解決方案應該可以做到 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)));

基於這樣一種思路,我們可以在通用的佈局組件中根據當前頁面的 pathname 使用 react-router-config 提供的 matchRoutes 方法來獲取到當前頁面 route 對象的所有配置項,也就意味着我們可以對所有的這些配置項做統一的處理。這不僅爲處理通用邏輯帶來了方便,同時對於編寫頁面代碼的同事來說也是一種約束,能夠讓不同開發者寫出的代碼帶有更少的個人色彩,方便對於代碼庫的整體管理。

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、設計策略

1、基於角色的訪問控制

基於角色的訪問控制不直接將系統操作的各種權限賦予具體用戶,而是在用戶與權限之間建立起角色集合,將權限賦予角色再將角色賦予用戶。這樣就實現了對於權限和角色的集中管理,避免用戶與權限之間直接產生複雜的多對多關係。

2、訪問控制列表

具體到角色與權限之間,訪問控制列表指代的是某個角色所擁有的系統權限列表。在傳統計算機科學中,權限一般指的是對於文件系統進行增刪改查的權力。而在 Web 應用中,大部分系統只需要做到頁面級別的權限控制即可,簡單來說就是根據當前用戶的角色來決定其是否擁有查看當前頁面的權利。

下面就讓我們按照這樣的思路實現一個基礎版的包含權限管理功能的應用路由。

4、實戰代碼

1、路由容器

在編寫權限管理相關的代碼前,我們需要先爲所有的頁面路由找到一個合適的容器,即 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 /> 組件。

在企業管理系統中因爲頁面與頁面之間一般都是平行且排他的關係,所以利用好 Switch 這個特性對於我們簡化頁面渲染邏輯有着極大的幫助。

另外值得一提的是,在 react-router 作者 Ryan Florence 的新作@reach/router 中, Switch 的這一特性被默認包含了進去,而且 @reach/router 會自動匹配最符合當前路徑的路由。這就使得使用者不必再去擔心路由的書寫順序,感興趣的朋友可以關注一下。

2、權限管理

現在我們的路由已經有了一個大體的框架,下面就讓我們爲其添加具體的權限判斷邏輯。

對於一個應用來說,除去需要鑑權的頁面外,一定還存在着不需要鑑權的頁面,讓我們先將這些頁面添加到我們的路由中,如登錄頁。

<Router>
 <Switch>
  <Route path="/login" component={Login}/>
 </Switch>
</Router>

對於需要鑑權的路由,我們需要先抽象出一個判斷當前用戶是否有權限的函數來作爲判斷依據,而根據具體的需求,用戶可以擁有單個角色或多個角色,抑或更復雜的一個鑑權函數。這裏筆者提供一個最基礎的版本,即我們將用戶的角色以字符串的形式存儲在後臺,如一個用戶的角色是 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;

在上面我們提到了路由的配置文件,這裏我們爲每一個需要鑑權的路由再添加一個屬性 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 纔可以訪問門店詳情頁面。

對於沒有權限查看當前頁面的情況,一般來講有兩種處理方式,一是直接重定向到另一個頁面(如首頁),二是渲染一個無權限頁面,提示用戶因爲沒有當前頁面的權限所以無法查看。二者是排他的,即每個頁面只需要使用其中一種即可,於是我們在路由配置中可以根據需要去配置 redirectunauthorized 屬性,分別對應 無權限重定向無權限顯示無權限頁面 兩種處理方式。具體代碼大家可以參考示例項目 react-acl-router 中的實現,這裏摘錄一小段核心部分。

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>
   )}
  />
 );
}

於是,在最終的路由中,我們會優先匹配無需鑑權的頁面路徑,保證所有用戶在訪問無需鑑權的頁面時,第一時間就可以看到頁面。然後再去匹配需要鑑權的頁面路徑,最終如果所有的路徑都匹配不到的話,再渲染 404 頁面告知用戶當前頁面路徑不存在。

需要鑑權的路由和不需要鑑權的路由作爲兩種不同的頁面,一般而言它們的頁面佈局也是不同的。如登錄頁面使用的就是普通頁面佈局:

在這裏我們可以將不同的頁面佈局與鑑權邏輯相結合以達到只需要在路由配置中配置相應的屬性,新增加的頁面就可以同時獲得鑑權邏輯和基礎佈局的效果。這將極大地提升開發者們的工作效率,尤其是對於項目組的新成員來說純配置的上手方式是最友好的。

5、應用集成

至此一個包含基礎權限管理的應用路由就大功告成了,我們可以將它抽象爲一個獨立的路由組件,使用時只需要配置需要鑑權的路由和不需要鑑權的路由兩部分即可。

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 組件將應用路由 connect 至 redux store,以方便我們直接讀取當前用戶的角色信息。一旦登錄用戶的角色發生變化,客戶端路由就可以進行相應的判斷與響應。

6、組合式開發:權限管理

對於頁面級別的權限管理來說,權限管理部分的邏輯是獨立於頁面的,是與頁面中的具體內容無關的。也就是說,權限管理部分的代碼並不應該成爲頁面中的一部分,而是應該在拿到用戶權限後創建應用路由時就將沒有權限的頁面替換爲重定向或無權限頁面。

這樣一來,頁面部分的代碼就可以實現與權限管理邏輯的徹底解耦,以至於如果抽掉權限管理這一層後,頁面就變成了一個無需權限判斷的頁面依然可以獨立運行。而通用部分的權限管理代碼也可以在根據業務需求微調後服務於更多的項目。

7、小結

文中我們從權限管理的基礎設計思想講起,實現了一套基於角色的頁面級別的應用權限管理系統並分別討論了無權限重定向及無權限顯示無權限頁面兩種無權限查看時的處理方法。

接下來我們來看一下多級菜單是如何實現的

五、菜單匹配邏輯

本節參考代碼:

react-sider

在大部分企業管理系統中,頁面的基礎佈局所採取的一般都是側邊欄菜單加頁面內容這樣的組織形式。在成熟的組件庫支持下,UI 層面想要做出一個漂亮的側邊欄菜單並不困難,但因爲在企業管理系統中菜單還承擔着頁面導航的功能,於是就導致了兩大難題,一是多級菜單如何處理,二是菜單項的子頁面(如點擊門店管理中的某一個門店進入的門店詳情頁在菜單中並沒有對應的菜單項)如何高亮其隸屬於的父級菜單。

1、多級菜單

爲了增強系統的可擴展性,企業管理系統中的菜單一般都需要提供多級支持,對應的數據結構就是在每一個菜單項中都要有 children 屬性來配置下一級菜單項。

const menuData = [{
 name: '儀表盤',
 icon: 'dashboard',
 path: 'dashboard',
 children: [{
  name: '分析頁',
  path: 'analysis',
  children: [{
   name: '實時數據',
   path: 'realtime',
  }, {
   name: '離線數據',
   path: 'offline',
  }],
 }],
}];

遞歸渲染父菜單及子菜單

想要支持多級菜單,首先要解決的問題就是如何統一不同級別菜單項的交互。

在大多數的情況下,每一個菜單項都代表着一個不同的頁面路徑,點擊後會觸發 url 的變化並跳轉至相應頁面,也就是上面配置中的 path 字段。

但對於一個父菜單來說,點擊還意味着打開或關閉相應的子菜單,這就與點擊跳轉頁面發生了衝突。爲了簡化這個問題,我們先統一菜單的交互爲點擊父菜單(包含 children 屬性的菜單項)爲打開或關閉子菜單,點擊子菜單(不包含 children 屬性的菜單項)爲跳轉至相應頁面。

首先,爲了成功地渲染多級菜單,菜單的渲染函數是需要支持遞歸的,即如果當前菜單項含有 children 屬性就將其渲染爲父菜單並優先渲染其 children 字段下的子菜單,這在算法上被叫做深度優先遍歷。

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>
  );
 })
)

這樣我們就擁有了一個支持多級展開、子菜單分別對應頁面路由的側邊欄菜單。細心的朋友可能還發現了,雖然父菜單並不對應一個具體的路由但在配置項中依然還有 path 這個屬性,這是爲什麼呢?

2、處理菜單高亮

在傳統的企業管理系統中,爲不同的頁面配置頁面路徑是一件非常痛苦的事情,對於頁面路徑,許多開發者唯一的要求就是不重複即可,如上面的例子中,我們把菜單數據配置成這樣也是可以的。

const menuData = [{
 name: '儀表盤',
 icon: 'dashboard',
 children: [{
  name: '分析頁',
  children: [{
   name: '實時數據',
   path: '/realtime',
  }, {
   name: '離線數據',
   path: '/offline',
  }],
 }],
}];

<Router>
 <Route path="/realtime" render={() => <div />}
 <Route path="/offline" render={() => <div />}
</Router>

用戶在點擊菜單項時一樣可以正確地跳轉到相應頁面。但這樣做的一個致命缺陷就是,對於 /realtime 這樣一個路由,如果只根據當前的 pathname 去匹配菜單項中 path 屬性的話,要怎樣才能同時也匹配到「分析頁」與「儀表盤」呢?因爲如果匹配不到的話,「分析頁」和「儀表盤」就不會被高亮了。我們能不能在頁面的路徑中直接體現出菜單項之間的繼承關係呢?來看下面這個工具函數。

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;
 })
);

這個工具函數把菜單項中可能有的 children 字段考慮了進去,將一開始的菜單數據傳入就可以得到如下完整的菜單數據。

[{
 name: '儀表盤',
 icon: 'dashboard',
 path: '/dashboard', // before is 'dashboard'
 children: [{
  name: '分析頁',
  path: '/dashboard/analysis', // before is 'analysis'
  children: [{
   name: '實時數據',
   path: '/dashboard/analysis/realtime', // before is 'realtime'
  }, {
   name: '離線數據',
   path: '/dashboard/analysis/offline', // before is 'offline'
  }],
 }],
}];

然後讓我們再對當前頁面的路由做一下逆向推導,即假設當前頁面的路由爲 /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 [];
};

上面的這個數組代表着不同級別的菜單項,將這三個值分別與菜單數據中的 path 屬性進行匹配就可以一次性地匹配到所有當前頁面應當被高亮的菜單項了。

這裏需要注意的是,雖然菜單項中的 path 一般都是普通字符串,但有些特殊的路由也可能是正則的形式,如 /outlets/:id 。所以我們在對二者進行匹配時,還需要引入 path-to-regexp 這個庫來處理類似 /outlets/1/outlets/:id 這樣的路徑。又因爲初始時菜單數據是樹形結構的,不利於進行 path 屬性的匹配,所以我們還需要先將樹形結構的菜單數據扁平化,然後再傳入 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)))
 ), []);

在這些工具函數的幫助下,多級菜單的高亮也不再是問題了。

3、知識點:記憶化(Memoization)

在側邊欄菜單中,有兩個重要的狀態:一個是 selectedKeys ,即當前選定的菜單項;另一個是 openKeys ,即多個多級菜單的打開狀態。這二者的含義是不同的,因爲在 selectedKeys 不變的情況下,用戶在打開或關閉其他多級菜單後, openKeys 是會發生變化的,如下面二圖所示, selectedKeys 相同但 openKeys 不同。

 

對於 selectedKeys 來說,由於它是由頁面路徑( pathname )決定的,所以每一次 pathname 發生變化都需要重新計算 selectedKeys 的值。又因爲通過 pathname 以及最基礎的菜單數據 menuData 去計算 selectedKeys 是一件非常昂貴的事情(要做許多數據格式處理和計算),有沒有什麼辦法可以優化一下這個過程呢?

Memoization 可以賦予普通函數記憶輸出結果的功能,它會在每次調用函數之前檢查傳入的參數是否與之前執行過的參數完全相同,如果完全相同則直接返回上次計算過的結果,就像常用的緩存一樣。

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)),
 };
}

在組件的構造器中我們可以根據當前 props 傳來的 pathnamemenuData 計算出當前的 selectedKeys 並將其當做 openKeys 的初始值初始化組件內部 state。因爲 openKeys 是由用戶所控制的,所以對於後續 openKeys 值的更新我們只需要配置相應的回調將其交給 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>

這樣我們就實現了對於 selectedKeysopenKeys 的分別管理,開發者在使用側邊欄組件時只需要將應用當前的頁面路徑同步到側邊欄組件中的 pathname 屬性即可,側邊欄組件會自動處理相應的菜單高亮( selectedKeys )和多級菜單的打開與關閉( openKeys )。

4、知識點:正確區分 prop 與 state

上述這個場景也是一個非常經典的關於如何正確區分 prop 與 state 的例子。

selectedKeys 由傳入的 pathname 決定,於是我們就可以將 selectedKeyspathname 之間的轉換關係封裝在組件中,使用者只需要傳入正確的 pathname 就可以獲得相應的 selectedKeys 而不需要關心它們之間的轉換是如何完成的。而 pathname 作爲組件渲染所需的基礎數據,組件無法從自身內部獲得,所以就需要使用者通過 props 將其傳入進來。

另一方面, openKeys 作爲組件內部的 state,初始值可以由 pathname 計算而來,後續的更新則與組件外部的數據無關而是會根據用戶的操作在組件內部完成,那麼它就是一個 state,與其相關的所有邏輯都可以徹底地被封裝在組件內部而不需要暴露給使用者。

簡而言之,一個數據如果想成爲 prop 就必須是組件內部無法獲得的,而且在它成爲了 prop 之後,所有可以根據它的值推導出來的數據都不再需要成爲另外的 props,否則將違背 React 單一數據源的原則。對於 state 來說也是同樣,如果一個數據想成爲 state,那麼它就不應該再能夠被組件外部的值所改變,否則也會違背單一數據源的原則而導致組件的表現不可預測,產生難解的 bug。

5、組合式開發:應用菜單

嚴格來說,在這一小節中着重探討的應用菜單部分的思路並不屬於組合式開發思想的範疇,更多地是如何寫出一個支持無限級子菜單及自動匹配當前路由的菜單組件。組件當然是可以隨意插拔的,但前提是應用該組件的父級部分不依賴於組件所提供的信息。這也是我們在編寫組件時所應當遵循的一個規範,即組件可以從外界獲取信息並在此基礎上進行組件內部的邏輯判斷。但當組件向其外界拋出信息時,更多的時候應該是以回調的形式讓調用者去主動觸發,然後更新外部的數據再以 props 的形式傳遞給組件以達到更新組件的目的,而不是強制需要在外部再配置一個回調的接收函數去直接改變組件的內部狀態。

從這點上來說,組合式開發與組件封裝其實是有着異曲同工之妙的,關鍵都在於對內部狀態的嚴格控制。不論一個模塊或一個組件需要向外暴露多少接口,在它的內部都應該是解決了某一個或某幾個具體問題的。就像工廠產品生產流水線上的一個環節,在經過了這一環節後產品相較於進入前一定產生了某種區別,不論是增加了某些功能還是被打上某些標籤,產品一定會變得更利於下游合作者使用。更理想的情況則是即使刪除掉了這一環節,原來這一環節的上下游依然可以無縫地銜接在一起繼續工作,這就是我們所說的模塊或者說組件的可插拔性。

六、後端路由服務的意義

在前後端分離架構的背景下,前端已經逐漸代替後端接管了所有固定路由的判斷與處理,但在動態路由這樣一個場景下,我們會發現單純前端路由服務的靈活度是遠遠不夠的。在用戶到達某個頁面後,可供下一步邏輯判斷的依據就只有當前頁面的 url,而根據 url 後端的路由服務是可以返回非常豐富的數據的。

常見的例子如頁面的類型。假設應用中營銷頁和互動頁的渲染邏輯並不相同,那麼在頁面的 DSL 數據之外,我們就還需要獲取到頁面的類型以進行相應的渲染。再比如頁面的 SEO 數據,創建和更新時間等等,這些數據都對應用能夠在前端靈活地展示頁面,處理業務邏輯有着巨大的幫助。

甚至我們還可以推而廣之,徹底拋棄掉由 react-router 等提供的前端路由服務,轉而寫一套自己的路由分發器,即根據頁面類型的不同分別調用不同的頁面渲染服務,以多種類型頁面的方式來組成一個完整的前端應用。

七、組合式開發

爲了解決大而全的方案在實踐中不夠靈活的問題,我們是不是可以將其中包含的各個模塊解耦後,獨立發佈出來供開發者們按需取用呢?讓我們先來看一段理想中完整的企業管理系統應用架構部分的僞代碼:

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>
);

在上面的這段僞代碼中,我們抽象出了多語言支持、基於路由的權限管理、登錄鑑權、基礎佈局、側邊欄菜單等多個獨立模塊,可以根據需求添加或刪除任意一個模塊,而且添加或刪除任意一個模塊都不會對應用的其他部分產生不可接受的副作用。這讓我們對接下來要做的事情有了一個大體的認識,但在具體的實踐中,如 props 如何傳遞、模塊之間如何共享數據、如何靈活地讓用戶自定義某些特殊邏輯等都仍然面臨着巨大的挑戰。我們需要時刻注意,在處理一個具體問題時哪些部分應當放在某個獨立模塊內部去處理,哪些部分應當暴露出接口供使用者自定義,模塊與模塊之間如何做到零耦合以至於使用者可以隨意插拔任意一個模塊去適應當前項目的需要。

八、學習路線

從一個具體的前端應用直接切入開發技巧與理念的講解,所以對於剛入門 React 的朋友來說可能存在着一定的基礎知識部分梳理的缺失,這裏爲大家提供一份較爲詳細的 React 開發者學習路線圖,希望能夠爲剛入門 React 的朋友提供一條規範且便捷的學習之路。

總結

到此react的路由鑑權映梳理完了歡迎大家轉發交流分享 轉載請註明出處 ,附帶一個近期相關項目案例代碼給大家一個思路:

react-router-config

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章