一款后台管理项目 - React-geek-PC
项目介绍
● 项目功能演示
- 登录、退出
- 首页
- 内容(文章)管理:文章列表、发布文章、修改文章
● 技术
- React 官方脚手架 create-react-app
- react hooks
- 状态管理:mobx
- UI 组件库:antd v4
- ajax 请求库:axios
- 路由:react-router-dom 以及 history
- 富文本编辑器:react-quill
- CSS 预编译器:sass
项目开始准备
搭建
- 使用
create-react-app
生成项目npx create-react-app projectName
- 进入根目录
cd projectName
- 启动项目
yarn start
yarn 配置使用参考
调整项目结构
/src
/assets 项目资源文件,比如,图片 等
/components 通用组件
/pages 页面
/store mobx 状态仓库
/utils 工具,比如,token、axios 的封装等
App.js 根组件
index.css 全局样式
index.js 项目入口
使用 Gitee
管理项目
- 在项目根目录打开终端,并初始化
git
仓库(如果已经有了 git 仓库,无需重复该步),命令:git init
- 添加项目内容到暂存区:
git add .
- 提交项目内容到仓库区:
git commit -m '项目初始化'
- 添加
remote
仓库地址:git remote add origin [gitee 仓库地址]
- 将项目内容推送到
gitee
:git push origin master -u
使用 scss
预处理器
SASS 是一种预编译的 CSS,作用类似于 Less。由于 React 中内置了处理 SASS 的配置,所以,在 CRA 创建的项目中,可以直接使用 SASS 来写样式
- 安装解析
sass
的包:yarn add sass -D
- 创建全局样式文件:
index.scss
配置路由基础
- 安装路由:
yarn add react-router-dom
- 在 pages 目录中创建两个文件夹:
Login、Layout
- 分别在两个目录中创建
index.js
、index.scss
文件,并创建一个简单的组件后导出 - 在
App
组件中,导入路由组件以及两个页面组件 - 配置
Login
和Layout
的路由规则
// 导入路由
import { BrowserRouter, Route, Routes } from 'react-router-dom'
// 导入页面组件
import Login from './pages/Login'
import Layout from './pages/Layout'
// 配置路由规则
function App() {
return (
<BrowserRouter>
<div className="App">
<Routes>
<Route path="/" element={<Layout />} />
<Route path="/login" element={<Login />} />
</Routes>
</div>
</BrowserRouter>
)
}
export default App
配置 antdesign
- 安装
antd
组件库:yarn add antd
- 全局导入
antd
组件库的样式 - 导入
Button
组件 - 在
Login
页面渲染Button
组件进行测试
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 = () => (
<div>
<Button type="primary">Button</Button>
</div>
)
配置路径别名
自定义 CRA 的默认配置
craco 配置文档
● CRA
将所有工程化配置,都隐藏在了 react-scripts
包中,所以项目中看不到任何配置信息
● 如果要修改 CRA
的默认配置,有以下几种方案:
a. 通过第三方库来修改,比如,@craco/craco
(推荐)
b. 通过执行 yarn eject
命令,释放 react-scripts
中的所有配置到项目中
实现步骤
- 安装修改
CRA
配置的包:yarn add -D @craco/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
目录
登录模块
基本结构搭建
- 在
Login/index.js
中创建登录页面基本结构 - 在 Login 目录中创建
index.scss
文件,指定组件样式
根据Antd文档
创建表单组件 -> 表单校验
表单校验
- 为
Form
组件添加validateTrigger
属性,指定校验触发时机的集合 - 为
Form.Item
组件添加name
属性,这样表单校验才会生效 [容易忽略] - 为
Form.Item
组件添加rules
属性,用来添加表单校验
<Form validateTrigger={['onBlur', 'onChange']}>
<Form.Item
name="mobile"
rules={[
{
pattern: /^1[3-9]\d{9}$/,
message: '手机号码格式不对',
validateTrigger: 'onBlur',
},
{ required: true, message: '请输入手机号' },
]}>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
</Form>
获取表单登录数据
- 为 Form 组件添加
onFinish
属性,该事件会在点击登录按钮时触发 - 创建
onFinish
函数,通过函数参数 values 拿到表单值 - Form 组件添加
initialValues
属性,来初始化表单值
// 点击登录按钮时触发 参数values即是表单输入数据
const onFinish = (values) => {
console.log(values)
}
;<Form
onFinish={onFinish}
initialValues={{
mobile: '13911111111',
code: '246810',
remember: true,
}}>
...
</Form>
封装http
工具模块
封装 axios
简化操作
安装 axios
: yarn add axios
- 创建
utils/http.js
文件 - 创建
axios
实例,配置baseURL
,请求拦截器,响应拦截器 - 在
utils/index.js
中,统一导出http
utils/http.js
import axios from 'axios'
// 创建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/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方法供组件使用数据
export const useStore = () =>
React.useContext(React.createContext(new RootStore()))
登录逻辑实现
- 使用
useStore
方法得到loginStore
实例对象 - 在校验通过之后,调用
loginStore
中的 login 函数 - 登录成功之后跳转到首页
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 持久化
统一处理 token 的持久化相关操作
- 创建
utils/token.js
文件 - 分别提供
getToken/setToken/clearToken
四个工具函数并导出 - 创建
utils/index.js
文件,统一导出 token.js 中的所有内容,来简化工具函数的导入 - 将登录操作中用到 token 的地方,替换为该工具函数
utils/token.js
const TOKEN_KEY = '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
把 token 通过请求拦截器注入到请求头中
utils/htts.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
文件 - 判断是否登录
- 登录时,直接渲染相应页面组件
- 未登录时,重定向到登录页面
- 将需要鉴权的页面路由配置,替换为
AuthRoute
组件渲染
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 <Navigate to="/login" replace />
}
}
// <AuthComponent> <Layout/> </AuthComponent>
// 登录:<><Layout/></>
// 非登录:<Navigate to="/login" replace />
export { AuthRoute }
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 (
<Router>
<Routes>
{/* 需要鉴权的路由 */}
<Route
path="/*"
element={
// 在这里!
<AuthRoute>
<Layout />
</AuthRoute>
}
/>
{/* 不需要鉴权的路由 */}
<Route path="/login" element={<Login />} />
</Routes>
</Router>
)
}
export default App
layout 模块
基本结构搭建
-
打开
antd/Layout
布局组件文档,找到示例:顶部-侧边布局-通栏 -
边删边调 边分析并调整页面布局
二级路由配置
- 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹
- 分别在三个文件夹中创建
index.js
并创建基础组件后导出 - 在
app.js
中配置嵌套子路由,在layout.js
中配置二级路由出口 - 使用
Link
修改左侧菜单内容,与子路由规则匹配实现路由切换
app.js
<Route path="/" element={
<AuthRoute>
<Layout />
</AuthRoute>
}>
{/* 二级路由默认页面 */}
<Route index element={<Home />} />
<Route path="article" element={<Article />} />
<Route path="publish" element={<Publish />} />
</Route>
<Route path="/login" element={<Login/>}></Route>
pages/layout/index.js
// 配置Link组件
<Menu
mode="inline"
theme="dark"
style={{ height: '100%', borderRight: 0 }}
selectedKeys={[selectedKey]}
>
<Menu.Item icon={<HomeOutlined />} key="/">
<Link to="/">数据概览</Link>
</Menu.Item>
<Menu.Item icon={<DiffOutlined />} key="/article">
<Link to="/article">内容管理</Link>
</Menu.Item>
<Menu.Item icon={<EditOutlined />} key="/publish">
<Link to="/publish">发布文章</Link>
</Menu.Item>
</Menu>
// 二级路由对应显示
<Layout className="layout-content" style={{ padding: 20 }}>
{/* 二级路由默认页面 */}
<Outlet />
</Layout>
菜单高亮显示
在页面刷新的时候保持对应菜单高亮
- Menu 组件的
selectedKeys
属性与Menu.Item
组件的 key 属性发生匹配的时候,Item 组件即可高亮- 页面刷新时,将当前访问页面的路由地址作为 Menu
选中项的值(selectedKeys)即可
- 将 Menu 的 key 属性修改为与其对应的路由地址
- 获取到当前正在访问页面的路由地址
- 将当前路由地址设置为
selectedKeys
属性的值
pages/Layout/index.js
import { useLocation } from 'react-router-dom'
const GeekLayout = () => {
const location = useLocation()
// 这里是当前浏览器上的路径地址
const selectedKey = location.pathname
return (
// ...
<Menu
mode="inline"
theme="dark"
selectedKeys={[selectedKey]}
style={{ height: '100%', borderRight: 0 }}>
<Menu.Item icon={<HomeOutlined />} key="/">
<Link to="/">数据概览</Link>
</Menu.Item>
<Menu.Item icon={<DiffOutlined />} key="/article">
<Link to="/article">内容管理</Link>
</Menu.Item>
<Menu.Item icon={<EditOutlined />} key="/publish">
<Link to="/publish">发布文章</Link>
</Menu.Item>
</Menu>
)
}
处理 token 失效问题
在响应拦截器中处理 token 失效
去检索一下 看官方的解决方案
或者直接使用 window.location.href = ''
home 页面内容展示
自定义…
文章管理模块
筛选结构
使用 antd 组件库搭建筛选区域结构,自己造!!!
表格区域结构
基于 Table 组件搭建表格结构,去看着文档造!!!
渲染表格数据
- 声明列表相关数据管理
- 声明参数相关数据管理
- 调用接口获取数据
- 使用接口数据渲染模板
// 文章列表数据管理
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 (
<Card title={`根据筛选条件共查询到 ${article.count} 条结果:`}>
<Table dataSource={article.list} columns={columns} />
</Card>
)
筛选功能实现
根据筛选条件筛选表格数据
- 为表单添加 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 <Form onFinish={onSearch}></Form>
分页功能实现
简单,自己造…
删除功能实现
简单,自己造…
文章修改功能实现
看后续…
发布文章模块
基本结构
自己看着造!!!
富文本编辑器
安装并初始化富文本编辑器
- 安装富文本编辑器:
yarn add react-quill@2.0.0-beta.2
[react-quill 需要安装 beta 版本适配 react18 否则无法输入中文] - 导入富文本编辑器组件以及样式文件
- 渲染富文本编辑器组件
- 通过 Form 组件的 initialValues 为富文本编辑器设置初始值,否则会报错
- 调整富文本编辑器的样式
pages/Publish/index.js
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
const Publish = () => {
return (
// ...
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
// 注意:此处需要为富文本编辑表示的 content 文章内容设置默认值
initialValues={{ content: '' }}>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}>
<ReactQuill
className="publish-quill"
theme="snow"
placeholder="请输入文章内容"
/>
</Form.Item>
</Form>
)
}
pages/Publish/index.scss
.publish-quill {
.ql-editor {
min-height: 300px;
}
}
上传封面
- 为 Upload 组件添加 action 属性,指定封面图片上传接口地址
- 创建状态 fileList 存储已上传封面图片地址,并设置为 Upload 组件的 fileList 属性值
- 为 Upload 添加 onChange 属性,监听封面图片上传、删除等操作
- 在 change 事件中拿到当前图片数据,并存储到状态 fileList 中
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 (
<Upload
name="image"
listType="picture-card"
className="avatar-uploader"
showUploadList
action="http://geek.itheima.net/v1_0/upload"
fileList={fileList}
onChange={onUploadChange}>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
)
}
切换图片类型
- 创建状态 maxCount
- 给 Radio 添加 onChange 监听单图、三图、无图的切换事件
- 在切换事件中修改 maxCount 值
- 只在 maxCount 不为零时展示 Upload 组件
const Publish = () => {
const [imgCount, setImgCount] = useState(1)
const changeType = (e) => {
const count = e.target.value
setImgCount(count)
}
return (
// ...
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group onChange={changeType}>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
{maxCount > 0 && (
<Upload
name="image"
listType="picture-card"
className="avatar-uploader"
showUploadList
action="http://geek.itheima.net/v1_0/upload">
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
)}
</Form.Item>
)
}
控制最大上传数量
- 修改 Upload 组件的 maxCount(最大数量)属性控制最大上传数量
- 控制 multiple (支持多图选择)属性 控制是否支持选择多张图片
自己看看咋造
maxCount={ maxCount }
multiple={ maxCount > 1 }
暂存图片列表实现
实现暂存已经上传的图片列表,能够在切换图片类型的时候完成切换
问题描述
如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张,该如何实现?
实现思路
在上传完毕之后通过 ref 存储所有图片,需要几张就显示几张,其实也就是把 ref 当仓库,用多少拿多少
实现步骤 (特别注意 useState 异步更新的坑)
- 通过 useRef 创建一个暂存仓库,在上传完毕图片的时候把图片列表存入
- 如果是单图模式,就从仓库里取第一张图,以数组的形式存入 fileList
- 如果是三图模式,就把仓库里所有的图片,以数组的形式存入 fileList
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 用来获取表单提交数据
- 在事件处理程序中,拿到表单数据按照接口需要 格式化数据
编辑文章适配
文章发布和编辑文章可以做一个单独的组件,根据是否有参数传入来显示不同的适配文案
编辑文案获取数据-数据回显
判断文章 id 是否存在,如果存在就根据 id 获取文章详情数据
调用 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])
回显 upload 相关
1.Upload 回显列表 fileList 2. 暂存列表 cacheImgList 3. 图片数量 imgCount
核心要点:fileList 和暂存列表要求格式统一
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
- 等待打包完成,打包生成的内容被放在根下的 build 文件夹中
本地预览
- 全局安装本地服务包 npm i -g serve 该包提供了 serve 命令,用来启动本地服务
- 在项目根目录中执行命令 serve -s ./build 在 build 目录中开启服务器
- 在浏览器中访问:http://localhost:3000/ 预览项目
打包分析
分析项目打包体积
分析说明通过分析打包体积,才能知道项目中的哪部分内容体积过大,才能知道如何来优化
- 安装分析打包体积的包:yarn add source-map-explorer
- 在 package.json 中的 scripts 标签中,添加分析打包体积的命令
- 对项目打包:yarn build(如果已经打过包,可省略这一步)
- 运行分析命令:yarn analyze
- 通过浏览器打开的页面,分析图表中的包体积
"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>
<!-- 加载第三发包的 CDN 链接 -->
<% htmlWebpackPlugin.userOptions.cdn.js.forEach(cdnURL => { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>
优化路由懒加载
对路由进行懒加载实现代码分隔
- 在 App 组件中,导入 Suspense 组件
- 在 路由 Router 内部,使用 Suspense 组件包裹组件内容
- 为 Suspense 组件提供 fallback 属性,指定 loading 占位内容
- 导入 lazy 函数,并修改为懒加载方式导入路由组件
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 (
<HistoryRouter history={history}>
<Suspense
fallback={
<div
style={{
textAlign: 'center',
marginTop: 200,
}}>
loading...
</div>
}>
<Routes>
{/* 需要鉴权的路由 */}
<Route
path="/"
element={
<AuthRoute>
<Layout />
</AuthRoute>
}>
{/* 二级路由默认页面 */}
<Route index element={<Home />} />
<Route path="article" element={<Article />} />
<Route path="publish" element={<Publish />} />
</Route>
{/* 不需要鉴权的路由 */}
<Route path="/login" element={<Login />} />
</Routes>
</Suspense>
</HistoryRouter>
)
}
export default App
我们可以在打包之后,通过切换路由,监控 network 面板资源的请求情况,验证是否分隔成功
源自点击