数据面板

image.png

01-路由鉴权

目标:能够实现未登录时访问拦截并跳转到登录页面
分析说明

  • 实现思路:自己封装 AuthRoute 路由鉴权组件,实现未登录拦截,并跳转到登录页面
  • 核心点1:
    • AuthRoute 组件的用法应该与 Route 组件完全一致,并且 AuthRoute 组件也能够实现路由配置功能
    • 所以,我们要封装的 AuthRoute 组件就是对 Route 组件的封装,并同时实现了鉴权功能
  • 核心点2:
    • 分别对登录或未登录,进行相应处理

// Route 用法:


// AuthRoute 用法:

步骤

  1. 在 components 目录中,创建 AuthRoute.jsx 文件
  2. 判断是否登录
  3. 登录时,直接渲染相应页面组件
  4. 未登录时,重定向到登录页面
  5. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件

核心代码
components/AuthRoute.jsx 中:
import { Route, Redirect } from ‘react-router-dom’
import { getToken } from ‘@/utils’

const AuthRoute = ({ component: Component, …rest }) => {
return (
{…rest}
render={props => {
// 判断是否登录
if (!getToken()) {
// 未登录
return (
to={{
pathname: ‘/login’,
state: { from: props.location.pathname }
}}
/>
)
}

// 登录
return
}}
/>
)
}

export { AuthRoute }
App.js 中:
import { AuthRoute } from ‘@/components/AuthRoute’

// 使用 AuthRoute 组件,替换 Route 组件

Login/index.jsx组件:
+import { useHistory, useLocation } from “react-router-dom”;
// 导入action
import { login } from “@/store/actions”;

const Login = () => {
const dispatch = useDispatch();
const history = useHistory();
+ const location = useLocation()
// location 获取路由信息:地址 ?字符串 hash #符号后 state路由传值
// 表单完成输入校验通过,触发的事件
const onFinish = async (values) => {
try {
// 需要提交数据
const { mobile, code } = values;
// 进行登录
await dispatch(login(mobile, code));
message.success(“登录成功”);
// 跳转首页(如果有来源页面returnUrl就跳转这个地址)
+ history.replace(location?.state?.returnUrl || “/“);
} catch (e) {
message.error(e.response?.data?.message || “登录失败”);
}
};

02-Layout组件

目标:能够根据antd布局组件搭建基础布局步骤

  1. 打开 antd/Layout 布局组件文档
  2. 拷贝示例代码到我们的 Layout 页面中
  3. 分析并调整页面布局

核心代码
pages/Layout/index.js 中:
import { Layout, Menu, Popconfirm, Button } from “antd”;
import”./index.scss”;
import {
PieChartOutlined,
SolutionOutlined,
FileWordOutlined,
LogoutOutlined,
} from “@ant-design/icons”;

const { Header, Sider, Content } = Layout;

const GeekLayout = () => {
return (


GEEK


} key=”1”>
数据面板

} key=”2”>
内容管理

} key=”3”>
发布文章





极客园自媒体端

{name}
placement=”bottomRight”
title=”您确认退出极客园自媒体端吗?”
okText=”确认”
cancelText=”取消”
>
}>
退出





内容



);
};

export default GeekLayout;
page/Layout/index.scss 中:
.geek-layout {
height: 100%;
overflow: hidden;
.ant-layout-header {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
}
.ant-menu.ant-menu-dark {
.ant-menu-item {
padding-left: 50px;
}
.ant-menu-item-selected {
background: #048;
}
}
.ant-layout-content {
height: 100%;
overflow-y: auto;
}
.logo {
width: 100%;
height: 64px;
color: #f8f8f8;
font-size: 30px;
text-align: center;
text-shadow: 3px 3px 5px #f8f8f8;
line-height: 64px;
letter-spacing: 0.2em;
transform: skew(-10deg);
margin-bottom: 20px;
user-select: none;
}
}

03-CSSModules介绍

目标:能够说出 CSSModules 如何解决组件之间的样式冲突问题
内容
参考文档:CSS Modules github
参考文档:React 脚手架使用 CSSModules

  • CSS Modules 即:CSS 模块,可以理解为对 CSS 进行模块化处理
  • 目的:为了在 React 开发时,解决组件之间类名重复导致的样式冲突问题
  • 使用 CSS Modules 前后的对比:
    • 使用前:自己手动为每个组件起一个唯一的类名
    • 使用后:自动生成类名,即使将来多人合作开发项目,也不会导致类名冲突
  • React 脚手架中为 CSSModules 自动生成的类名格式为:[filename]_[classname]__[hash]
    • filename:文件名称
    • classname:自己在 CSS 文件中写的类名
    • hash:随机生成的哈希值

