本节目标:
了解项目的定位和功能
create-react-app
antd
v4axios
react-router-dom
以及 history
react-quill
sass
本节目标:
能够基于脚手架搭建项目基本结构
实现步骤
使用create-react-app生成项目 npx create-react-app geek-pc
进入根目录 cd geek-pc
启动项目 yarn start
调整项目目录结构
/src
/assets 项目资源文件,比如,图片 等
/components 通用组件
/pages 页面
/store mobx 状态仓库
/utils 工具,比如,token、axios 的封装等
App.js 根组件
index.css 全局样式
index.js 项目入口
保留核心代码
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
,
document.getElementById('root')
)
src/App.js
export default function App() {
return 根组件
}
本节目标:
能够将项目推送到gitee远程仓库
实现步骤
git init
git add .
git commit -m '项目初始化'
git remote add origin [gitee 仓库地址]
git push origin master -u
本节目标:
能够在CRA中使用sass书写样式
SASS
是一种预编译的 CSS,作用类似于 Less。由于 React 中内置了处理 SASS 的配置,所以,在 CRA 创建的项目中,可以直接使用 SASS 来写样式
实现步骤
安装解析 sass 的包:yarn add sass -D
创建全局样式文件:index.scss
body {
margin: 0;
}
#root {
height: 100%;
}
本节目标:
能够配置登录页面的路由并显示到页面中
实现步骤
yarn add react-router-dom
代码实现
pages/Login/index.js
const Login = () => {
return login
}
export default Login
pages/Layout/index.js
const Layout = () => {
return layout
}
export default Layout
app.js
// 导入路由
import { BrowserRouter, Route, Routes } from 'react-router-dom'
// 导入页面组件
import Login from './pages/Login'
import Layout from './pages/Layout'
// 配置路由规则
function App() {
return (
}/>
}/>
)
}
export default App
本节目标:
能够使用antd的Button组件渲染按钮
实现步骤
yarn add antd
代码实现
src/index.js
// 先导入 antd 样式文件
// https://github.com/ant-design/ant-design/issues/33327
import 'antd/dist/antd.min.css'
// 再导入全局样式文件,防止样式覆盖!
import './index.css'
pages/Login/index.js
import { Button } from 'antd'
const Login = () => (
)
易错总结
本节目标:
能够配置@路径简化路径处理
自定义 CRA 的默认配置
craco 配置文档
- CRA 将所有工程化配置,都隐藏在了
react-scripts
包中,所以项目中看不到任何配置信息- 如果要修改 CRA 的默认配置,有以下几种方案:
- 通过第三方库来修改,比如,
@craco/craco
(推荐)- 通过执行
yarn eject
命令,释放react-scripts
中的所有配置到项目中
实现步骤
yarn add -D @craco/craco
craco.config.js
,并在配置文件中配置路径别名package.json
中的脚本命令@
来表示 src 目录的绝对路径代码实现
craco.config.js
const path = require('path')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
}
}
}
package.json
// 将 start/build/test 三个命令修改为 craco 方式
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
本节目标:
能够让vscode识别@路径并给出路径提示
实现步骤
jsconfig.json
配置文件代码实现
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
vscode会自动读取jsconfig.json
中的配置,让vscode知道@就是src目录
https://gitee.com/react-cp/react-pc-doc 这里找到dev-tools.crx文件
本节目标:
能够使用antd搭建基础布局
实现步骤
代码实现
pages/Login/index.js
import { Card } from 'antd'
import logo from '@/assets/logo.png'
import './index.scss'
const Login = () => {
return (
{/* 登录表单 */}
)
}
export default Login
pages/Login/index.scss
.login {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: center/cover url('~@/assets/login.png');
.login-logo {
width: 200px;
height: 60px;
display: block;
margin: 0 auto 20px;
}
.login-container {
width: 440px;
height: 360px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 50px rgb(0 0 0 / 10%);
}
.login-checkbox-label {
color: #1890ff;
}
}
本节目标:
能够使用antd的Form组件创建登录表单
实现步骤
<>
(显示代码),并拷贝代码到组件中代码实现
pages/Login/index.js
import { Form, Input, Button, Checkbox } from 'antd'
const Login = () => {
return (
我已阅读并同意「用户协议」和「隐私条款」
)
}
本节目标:
能够为手机号和密码添加表单校验
实现步骤
validateTrigger
属性,指定校验触发时机的集合rules
属性,用来添加表单校验代码实现
page/Login/index.js
const Login = () => {
return (
我已阅读并同意「用户协议」和「隐私条款」
)
}
本节目标:
能够拿到登录表单中用户的手机号码和验证码
实现步骤
onFinish
属性,该事件会在点击登录按钮时触发initialValues
属性,来初始化表单值代码实现
pages/Login/index.js
// 点击登录按钮时触发 参数values即是表单输入数据
const onFinish = values => {
console.log(values)
}
本节目标:
封装axios,简化操作
实现步骤
代码实现
utils/http.js
import axios from 'axios'
const http = axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
// 添加请求拦截器
http.interceptors.request.use((config)=> {
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器
http.interceptors.response.use((response)=> {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response
}, (error)=> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export { http }
utils/index.js
import { http } from './http'
export { http }
本节目标:
基于mobx封装管理用户登录的store
store/login.Store.js
// 登录模块
import { makeAutoObservable } from "mobx"
import { http } from '@/utils'
class LoginStore {
token = ''
constructor() {
makeAutoObservable(this)
}
// 登录
login = async ({ mobile, code }) => {
const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
mobile,
code
})
this.token = res.data.token
}
}
export default LoginStore
store/index.js
import React from "react"
import LoginStore from './login.Store'
class RootStore {
// 组合模块
constructor() {
this.loginStore = new LoginStore()
}
}
// 导入useStore方法供组件使用数据
const StoresContext = React.createContext(new RootStore())
export const useStore = () => React.useContext(StoresContext)
本节目标:
在表单校验通过之后通过封装好的store调用登录接口
实现步骤
代码实现
import { useStore } from '@/store'
const Login = () => {
// 获取跳转实例对象
const navigate = useNavigate()
const { loginStore } = useStore()
const onFinish = async values => {
const { mobile, code } = values
try {
await loginStore.login({ mobile, code })
navigate('/')
} catch (e) {
message.error(e.response?.data?.message || '登录失败')
}
}
return (...)
}
本节目标:
能够统一处理 token 的持久化相关操作
实现步骤
代码实现
utils/token.js
const TOKEN_KEY = 'itcast_geek_pc'
const getToken = () => localStorage.getItem(TOKEN_KEY)
const setToken = token => localStorage.setItem(TOKEN_KEY, token)
const clearToken = () => localStorage.removeItem(TOKEN_KEY)
export { getToken, setToken, clearToken }
本节目标:
使用token函数持久化配置
实现步骤
代码实现
store/login.Store.js
// 登录模块
import { makeAutoObservable } from "mobx"
import { setToken, getToken, clearToken, http } from '@/utils'
class LoginStore {
// 这里哦!!
token = getToken() || ''
constructor() {
makeAutoObservable(this)
}
// 登录
login = async ({ mobile, code }) => {
const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
mobile,
code
})
this.token = res.data.token
// 还有这里哦!!
setToken(res.data.token)
}
}
export default LoginStore
本节目标:
把token通过请求拦截器注入到请求头中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HYd0RfEu-1657264065747)(assets/token.png)]
拼接方式:config.headers.Authorization =
Bearer ${token}}
utils/http.js
http.interceptors.request.use(config => {
// if not login add token
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
本节目标:
能够实现未登录时访问拦截并跳转到登录页面
实现思路
自己封装
AuthRoute
路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面思路为:判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login
实现步骤
代码实现
components/AuthRoute/index.js
// 1. 判断token是否存在
// 2. 如果存在 直接正常渲染
// 3. 如果不存在 重定向到登录路由
// 高阶组件:把一个组件当成另外一个组件的参数传入 然后通过一定的判断 返回新的组件
import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
function AuthRoute ({ children }) {
const isToken = getToken()
if (isToken) {
return <>{children}>
} else {
return
}
}
//
// 登录:<> >
// 非登录:
export {
AuthRoute
}
src/app.js
import { Router, Route } from 'react-router-dom'
import { AuthRoute } from '@/components/AuthRoute'
import Layout from '@/pages/Layout'
import Login from '@/pages/Login'
function App() {
return (
{/* 需要鉴权的路由 */}
} />
{/* 不需要鉴权的路由 */}
} />
)
}
export default App
本节目标:
能够使用antd搭建基础布局
实现步骤
代码实现
pages/Layout/index.js
import { Layout, Menu, Popconfirm } from 'antd'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined
} from '@ant-design/icons'
import './index.scss'
const { Header, Sider } = Layout
const GeekLayout = () => {
return (
user.name
退出
内容
)
}
export default GeekLayout
pages/Layout/index.scss
.ant-layout {
height: 100%;
}
.header {
padding: 0;
}
.logo {
width: 200px;
height: 60px;
background: url('~@/assets/logo.png') no-repeat center / 160px auto;
}
.layout-content {
overflow-y: auto;
}
.user-info {
position: absolute;
right: 0;
top: 0;
padding-right: 20px;
color: #fff;
.user-name {
margin-right: 20px;
}
.user-logout {
display: inline-block;
cursor: pointer;
}
}
.ant-layout-header {
padding: 0 !important;
}
本节目标:
能够在右侧内容区域展示左侧菜单对应的页面内容
使用步骤
代码实现
pages/Home/index.js
const Home = () => {
return Home
}
export default Home
pages/Article/index.js
const Article = () => {
return Article
}
export default Article
pages/Publish/index.js
const Publish = () => {
return Publish
}
export default Publish
app.js
}>
{/* 二级路由默认页面 */}
} />
} />
} />
}>
pages/Layout/index.js
// 配置Link组件
// 二级路由对应显示
{/* 二级路由默认页面 */}
本节目标:
能够在页面刷新的时候保持对应菜单高亮
思路
Menu组件的selectedKeys属性与Menu.Item组件的key属性发生匹配的时候,Item组件即可高亮
页面刷新时,将
当前访问页面的路由地址
作为 Menu 选中项的值(selectedKeys)即可
实现步骤
key
属性修改为与其对应的路由地址selectedKeys
属性的值代码实现
pages/Layout/index.js
import { useLocation } from 'react-router-dom'
const GeekLayout = () => {
const location = useLocation()
// 这里是当前浏览器上的路径地址
const selectedKey = location.pathname
return (
// ...
)
}
本节目标:
能够在页面右上角展示登录用户名
实现步骤
代码实现
store/user.Store.js
// 用户模块
import { makeAutoObservable } from "mobx"
import { http } from '@/utils'
class UserStore {
userInfo = {}
constructor() {
makeAutoObservable(this)
}
async getUserInfo() {
const res = await http.get('/user/profile')
this.userInfo = res.data
}
}
export default UserStore
store/index.js
import React from "react"
import LoginStore from './login.Store'
import UserStore from './user.Store'
class RootStore {
// 组合模块
constructor() {
this.loginStore = new LoginStore()
this.userStore = new UserStore()
}
}
const StoresContext = React.createContext(new RootStore())
export const useStore = () => React.useContext(StoresContext)
pages/Layout/index.js
import { useEffect } from 'react'
import { observer } from 'mobx-react-lite'
const GeekLayout = () => {
const { userStore } = useStore()
// 获取用户数据
useEffect(() => {
try {
userStore.getUserInfo()
} catch { }
}, [userStore])
return (
{userStore.userInfo.name}
{/* 省略无关代码 */}
)
}
export default observer(GeekLayout)
本节目标:
能够实现退出登录功能
实现步骤
store/login.Store.js
中新增退出登录的action函数,在其中删除token代码实现
store/login.Store.js
class LoginStore {
// 退出登录
loginOut = () => {
this.token = ''
clearToken()
}
}
export default LoginStore
pages/Layout/index.js
// login out
const navigate = useNavigate()
const onLogout = () => {
loginStore.loginOut()
navigate('/login')
}
退出
本节目标:
能够在响应拦截器中处理token失效
说明:为了能够在非组件环境下拿到路由信息,需要我们安装一个history包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FM82Evvt-1657264065748)(assets/historyoutside.png)]
实现步骤
yarn add history
utils/history.js
文件代码实现
utils/history.js
// https://github.com/remix-run/react-router/issues/8264
import { createBrowserHistory } from 'history'
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom'
const history = createBrowserHistory()
export {
HistoryRouter,
history
}
app.js
import { HistoryRouter, history } from './utils/history'
function App() {
return (
...省略无关代码
)
}
export default App
utils/http.js
import { history } from './history'
http.interceptors.response.use(
response => {
return response.data
},
error => {
if (error.response.status === 401) {
// 删除token
clearToken()
// 跳转到登录页
history.push('/login')
}
return Promise.reject(error)
}
)
本节目标:
实现首页echart图表封装展示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yeyIGoia-1657264065748)(assets/home.png)]
需求描述:
代码实现
components/Bar/index.js
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
function echartInit (node, xData, sData, title) {
const myChart = echarts.init(node)
// 绘制图表
myChart.setOption({
title: {
text: title
},
tooltip: {},
xAxis: {
data: xData
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: sData
}
]
})
}
function Bar ({ style, xData, sData, title }) {
// 1. 先不考虑传参问题 静态数据渲染到页面中
// 2. 把那些用户可能定制的参数 抽象props (1.定制大小 2.data 以及说明文字)
const nodeRef = useRef(null)
useEffect(() => {
echartInit(nodeRef.current, xData, sData, title)
}, [xData, sData])
return (
)
}
export default Bar
pages/Home/index.js
import Bar from "@/components/Bar"
import './index.scss'
const Home = () => {
return (
)
}
export default Home
pages/Home/index.scss
.home {
width: 100%;
height: 100%;
align-items: center;
}
本节目标:
能够使用antd组件库搭建筛选区域结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4SxhpvU-1657264065748)(assets/search.png)]
重点关注
如何让RangePicker日期范围选择框选择中文
Select组件配合Form.Item使用时,如何配置默认选中项
代码实现
import { Link } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import 'moment/locale/zh-cn'
import locale from 'antd/es/date-picker/locale/zh_CN'
import './index.scss'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
return (
首页
内容管理
}
style={{ marginBottom: 20 }}
>
全部
草稿
待审核
审核通过
审核失败
{/* 传入locale属性 控制中文显示*/}
)
}
export default Article
本节目标:
能够基于Table组件搭建表格结构
重点关注
- 通过哪个属性指定Table组件的列信息
- 通过哪个属性指定Table数据
- 通过哪个属性指定Table列表用到的key属性
代码实现
import { Link } from 'react-router-dom'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import img404 from '@/assets/error.png'
const Article = () => {
const columns = [
{
title: '封面',
dataIndex: 'cover',
width:120,
render: cover => {
return
}
},
{
title: '标题',
dataIndex: 'title',
width: 220
},
{
title: '状态',
dataIndex: 'status',
render: data => 审核通过
},
{
title: '发布时间',
dataIndex: 'pubdate'
},
{
title: '阅读数',
dataIndex: 'read_count'
},
{
title: '评论数',
dataIndex: 'comment_count'
},
{
title: '点赞数',
dataIndex: 'like_count'
},
{
title: '操作',
render: data => {
return (
} />
}
/>
)
}
}
]
const data = [
{
id: '8218',
comment_count: 0,
cover: {
images:['http://geek.itheima.net/resources/images/15.jpg'],
},
like_count: 0,
pubdate: '2019-03-11 09:00:00',
read_count: 2,
status: 2,
title: 'wkwebview离线化加载h5资源解决方案'
}
]
return (
)
}
本节目标:
使用接口数据渲染频道列表
实现步骤
代码实现
pages/Article/index.js
// 获取频道列表
const [channels, setChannels] = useState([])
useEffect(() => {
async function fetchChannels() {
const res = await http.get('/channels')
setChannels(res.data.channels)
}
fetchChannels()
}, [])
// 渲染模板
return (
)
本节目标:
使用接口数据渲染表格数据
实现步骤
代码实现
// 文章列表数据管理
const [article, setArticleList] = useState({
list: [],
count: 0
})
// 参数管理
const [params, setParams] = useState({
page: 1,
per_page: 10
})
// 发送接口请求
useEffect(() => {
async function fetchArticleList() {
const res = await http.get('/mp/articles', { params })
const { results, total_count } = res.data
setArticleList({
list: results,
count: total_count
})
}
fetchArticleList()
}, [params])
// 模板渲染
return (
)
本节目标:
能够根据筛选条件筛选表格数据
实现步骤
onFinish
属性监听表单提交事件,获取参数params
触发接口的重新发送代码实现
// 筛选功能
const onSearch = values => {
const { status, channel_id, date } = values
// 格式化表单数据
const _params = {}
// 格式化status
_params.status = status
if (channel_id) {
_params.channel_id = channel_id
}
if (date) {
_params.begin_pubdate = date[0].format('YYYY-MM-DD')
_params.end_pubdate = date[1].format('YYYY-MM-DD')
}
// 修改params参数 触发接口再次发起
setParams({
...params,
..._params
})
}
// Form绑定事件
return (
)
本节目标:
能够实现分页获取文章列表数据
实现步骤
代码实现
const pageChange = (page) => {
// 拿到当前页参数 修改params 引起接口更新
setParams({
...params,
page
})
}
return (
)
本节目标:
能够实现点击删除按钮弹框确认
实现步骤
代码实现
// 删除回调
const delArticle = async (data) => {
await http.delete(`/mp/articles/${data.id}`)
// 更新列表
setParams({
page: 1,
per_page: 10
})
}
const columns = [
// ...
{
title: '操作',
render: data => {
return (
} />
delArticle(data)}
okText="确认"
cancelText="取消"
>
}
/>
)
}
]
本节目标:
能够实现编辑文章跳转功能
代码实现
const columns = [
// ...
{
title: '操作',
render: data => (
}
onClick={() => history.push(`/home/publish?id=${data.id}`)}
/>
)
}
]
本节目标:
能够搭建发布文章页面的基本结构
实现步骤
代码实现
pages/Publish/index.js
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import './index.scss'
const { Option } = Select
const Publish = () => {
return (
首页
发布文章
}
>
单图
三图
无图
)
}
export default Publish
pages/Publish/index.scss
.publish {
position: relative;
}
.ant-upload-list {
.ant-upload-list-picture-card-container,
.ant-upload-select {
width: 146px;
height: 146px;
}
}
本节目标:
能够安装并初始化富文本编辑器
实现步骤
yarn add react-quill
initialValues
为富文本编辑器设置初始值,否则会报错代码实现
pages/Publish/index.js
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
const Publish = () => {
return (
// ...
)
}
pages/Publish/index.scss
.publish-quill {
.ql-editor {
min-height: 300px;
}
}
本节目标:
实现频道数据的获取和渲染
实现步骤
代码实现
// 频道列表
const [channels, setChannels] = useState([])
useEffect(() => {
async function fetchChannels() {
const res = await http.get('/channels')
setChannels(res.data.channels)
}
fetchChannels()
}, [])
// 模板渲染
return (
)
本节目标:
能够实现上传图片
实现步骤
代码实现
import { useState } from 'react'
const Publish = () => {
const [fileList, setFileList] = useState([])
// 上传成功回调
const onUploadChange = info => {
const fileList = info.fileList.map(file => {
if (file.response) {
return {
url: file.response.data.url
}
}
return file
})
setFileList(fileList)
}
return (
)
}
本节目标:
实现点击切换图片类型
实现步骤
代码实现
pages/Publish/index.js
const Publish = () => {
const [imgCount, setImgCount] = useState(1)
const changeType = e => {
const count = e.target.value
setImgCount(count)
}
return (
// ...
单图
三图
无图
{maxCount > 0 && (
)}
)
}
本节目标:
控制Upload组件的最大上传数量和是否支持多张图片
实现步骤
maxCount(最大数量)
属性控制最大上传数量multiple (支持多图选择)属性
控制是否支持选择多张图片代码实现
pages/Publish/index.js
const Publish = () => {
return (
// ...
单图
三图
无图
{maxCount > 0 && (
1 }
>
)}
)
}
本节目标:
能够实现暂存已经上传的图片列表,能够在切换图片类型的时候完成切换
问题描述
如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张,该如何实现?
实现思路
在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实也就是把ref当仓库,用多少拿多少
实现步骤 (特别注意useState异步更新的巨坑)
代码实现
const Publish = () => {
// 1. 声明一个暂存仓库
const fileListRef = useRef([])
// 2. 上传图片时,将所有图片存储到 ref 中
const onUploadChange = info => {
// ...
fileListRef.current = imgUrls
}
// 3. 切换图片类型
const changeType = e => {
// 使用原始数据作为判断条件
const count = e.target.value
setMaxCount(count)
if (count === 1) {
// 单图,只展示第一张
const firstImg = fileListRef.current[0]
setFileList(!firstImg ? [] : [firstImg])
} else if (count === 3) {
// 三图,展示所有图片
setFileList(fileListRef.current)
}
}
}
本节目标:
能够在表单提交时组装表单数据并调用接口发布文章
实现步骤
给 Form 表单添加 onFinish
用来获取表单提交数据
在事件处理程序中,拿到表单数据按照接口需要格式化数据
调用接口实现文章发布,其中的接口数据格式为:
{
channel_id: 1
content: "测试
"
cover: {
type: 1,
images: ["http://geek.itheima.net/uploads/1647066600515.png"]
},
type: 1
title: "测试文章"
}
代码实现
const Publish = () => {
const onFinish = async (values) => {
// 数据的二次处理 重点是处理cover字段
const { channel_id, content, title, type } = values
const params = {
channel_id,
content,
title,
type,
cover: {
type: type,
images: fileList.map(item => item.response.data.url)
}
}
await http.post('/mp/articles?draft=false', params)
}
}
本节目标:
能够在编辑文章时展示数据
实现步骤
代码实现
import { useSearchParams } from 'react-router-dom'
const Publish = () => {
const [params] = useSearchParams()
const articleId = params.get('id')
return (
首页
{articleId ? '修改文章' : '发布文章'}
}
>
// ...
)
}
本节目标:
使用id获取文章详情
判断文章 id 是否存在,如果存在就根据 id 获取文章详情数据
useEffect(() => {
async function getArticle () {
const res = await http.get(`/mp/articles/${articleId}`)
}
if (articleId) {
// 拉取数据回显
getArticle()
}
}, [articleId])
本节目标:
完成Form组件的回填操作
调用Form组件的实例对象方法
setFieldsValue
useEffect(() => {
async function getArticle () {
const res = await http.get(`/mp/articles/${articleId}`)
const { cover, ...formValue } = res.data
// 动态设置表单数据
form.setFieldsValue({ ...formValue, type: cover.type })
}
if (articleId) {
// 拉取数据回显
getArticle()
}
}, [articleId])
1.Upload回显列表 fileList 2. 暂存列表 cacheImgList 3. 图片数量 imgCount
核心要点:fileList和暂存列表要求格式统一
表单的赋值回显需要调用setFieldsValue
方法,其中图片上传upload组件的回显依赖的数据格式如下:
[
{ url: 'http://geek.itheima.net/uploads/1647066120170.png' }
...
]
代码实现
useEffect(() => {
async function getArticle () {
const res = await http.get(`/mp/articles/${articleId}`)
const { cover, ...formValue } = res.data
// 动态设置表单数据
form.setFieldsValue({ ...formValue, type: cover.type })
// 格式化封面图片数据
const imageList = cover.images.map(url => ({ url }))
setFileList(imageList)
setMaxCount(cover.type)
fileListRef.current = imageList
}
if (articleId) {
// 拉取数据回显
getArticle()
}
}, [articleId])
本节目标:
能够在编辑文章时对文章进行修改
代码实现
// 提交表单
const onFinish = async (values) => {
const { type, ...rest } = values
const data = {
...rest,
// 注意:接口会按照上传图片数量来决定单图 或 三图
cover: {
type,
images: fileList.map(item => item.url)
}
}
if(articleId){
// 编辑
await http.put(`/mp/articles/${data.id}?draft=false`,data)
}else{
// 新增
await http.post('/mp/articles?draft=false', data)
}
}
本节目标:
能够通过命令对项目进行打包
使用步骤
yarn build
本节目标:
能够在本地预览打包后的项目
使用步骤
npm i -g serve
该包提供了serve命令,用来启动本地服务serve -s ./build
在build目录中开启服务器http://localhost:3000/
预览项目本节目标:
能够分析项目打包体积
分析说明通过分析打包体积,才能知道项目中的哪部分内容体积过大,才能知道如何来优化
使用步骤
yarn add source-map-explorer
yarn build
(如果已经打过包,可省略这一步)yarn analyze
核心代码:
package.json 中:
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
}
本节目标:
能够对第三方包使用CDN优化
分析说明:通过 craco 来修改 webpack 配置,从而实现 CDN 优化
核心代码
craco.config.js
// 添加自定义对于webpack的配置
const path = require('path')
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
},
// 配置webpack
// 配置CDN
configure: (webpackConfig) => {
// webpackConfig自动注入的webpack配置对象
// 可以在这个函数中对它进行详细的自定义配置
// 只要最后return出去就行
let cdn = {
js: [],
css: []
}
// 只有生产环境才配置
whenProd(() => {
// key:需要不参与打包的具体的包
// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
// 通过import 导入的 react / react-dom
webpackConfig.externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
// 配置现成的cdn 资源数组 现在是公共为了测试
// 实际开发的时候 用公司自己花钱买的cdn服务器
cdn = {
js: [
'https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js',
],
css: []
}
})
// 都是为了将来配置 htmlWebpackPlugin插件 将来在public/index.html注入
// cdn资源数组时 准备好的一些现成的资源
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin')
)
if (isFound) {
// 找到了HtmlWebpackPlugin的插件
match.userOptions.cdn = cdn
}
return webpackConfig
}
}
}
public/index.html
<body>
<div id="root">div>
<% htmlWebpackPlugin.userOptions.cdn.js.forEach(cdnURL => { %>
<script src="<%= cdnURL %>">script>
<% }) %>
body>
本节目标:
能够对路由进行懒加载实现代码分隔
使用步骤
代码实现
App.js
import { Routes, Route } from 'react-router-dom'
import { HistoryRouter, history } from './utils/history'
import { AuthRoute } from './components/AuthRoute'
// 导入必要组件
import { lazy, Suspense } from 'react'
// 按需导入路由组件
const Login = lazy(() => import('./pages/Login'))
const Layout = lazy(() => import('./pages/Layout'))
const Home = lazy(() => import('./pages/Home'))
const Article = lazy(() => import('./pages/Article'))
const Publish = lazy(() => import('./pages/Publish'))
function App () {
return (
loading...
查看效果
我们可以在打包之后,通过切换路由,监控network面板资源的请求情况,验证是否分隔成功