安装React脚手架不多说了,上一个代码:
npx create-react-app my-app
一.使用SCSS预处理器
装包:yarn add sass -D 需要安装,react中内置了SASS的配置,所以装包就行,生产环境不需要这个包,加-D;
创建全局样式文件:index.scss
body{
margin: 0;
}
#root{
height: 100%;
}
二.配置基础路由
安装路由:react-router-dom;
pages下创建layout和login两个文件夹
App.js中代码:
import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './pages/Layout'
import Login from './pages/Login'
export default function App() {
return (
//配置路由
{/* 创建路由path和对应组件关系 */}
}>
}>
)
}
三.antd组件库的使用
npm i antd -S
导入antd样式文件
import 'antd/dist/antd.min.css'
用@代替根路径src
.安装修改CRA配置的包:yarn add -D @craco/craco;
项目根目录下创建文件 craco.config.js
//添加自定义对于webpack的配置
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"
},
五.@别名路径提示
在根目录下创建jsconfig.json配置文件
{
"compilerOptions":{
"baseUrl":"./",
"paths":{
"@/*":["src/*"]
}
}
}
六.登录模块
(1)登录表单: pages/Login/index.js:
import { Card } from 'antd'
import logo from '../../assets/images/logo.png'
import './index.scss'//导入样式文件
export default function Login() {
return (
{/* 登录表单 */}
)
}
样式文件index.scss
.login {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: center/cover url('../../assets/images/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 rgba(0, 0, 0, 0.1);
}
.login-checkbox-label {
color: #1890ff;
}
}
(2)创建表单结构+添加校验功能
为Form.Item添加rules属性,用来添加表单校验.
import { Card, Form, Input, Checkbox, Button,message } from 'antd'
import logo from '../../assets/images/logo.png'
import './index.scss'//导入样式文件
export default function Login() {
return (
{/* 登录表单 */}
{/* 子项用到的触发事件 需要在Form中都声明一下才可以 */}
我已阅读并同意「用户协议」和「隐私条款」
)
}
(3)获取登录表单数据
1.为Form组件添加onFinish属性,该事件会在点击登录按钮时触发;
2.创建onFinish函数,通过函数参数values拿到表单值,
Form组件添加initialValues属性,来初始化表单值.
function onFinish(values) {
//values:放置的是所有表单项中用户输入的内容
console.log(values);
}
(4)登录axios统一封装处理
npm i axios -S
1.创建utils/http.js文件;
2.创建axios实例,配置baseURL,请求拦截器,响应拦截器;
在utils/http.js中,统一导出http.
// 封装axios
// 实例化 请求拦截器 响应拦截器
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 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.dir(error)
return Promise.reject(error)
})
export { http }
(5)配置登录Mobx
Mobx类似redux和vuex.
装包:yarn add mobx mobx-react-lite,管理用户登录的store
store/login.Store.js中:(接口应该是有错了,报400错误)
// login 模块
import { makeAutoObservable } from 'mobx'
import { http } from '../utils'
class LoginStore {
token = ''
constructor() {
//响应式
makeAutoObservable(this)
}
getToken = async ({ mobile, code }) => {
//调用登录接口
const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
mobile, code
})
//存入token
this.token = res.data.token
}
}
export default LoginStore
store/index.js中,管理所有的模块
// 把所有的模块做统一处理
// 导出一个统一的方法 useStore
import React from "react"
import LoginStore from './login.Store'
class RootStore{
constructor() {
this.loginStore = new LoginStore()
}
}
// 实例化根
// 导出useStore context
const rootStore = new RootStore()
const context = React.createContext(rootStore)
const useStore = () => React.useContext(context)
export { useStore }
(6)token持久化
1.封装工具函数
创建utils/token.js
// 封装ls存取token
const key = 'pc-key'
const setToken = (token) => {
return window.localStorage.setItem(key, token)
}
const getToken = () => {
return window.localStorage.getItem(key)
}
const removeToken = () => {
return window.localStorage.removeItem(key)
}
export {
setToken,
getToken,
removeToken
}
在utils/index.js统一封装的工具函数中:
// 先把所有的工具函数导出的模块在这里导入
// 然后再统一导出
import { http } from './http'
import {
setToken,
getToken,
removeToken
} from './token'
export {
http,
setToken,
getToken,
removeToken
}
在store/login.Store.js中新增下列代码:
import { setToken,getToken} from '../utils'
token =getToken() || ''//取的时候优先从本地拿token
setToken(this.token)//往本地存一份
(7)请求拦截器注入token
在utils/http.js中新增下列代码
import {getToken} from './token'//新增
// 添加请求拦截器
http.interceptors.request.use((config) => {
const token = getToken()//新增
if (token) { //新增
config.headers.Authorization = `Bearer ${token}`
}
return config
}, (error) => {
return Promise.reject(error)
})
(8)路由鉴权实现
思路:自己封装AuthRoute路由鉴权高阶组件,实现未登陆拦截,并返回到登录页面.
判断本地是否有token,有的话,就返回子组件,否则就重定向到登录Login.
components/AuthComponent.js中:
// 1. 判断token是否存在
// 2. 如果存在 直接正常渲染
// 3. 如果不存在 重定向到登录路由
// 高阶组件:把一个组件当成另外一个组件的参数传入
// 然后通过一定的判断 返回新的组件
import { getToken } from '../utils'
import { Navigate } from 'react-router-dom'
function AuthComponent({ children }) {
const isToken = getToken()
if (isToken) {
return <>{children}>
} else {
return
}
}
//
// 登录:<> >--登录就渲染对应的组件
// 非登录:
export {
AuthComponent
}
在App.js中:(接口有误,我自己项目就不添加这步了)
import { AuthComponent } from './components/AuthComponent'
{/* 创建路由path和对应组件关系 */}
{/* Layout需要鉴权处理的 */}
{/* 这里的Layout一定不能写死 要根据是否登录进行判断 */}
//包裹一下
}>
{/* 这个不需要,登录页嘛 */}
}>
看下登录页面的效果静态图:
五.layout模块
看下要实现的效果图:
(1)解决layout首页显示高度的问题
App.scss中:
.App{
height: 100vh;
}
Layout/index.js结构:
import { Layout, Menu, Popconfirm } from 'antd'
import { Outlet, Link } from 'react-router-dom'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined
} from '@ant-design/icons'
import './index.scss'
const { Header, Sider } = Layout
const GeekLayout = () => {
return (
用户名
< LogoutOutlined/> 退出
{/* 高亮原理:defaultSelectedKeys === item key */}
{/* 获取当前激活的path路径? */}
{/*
defaultSelectedKeys: 初始化渲染的时候生效一次
selectedKeys: 每次有值更新时都会重新渲染视图
*/}
{/* 二级路由出口 */}
)
}
export default GeekLayout
样式index.scss
.ant-layout {
height: 100%;
}
.header {
padding: 0;
}
.logo {
width: 200px;
height: 60px;
background: url('../../assets//images/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;
}
(2)二级路由跳转
新建Home,Article和Publish三个文件
在App.js中引入,并注册二级路由,默认展示Home页:
import Article from './pages/Article'
import Home from './pages/Home'
import Publish from './pages/Publish'
}>
}>
}>
}>
此处注意,必须要在Layout/index.js中,Link可以实现跳转到响应的页面
import { Outlet} from 'react-router-dom'//这是出口,不写的话页面什么也没有
(3)路由跳转配置
Layout/index.js中
import { Link } from 'react-router-dom'
} key="/">
数据概览
} key="/article">
内容管理
} key="/publish">
发布文章
(4)菜单反向高亮
Layout/index.js中
import { useLocation } from 'react-router-dom'
const {pathname} = useLocation()//解构一下
{/* 高亮原理:defaultSelectedKeys === item key */}
{/* 获取当前激活的path路径? */}
{/*
defaultSelectedKeys: 初始化渲染的时候生效一次
selectedKeys: 每次有值更新时都会重新渲染视图
*/}
(5)展示用户信息
store/user.Store.js中:
import { makeAutoObservable } from 'mobx'
import {http} from '../utils'
class UserStore{
userInfo = {}
constructor() {
makeAutoObservable(this)
}
getUserInfo =async () => {
//调用接口
const res = await http.get('/user/profile')
this.userInfo=res.data
}
}
export default UserStore
store/index.js中:
import UserStore from './user.Store'
Layout/index.js中:
import {observer} from 'mobx-react-lite' //引入中间件
import {useStore} from '../../store'
import { useEffect } from 'react'
+ const {userStore}=useStore()
+ useEffect(() => {
userStore.getUserInfo()
},[userStore])//填入userStore仅仅是让他不提醒没有使用
用户名//正常这里的用户名是{{userStore.userInfo.name}}
+export default observer(GeekLayout) //observer解决用户名一刷新就丢失的问题
(6)退出登录模块
store/login.Store.js中:
import { http ,setToken,getToken,+removeToken} from '../utils'
+ loginOut = () => {
this.token = ''
removeToken()
}
Layout/index.js中:
import { Outlet, Link, useLocation, +useNavigate } from 'react-router-dom'
//点击退出的回调
const navigate = useNavigate()
const onConfirm = () => {
//退出登录 清空token 返回登录页
loginStore.loginOut()
navigate('/login')
}
< LogoutOutlined /> 退出
(7)处理Token失效
思路:在响应拦截器中处理token失效
**难点:怎么在组件之外实现退出登录返回首页:
来一个链接,详细讲解的:
在组件之外实现退出登录返回首页
//示例代码
import { createBrowserHistory } from 'history';
import { +unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
let history = createBrowserHistory();
function App() {
return (
// The rest of your app
);
}
history.push("/foo");
安装包:yarn add history (这一部分我就在这展示,要不然我就登录不上去了)
常规的js文件中,那些useXXX文件都是失效的,所以得从history 中引入一个方法,需要在路由中配置
在util中新建history.js
import { createBrowserHistory} from 'history'
const history = createBrowserHistory()
export {history}
在util/http.js中:
import { history } from './history'
if (error.response.status === 401) {
// 跳回到登录 reactRouter默认状态下 并不支持在组件之外完成路由跳转
// 需要自己来实现
console.log('login')
history.push('/login')
}
在App.js中:
+import { +unstable_HistoryRouter as HistoryRouter , Routes, Route } from 'react-router-dom'
+
六.首页echart实现
yarn add echarts
echarts官网
components中新建Bar.js:
// 封装图表bar组件
import { useEffect, useRef } from 'react'
import * as echarts from 'echarts';
export default function Bar({title,xData,yData,style}) {
const domRef = useRef()
const chartInit = () => {
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(domRef.current);
// 绘制图表
myChart.setOption({
title: {
text: title
},
tooltip: {},
xAxis: {
data: xData
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: yData
}
]
});
}
//执行这个初始化的函数
useEffect(() => {
chartInit()
},[])
return (
{/* 准备一个挂载节点 */}
)
}
Home/index.js中:
// 思路:
// 1. 看官方文档 把echarts加入项目
// 如何在react获取dom -> useRef
// 在什么地方获取dom节点 -> useEffect
// 2. 不抽离定制化的参数 先把最小化的demo跑起来
// 3. 按照需求,哪些参数需要自定义 抽象出来
import Bar from '../../components/Bar'
export default function Home() {
return (
{/* 渲染Bar组件 */}
)
}
七.内容管理
(1)筛选区结构
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 = () => {
const onFinish = (values) => {
console.log(values);
}
return (
{/* 筛选区域 */}
首页
内容管理
}
style={{ marginBottom: 20 }}
>
全部
草稿
待审核
审核通过
审核失败
{/* 传入locale属性 控制中文显示*/}
{/* 文章列表区域 */}
)
}
export default Article
(2)表格区域结构
import { +Table,+Tag, +Space,Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import img404 from '../../assets/images/error.png'
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 (
}
/>
}
/>
)
},
fixed: 'right'
}
]
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:'理想化处置方案'
}
]
{/* 文章列表区域 */}
(3)频道列表渲染
Article/index.js中:
import { http } from '../../utils/http'
//频道列表管理
const [channelList, setChannelList] = useState([])
const loadChannelList = async () => {
const res = await http.get('/channels')
console.log('1',res);
setChannelList(res.data.data.channels)
}
useEffect(() => {
loadChannelList()
}, [])
(4)获取文章列表
Article/index.js中:
//文章列表管理 统一管理数据 将来修改给setArticleData传对象
const [list, setList] = useState({
list: [],//文章列表
count: 0//文章数量
})
//文章参数管理
const [params, setParams] = useState({
page: 1,
per_page: 10
})
//如果异步请求函数需要依赖一些数据的变化而重新执行
//推荐把它写到内部
//统一不抽离函数到外面,只要涉及到异步请求的函数,都放到useEffect内部
//本质区别:写到外面每次组件更新都会重新进行函数初始化,是一次性能浪费
//而写到useEffect中,只会在依赖项发送变化时,函数才会重新进行初始化
useEffect(() => {
const loadList = async () => {
const res = await http.get('/mp/articles', { params })
console.log('2', res);
}
loadList()
}, [params])
(5)渲染文章列表
Article/index.js中:接口走不通,在这写一下过程把,代码我是直接铺设自己的数据了
+ useEffect(() => {
const loadList = async () => {
const res = await http.get('/mp/articles', { params })
const { results, total_count } = res.data
setArticleData({
list: results,
count: total_count
})
}
loadList()
}, [params])
+
+ dataSource={articleData.list}
(6)筛选功能实现
思路:1.为表单添加onFinish属性,监听表单提交;
2.根据接口字段格式要求,格式化参数的格式;
修改params,触发接口重新发送.
(7)分页器功能
//改变分页
+ const pageChange = (page) => {
setParams({
...params,
page
})
}
(8)文章删除功能
// 删除文章
const delArticle = async (data) => {
await http.delete(`/mp/articles/${data.id}`)
// 刷新一下列表
setParams({
...params,
page: 1
})
}
}
+onClick={() => delArticle(data)}
/>
(9)跳转到编辑页
import { Link, +useNavigate } from 'react-router-dom'
//跳转到编辑页
const navigate = useNavigate()//必须写这个地方,写到内部就报错了
const goPublish = (data) => {
navigate(`/publish?id=${data.id}`)
}
render: data => {
return (
}
+ onClick={() => goPublish(data)}
/>
)
},
八.发布文章结构搭建
(1)基本结构搭建
Publish/index.js中:
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select,
message
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import './index.scss'
const { Option } = Select
const Publish = () => {
return (
首页
文章
}
>
单图
三图
无图
{/* 这里的富文本组件 已经被Form.Item控制 */}
{/* 它的输入内容 会在onFinished回调中收集起来 */}
)
}
export default Publish
样式index.scss:
.publish {
position: relative;
.ql-container {//富文本的;类名
height: 400px !important;
}
}
.ant-upload-list {
.ant-upload-list-picture-card-container,
.ant-upload-select {
width: 146px;
height: 146px;
}
}
(2)富文本编辑器实现
1.安装富文本编辑器yarn add react-quill;
2.导入富文本编辑器以及样式文件,并渲染;
通过Form组件的initialValues为富文本设置初始值,否则会报错.
富文本编辑器
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
+
(3)重构频道获取
1.频道列表专门封装
store中新建channel.Store.js
import { makeAutoObservable } from 'mobx'
import {http} from '../utils'
class ChannelStore{
channelList = []
constructor() {
makeAutoObservable(this)
}
// article publish 哪里调用这个函数呢?
loadChannelList = async() => {
const res = await http.get('/channels')
this.channelList=res.data.data.channels
}
}
export default ChannelStore
改造内容管理Article中的频道列表
//频道列表管理
- const [channelList, setChannelList] = useState([])
- const loadChannelList = async () => {
- const res = await http.get('/channels')
- console.log('1',res);
- setChannelList(res.data.data.channels)
}
- useEffect(() => {
- loadChannelList()
- }, [])
//替换成
+import {observer} from 'mobx-react-lite'
+ const {channelStore}=useStore()
{
+ channelStore.channelList.map(channel => )
}
+export default observer(Article)
在发布文章Publish.js中使用:(和上面一样的操作)
+import {observer} from 'mobx-react-lite'
+ const {channelStore}=useStore()
{
+ channelStore.channelList.map(channel => )
}
+export default observer(Publish)
(4)基础上传实现
+import { useState } from 'react';
+ const { channelStore } = useStore()
//存放上传图片的列表(在最后一次阶段才能拿到图片的响应路径,控制台输出第三次的时候)
//也就是图片是分阶段上传的 这里区别Vue 成功失败都在这里显示
+const [fileList, setFileList] = useState([])
+const onUploadChange = ({fileList}) => {
console.log(fileList);
setFileList(fileList)
}
(5)控制封面数量
//切换图片
const [imgCount, setImgCount] = useState()
const radioChange = (e) => {
console.log(e.target.value);
setImgCount(e.target.value)
}
单图
三图
无图
{//判断是否是无图,无图就不展示上传了
imgCount > 0 && (
)
}
(6)控制最大上传数量
1.修改Upload组件的maxCount(最大数量)属性控制最大上传数量;
控制multiple(支持多图选择属性)控制是否支持选择多张图片.
1}
+ maxCount={imgCount} 最大数量
>
(7)发布文章实现
接口有误,正常的话点击发布后会在文章管理Table中多一条数据.
//发布文章
const onFinish = async(values) => {
console.log(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)
}
}
console.log(params);
await http.post('/mp/articles?draft=false', params)
}
(8)暂存图片列表实现
思路:在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实是把ref当仓库,需要几张拿几张.
import { useState, +useRef } from 'react';
const Publish = () => {
// 使用useRef声明一个暂存仓库
+const cacheImgList = useRef()
const [fileList, setFileList] = useState([])
const onUploadChange = ({ fileList }) => {
console.log(fileList);
setFileList(fileList)
//同时把图片列表存入仓库一份
+ cacheImgList.current = fileList
}
//切换图片
const [imgCount, setImgCount] = useState()
const radioChange = (e) => {
+ const rawValue = e.target.value
setImgCount(rawValue)
//从仓库取对应的图片数量,交给渲染图片列表的fileList
//通过调用setFileList
if (cacheImgList.current.length === 0) {
return false
}
+ if (rawValue === 1) {
const img = cacheImgList.current ? cacheImgList.current[0] : []
setFileList([img])
+ } else if (rawValue === 3) {
setFileList(cacheImgList.current)
}
}
}
(9)编辑文章-适配文案
思路:1.通过路由参数拿到文章id;
2.根据文章id是否存在判断是否是编辑状态;
如果是编辑状态,展示编辑时的文案信息.
import { Link, +useSearchParams } from 'react-router-dom'
//编辑功能
const [params] = useSearchParams()
const id = params.get('id')
console.log('文章id是', id);
首页
+ {id ? '编辑' : '发布'}文章
}
>
(10)回显基础数据
因为接口错误,目前实现的效果是点击内容管理的编辑会跑到发布文章页,除了上传图片,剩下的内容都能回显出来.
//编辑功能
const [params] = useSearchParams()
const id = params.get('id')
console.log('文章id是', id);
// 数据回填 id调用接口 1.表单回填 2.暂存列表 3.Upload组件fileList
+ const form = useRef(null)
useEffect(() => {
const loadDetail = async () => {
const res = await http.get(`/mp/articles/${id}`)
//表单数据回填,实例方法
+ form.current.setFieldsValue(res.data)
console.log(res);
}
if (id) {//id存在的话,才发起编辑
loadDetail()
console.log('form', form.current);
}
}, [id])
(11)回显upload上传图片
//编辑功能
const [params] = useSearchParams()
const id = params.get('id')
console.log('文章id是', id);
// 数据回填 id调用接口 1.表单回填 2.暂存列表 3.Upload组件fileList
const form = useRef(null)
useEffect(() => {
const loadDetail = async () => {
const res = await http.get(`/mp/articles/${id}`)
//表单数据回填,实例方法
const data = res.data
+ form.current.setFieldsValue({ ...data, type: data.cover.type })
//调用setFileList方法回填upload
+const formatImgList=data.cover.images.map(url =>({url}))
+ setFileList(formatImgList)
//暂存列表里也存一份(暂存列表和fileList回显列表数据保持一致就能正常显示)
+ cacheImgList.current = formatImgList
}
if (id) {//id存在的话,才发起编辑
loadDetail()
console.log('form', form.current);
}
}, [id])
(12)点击修改保存提交
const onUploadChange = ({ fileList }) => {
console.log(fileList);
setFileList(fileList)
//同时把图片列表存入仓库一份
const formatList = fileList.map(file => {
//上传完毕,作数据处理
+ if (file.response) {
return {
url:file.response.data.url
}
}
//否则正在上传,不做处理
+ return file
})
cacheImgList.current = fileList
}
//发布文章
const navigator=useNavigate()
const onFinish = async (values) => {
console.log(values);
// 数据的二次处理 重点是处理cover字段
const { channel_id, content, title, type } = values
const params = {
channel_id, content, title, type,
cover: {
type: type,
+ images: fileList.map(item => item.url)
}
}
if (id) {
await http.post(`/mp/articles/${id}?draft=false`, params)
} else {
await http.post('/mp/articles?draft=false', params)
}
navigator('/article')
message.success(`${id?'编辑成功':'发布成功'}`)
}
九.项目打包
(1)打包
yarn build
(2)打包体积分析
安装分析打包体积的包
yarn add source-map-explorer
2.在package.json中的scripts标签中,添加分析打包体积的命令;
3.对项目打包:yarn build,执行过的话这步省略;
4.运行分析命令:yarn analyze;
通过浏览器打开的命令,分析图表中的包体积.
"scripts":{
"report":"source-map-explorer 'build/static/js/*.js'"
}
(3)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://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js',
'https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/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中:
<% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL=> { %>
<% }) %>
再次执行yarn build
(4)路由懒加载
1.在App.js组件中,导入Suspense组件(异步加载);
2.在路由Router内部,使用Suspense组件包裹组件内容;
3.为Suspense组件提供fallback属性,指定loading占位内容;
导入lazy函数,并修改为懒加载方式导入路由组件.
App.js:
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'))
loading...
觉得还不错的话,动动动您的小手点个赞呗!!