/ GeekLayout 组件的 css 文件中:/
.header {}

/ React 项目中,CSS Modules 处理后生成的类名:/
.GeekLayout_header__adb4t {}

04-CSSModules使用

目标:能够在 React 项目中使用 CSSModules
内容

  1. CSS 文件名称以 .module.css 结尾的,此时,React 就会将其当做 CSSModules 来处理,比如,index.module.scss
  2. 如果不想使用 CSSModules 的功能,只需要让样式文件名称中不带.module 即可,比如,index.css

步骤

  1. 创建样式文件,名称格式为:index.module.scss
  2. 在 index.module.scss 文件中,按照原来的方式写 CSS 即可
  3. 在 JS 中通过 import styles from ‘./index.module.scss’ 来导入样式文件
  4. 在 JSX 结构中,通过 className={styles.类名} 形式来使用样式(此处的 类名 就是 CSS 中写的类名)

核心代码
// Login/index.module.css
.a {
color: red;
}

// Login/index.js
import styles from ‘./index.module.css’

// 对象中的属性 a 就是:我们自己写的类名
// 属性的值 就是:React 脚手架帮我们自动生成的一个类名,这个类名是随机生成的,所以,是全局唯一的!!!
// styles => { a: “Login_a__2O2Gg” }

const Login = () => {
return (


Login

)
}

export default Login

05-CSSModules规则

目标:能够说出为什么 CSSModules 中的类名推荐使用驼峰命名法
内容
使用 CSSModules 时,建议遵循以下 2 个规则:

  1. CSSModules 类名推荐使用驼峰命名法,这有利于在组件的 JS 代码中访问

/ index.mdouel.css /

/ 推荐使用 驼峰命名法 /
.a {
color: red;
}
.listItem {
font-size: 30px;
}

/ 不推荐使用 短横线(-)链接的形式 /
.list-item {
font-size: 30px;
}
import styles from ‘./index.module.css’

// 推荐:这样用起来更加访问



// 不推荐这种:写起来太繁琐了
//

// 错误的使用方式

不推荐

  1. 不推荐嵌套样式
    • 对于 CSS 来说,嵌套样式,很重要的一个目的就是提升 CSS 样式权重,避免样式冲突
    • 但是,CSSModules 生成的类名是全局唯一的,就不存在权重不够或者类名重复导致的样式冲突问题

      06-CSSModules全局样式

      目标:能够在 CSSModules 中使用全局样式
      内容
  • 在 *.module.css 文件中,类名都是“局部的”,也就是只在当前组件内生效
  • 有些特殊情况下,如果不想要让某个类名是局部的,就需要通过 :global() 来处理,处理后,这个类名就变为全局的了
  • 从代码上来看,全局的类名是不会被 CSSModules 处理的

/ 该类型会被 CSSModules 处理 /
.title {
color: yellowgreen;
}

/ 如果这个类名,不需要进行 CSSModules 处理,可以通过添加 :global() 来包裹 /
:global(.title) {
color: yellowgreen;
}

07-CSSModules配合SASS使用

目标:能够将 CSSModules 配合 SASS 使用
内容
推荐以下方式来将 CSSModules 配合 SASS 使用:

  • 每个组件的根节点使用 CSSModules 形式的类名( 根元素的类名: root )
  • 其他所有的子节点,都使用普通的 CSS 类名

这样处理的优势:解决组件间样式冲突问题的同时,让给组件添加样式尽量简单
.root {
// 根节点自己的样式

:global {
// 所有子节点的样式,都放在此处,因为是在 global 中,所以,此处的类名不会被 CSSModules 处理
.header {}
.logo {}
.user-info {}
}
}
组件中使用 CSSModules:
import styles from ‘./index.module.scss’

const GeekLayout = () => {
return (







)
}

08-嵌套路由配置

目标:能够在右侧内容区域展示左侧菜单对应的页面内容
分析说明
嵌套路由:由于 React 路由是组件,所以,组件写在哪就会在哪个地方渲染。因此,对于 Route 来说,根据实际需求放在相应的页面位置即可

  • 需要注意的是:由于嵌套路由展示的内容是放在某个父级路由中的,所以,要展示嵌套路由的前提就是先展示父级路由内容
  • 因此,嵌套路由的路径是基于父级路由路径的
  • 比如,数据面板是展示布局页面中的,所以内容管理的路由 /dashboard 就是在父级布局页面路由 /的基础上,添加了 /dashboard

