一、使用 CRA 创建项目
javascript
npx create-react-app name
二、按照业务规范整理项目目录 (重点src目录)
三、安装插件
javascript
npm install sass -D
npm install antd --save
npm install react-router-dom
四、配置基础路由 Router
-
安装路由包 react-router-dom
-
准备两个基础路由组件 Layout 和 Login
-
在 router/index.js 文件中引入组件进行路由配置,导出 router 实例
-
在入口文件中渲染 <RouterProvider />,传入 router 实例
router/index.js
javascript
import { createBrowserRouter } from "react-router-dom";
// 配置路由实例
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
},
{
path: "/login",
element: <Login />,
},
]);
export default router;
javascript
import { RouterProvider } from "react-router-dom";
import router from "./router";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<RouterProvider router={router} />);
五、登录
使用 AntD 现成的组件 创建登录页的内容结构

主要组件:Card、Form、Input、Button
html
<div>
<Card>
<img />
{/* 登录表单 */}
<Form>
<Form.Item>
<Input />
</Form.Item>
<Form.Item>
<Input />
</Form.Item>
</Form>
</Card>
</div>
1. 表单校验实现

表单校验可以在提交登录之前,校验用户的输入是否符合预期,如果不符合就阻止提交,显示错误信息
javascript
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: '请输入用户名!' }]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
增加失焦时校验
html
<Form validateTrigger="onBlur">
...
</Form>
手机号为有效格式
html
<Form.Item
name="mobile"
// 多条校验逻辑 先校验第一条 第一条通过之后再校验第二条
rules={[
{
required: true,
message: '请输入手机号',
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式'
}
]}>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
2. 获取表单数据
当用户输入了正确的表单内容,点击确认按钮时,需要收集用户输入的内容,用来提交接口请求
解决方案:给 Form 组件绑定 onFinish 回调函数,通过回调函数的参数获取用户输入的内容
javascript
const onFinish = async (values) => {
console.log(values)
}
<Form onFinish={onFinish} validateTrigger="onBlur">
...
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
3. 封装 request 请求模块
在整个项目中会发送很多网络请求,使用 axios 三方库做好统一封装,方便统一管理和复用
javascript
npm i axios
utils/request.js
javascript
// axios的封装处理
import axios from "axios"
// 1. 根域名配置
// 2. 超时时间
// 3. 请求拦截器 / 响应拦截器
const request = axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {
return config
}, (error) => {
return Promise.reject(error)
})
// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export { request }
utils/index.js
javascript
// 统一中转工具模块函数
// import {request} from '@/utils'
import { request } from './request'
export {
request,
}
4. 使用 redux 管理 token
Token 作为一个用户的标识数据,需要在很多个模块中共享,Redux 可以方便的解决共享问题
(1)redux 中编写获取 Token 的 异步获取和同步修改
(2)Login 组件负责提交 action 并且把表单数据传递过来
javascript
npm i react-redux @reduxjs/toolkit
store/modules/user.js
javascript
// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken } from '@/utils'
const userStore = createSlice({
name: "user",
// 数据状态
initialState: {
token: getToken() || '',
},
// 同步修改方法
reducers: {
setToken (state, action) {
state.token = action.payload
_setToken(action.payload)
},
}
})
// 解构出actionCreater
const { setToken } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
// 1. 发送异步请求
const res = await request.post('authorizations', loginForm)
// 2. 提交同步 action 进行 token 存入
dispatch(setToken(res.data.token))
}
}
export { fetchLogin, setToken }
export default userReducer
store/index.js
javascript
// 组合redux子模块 + 导出store实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
reducer: {
user: userReducer
}
})
index.js
javascript
import { RouterProvider } from 'react-router-dom'
import router from './router'
import { Provider } from 'react-redux'
import store from './store'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
)
封装 localStorage - Token 持久化
现存问题:
Redux 存入 Token 之后如果刷新浏览器,Token 会丢失(持久化就是防止刷新时丢失 Token)
问题原因:
Redux 是基于浏览器内存的储存方式,刷新时状态恢复为初始值
utils/token.js
javascript
// 封装基于ls存取删三个方法
const TOKENKEY = 'token_key'
function setToken (token) {
return localStorage.setItem(TOKENKEY, token)
}
function getToken () {
return localStorage.getItem(TOKENKEY)
}
function removeToken () {
return localStorage.removeItem(TOKENKEY)
}
export {
setToken,
getToken,
removeToken
}
utils/index.js
javascript
// 统一中转工具模块函数
// import {request} from '@/utils'
import { request } from './request'
import { setToken, getToken, removeToken } from './token'
export {
request,
setToken,
getToken,
removeToken
}
pages/login/index.js
javascript
import { useDispatch } from 'react-redux'
import { fetchLogin } from '@/store/modules/user'
import { useNavigate } from 'react-router-dom'
const Login = () => {
const dispatch = useDispatch()
const navigate = useNavigate()
const onFinish = async (values) => {
console.log(values)
// 触发异步action fetchLogin
await dispatch(fetchLogin(values))
// 1. 跳转到首页
navigate('/')
// 2. 提示一下用户
message.success('登录成功')
}
return (...)
}
5. Axios 请求拦截器注入 Token
Token 作为用户的一个标识数据,后端很多接口都会以它作为接口权限判断的依据;请求拦截器注入 Token 之后,所有用到 Axios 实例的接口请求都自动携带了 Token
utils/request.js
javascript
import { getToken } from "./token"
// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {
// 操作这个config 注入token数据
// 1. 获取到token
// 2. 按照后端的格式要求做token拼接
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, (error) => {
return Promise.reject(error)
})
6. 使用 Token 做路由权限控制
有些路由页面的内容信息比较敏感,如果用户没有经过登录获取到有效 Token,是没有权限跳转的,根据 Token 的有无控制当前路由是否可以跳转,就是路由的权限控制
components/AuthRoute.js
javascript
// 封装高阶组件
// 核心逻辑: 有token 正常跳转 无token 去登录
import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
export function AuthRoute ({ children }) {
const token = getToken()
if (token) {
return <>{children}</>
} else {
return <Navigate to={'/login'} replace />
}
}
router/index.js
javascript
import { createBrowserRouter } from 'react-router-dom'
import { AuthRoute } from '@/components/AuthRoute'
// 配置路由实例
const router = createBrowserRouter([
{
path: "/",
element: <AuthRoute> <Layout /></AuthRoute>,
},
{
path: "/login",
element: <Login />
}
])
export default router
六、Layout
1. 样式初始化
样式 reset
javascript
npm install normalize.css
index.js
javascript
import 'normalize.css'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
)
index.scss
javascript
html,
body {
margin: 0;
height: 100%;
}
#root {
height: 100%;
}
2. 二级路由配置

