数据面板
01-路由鉴权
目标:能够实现未登录时访问拦截并跳转到登录页面
分析说明:
- 实现思路:自己封装 AuthRoute 路由鉴权组件,实现未登录拦截,并跳转到登录页面
- 核心点1:
- AuthRoute 组件的用法应该与 Route 组件完全一致,并且 AuthRoute 组件也能够实现路由配置功能
- 所以,我们要封装的 AuthRoute 组件就是对 Route 组件的封装,并同时实现了鉴权功能
- 核心点2:
- 分别对登录或未登录,进行相应处理
// Route 用法:
// AuthRoute 用法:
步骤:
- 在 components 目录中,创建 AuthRoute.jsx 文件
- 判断是否登录
- 登录时,直接渲染相应页面组件
- 未登录时,重定向到登录页面
- 将需要鉴权的页面路由配置,替换为 AuthRoute 组件
核心代码:
components/AuthRoute.jsx 中:
import { Route, Redirect } from ‘react-router-dom’
import { getToken } from ‘@/utils’
const AuthRoute = ({ component: Component, …rest }) => {
return (
render={props => {
// 判断是否登录
if (!getToken()) {
// 未登录
return (
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布局组件搭建基础布局步骤:
- 打开 antd/Layout 布局组件文档
- 拷贝示例代码到我们的 Layout 页面中
- 分析并调整页面布局
核心代码:
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 (
} key=”1”>
数据面板
内容管理
发布文章
极客园自媒体端
{name}
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
内容:
- CSS 文件名称以 .module.css 结尾的,此时,React 就会将其当做 CSSModules 来处理,比如,index.module.scss
- 如果不想使用 CSSModules 的功能,只需要让样式文件名称中不带.module 即可,比如,index.css
步骤:
- 创建样式文件,名称格式为:index.module.scss
- 在 index.module.scss 文件中,按照原来的方式写 CSS 即可
- 在 JS 中通过 import styles from ‘./index.module.scss’ 来导入样式文件
- 在 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 个规则:
- CSSModules 类名推荐使用驼峰命名法,这有利于在组件的 JS 代码中访问
/ index.mdouel.css /
/ 推荐使用 驼峰命名法 /
.a {
color: red;
}
.listItem {
font-size: 30px;
}
/ 不推荐使用 短横线(-)链接的形式 /
.list-item {
font-size: 30px;
}
import styles from ‘./index.module.css’
// 推荐:这样用起来更加访问
// 不推荐这种:写起来太繁琐了
// // 错误的使用方式
不推荐
- 不推荐嵌套样式
- 在 *.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
步骤:
- 在 pages 目录中,分别创建:Dashboard(数据概览)、Article(内容管理)、Publish(发布文章)、NotFound(404)页面文件夹
- 分别在四个文件夹中创建 index.js 并创建基础组件后导出
- 在 Layout 页面组件中,配置子路由
- 使用 Link 修改左侧菜单内容,与子路由规则匹配实现路由切换
核心代码:
pages/Dashboard/index.js 中:
const Dashboard = () => {
return
}
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”>
数据面板
内容管理
发布文章
// …
总结:
- 嵌套路由的路径有什么特点?
- 嵌套路由的路径可以和父级路由的路径完全相同吗?
- 如何在配置路由规则时指定路由参数?
- 如何让路由参数变为可选?
- 404页面如何配置?
09-菜单高亮
目标:能够在刷新页面时保持对应菜单高亮
分析说明:
思路:将当前访问页面的路由地址作为 Menu 选中项的值(selectedKeys)即可
- 注意:当我们点击菜单切换路由时,Layout 组件会重新渲染,因此,每次都可以拿到当前页面的路由地址
步骤:
- 将 Menu 的 key 属性修改为与其对应的路由地址
- 获取到当前正在访问页面的路由地址
- 将当前路由地址设置为 selectedKeys 属性的值
- 处理动态路由有参数的情况
核心代码:
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 (
// …
数据面板
内容管理
发布文章
)
}
总结:
- 通过哪个属性指定 Menu 组件的选中项?
-
10-展示个人信息
目标:能够在布局页面右上角展示登录用户名
步骤: 在 Layout 组件中 dispatch 分发获取个人信息的异步 action
- 在 actions/user.js 中,创建异步 action 并获取个人信息
- 将接口返回的个人信息 dispatch 到 reducer 来存储该状态
- 在 reducers/user.js 中,处理个人信息的 action,将状态存储到 redux 中
- 在 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-退出登录
目标:能够实现退出功能步骤:
- 为气泡确认框添加确认回调事件
- 在回调事件中,分发退出的异步 action
- 在异步 action 中删除本地 token,并且分发 action 来清空 redux 状态
- 清空用户信息
- 退出后,返回到登录页面
核心代码:
pages/Layout/index.js 中:
import { useHistory } from ‘react-router-dom’
const GeekLayout = () => {
const history = useHistory()
const onLogout = () => {
dispatch(logout())
history.push(‘/login’)
}
render() {
return (
// …
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 状态
步骤:
- 导入 store
- 判断是否是登录请求
- 如果是,不做任何处理
- 如果不是,统一添加 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’)
}
}
总结:
- 目的:为了能够在非组件环境下拿到路由信息,进行路由跳转等操作,需要使用路由中提供的 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 进行路由跳转?
- 非组件环境中使用 customHistory
- 组件中,继续使用 useHistory hook
步骤:
- 安装:yarn add history@4.10.1(固定版本)
- 创建 utils/history.js 文件
- 在该文件中,创建一个 hisotry 对象并导出
- 在 App.js 中导入 history 对象,并设置为 Router 的 history
- 通过响应拦截器处理 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)
})
*总结:
核心代码:
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 (
status=”404”
title=”404”
subTitle=”Sorry, the page you visited does not exist.”
extra={
}
/>
);
};
export default NotFound;