步骤

  1. 在 pages 目录中,分别创建:Dashboard(数据概览)、Article(内容管理)、Publish(发布文章)、NotFound(404)页面文件夹
  2. 分别在四个文件夹中创建 index.js 并创建基础组件后导出
  3. 在 Layout 页面组件中,配置子路由
  4. 使用 Link 修改左侧菜单内容,与子路由规则匹配实现路由切换

核心代码
pages/Dashboard/index.js 中:
const Dashboard = () => {
return

Dashboard

}

export default Dashboard
pages/Layout/index.js 中:
import Dashboard from “../Dashboard”;
import Article from “../Article”;
import Publish from “../Publish”;
import NotFound from “../NotFound”;

// …


} key=”1”>
数据面板

} key=”2”>
内容管理

} key=”3”>
发布文章



// …


} />







总结

  1. 嵌套路由的路径有什么特点?
  2. 嵌套路由的路径可以和父级路由的路径完全相同吗?
  3. 如何在配置路由规则时指定路由参数?
  4. 如何让路由参数变为可选?
  5. 404页面如何配置?

    09-菜单高亮

    目标:能够在刷新页面时保持对应菜单高亮
    分析说明
    思路:将当前访问页面的路由地址作为 Menu 选中项的值(selectedKeys)即可
  • 注意:当我们点击菜单切换路由时,Layout 组件会重新渲染,因此,每次都可以拿到当前页面的路由地址

步骤

  1. 将 Menu 的 key 属性修改为与其对应的路由地址
  2. 获取到当前正在访问页面的路由地址
  3. 将当前路由地址设置为 selectedKeys 属性的值
  4. 处理动态路由有参数的情况

核心代码
pages/Layout/index.js 中:
import { useLocation } from ‘react-router-dom’

const GeekLayout = () => {
const location = useLocation()
// 激活菜单的key
let defaultKey = location.pathname;
if (defaultKey.startsWith(“/publish”)) {
defaultKey = “/publish”;
}

return (
// …


} key=”/dashboard”>
数据面板

} key=”/article”>
内容管理

} key=”/publish”>
发布文章


)
}
总结

  1. 通过哪个属性指定 Menu 组件的选中项?
  2. 如何做到切换页面时对应菜单高亮?

    10-展示个人信息

    目标:能够在布局页面右上角展示登录用户名
    步骤

  3. 在 Layout 组件中 dispatch 分发获取个人信息的异步 action

  4. 在 actions/user.js 中,创建异步 action 并获取个人信息
  5. 将接口返回的个人信息 dispatch 到 reducer 来存储该状态
  6. 在 reducers/user.js 中,处理个人信息的 action,将状态存储到 redux 中
  7. 在 Layout 组件中获取个人信息并展示

核心代码
pages/Layout/index.js 中:
import { useEffect } from ‘react’
import { useDispatch, useSelector } from ‘react-redux’
import { getUserInfo } from ‘@/store/actions’

const GeekLayout = () => {
const dispatch = useDispatch()
const user = useSelector(state => state.user)

useEffect(() => {
try {
dispatch(getUserInfo())
} catch {}
}, [dispatch])

render() {
return (
// …


{user.name}

// …
)
}
}
actions/user.js 中:
import { http } from ‘@/utils’

export const getUserInfo = () => {
return async (dispatch, getState) => {
const data = await http.get(‘/user/profile’, {
headers: {
Authorization: Bearer ${getState().user.token}
}
})
dispatch({ type: ‘user/getUserInfo’, payload: data.name })
}
}
reducers/user.js 中:
const user = (state = initialState, action) => {
switch (action.type) {
case ‘user/setToken’:
return {
…state,
name: action.payload
}
case ‘user/setName’:
return {
…state,
name: action.payload
}
default:
return state
}
}

export default user

11-退出登录

目标:能够实现退出功能步骤

  1. 为气泡确认框添加确认回调事件
  2. 在回调事件中,分发退出的异步 action
  3. 在异步 action 中删除本地 token,并且分发 action 来清空 redux 状态
  4. 清空用户信息
  5. 退出后,返回到登录页面

核心代码
pages/Layout/index.js 中:
import { useHistory } from ‘react-router-dom’

const GeekLayout = () => {
const history = useHistory()

const onLogout = () => {
dispatch(logout())
history.push(‘/login’)
}

render() {
return (
// …
title=”是否确认退出?”
okText=”退出”
cancelText=”取消”
onConfirm={onLogout}
>
退出

// …
)
}
}
actions/user.js 中:
import { clearToken } from ‘@/store’