(1)准备三个二级路由
(2)router 中通过 children 配置项进行配置
(3)Layout 组件中配置二级路由出口
router/index.js
javascript
const router = createBrowserRouter([
{
path: "/",
element: <AuthRoute> <Layout /></AuthRoute>,
children: [
{
index: true,
element: <Home />
},
{
path: 'article',
element: <Article />
},
{
path: 'publish',
element: <Publish />
}
]
},
{
path: "/login",
element: <Login />
}
])
export default router
pages/Layout/index.js
javascript
<Layout>
<Header>
...
</Header>
<Layout>
<Sider>
<Menu></Menu>
</Sider>
<Layout style={{ padding: 20 }}>
{/* 二级路由的出口 */}
<Outlet />
</Layout>
</Layout>
</Layout>
3. 点击菜单跳转路由
实现效果:点击左侧菜单可以跳转到对应的目标路由
思路分析:
(1)左侧菜单要和路由形成一一对应的关系
(2)点击时拿到路由路径,调用路由方法跳转(跳转到对应的路由下面)
具体操作:
(1)菜单参数 Item 中 key 属性换成路由的路由地址
(2)点击菜单时通过 key 获取路由地址跳转
pages/Layout/index.js
javascript
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate()
const onMenuClick = (route) => {
const path = route.key
navigate(path)
}
return (
<Layout>
<Header className="header">
...
</Header>
<Layout>
<Sider>
<Menu
mode="inline"
theme="dark"
defaultSelectedKeys={['1']}
onClick={onMenuClick}
items={items}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout>
{/* 二级路由的出口 */}
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout
4. 根据当前路由路径高亮菜单
实现效果:页面在刷新时可以根据当前的路由路径让对应的左侧菜单高亮显示
思路分析;
(1)获取当前 url 上的路由路径
(2)找到菜单组件负责高亮的属性,绑定当前的路由路径
javascript
// 反向高亮
// 1. 获取当前路由路径
const location = useLocation()
console.log(location.pathname)
const selectedkey = location.pathname
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
// 修改
selectedKeys={selectedkey}
onClick={onMenuClick}
items={items}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
5. 展示个人信息
关键问题:用户信息应该放到哪里维护?
和 Token 令牌类似,用户的信息通常很有可能在多个组件中都需要共享使用,所以同样应该放到Redux 中维护
(1)使用 Redux 进行信息管理
(2)Layout 组件中提交 action
(3)Layout 组件中完成渲染
store/modules/user.js
javascript
// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken, removeToken } from '@/utils'
import { loginAPI, getProfileAPI } from '@/apis/user'
const userStore = createSlice({
name: "user",
// 数据状态
initialState: {
token: getToken() || '',
userInfo: {}
},
// 同步修改方法
reducers: {
setToken (state, action) {
state.token = action.payload
_setToken(action.payload)
},
setUserInfo (state, action) {
state.userInfo = action.payload
},
}
})
// 解构出actionCreater
const { setToken, setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setToken(res.data.token))
}
}
// 获取个人用户信息异步方法
const fetchUserInfo = () => {
return async (dispatch) => {
const res = await request.get('/user/profile')
dispatch(setUserInfo(res.data))
}
}
export { fetchLogin, fetchUserInfo }
export default userReducer
pages/Layout/index.js
javascript
// 触发个人用户信息action
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchUserInfo())
}, [dispatch])
const name = useSelector(state => state.user.userInfo.name)
<span className="user-name">{name}</span>
6. 退出登录
(1)提示用户是否确认要退出(危险操作,二次确认)
(2)用户确认之后清除用户信息(Token 以及其他个人信息)
(3)跳转到登录页(为下次登录做准备)
pages/Layout/index.js
javascript
// 退出登录确认回调
const onConfirm = () => {
console.log('确认退出')
dispatch(clearUserInfo())
navigate('/login')
}
...
<span className="user-logout">
<Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={onConfirm}>
<LogoutOutlined /> 退出
</Popconfirm>
</span>
store/modules/user.js
javascript
// 同步修改方法
reducers: {
setToken (state, action) {
state.token = action.payload
_setToken(action.payload)
},
setUserInfo (state, action) {
state.userInfo = action.payload
},
clearUserInfo (state) {
state.token = ''
state.userInfo = {}
removeToken()
}
}
})
7. 处理 Token 失效
什么是 Token 失效?
为了用户的安全和隐私考虑,在用户长时间未在网络中做出任何操作且规定的失效时间到达之后,当前的 Token 就会失效。一旦失效,不能再作为用户令牌标识请求隐私数据
前端如何知道 Token 已经失效了?
通常在 Token 失效之后再去请求接口,后端会返回401状态码,前端可以监控这个状态,做后续的操作
Token 失效了前端做什么?
(1)在 axios 拦截中监控 401 状态码
(2)清除失效 Token, 跳转登录
utils/request.js
javascript
import router from "@/router"
// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
// 监控 401 token失效
console.dir(error)
if (error.response.status === 401) {
removeToken()
router.navigate('/login')
// 强制刷新
window.location.reload()
}
return Promise.reject(error)
})
七、Home
1. Echarts 基础图表渲染
三方图表插件如何在项目中快速使用起来?
(1)按照图表插件文档中的"快速开始",快速跑起来 Demo
(2)按照业务需求修改配置项做定制处理
javascript
npm install echarts
pages/Home/index.js
javascript
// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
const Home = () => {
const chartRef = useRef(null)
useEffect(() => {
// 保证dom可用 才进行图表的渲染
// 1. 获取渲染图表的dom节点
const chartDom = chartRef.current
// 2. 图表初始化生成图表实例对象
const myChart = echarts.init(chartDom)
// 3. 准备图表参数
const option = {
title: {
text: title
},
xAxis: {
type: 'category',
data: ['Vue', 'React', 'Angular']
},
yAxis: {
type: 'value'
},
series: [
{
data: [10, 40, 70],
type: 'bar'
}
]
}
// 4. 使用图表参数完成图表的渲染
option && myChart.setOption(option)
}, [title])
return (
<div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
)
}
export default Home
2. Echarts 组件封装实现
pages/Home/index.js
javascript
import BarChart from "./components/BarChart"
const Home = () => {
return (
<div>
<BarChart title={'三大框架满意度'} />
<BarChart title={'三大框架使用度'} />
</div>
)
}
export default Home
pages/Home/components/BarCharts.js
javascript
// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
// 1. 把功能代码都放到这个组件中
// 2. 把可变的部分抽象成prop参数
const BarChart = ({ title }) => {
const chartRef = useRef(null)
useEffect(() => {
// 保证dom可用 才进行图表的渲染
// 1. 获取渲染图表的dom节点
const chartDom = chartRef.current
// 2. 图表初始化生成图表实例对象
const myChart = echarts.init(chartDom)
// 3. 准备图表参数
const option = {
title: {
text: title
},
xAxis: {
type: 'category',
data: ['Vue', 'React', 'Angular']
},
yAxis: {
type: 'value'
},
series: [
{
data: [10, 40, 70],
type: 'bar'
}
]
}
// 4. 使用图表参数完成图表的渲染
option && myChart.setOption(option)
}, [title])
return <div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
}
export default BarChart
八、拓展 - API 模块封装
现存问题:
当前的接口请求放到了功能实现的位置,没有在固定的模块内维护,后期查找维护困难
解决思路:
把项目中的所有接口按照业务模块以函数的形式统一封装到 apis 模块中
apis/user.js
javascript
// 用户相关的所有请求
import { request } from "@/utils"
// 1. 登录请求
export function loginAPI (formData) {
return request({
url: '/authorizations',
method: 'POST',
data: formData
})
}
// 2. 获取用户信息
export function getProfileAPI () {
return request({
url: '/user/profile',
method: 'GET'
})
}
store/modules/user.js
javascript
import { loginAPI, getProfileAPI } from '@/apis/user'
// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await loginAPI(loginForm)
dispatch(setToken(res.data.token))
}
}
// 获取个人用户信息异步方法
const fetchUserInfo = () => {
return async (dispatch) => {
const res = await getProfileAPI()
dispatch(setUserInfo(res.data))
}
}