導(dǎo)航
[React 從零實(shí)踐01-后臺(tái)] 代碼分割
[React 從零實(shí)踐02-后臺(tái)] 權(quán)限控制
[React 從零實(shí)踐03-后臺(tái)] 自定義hooks
[React 從零實(shí)踐04-后臺(tái)] docker-compose 部署react+egg+nginx+mysql
[React 從零實(shí)踐05-后臺(tái)] Gitlab-CI使用Docker自動(dòng)化部署
[源碼-webpack01-前置知識(shí)] AST抽象語法樹
[源碼-webpack02-前置知識(shí)] Tapable
[源碼-webpack03] 手寫webpack - compiler簡(jiǎn)單編譯流程
[源碼] Redux React-Redux01
[源碼] axios
[源碼] vuex
[源碼-vue01] data響應(yīng)式 和 初始化渲染
[源碼-vue02] computed 響應(yīng)式 - 初始化往扔,訪問,更新過程
[源碼-vue03] watch 偵聽屬性 - 初始化和更新
[源碼-vue04] Vue.set 和 vm.$set
[源碼-vue05] Vue.extend
[源碼-vue06] Vue.nextTick 和 vm.$nextTick
[部署01] Nginx
[部署02] Docker 部署vue項(xiàng)目
[部署03] gitlab-CI
[深入01] 執(zhí)行上下文
[深入02] 原型鏈
[深入03] 繼承
[深入04] 事件循環(huán)
[深入05] 柯里化 偏函數(shù) 函數(shù)記憶
[深入06] 隱式轉(zhuǎn)換 和 運(yùn)算符
[深入07] 瀏覽器緩存機(jī)制(http緩存機(jī)制)
[深入08] 前端安全
[深入09] 深淺拷貝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模塊化
[深入13] 觀察者模式 發(fā)布訂閱模式 雙向數(shù)據(jù)綁定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手寫Promise
[深入20] 手寫函數(shù)
[深入21] 算法 - 查找和排序
前置知識(shí)
(1) 一些單詞
graph:圖熊户,圖表
intelligence:智能的
contrast:對(duì)比
persistence:持久化
( data persistence:數(shù)據(jù)持久化 )
(2) 權(quán)限控制的類型
- 登陸權(quán)限控制
- 是否登陸
登陸才能訪問的頁面/路由
不登陸就可以訪問的頁面/路由萍膛,比如 login 頁面
- 是否登陸
- 頁面權(quán)限控制
- 菜單
菜單中的頁面/路由是否顯示
如果只是控制菜單,還不夠嚷堡,因?yàn)槿绻?cè)了所有路由蝗罗,即使菜單隱藏,還是可以通過地址欄訪問到
- 頁面
頁面的路由是否注冊(cè)
退一步蝌戒,如果不根據(jù)權(quán)限就行路由注冊(cè)串塑,即使注冊(cè)了所有路由,沒權(quán)限就從定向到404北苟,這樣雖然不是最好桩匪,但也能用
- 按鈕
頁面中的按鈕(增、刪友鼻、改傻昙、查)是否顯示
- 菜單
- 接口權(quán)限控制
- 兜底
路由可能配置失誤,按鈕可能忘了加權(quán)限桃移,這種時(shí)候請(qǐng)求控制可以用來兜底屋匕,越權(quán)請(qǐng)求將在前端被攔截
通過axios請(qǐng)求響應(yīng)攔截來實(shí)現(xiàn)
- 兜底
(3) react-router-dom 中的 Redirect 組件
Redirect => to => state
當(dāng)to屬性是一個(gè)對(duì)象時(shí) state 屬性可以傳遞一個(gè)對(duì)象,在to頁面中可以通過 this.props.state 獲取借杰,應(yīng)用場(chǎng)景:比如重定向到login頁面过吻,登陸成功后要返回之前所在的頁面,就可以把當(dāng)前的location信息通過state帶入到login頁面
(4) react-router-dom 實(shí)現(xiàn)在 Form 未保存時(shí)跳轉(zhuǎn)別的路由提示
- (
Prompt
) 組件 和 (router.getUserConfirmation
) 配合 -
Prompt
-
message 屬性:
字符串
或者函數(shù)
- 函數(shù)
- 返回true蔗衡,允許跳轉(zhuǎn)
- 返回false纤虽,不允許跳轉(zhuǎn),沒有任何提示
- 返回字符串绞惦,會(huì)彈出是否可以跳轉(zhuǎn)的彈窗逼纸,提示就是字符串內(nèi)的內(nèi)容,確定和取消
- 字符串
- 將上面的返回字符串
- 函數(shù)
-
when:boolean
- true:彈窗
- false:順利跳轉(zhuǎn)
-
message 屬性:
-
router.getUserConfirmation(message, callback)
- 問題:為什么需要getUserConfirmation济蝉?
- 因?yàn)椋篜rompt默認(rèn)使用window.confirm杰刽,丑,可以通過getUserConfirmation自定義樣式DOM王滤,阻止默認(rèn)彈窗
- 參數(shù):
- messag:就是Prompt的message指定的字符串
- callback:true允許跳轉(zhuǎn)贺嫂,false不允許跳轉(zhuǎn)
在表單組件中使用 Prompt
<Prompt message={() => isSave ? true : '表單還未保存,真的需要跳轉(zhuǎn)嗎雁乡?'} ></Prompt>
ReactDOM.render(
<Provider store={store}>
<Router getUserConfirmation={getUserConfirmation}> // ----------- getUserConfirmation
<App />
</Router>
</Provider>,
document.getElementById('root')
);
function getUserConfirmation(message: string, callback: any) {
Modal.confirm({ // ----------------------------------------------- antd Modal
content: message, // ------------------------------------------- message就是Pormpt組件的message返回的字符串
cancelText: '取消',
okText: '確定',
onCancel: () => {
callback(false) // ------------------------------------------- callback(false) 不跳轉(zhuǎn)
},
onOk: () => {
callback(true) // -------------------------------------------- callback(true) 跳轉(zhuǎn)
}
})
}
(5) react-router-config 源碼分析
為啥要分析 react-router-config
因?yàn)樽雎酚蓹?quán)限時(shí)第喳,需要向route配置對(duì)象中添加一些權(quán)限相關(guān)的自定義屬性,但我們又想用集中式路由來管理
react-router-config => renderRoutes 源碼分析
renderRoutes 一個(gè)最重要的api
----
import React from "react";
import { Switch, Route } from "react-router";
function renderRoutes(routes, extraProps = {}, switchProps = {}) {
return routes ? (
<Switch {...switchProps}>
{routes.map((route, i) => (
<Route
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props =>
route.render ? (
route.render({ ...props, ...extraProps, route: route })
) : (
<route.component {...props} {...extraProps} route={route} />
)
}
/>
))}
</Switch>
) : null;
}
export default renderRoutes;
解析:
1. renderRoutes()只遍歷一層routes踱稍,不管你嵌套多少層routes數(shù)組曲饱,你都需要在對(duì)應(yīng)的組件中再次調(diào)用renderRoutes()傳入該層該routes
2. 所以:在每層的render和componet兩個(gè)屬性中悠抹,都需要傳入該層的route配置對(duì)象,在組件中通過props.route.routes獲取該層的routes (重要)
3. exact和strict都是boolean類型的數(shù)據(jù)扩淀,所以當(dāng)配置對(duì)象中不存在這兩個(gè)屬性時(shí)楔敌,boolen相當(dāng)于傳入false即不生效
4. render屬性是一個(gè)函數(shù),(routesProps) => {...} 引矩,routeProps包含 match, location and history
(6) antd4版本以上 自定義圖標(biāo)組件
import { createFromIconfontCN } from '@ant-design/icons';
const MyIcon = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成 => Symbol方式A呵稹!M隆7彰铡!
});
ReactDOM.render(<MyIcon type="icon-example" />, mountedNode);
(7) 添加別名 @
映射 src
在TS的項(xiàng)目中
- create-react-app構(gòu)建的項(xiàng)目区端,eject后值漫,找到 config/webpack.config.js => resolve.alias
- tsconfig.json 中刪除
baseUrl
和paths
,添加"extends": "./paths.json"
- tsconfig.json 中刪除
- 在根目錄新建
paths.json
文件织盼,寫入baseUrl
和paths
配置
- 在根目錄新建
- 教程地址
1. webpack.config.js => resolve => alias
module.export = {
resolve: {
alias: {
"@": path.resolve(__dirname, '../src')
}
}
}
2. 根目錄新建 paths.json 寫入以下配置
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}
3. 在 tsconfig.json 中做如下修改杨何,添加( extends ), 刪除( baseUrl,paths )
{
// "baseUrl": "src",
// "paths": {
// "@/*": ["src/*"]
// },
"extends": "./paths.json"
}
(8) create-react-app 配置全局的 scss ,而不需要每次 @import
- 安裝
sass-resources-loader
- 修改 config/webpack.config.js 如下
注意:很多教程修改use:getStyleLoaders().concat()這樣修改不行
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [......].filter(Boolean);
if (preProcessor) {
loaders.push(......);
}
if (preProcessor === 'sass-loader') { // ------------ 如果第二個(gè)參數(shù)是 sass-loader沥邻,就 push sass-resources-loader
loaders.push({
loader: 'sass-resources-loader',
options: {
resources: [
// 這里按照你的文件路徑填寫../../../ 定位到根目錄下, 可以引入多個(gè)文件
path.resolve(__dirname, '../src/style/index.scss'),
]
}
})
}
return loaders;
};
(9) eslint 檢查 react-hooks 語法
- eslint-plugin-react-hooks
比如:可以檢查 hooks 不能在循環(huán)危虱,條件等地方使用,不能在回調(diào)中使用等等
- 安裝:yarn add eslint-plugin-react-hooks --dev
- 使用:在
.eslintrc.js
中 添加plugin
和rules
配置
/* eslint-disable */
module.exports = {
"env": {
"es6": true, // 在開發(fā)環(huán)境唐全,啟用es6語法埃跷,包括全局變量
"node": true,
"browser": true
},
"parser": "babel-eslint", // 解析器
"parserOptions": { // 解析器選項(xiàng)
"ecmaVersion": 6, // 啟用es6語法,不包括全局變量
"sourceType": "module",
"ecmaFeatures": { //額外的語言特性
"jsx": true // 啟用jsx語法
}
},
"plugins": [
// ...
"react-hooks"
],
rules: {
'no-console': 'off', // 可以console
'no-debugger': 'off',
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
}
/* eslint-disable */
(一) react中實(shí)現(xiàn)權(quán)限控制
(1) ( 嵌套路由注冊(cè) ) 和 ( menu ) 和 ( breadcrumb面包屑 ) 共用同一份 ( routes )
-
好處
- 路由注冊(cè)的path和menu的path共用一個(gè)弥雹,而不用分開維護(hù)
- 集中式路由,統(tǒng)一維護(hù)延届,雖然router4是分布式路由思想
-
注意點(diǎn)
- ( menu根據(jù)不同權(quán)限顯示隱藏 ) 和 ( 路由根據(jù)權(quán)限注冊(cè)和不注冊(cè) ) 是兩個(gè)概念剪勿,如果只是控制menu的顯示隱藏,而所有的路由都注冊(cè)的話方庭,即使頁面上沒有出現(xiàn)別的權(quán)限的菜單厕吉,但是通過地址欄輸入地址等方式還是可以導(dǎo)航到路由注冊(cè)的頁面,這就需要不在權(quán)限的路由不注冊(cè)或者跳轉(zhuǎn)到404頁面或者做提示沒權(quán)限等處理
- ( 子菜單 ) 用 ( subs ) 數(shù)組屬性表示械念,( 嵌套路由 ) 用 ( routes ) 數(shù)組屬性表示
- menu是樹形菜單头朱,所以注冊(cè)路由時(shí)要遞歸遍歷注冊(cè)每一層,menu中有子菜單我們用
subs
表示 - 如果menu的item存在subs订讼,則該item層級(jí)不應(yīng)該有
path
和component
屬性 - ( 即只有menu.item有上面這兩個(gè)屬性,submenu沒有扇苞,因?yàn)椴恍枰@示和跳轉(zhuǎn) )
- 全局下renderRoutes遍歷一次routes欺殿,即只注冊(cè)第一層的routes寄纵,嵌套路由存在routes屬性,在相應(yīng)的路由頁面中再次調(diào)用renderRoutes注冊(cè)路由脖苏,但是再遞歸遍歷所有的menu相關(guān)的subs進(jìn)行路由注冊(cè)
- 代碼
- routes
routes是這樣一個(gè)數(shù)組
----
const totalRoutes: IRouteModule[] = [
{
path: '/login',
component: Login,
},
{
path: '/404',
component: NotFound,
},
{
path: '/',
component: Layout,
routes: [
// routes:用于嵌套路由程拭,注意不是嵌套菜單
// subs:主要還遍歷注冊(cè)menu樹形菜單,和渲染menu樹形菜單棍潘,在不同系統(tǒng)的路由中定義了subs
// ----------------------------------------------------------- 嵌套路由通過 renderRoutes 做處理
...adminRoutes, // ------------------------------------ ( 后臺(tái)系統(tǒng)路由 )恃鞋,單獨(dú)維護(hù),同時(shí)用于menu
...bigScreenRoutes, // -------------------------------- ( 大屏系統(tǒng)路由 )亦歉,單獨(dú)維護(hù)恤浪,同時(shí)用于menu
]
}
]
---- 分割線 ----
const adminRoutes: IRouteModule[] = [{
// ---------------- adminRoutes 用于menu的樹形菜單的 ( 渲染 )和 ( 路由注冊(cè),注冊(cè)可以在同一層級(jí),因?yàn)閙une視口一樣 )
title: '首頁',
icon: 'anticon-home--line',
key: '/admin-home',
path: '/admin-home',
component: AdminHome,
}, {
title: 'UI',
icon: 'anticon-uikit',
key: '/admin-ui',
subs: [{
// -------------------------------------------------------- subs用于注冊(cè)路由肴楷,并且用于menu樹形菜單的渲染
// -------------------------------------------------------- ( 路由注冊(cè):其實(shí)就是在不同的地方渲染 <Route /> 組件 )
// -------------------------------------------------------- ( 菜單渲染:其實(shí)就是menu菜單在頁面上顯示 )
title: 'Antd',
icon: 'anticon-ant-design',
key: '/admin-ui/antd',
subs: [{
title: '首頁',
icon: 'anticon-codev1',
key: '/admin-ui/antd/index',
path: '/admin-ui/antd/index',
component: UiAntd,
}]
}, {
title: 'Vant',
icon: 'anticon-relevant-outlined',
key: '/admin-ui/vant',
path: '/admin-ui/vant',
component: UiAntd,
}]
}]
- renderRoutes - 重點(diǎn)
import React from 'react'
import { IRouteModule } from '../../global/interface'
import { Switch, Route } from 'react-router-dom'
/**
* @function normolize
* @description 遞歸的對(duì)route.subs做normalize水由,即把所有嵌套展平到一層,主要對(duì)menu樹就行路由注冊(cè)
* @description 因?yàn)閙enu樹都在同一個(gè)路由視口赛蔫,所以可以在同一層級(jí)就行路由注冊(cè)
* @description 注意:path 和 component 在存在subs的那層menu-route對(duì)象中同時(shí)存在和同時(shí)不存在
*/
function normolize(routes?: IRouteModule[]) {
let result: IRouteModule[] = []
routes?.forEach(route => {
!route.subs
? result.push(route)
: result = result.concat(normolize(route.subs)) // ---------------- 拼接
})
return result
}
/**
* @function renderRoutes
* @description 注冊(cè)所有路由砂客,并向嵌套子路由組件傳遞 route 對(duì)象屬性,子組件就可以獲取嵌套路由屬性 routes
*/
const renderRoutes = (routes?: IRouteModule[], extraProps = {}, switchProps = {}) => {
return routes
? <Switch {...switchProps}>
{normolize(routes).map((route, index) => { // --------------------- 先對(duì)subs做處理呵恢,再map
return route.path && route.component &&
// path 并且 component 同時(shí)存在才進(jìn)行路由注冊(cè)
// path 和 componet 總是同時(shí)存在鞠值,同時(shí)不存在
<Route
key={route.key || `${index + +new Date()}`}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props => {
return route.render
? route.render({ ...props, ...extraProps, route: route })
: <route.component {...props} {...extraProps} route={route} />
// 向嵌套組件中傳遞 route屬性,通過route.routes在嵌套路由組件中可以再注冊(cè)嵌套路由
}} />
})}
</Switch>
: null
}
export {
renderRoutes
}
- menu
/**
* @function renderMenu
* @description 遞歸渲染菜單
*/
const renderMenu = (adminRoutes: IRouteModule[]) => {
return adminRoutes.map(({ subs, key, title, icon }) => {
return subs
?
<SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>
{renderMenu(subs)}
</SubMenu>
:
<Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>
})
}
- 嵌套路由
<Layout className={styles.layoutAdmin}>
// -------------------------------------------------------------------------------- Layout 是 '/' 路由對(duì)應(yīng)的組件
// ---------------- {renderRoutes(props.route.routes)} 就是在 '/' 路由中渲染的 <Route path="" compoent="" />組件
<Sider>
<Menu
mode="inline"
theme="dark"
onClick={goPage}
>
{renderMenu(adminRoutes)}
</Menu>
</Sider>
<Layout>
<Header className={styles.header}>
<ul className={styles.topMenu}>
<li>退出</li>
</ul>
</Header>
<Content className={styles.content}>
{renderRoutes(props.route.routes)} // --------------- 再次執(zhí)行渗钉,注冊(cè)嵌套的路由彤恶,成為父組件的子組件
</Content>
</Layout>
</Layout>
(2) 在(1)的基礎(chǔ)上加入權(quán)限 ( 登陸,頁面晌姚,菜單 )
-
要達(dá)到的效果 ( 菜單和路由兩個(gè)方面考慮 )
- menu根據(jù)權(quán)限顯示和隱藏
注意menu中由于存在樹形粤剧,為了控制粒度更細(xì),在 submenu 和 menu.item 上都加入權(quán)限的判斷比較好
- router根據(jù)權(quán)限注冊(cè)和不注冊(cè)
- menu根據(jù)權(quán)限顯示和隱藏
-
需要添加的字段
-
needLoginAuth:boolen
- 表示路由/菜單是否需要登陸權(quán)限
- ( 只要登陸挥唠,后端就會(huì)返回角色抵恋,不同角色的權(quán)限可以用rolesAuth數(shù)組表示,如果返回的角色在rolesAuth數(shù)組中宝磨,就注冊(cè)路由 或 顯示菜單)
如果 needLoginAuth是false弧关,則就不需要有 rolesAuth 字段了,即任何角色都會(huì)有的路由或菜單
-
rolesAuth:array
- 該路由注冊(cè)/菜單顯示 需要的角色數(shù)組
-
meta: object
- 可以把
needLoginAuth
和rolesAuth
放入meta
對(duì)象中唤锉,便于管理
- 可以把
-
visiable
- visiable主要用于 list 和 detail 這兩種類型的頁面世囊,詳情頁在menu中是不展示的,但是需要注冊(cè)Route窿祥,需要用字段來判斷隱藏掉詳情頁
-
needLoginAuth:boolen
-
模擬需求
- 角色有兩種:user 和 admin
- 菜單權(quán)限
- 首頁:登陸后株憾,兩種角色都可以訪問
- UI:
- ui 這個(gè)菜單兩種角色都顯示
- ui/antd 這個(gè)菜單只有 admin 可以訪問和顯示
- ui/vant 這個(gè)菜單兩種角色都可以顯示
- JS:
- 只有admin可以顯示
- 代碼
- 改造后的routes
const totalRoutes: IRouteModule[] = [
{
path: '/login',
component: Login,
meta: {
needLoginAuth: false
}
},
{
path: '/404',
component: NotFound,
meta: {
needLoginAuth: false
}
},
{
path: '/',
component: Layout,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
routes: [
// routes:用于嵌套路由,注意不是嵌套菜單
// subs:主要還遍歷注冊(cè)menu樹形菜單,和渲染menu樹形菜單嗤瞎,在不同系統(tǒng)的路由中定義了subs
// 嵌套路由通過 renderRoutes函數(shù) 做處理
...adminRoutes, // --------------------------- 后臺(tái)系統(tǒng)路由表
...bigScreenRoutes, // ----------------------- 大屏系統(tǒng)路由表
]
}
]
---- 分割線 ----
const adminRoutes: IRouteModule[] = [{
title: '首頁',
icon: 'anticon-home--line',
key: '/admin-home',
path: '/admin-home',
component: AdminHome,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}, {
title: 'UI',
icon: 'anticon-uikit',
key: '/admin-ui',
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
subs: [{ // subs用于注冊(cè)路由墙歪,并且用于menu樹形菜單
title: 'Antd',
icon: 'anticon-ant-design',
key: '/admin-ui/antd',
meta: {
needLoginAuth: true,
rolesAuth: ['user','admin']
},
subs: [{
title: '首頁',
icon: 'anticon-codev1',
key: '/admin-ui/antd/index',
path: '/admin-ui/antd/index',
component: UiAntd,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}, {
title: 'Form表單',
icon: 'anticon-yewubiaodan',
key: '/admin-ui/antd/form',
path: '/admin-ui/antd/form',
component: UiAntdForm,
meta: {
needLoginAuth: true,
rolesAuth: ['admin']
},
}]
}, {
title: 'Vant',
icon: 'anticon-relevant-outlined',
key: '/admin-ui/vant',
path: '/admin-ui/vant',
component: UiVant,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}]
}, {
title: 'JS',
icon: 'anticon-js',
key: '/admin-js',
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
subs: [{
title: 'ES6',
icon: 'anticon-6',
key: '/admin-js/es6',
path: '/admin-js/es6',
component: JsEs6,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}, {
title: 'ES5',
icon: 'anticon-js',
key: '/admin-js/es5',
path: '/admin-js/es5',
component: UiAntd,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}]
}]
- 對(duì)routes和menu過濾的函數(shù)
/**
* @function routesFilter routes的權(quán)限過濾
*/
export function routesFilter(routes: IRouteModule[], roles: string) {
return routes.filter(({ meta: { needLoginAuth, rolesAuth }, routes: nestRoutes, subs }) => {
if (nestRoutes) { // 存在routes,對(duì)routes數(shù)組過濾贝奇,并重新賦值過濾后的routes
nestRoutes = routesFilter(nestRoutes, roles) // 遞歸
}
if (subs) { // 存在subs虹菲,對(duì)subs數(shù)組過濾,并重新賦值過濾后的subs
subs = routesFilter(subs, roles) // 遞歸
}
return !needLoginAuth
? true
: rolesAuth?.includes(roles)
? true
: false
})
}
- renderRoutes 登陸權(quán)限的驗(yàn)證掉瞳,路由注冊(cè)過濾即路由注冊(cè)權(quán)限毕源,menu的過濾顯示隱藏不在這里進(jìn)行
/**
* @function renderRoutes
* @description 注冊(cè)所有路由,并向嵌套子路由組件傳遞 route 對(duì)象屬性陕习,子組件就可以獲取嵌套路由屬性 routes
*/
const renderRoutes = (routes: IRouteModule[], extraProps = {}, switchProps = {}) => {
const history = useHistory()
const token = useSelector((state: {app: {loginMessage: {token: string}}}) => state.app.loginMessage.token)
const roles = useSelector((state: {app: {loginMessage: {roles: string}}}) => state.app.loginMessage.roles)
if (!token) {
history.push('/login') // token未登錄去登陸頁面霎褐,即登陸權(quán)限的驗(yàn)證!:獠椤4衿邸!0枭>愣觥!K觥E牟骸!M辆印T婀骸!2烈C奕Α!>祢选7竹!吁系!
}
routes = routesFilter(routes, roles) // 權(quán)限過濾德召,這里只用于路由注冊(cè),menu過濾還需在menu頁面調(diào)用routesFilter
routes = normalize(routes) // 展平 subs
return routes
? <Switch {...switchProps}>
{
routes.map((route, index) => { // 先對(duì)subs做處理
return route.path && route.component &&
// path 并且 component 同時(shí)存在才進(jìn)行路由注冊(cè)
// path 和 componet 總是同時(shí)存在汽纤,同時(shí)不存在
<Route
key={route.key || `${index + +new Date()}`}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props => {
return route.render
? route.render({ ...props, ...extraProps, route: route })
: <route.component {...props} {...extraProps} route={route} />
// 向嵌套組件中傳遞 route屬性上岗,通過route.routes在嵌套路由組件中可以再注冊(cè)嵌套路由
}} />
})}
</Switch>
: null
}
- menu的過濾
/**
* @function renderMenu
* @description 遞歸渲染菜單
*/
const renderMenu = (adminRoutes: IRouteModule[]) => {
const roles =
useSelector((state: { app: { loginMessage: { roles: string } } }) => state.app.loginMessage.roles) ||
getLocalStorage('loginMessage').roles;
// 這里用 eslint-plugin-react-hooks 會(huì)報(bào)錯(cuò),因?yàn)?hooks 必須放在最頂層
// useSelector
adminRoutes = routesFilter(adminRoutes, roles) // adminRoutes權(quán)限過濾T唐骸K貉帧!!!P衣啤!!2ú摹!MφC鞴!7岵础J矶ā!M骸;爸丁!Q年堆!
return adminRoutes.map(({ subs, key, title, icon }) => {
return subs
?
<SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>
{renderMenu(subs)}
</SubMenu>
:
<Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>
})
}
(3) breadcrumb 面包屑
-
面包屑要解決的基本問題
- 對(duì)于導(dǎo)航到詳情頁的動(dòng)態(tài)路由,要顯示到面包屑
- 對(duì)于有menu.item即routes中有component的route對(duì)象盏浇,要能夠點(diǎn)擊并導(dǎo)航
- 對(duì)于submenu的item不能點(diǎn)擊变丧,并置灰
- 如何判斷是否可以點(diǎn)擊? 如果routes具有subs數(shù)組绢掰,就不可以點(diǎn)擊痒蓬;只有menu.item的route可以點(diǎn)擊
- 因?yàn)槊姘际歉鶕?jù)當(dāng)前的url的pathname來進(jìn)行判斷的,所以無需做持久化滴劲,只要刷新地址欄不變就不會(huì)變
但是有點(diǎn)需要注意:就是退出登陸時(shí)攻晒,應(yīng)該清除掉 localStorage 中的用于緩存menu等所有數(shù)據(jù),而刷新時(shí)候不需要班挖,如果退出時(shí)不清除localStorage鲁捏,登陸重定向到首頁,就會(huì)加載首頁的面包屑和緩存的menu聪姿,造成不匹配
import { Breadcrumb } from 'antd'
import React from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import styles from './breadcrumb.module.scss'
import { routesFilter } from '@/utils/render-routes/index'
import adminRoutes from '@/router/admin-routes'
import { useSelector } from 'react-redux'
import { IRouteModule } from '@/global/interface'
import { getLocalStorage } from '@/utils'
import _ from 'lodash'
const CustomBreadcrumb = () => {
const roles = useSelector((state: any) => state.app.loginMessage.roles) || getLocalStorage('loginMessage').roles
const pathname = useLocation().pathname // 獲取url的path
const history = useHistory()
// routeParams => 獲取useParams的params對(duì)象碴萧,對(duì)象中包含動(dòng)態(tài)路由的id屬性
const routeParams = getLocalStorage('routeParams')
// 深拷貝 權(quán)限過濾后的adminRoutes
const routesAmin = _.cloneDeep([...routesFilter(adminRoutes, roles)]) // 權(quán)限過濾,為了和menu同步
// generateRouteMap => 生成面包屑的 path,title映射
const generateRouteMap = (routesAmin: IRouteModule[]) => {
const routeMap = {}
function step(routesAmin: IRouteModule[]) {
routesAmin.forEach((item, index) => {
if (item.path.includes(Object.keys(routeParams)[0])) { // 動(dòng)態(tài)路由存在:符號(hào)末购,緩存該 route破喻,用于替換面包屑的最后一級(jí)名字
item.path = item.path.replace(`:${Object.keys(routeParams)[0]}`, routeParams[Object.keys(routeParams)[0]])
// 把動(dòng)態(tài)路由參數(shù)(:id) 替換成真實(shí)的(params)
}
routeMap[item.path] = item.title
item.subs && step(item.subs)
})
}
step(routesAmin) // 用于遞歸
return routeMap
}
const routeMap = generateRouteMap(routesAmin)
// generateBreadcrumbData => 生成面包屑的data
const generateBreadcrumbData = (pathname: string) => {
const arr = pathname.split('/')
return arr.map((item, index) => {
return arr.slice(0, index + 1).join('/')
}).filter(v => !!v)
}
const data = generateBreadcrumbData(pathname)
// pathFilter
// 面包屑是否可以點(diǎn)擊導(dǎo)航
// 同時(shí)用來做可點(diǎn)擊,不可點(diǎn)擊的 UI
const pathFilter = (path: string) => {
// normalizeFilterdAdminRoutes => 展平所有subs
function normalizeFilterdAdminRoutes(routesAmin: IRouteModule[]) {
let normalizeArr: IRouteModule[] = []
routesAmin.forEach((item, index: number) => {
item.subs
?
normalizeArr = normalizeArr.concat(normalizeFilterdAdminRoutes(item.subs))
:
normalizeArr.push(item)
})
return normalizeArr
}
const routes = normalizeFilterdAdminRoutes(_.cloneDeep(routesAmin))
// LinkToWhere => 是否可以點(diǎn)擊面包屑
function LinkToWhere(routes: IRouteModule[]) {
let isCanGo = false
routes.forEach(item => {
if (item.path === path && item.component) {
isCanGo = true
}
})
return isCanGo
}
return LinkToWhere(routes)
}
// 點(diǎn)擊時(shí)的導(dǎo)航操作
const goPage = (item: string) => {
pathFilter(item) && history.push(item)
// 函數(shù)組合盟榴,可以點(diǎn)擊就就跳轉(zhuǎn)
}
// 渲染 breadcrumb
const renderData = (item: string, index: number) => {
return (
<Breadcrumb.Item key={index} onClick={() => goPage(item)}>
<span
style={{
cursor: pathFilter(item) ? 'pointer' : 'not-allowed',
color: pathFilter(item) ? '#4DB2FF' : 'silver'
}}
>
{routeMap[item]}
</span>
</Breadcrumb.Item>
)
}
return (
<Breadcrumb className={styles.breadcrumb} separator="/">
{data.map(renderData)}
</Breadcrumb>
)
}
export default CustomBreadcrumb
-
上面的面包屑存在的問題:
需求:面包屑在點(diǎn)擊到詳情時(shí)曹质,更新全局面包屑
不足:使用localstore,在子組件set,在父組件get羽德,但是父組件先執(zhí)行几莽,子組件后執(zhí)行,并且localstore不會(huì)更新組件宅静,所以導(dǎo)致面包屑不更新
代替:在子組件 es6detail 中 dispatch 了一個(gè)action章蚣,但不是在onClick的事件中,觸發(fā)了警告
// 需求:面包屑在點(diǎn)擊到詳情時(shí)姨夹,更新全局面包屑
// 不足:使用localstore纤垂,在子組件set,在父組件get磷账,但是父組件先執(zhí)行峭沦,子組件后執(zhí)行,并且localstore不會(huì)更新組件逃糟,所以導(dǎo)致面包屑不更新
// 代替:在子組件 es6detail 中 dispatch 了一個(gè)action吼鱼,但不是在onClick的事件中,觸發(fā)了警告
// 之所以還這樣做绰咽,是要在子組件es6detail更新后菇肃,b更新CustomBreadcrumb
// 因?yàn)樽咏M件es6detail更新了store,而父組件 CustomBreadcrumb 有引用store中的state取募,所以會(huì)更新
// 不足:觸發(fā)了警告
const CustomBreadcrumb = () => {
const roles = useSelector((state: any) => state.app.loginMessage.roles) || getLocalStorage('loginMessage').roles
const pathname = useLocation().pathname // 獲取url的path
const history = useHistory()
// routeParams => 獲取useParams的params對(duì)象巷送,對(duì)象中包含動(dòng)態(tài)路由的id屬性
const routeParams = getLocalStorage('routeParams')
// debugger
// 深拷貝 權(quán)限過濾后的adminRoutes
const routesAmin = _.cloneDeep([...routesFilter(adminRoutes, roles)]) // 權(quán)限過濾,為了和menu同步
// generateRouteMap => 生成面包屑的 path,title映射
const generateRouteMap = (routesAmin: IRouteModule[]) => {
const routeMap = {}
function step(routesAmin: IRouteModule[]) {
routesAmin.forEach((item, index) => {
if (item.path.includes(routeParams && Object.keys(routeParams)[0])) { // 動(dòng)態(tài)路由存在:符號(hào)矛辕,緩存該 route笑跛,用于替換面包屑的最后一級(jí)名字
item.path = item.path.replace(`:${Object.keys(routeParams)[0]}`, routeParams[Object.keys(routeParams)[0]])
// 把動(dòng)態(tài)路由參數(shù)(:id) 替換成真實(shí)的(params)
}
routeMap[item.path] = item.title
item.subs && step(item.subs)
})
}
step(routesAmin) // 用于遞歸
return routeMap
}
const routeMap = generateRouteMap(routesAmin)
// generateBreadcrumbData => 生成面包屑的data
const generateBreadcrumbData = (pathname: string) => {
const arr = pathname.split('/')
return arr.map((item, index) => {
return arr.slice(0, index + 1).join('/')
}).filter(v => !!v)
}
const data = generateBreadcrumbData(pathname)
// pathFilter
// 面包屑是否可以點(diǎn)擊導(dǎo)航
// 同時(shí)用來做可點(diǎn)擊,不可點(diǎn)擊的 UI
const pathFilter = (path: string) => {
// normalizeFilterdAdminRoutes => 展平所有subs
function normalizeFilterdAdminRoutes(routesAmin: IRouteModule[]) {
let normalizeArr: IRouteModule[] = []
routesAmin.forEach((item, index: number) => {
item.subs
?
normalizeArr = normalizeArr.concat(normalizeFilterdAdminRoutes(item.subs))
:
normalizeArr.push(item)
})
return normalizeArr
}
const routes = normalizeFilterdAdminRoutes(_.cloneDeep(routesAmin))
// LinkToWhere => 是否可以點(diǎn)擊面包屑
function LinkToWhere(routes: IRouteModule[]) {
let isCanGo = false
routes.forEach(item => {
if (item.path === path && item.component) {
isCanGo = true
}
})
return isCanGo
}
return LinkToWhere(routes)
}
// 點(diǎn)擊時(shí)的導(dǎo)航操作
const goPage = (item: string) => {
pathFilter(item) && history.push(item)
// 函數(shù)組合聊品,可以點(diǎn)擊就就跳轉(zhuǎn)
}
// 渲染 breadcrumb
const renderData = (item: string, index: number) => {
return (
<Breadcrumb.Item key={index} onClick={() => goPage(item)}>
<span
style={{
cursor: pathFilter(item) ? 'pointer' : 'not-allowed',
color: pathFilter(item) ? '#4DB2FF' : 'silver'
}}
>
{routeMap[item]}
</span>
</Breadcrumb.Item>
)
}
return (
<Breadcrumb className={styles.breadcrumb} separator="/">
{data.map(renderData)}
</Breadcrumb>
)
}
export default CustomBreadcrumb
(4) menu數(shù)據(jù)持久化
- 相關(guān)屬性
openKeys
onOpenChange()
selectedKeys
onClick()
- 存入localStorage飞蹂,在effect中初始化
import React, { useEffect, useState } from 'react'
import { renderRoutes, routesFilter } from '@/utils/render-routes/index'
import styles from './index.module.scss'
import { Button, Layout, Menu } from 'antd';
import adminRoutes from '@/router/admin-routes'
import { IRouteModule } from '@/global/interface'
import IconFont from '@/components/Icon-font'
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { getLocalStorage, setLocalStorage } from '@/utils';
import CustomBreadcrumb from '@/components/custorm-breadcrumb';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
const { SubMenu } = Menu;
const { Header, Sider, Content } = Layout;
const Admin = (props: any) => {
const [collapsed, setcollapsed] = useState(false)
const [selectedKeys, setSelectedKeys] = useState(['/admin-home'])
const [openKeys, setOpenKeys]: any = useState(['/admin-home'])
const history = useHistory()
useEffect(() => {
// 初始化,加載持久化的 selectedKeys 和 openKeys
const selectedKeys = getLocalStorage('selectedKeys')
const openKeys = getLocalStorage('openKeys')
setSelectedKeys(v => v = selectedKeys)
setOpenKeys((v: any) => v = openKeys)
}, [])
/**
* @function renderMenu
* @description 遞歸渲染菜單
*/
const renderMenu = (adminRoutes: IRouteModule[]) => {
const roles =
useSelector((state: { app: { loginMessage: { roles: string } } }) => state.app.loginMessage.roles) ||
getLocalStorage('loginMessage').roles;
const adminRoutesDeepClone = routesFilter([...adminRoutes], roles) // adminRoutes權(quán)限過濾
return adminRoutesDeepClone.map(({ subs, key, title, icon, path }) => {
return subs
?
<SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>
{renderMenu(subs)}
</SubMenu>
:
!path.includes(':') && <Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>
// 動(dòng)態(tài)路由不進(jìn)行顯示翻屈,因?yàn)橐话銊?dòng)態(tài)路由是詳情頁
// 雖然不顯示陈哑,但是需要注冊(cè)路由,只是menu不顯示
})
}
// 點(diǎn)擊 menuItem 觸發(fā)的事件
const goPage = ({ keyPath, key }: { keyPath: any[], key: any }) => {
history.push(keyPath[0])
setSelectedKeys(v => v = [key])
setLocalStorage('selectedKeys', [key]) // 記住當(dāng)前點(diǎn)擊的item伸眶,刷新持久化
}
// 展開/關(guān)閉的回調(diào)
const onOpenChange = (openKeys: any) => {
setOpenKeys((v: any) => v = openKeys)
setLocalStorage('openKeys', openKeys) // 記住展開關(guān)閉的組惊窖,刷新持久化
}
const toggleCollapsed = () => {
setcollapsed(v => v = !v)
};
return (
<Layout className={styles.layoutAdmin}>
<Sider collapsed={collapsed}>
<Menu
mode="inline"
theme="dark"
onClick={goPage}
// inlineCollapsed={} 在有 Sider 包裹的情況下,需要在Sider中設(shè)置展開隱藏
inlineIndent={24}
selectedKeys={selectedKeys}
openKeys={openKeys}
onOpenChange={onOpenChange}
>
{renderMenu([...adminRoutes])}
</Menu>
</Sider>
<Layout>
<Header className={styles.header}>
<aside>
<span onClick={toggleCollapsed}>
{collapsed
? <MenuUnfoldOutlined className={styles.toggleCollapsedIcon} />
: <MenuFoldOutlined className={styles.toggleCollapsedIcon} />
}
</span>
</aside>
<ul className={styles.topMenu}>
<li onClick={() => history.push('/login')}>退出</li>
</ul>
</Header>
<Content className={styles.content}>
<CustomBreadcrumb />
{renderRoutes(props.route.routes)}
{/* renderRoutes(props.route.routes) 再次執(zhí)行厘贼,注冊(cè)嵌套的路由界酒,成為父組件的子組件 */}
</Content>
</Layout>
</Layout>
)
}
export default Admin
項(xiàng)目源碼
資料
react路由鑒權(quán)(完善) https://juejin.im/post/6844903924441284615
快速打造react管理系統(tǒng)(項(xiàng)目) https://juejin.im/post/6844903981945208839
權(quán)限控制的類型 https://juejin.im/post/6844903882338861063
React-Router實(shí)現(xiàn)前端路由鑒權(quán):https://juejin.im/post/6857055615739985933
react-router-config路由鑒權(quán):https://github.com/leishihong/react-router-config