export const logout = () => {
return (dispatch, getState) => {
clearToken()
// 清除 token 和 name
dispatch({ type: ‘login/setToken’, payload: ‘’ })
dispatch({ type: ‘user/setName’, payload: ‘’ })
}
}

12-统一添加token

目标:能够通过拦截器统一添加token
分析说明
因为不管是登录时,还是每次刷新页面时,已经将 token 存储在 redux 中了,
所以,可以直接通过 store.getState() 来获取到 redux 状态
步骤

  1. 导入 store
  2. 判断是否是登录请求
  3. 如果是,不做任何处理
  4. 如果不是,统一添加 Authorization 请求头

核心代码
utils/http.js 中:
import store from ‘@/store’

// 统一添加token在请求头
http.interceptors.request.use(config => {
// 对config进行修改,每次请求前做的事情
const state = store.getState()
if (state.user.token) {
config.headers.Authorization = Bearer ${state.user.token}
}
return config
}, e => Promise.reject(e))
actions/user.js 中:
const getUserInfo = () => {
return async dispatch => {
const res = await http.get(‘/user/profile’)
}
}
总结

  1. 如何在非组件环境下获取到 redux 状态?

    13-处理token失效

    目标:能够统一处理token失效重定向到登录页面
    分析说明
  • 目的:为了能够在非组件环境下拿到路由信息,进行路由跳转等操作,需要使用路由中提供的 Router 组件,并自定义 history 对象

// utils/history.js 中:

// 导入创建自定义 history 的函数:
import { createBrowserHistory } from ‘history’

// 创建自定义 history
const customHistory = createBrowserHistory()

export { customHistory }
// App.js 中:

import { Router } from ‘react-router-dom’

// 导入自定义的 history 对象
import { customHistory } from ‘@/utils’

const App = () => {
return (

// …

)
}

  • 然后,就可以在非组件环境下通过 customHistory 进行路由跳转等操作了。比如,在 http.js 中

import { customHistory } from ‘@/utils’

customHistory.push(‘/login’)

  • 何时使用 customHistory 进行路由跳转?
    1. 非组件环境中使用 customHistory
    2. 组件中,继续使用 useHistory hook

步骤

  1. 安装:yarn add history@4.10.1(固定版本)
  2. 创建 utils/history.js 文件
  3. 在该文件中,创建一个 hisotry 对象并导出
  4. 在 App.js 中导入 history 对象,并设置为 Router 的 history
  5. 通过响应拦截器处理 token 失效

核心代码
utils/history.js 中:
import { createBrowserHistory } from ‘history’

const customHistory = createBrowserHistory()

export { customHistory }
utils/index.js 中:
export from ‘./history’
App.js 中:
// 注意:此处,导入的是 Router 组件!
import { Router } from ‘react-router-dom’

import { customHistory } from ‘@/utils’

const App = () => {
return (

)
}
utils/http.js 中:
import { customHistory } from ‘./history’
import { logout } from ‘@/store/actions’

instance.interceptors.response.use(res => {
return res?.data?.data || res
}, e => {
if (e.response.status === 401) {
message.error(‘登录失效’)
store.dispatch(logout())
// 防止跳转login的时候接口才处理401
if ( customHistory.location.pathname !== ‘/login’) {
customHistory.push({
pathname: ‘/login’,
state: { from: customHistory.location.pathname }
})
}
}
Promise.reject(e)
})
*总结

  1. 如何在非组件环境下实现路由跳转?
  2. 使用自定义 history 时,需要使用哪个路由组件?

    14-数据面板组件&404组件

    目标:能够渲染首页
    步骤

  3. 将 dashboard.png 拷贝到 assets 目录中

  4. 为 Dashboard组件添加背景样式

核心代码
pages/Dashboard/index.js 中:
import styles from ‘./index.module.scss’

const Dashboard = () => {
return

;
};

export default Dashboard;
pages/Dashboard/index.module.scss 中:
.root {
width: 100%;
height: 100%;
background: url(../../assets/dashboard.png) no-repeat center top / 100% auto;
}
pages/NotFound/index.js
import { Result, Button } from “antd”;
import { useHistory } from “react-router-dom”;
const NotFound = () => {
const history = useHistory();
return (
style={{ paddingTop: 100 }}
status=”404”
title=”404”
subTitle=”Sorry, the page you visited does not exist.”
extra={

}
/>
);
};
export default NotFound;