一、使用 CRA 创建项目
npx create-react-app name
二、按照业务规范整理项目目录 (重点src目录)
三、安装插件
npm install sass -D
npm install antd --save
npm install react-router-dom
四、配置基础路由 Router
1. 安装路由包 react-router-dom
2. 准备两个基础路由组件 Layout 和 Login
3. 在 router/index.js 文件中引入组件进行路由配置,导出 router 实例
4. 在入口文件中渲染 <RouterProvider />,传入 router 实例
router/index.js
import { createBrowserRouter } from "react-router-dom";
// 配置路由实例
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
},
{
path: "/login",
element: <Login />,
},
]);
export default router;
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
<div>
<Card>
<img />
{/* 登录表单 */}
<Form>
<Form.Item>
<Input />
</Form.Item>
<Form.Item>
<Input />
</Form.Item>
</Form>
</Card>
</div>
1. 表单校验实现
表单校验可以在提交登录之前,校验用户的输入是否符合预期,如果不符合就阻止提交,显示错误信息
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: '请输入用户名!' }]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
增加失焦时校验
<Form validateTrigger="onBlur">
...
</Form>
手机号为有效格式
<Form.Item
name="mobile"
// 多条校验逻辑 先校验第一条 第一条通过之后再校验第二条
rules={[
{
required: true,
message: '请输入手机号',
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式'
}
]}>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
2. 获取表单数据
当用户输入了正确的表单内容,点击确认按钮时,需要收集用户输入的内容,用来提交接口请求
解决方案:给 Form 组件绑定 onFinish 回调函数,通过回调函数的参数获取用户输入的内容
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 三方库做好统一封装,方便统一管理和复用
npm i axios
utils/request.js
// 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
// 统一中转工具模块函数
// import {request} from '@/utils'
import { request } from './request'
export {
request,
}
4. 使用 redux 管理 token
Token 作为一个用户的标识数据,需要在很多个模块中共享,Redux 可以方便的解决共享问题
(1)redux 中编写获取 Token 的 异步获取和同步修改
(2)Login 组件负责提交 action 并且把表单数据传递过来
npm i react-redux @reduxjs/toolkit
store/modules/user.js
// 和用户相关的状态管理
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
// 组合redux子模块 + 导出store实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
reducer: {
user: userReducer
}
})
index.js
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
// 封装基于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
// 统一中转工具模块函数
// import {request} from '@/utils'
import { request } from './request'
import { setToken, getToken, removeToken } from './token'
export {
request,
setToken,
getToken,
removeToken
}
pages/login/index.js
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
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
// 封装高阶组件
// 核心逻辑: 有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
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
npm install normalize.css
index.js
import 'normalize.css'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
)
index.scss
html,
body {
margin: 0;
height: 100%;
}
#root {
height: 100%;
}
2. 二级路由配置
(1)准备三个二级路由
(2)router 中通过 children 配置项进行配置
(3)Layout 组件中配置二级路由出口
router/index.js
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
<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
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)找到菜单组件负责高亮的属性,绑定当前的路由路径
// 反向高亮
// 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
// 和用户相关的状态管理
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
// 触发个人用户信息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
// 退出登录确认回调
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
// 同步修改方法
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
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)按照业务需求修改配置项做定制处理
npm install echarts
pages/Home/index.js
// 柱状图组件
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
import BarChart from "./components/BarChart"
const Home = () => {
return (
<div>
<BarChart title={'三大框架满意度'} />
<BarChart title={'三大框架使用度'} />
</div>
)
}
export default Home
pages/Home/components/BarCharts.js
// 柱状图组件
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
// 用户相关的所有请求
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
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))
}
}
九、文章发布
1. 创建并熟悉基础结构
(1)面包屑导航组件 Breadcrumb
(2)表单组件 Form
(3)输入框组件 Input
(4)下拉框组件 Select - Option
(5)按钮组件 Button
2. 准备富文本编辑器
(1)安装 react-quill 富文本编辑器
// react 18
npm i react-quill@2.0.0-beta.2
// react 19
npm i react-quill-new --save
(2)导入编辑器组件和配套样式文件
(3)渲染编辑器组件
(4)调整编辑器组件样式
pages/Publish/index.js
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
const { Option } = Select
const Publish = () => {
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: `${articleId ? '编辑' : '发布'}文章` },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 1 }}
>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入文章标题' }]}
>
<Input placeholder="请输入文章标题" style={{ width: 400 }} />
</Form.Item>
<Form.Item
label="频道"
name="channel_id"
rules={[{ required: true, message: '请选择文章频道' }]}
>
<Select placeholder="请选择文章频道" style={{ width: 400 }}>
<Option value={0}>推荐</Option>
</Select>
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
{/* 富文本编辑器 */}
<ReactQuill
className="publish-quill"
theme="snow"
placeholder="请输入文章内容"
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 4 }}>
<Space>
<Button size="large" type="primary" htmlType="submit">
发布文章
</Button>
</Space>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Publish
3. 频道数据获取渲染
(1)根据接口文档在 APIS 模块中封装接口函数
(2)使用 useState 维护数据
(3)在 useEffect 中调用接口获取数据并存入 state
(4)绑定数据到下拉框组件
apis/article.js
// 封装和文章相关的接口函数
import { request } from "@/utils"
// 1. 获取频道列表
export function getChannelAPI () {
return request({
url: '/channels',
method: 'GET'
})
}
// 2. 提交文章表单
export function createArticleAPI (data) {
return request({
url: '/mp/articles?draft=false',
method: 'POST',
data
})
}
pages/Publish/index.js
// 获取频道列表
const [channelList, setChannelList] = useState([])
useEffect(() => {
// 1. 封装函数 在函数体内调用接口
const getChannelList = async () => {
const res = await getChannelAPI()
setChannelList(res.data.channels)
}
// 2. 调用函数
getChannelList()
}, [])
pages/Publish/index.js
<Form.Item
label="频道"
name="channel_id"
rules={[{ required: true, message: '请选择文章频道' }]}
>
<Select placeholder="请选择文章频道" style={{ width: 400 }}>
{/* value属性用户选中之后会自动收集起来作为接口的提交字段 */}
{channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}
</Select>
</Form.Item>
4. 收集表单数据提交表单
(1)使用 Form 组件收集表单数据
(2)按照接口文档封装接口函数
(3)按照接口文档处理表单数据
(4)提交接口并验证是否成功
pages/Publish/index.js
// 提交表单
const onFinish = (formValue) => {
console.log(formValue)
const { title, content, channel_id } = formValue
// 1. 按照接口文档的格式处理收集到的表单数据
const reqData = {
title,
content,
cover: {
type: 0,
images: [],
},
channel_id
}
// 2. 调用接口提交
createArticleAPI(reqData)
}
5. 上传文章封面基础功能实现
(1)使用现成组件搭建结构
(2)按照 Upload 组件添加配置实现上传
pages/Publish/index.js
// 上传回调
const [imageList, setImageList] = useState([])
const onChange = (value) => {
console.log('正在上传中', value)
setImageList(value.fileList)
}
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
{/*
listType: 决定选择文件框的外观样式
showUploadList: 控制显示上传列表
*/}
<Upload
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
name='image'
onChange={onChange}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
</Form.Item>
6. 实现切换封面类型
实现效果:只有当前模式为单图或者三图模式时才显示上传组件
(1)获取到当前的封面类型
(2)对上传组件进行条件渲染
pages/Publish/index.js
// 切换图片封面类型
const [imageType, setImageType] = useState(0)
const onTypeChange = (e) => {
console.log('切换封面了', e.target.value)
setImageType(e.target.value)
}
...
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
// 控制单选框区域 初始的时候为 0,图片数量为 0
initialValues={{ type: 0 }}
onFinish={onFinish}
form={form}
>
...
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group onChange={onTypeChange}>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
{/*
listType: 决定选择文件框的外观样式
showUploadList: 控制显示上传列表
*/}
{imageType > 0 && <Upload
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
name='image'
onChange={onChange}
maxCount={imageType}
fileList={imageList}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</Form.Item>
...
</Form>
7. 控制上传图片的数量
实现的效果:
(1)单图模式时,最多能上传一张图片
(2)三图模式时,最多能上传三张图片
如何实现:
(1)找到限制上传数量的组件属性
(2)使用 imageType 进行绑定控制
pages/Publish/index.js
{imageType > 0 && <Upload
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
name='image'
onChange={onChange}
// 控制图片上传数量
maxCount={imageType}
fileList={imageList}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
8. 发布带封面的文章
pages/Publish/index.js
// 提交表单
const onFinish = (formValue) => {
console.log(formValue)
// 校验封面类型imageType是否和实际的图片列表imageList数量是相等的
if (imageList.length !== imageType) return message.warning('封面类型和图片数量不匹配')
const { title, content, channel_id } = formValue
// 1. 按照接口文档的格式处理收集到的表单数据
const reqData = {
title,
content,
cover: {
type: imageType, // 封面模式
images: imageList.map(item => item.response.data.url), // 图片列表
},
channel_id
}
// 2. 调用接口提交
createArticleAPI(reqData)
}
十、文章列表模块
1. 功能描述和静态结构创建
pages/Article/index.js
// 引入汉化包 时间选择器显示中文
import locale from 'antd/es/date-picker/locale/zh_CN'
const Article = () => {
...
return (
<div>
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '文章列表' },
]} />
}
style={{ marginBottom: 20 }}
>
<Form initialValues={{ status: null }}>
<Form.Item label="状态" name="status">
<Radio.Group>
<Radio value={''}>全部</Radio>
<Radio value={1}>待审核</Radio>
<Radio value={2}>审核通过</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="频道" name="channel_id">
<Select
placeholder="请选择文章频道"
style={{ width: 120 }}
>
<Option value="jack">jack</Option>
<Option value="jack">jack</Option>
</Select>
</Form.Item>
<Form.Item label="日期" name="date">
{/* 传入locale属性 控制中文显示*/}
<RangePicker locale={locale}></RangePicker>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" style={{ marginLeft: 40 }}>
筛选
</Button>
</Form.Item>
</Form>
</Card>
{/* 表格区域 */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={data} />
</Card>
</div>
)
}
export default Article
2. 渲染频道数据
在文章管理里面和创建文章里面都要使用 channelList,把这段代码封装成 hook
使用自定义 hook
(1)创建一个 use 打头的函数
(2)在函数中封装业务逻辑,并 return 出组件中要用到的状态数据
(3)组件中导入函数执行并结构状态数据使用
hooks/useChannel.js
// 封装获取频道列表的逻辑
import { useState, useEffect } from 'react'
import { getChannelAPI } from '@/apis/article'
function useChannel () {
// 1. 获取频道列表所有的逻辑
// 获取频道列表
const [channelList, setChannelList] = useState([])
useEffect(() => {
// 1. 封装函数 在函数体内调用接口
const getChannelList = async () => {
const res = await getChannelAPI()
setChannelList(res.data.channels)
}
// 2. 调用函数
getChannelList()
}, [])
// 2. 把组件中要用到的数据return出去
return {
channelList
}
}
export { useChannel }
pages/Publish/index.js
import { useChannel } from '@/hooks/useChannel'
const Publish = () => {
// 获取频道列表
const { channelList } = useChannel()
...
}
pages/Article/index.js
import { useChannel } from '@/hooks/useChannel'
const Article = () => {
const { channelList } = useChannel()
...
return (
<div>
...
<Form.Item label="频道" name="channel_id">
<Select
placeholder="请选择文章频道"
style={{ width: 120 }}
>
{channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}
</Select>
</Form.Item>
...
</div>
)
}
export default Article
3. 渲染 table 文章列表
(1)封装请求接口
(2)使用 useState 维护状态数据
(3)使用 useEffect 发送请求
(4)在组件上绑定对应属性完成渲染
apis/articles.js
// 获取文章列表
export function getArticleListAPI (params) {
return request({
url: "/mp/articles",
method: 'GET',
params
})
}
pages/Article/index.js
const Article = () => {
// 获取文章列表
const [list, setList] = useState([])
const [count, setCount] = useState(0)
useEffect(() => {
async function getList () {
const res = await getArticleListAPI(reqData)
setList(res.data.results)
setCount(res.data.total_count)
}
getList()
}, [reqData])
...
return (
{/* 表格区域 */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={list} />
</Card>
)
}
4. 适配文章状态
实现效果:根据文章不同状态在状态列显示不同 Tag
实现思路:
(1)如果要适配的状态只有两个 - 三元条件渲染
(2)如果要适配的状态有多个 - 枚举渲染
const Article = () => {
const navigate = useNavigate()
const { channelList } = useChannel()
// 准备列数据
// 定义状态枚举
const status = {
1: <Tag color='warning'>待审核</Tag>,
2: <Tag color='success'>审核通过</Tag>,
}
const columns = [
...
{
title: '状态',
dataIndex: 'status',
// data - 后端返回的状态status 根据它做条件渲染
// data === 1 => 待审核
// data === 2 => 审核通过
render: data => status[data]
},
...
return (
{/* 表格区域 */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={list} />
</Card>
)
]
5. 筛选功能实现
筛选功能的本质:给请求列表接口传递不同的参数和后端要不同的数据
实现步骤:
(1)准备完整的请求参数对象
(2)获取用户选择的表单数据
(3)把表单数据放置到接口对应的字段中
(4)重新调用文章列表接口渲染 Table 列表
pages/Article/index.js
// 筛选功能
// 1. 准备参数
const [reqData, setReqData] = useState({
status: '',
channel_id: '',
begin_pubdate: '',
end_pubdate: '',
page: 1,
per_page: 4
})
// 获取文章列表
const [list, setList] = useState([])
const [count, setCount] = useState(0)
useEffect(() => {
async function getList () {
const res = await getArticleListAPI(reqData)
setList(res.data.results)
setCount(res.data.total_count)
}
getList()
}, [reqData])
// 2. 获取筛选数据
const onFinish = (formValue) => {
console.log(formValue)
// 3. 把表单收集到数据放到参数中(不可变的方式)
setReqData({
...reqData,
channel_id: formValue.channel_id,
status: formValue.status,
begin_pubdate: formValue.date[0].format('YYYY-MM-DD'),
end_pubdate: formValue.date[1].format('YYYY-MM-DD')
})
// 4. 重新拉取文章列表 + 渲染table逻辑重复的 - 复用
// reqData依赖项发生变化 重复执行副作用函数
}
...
<Form initialValues={{ status: '' }} onFinish={onFinish}>
6. 分页功能实现
实现效果:点击页数,在 Table 中显示当前页的数据列表
如何实现:
(1)实现分页展示( 页数 = 总数 / 每条页数 )
(2)点击分页拿到当前点击的页数
(3)使用页数作为请求参数重新获取文章列表渲染
pages/Article/index.js
// 分页
const onPageChange = (page) => {
console.log(page)
// 修改参数依赖项 引发数据的重新获取列表渲染
setReqData({
...reqData,
page
})
}
{/* 表格区域 */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={list} pagination={{
total: count,
pageSize: reqData.per_page,
onChange: onPageChange
}} />
</Card>
7. 删除功能实现
实现效果:点击删除按钮,删除当前文章
如何实现:
(1)点击删除弹出确认框
(2)得到文章 id,使用 id 调用删除接口
(3)更新文章列表
pages/Article/index.js
const columns = [
...
{
title: '操作',
render: data => {
return (
<Space size="middle">
<Button type="primary" shape="circle" icon={<EditOutlined />} />
<Popconfirm
title="删除文章"
description="确认要删除当前文章吗?"
onConfirm={() => onConfirm(data)}
okText="Yes"
cancelText="No"
>
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
)
}
}
]
// 删除
const onConfirm = async (data) => {
console.log('删除点击了', data)
await delArticleAPI(data.id)
// 重新获取列表
setReqData({
...reqData
})
}
apis/article.js
// 删除文章
export function delArticleAPI (id) {
return request({
url: `/mp/articles/${id}`,
method: 'DELETE'
})
}
8. 携带 id 跳转到编辑页
实现效果:点击编辑文章跳转到文章编辑页
如何实现:
(1)获取当前文章 id
(2)跳转到创建(编辑)文章的路由
const columns = [
...
{
title: '操作',
render: data => {
return (
<Space size="middle">
<Button type="primary" shape="circle" icon={<EditOutlined />}
onClick={() => navigate(`/publish?id=${data.id}`)} />
<Popconfirm
title="删除文章"
description="确认要删除当前文章吗?"
onConfirm={() => onConfirm(data)}
okText="Yes"
cancelText="No"
>
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
)
}
}
]
十一、编辑文章
1. 回填基础数据
实现效果:把页面中除了封面之外的其余字段完成回填
如何实现:
(1)通过文章 id 获取到文章详情数据
(2)调用 Form 组件实例方法 setFieldsValue 回显数据
apis/article.js
// 获取文章详情
export function getArticleById (id) {
return request({
url: `/mp/articles/${id}`
})
}
pages/Publish/index.js
// 回填数据
const [searchParams] = useSearchParams()
const articleId = searchParams.get('id')
// 获取实例
const [form] = Form.useForm()
useEffect(() => {
// 1. 通过id获取数据
async function getArticleDetail () {
const res = await getArticleById(articleId)
form.setFieldsValue(res.data)
}
getArticleDetail()
// 2. 调用实例方法 完成回填
}, [articleId, form])
...
<Form
form={form}
>
2. 回填封面信息
实现效果:回填封面的类型以及上传过的封面图片
如何实现:
(1)使用 cover 中的 type 字段回填封面类型
(2)使用 cover 中的 images 字段回填封面图片
pages/Publish/index.js
const [imageList, setImageList] = useState([])
const [imageType, setImageType] = useState(0)
useEffect(() => {
// 1. 通过id获取数据
async function getArticleDetail () {
const res = await getArticleById(articleId)
const data = res.data
const { cover } = data
form.setFieldsValue({
...data,
type: cover.type
})
// 为什么现在的写法无法回填封面?
// 数据结构的问题 set方法 -> { type: 3 } { cover: { type: 3}}
// 回填图片列表
setImageType(cover.type)
// 显示图片({url:url})
setImageList(cover.images.map(url => {
return { url }
}))
}
getArticleDetail()
// 2. 调用实例方法 完成回填
}, [articleId, form])
...
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group onChange={onTypeChange}>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
{/*
listType: 决定选择文件框的外观样式
showUploadList: 控制显示上传列表
*/}
{imageType > 0 && <Upload
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
name='image'
onChange={onChange}
maxCount={imageType}
fileList={imageList}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</Form.Item>
3. 根据 id 适配 编辑 和 新增 状态
实现效果:发布文章时显示发布文章,编辑文章状态下显示编辑文章
如何实现:
核心就是看是否有 id, 有文章 id 代表编辑状态,没有文章 id 代表发布状态
pages/Publish/index.js
useEffect(() => {
async function getArticleDetail () {
...
}
// 只有有id的时候才能调用此函数回填
if (articleId) {
getArticleDetail()
}
}, [articleId, form])
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: `${articleId ? '编辑' : '发布'}文章` },
]}
/>
}
>
...
</Card>
</div>
)
4. 更新文章
实现效果:当用户对文章内容做修改之后,点击确定更新文章内容
如何实现:
更新文章和新增文章相比,大部分的逻辑都是一致的,稍作参数适配调用不同接口即可
(1)适配 url 参数
(2)调用文章更新接口
pages/Publish/index.js
// 提交表单
const onFinish = (formValue) => {
const reqData = {
title,
content,
cover: {
type: imageType, // 封面模式
// 这里的url处理逻辑只是在新增时候的逻辑
// 编辑的时候需要做处理
images: imageList.map(item => {
if (item.response) {
return item.response.data.url
} else {
return item.url
}
}) // 图片列表
},
channel_id
}
// 2. 调用接口提交
// 处理调用不同的接口 新增 - 新增接口 编辑状态 - 更新接口 id
if (articleId) {
// 更新接口
updateArticleAPI({ ...reqData, id: articleId })
} else {
createArticleAPI(reqData)
}
}
apis/article.js
// 更新文章表单
export function updateArticleAPI (data) {
return request({
url: `/mp/articles/${data.id}?draft=false`,
method: 'PUT',
data
})
}
十二、项目打包
1. 项目打包和本地预览
(1)项目打包
打包指的是将项目中的源代码和资源文件进行处理,生成可在生产环境中运行的静态文件的过程
打包命令:npm run build
(2)本地预览(模拟服务器运行项目)
本地预览是指在本地通过静态服务器模拟生成服务器运行项目的过程
a. 安装本地服务包 npm i -g serve
b. serve -s build
c. 浏览器中访问 http://localhose:3000/
2. 配置路由懒加载
什么是路由懒加载?
路由懒加载是指路由的 JS 资源只有在被访问时才会动态获取,目的是为了优化项目首次打开的时间
如何进行配置?
(1)把路由修改为由 React 提供的 lazy 函数进行动态导入
(2)使用 React 内置的 Suspense 组件 包裹路由中 element 选项对应的组件
router/index.js
// import Home from '@/pages/Home'
// import Article from '@/pages/Article'
// import Publish from '@/pages/Publish'
import { Suspense, lazy } from 'react'
// 1. lazy函数对组件进行导入
const Home = lazy(() => import('@/pages/Home'))
const Article = lazy(() => import('@/pages/Article'))
const Publish = lazy(() => import('@/pages/Publish'))
// 配置路由实例
const router = createBrowserRouter([
{
path: "/",
element: <AuthRoute> <Layout /></AuthRoute>,
children: [
{
index: true,
element: <Suspense fallback={'加载中'}><Home /></Suspense>
},
{
path: 'article',
element: <Suspense fallback={'加载中'}><Article /></Suspense>
},
{
path: 'publish',
element: <Suspense fallback={'加载中'}><Publish /></Suspense>
}
]
},
{
path: "/login",
element: <Login />
}
])
export default router
3. 包体积分析
通过可视化的方式,直观的体现项目中各种包打包之后的体积大小,方便做优化
怎么做到?source-map-explorer
a. 安装包
b. 配置命令指定要分析的 js 文件
npm i source-map-explorer
package.json
"scripts": {
...
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
npm run analyze
4. CDN 优化
CDN 是一种内容分发网络服务,当用户请求网站内容时,由离用户最近的服务器将缓存的资源内容传递给用户
哪些资源可以放到 CDN 服务器?
体积较大的非业务 JS 文件,比如 react、react-dom
(1)体积较大,需要利用 CDN 文件在浏览器的缓存特性,加快加载时间
(2)非业务 JS 文件,不需要经常做变动,CDN 不用频繁更新缓存
项目中怎么做?
(1)把需要做 CDN 缓存的文件排除在打包之外(react、react-dom)
(2)以 CDN 的方式重新引入资源(react、react-dom)
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')
},
// 配置CDN
configure: (webpackConfig) => {
let cdn = {
js: []
}
whenProd(() => {
// key: 不参与打包的包(由dependencies依赖项中的key决定)
// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
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',
]
}
})
// 通过 htmlWebpackPlugin插件 在public/index.html注入cdn资源url
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资源url -->
<% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL=> { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>
重启一下项目,预览一下
npm run build
serve -s build
十三、其他
1. useReducer
作用:和 useState 的作用类似,用来管理相对复杂的状态数据
(1)定义一个 reducer 函数(根据不同的 action 返回不同的新状态)
(2)在组件中调用 useReducer,并传入 reducer 函数和状态的初始值
(3)事件发生时,通过 dispatch 函数分派一个 action 对象(通知 reducer 要返回哪个新状态并渲染UI)
// useReducer
import { useReducer } from "react";
// 1. 定义 reducer 函数 根据不同的 action 返回不同的状态
function reducer(state, action) {
switch (action.type) {
case "INC":
return state + 1;
case "DEC":
return state - 1;
case "SET":
return action.payload;
default:
return state;
}
}
// 2. 组件中调用 useReducer(reducer, 0) => [state, dispatch]
// 3. 调用 dispatch({type:'INC}) => 通知 reducer 产生一个新的状态 使用这个新状态更新 UI
function App() {
const [state, dispatch] = useReducer(reducer, 0);
return (
<div className="App">
<button onClick={() => dispatch({ type: "DEC" })}>-</button>
this is app
{state}
<button onClick={() => dispatch({ type: "INC" })}>+</button>
<button onClick={() => dispatch({ type: "SET", payload: 100 })}>
update
</button>
</div>
);
}
export default App;
2. useMemo
作用:在组件每次重新渲染的时候缓存计算的结果
3. React.memo
(1)基础使用
作用:允许组件在 Props 没有改变的情况下跳过渲染
React 组件默认的渲染机制:只要父组件重新渲染,子组件就会重新渲染
说明:经过 memo 函数包裹生成的缓存组件只有在 props 发生变化的时候才会重新渲染
(2)props 的比较机制
机制:在使用 memo 缓存组件之后,React 会对每一个 prop 使用 Object.is 比较新值和老值,返回 true,表示没有变化
prop 是简单类型
Object.is(3, 3) => true 没有变化
prop 是引用类型(对象 / 数组)
Object.is([ ], [ ]) => false 有变化,React 只关心引用是否变化
// React.memo props 比较机制
// 1. 传递一个简单类型的 prop prop 变化时组件重新渲染
// 2. 传递一个引用类型的 prop 比较的是新值和旧值的引用是否相等 当父组件的函数重新执行时,实际上形成的是新的数组引用
// 3. 保证引用稳定 -> useMemo 组件渲染的过程中缓存一个值
import { memo, useMemo, useState } from "react";
const MemoSon = memo(function Son({ list }) {
console.log("子组件重新渲染了");
return <div>this is Son {list}</div>;
});
function App() {
const [count, setCount] = useState(0);
// const num = 100;
const list = useMemo(() => {
return [1, 2, 3];
}, []);
return (
<div className="App">
<MemoSon />
<button onClick={() => setCount(count + 1)}>change Count</button>
</div>
);
}
export default App;
4. useCallback
作用:在组件多次重新渲染的时候缓存函数
// useCallback
import { memo, useCallback, useState } from "react";
const Input = memo(function Input({ onChange }) {
console.log("子组件重新渲染了");
return <input type="text" onChange={(e) => onChange(e.target.value)} />;
});
function App() {
// 传给子组件的函数
const changeHandler = useCallback((value) => console.log(value), []);
// 触发父组件重新渲染的函数
const [count, setCount] = useState(0);
return (
<div className="App">
{/* 把函数作为 prop 传给子组件 */}
<Input onChange={changeHandler} />
<button onClick={() => setCount(count + 1)}>change Count</button>
</div>
);
}
export default App;
5. React.forwardRef (使用 ref 暴露 DOM 节点给父组件)
import { forwardRef, useRef } from "react";
// 子组件
const Son = forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});
// 父组件
function App() {
const sonRef = useRef(null);
const showRef = () => {
console.log(sonRef);
sonRef.current.focus();
};
return (
<div className="App">
<Son ref={sonRef} />
<button onClick={showRef}>focus</button>
</div>
);
}
export default App;
6. useImperativeHandle(通过 ref 暴露子组件的方法)
import { forwardRef, useImperativeHandle, useRef } from "react";
// 子组件
const Son = forwardRef((props, ref) => {
// 实现聚焦逻辑
const inputRef = useRef(null);
const focusHandler = () => {
inputRef.current.focus();
};
// 把聚焦方法暴露出去
useImperativeHandle(ref, () => {
return {
// 暴露的方法
focusHandler,
};
});
return <input type="text" ref={inputRef} />;
});
// 父组件
function App() {
const sonRef = useRef(null);
const focusHandler = () => {
console.log(sonRef.current);
};
return (
<div className="App">
<Son ref={sonRef} />
<button onClick={focusHandler}>focus</button>
</div>
);
}
export default App;
十四、Class 类组件
1. 基础结构
类组件就是通过 JS 中的类来阻止组件的代码
(1)通过类属性 state 定义状态数据
(2)通过 setState 方法来修改状态数据
(3)通过 render 来写 UI 模版 (JSX语法一致)
// Class API
import { Component } from "react"
class Counter extends Component {
// 编写组件的逻辑代码
// 1. 状态变量 2. 事件回调 3.UI(JSX)
// 1. 定义状态变量
state = {
count: 0
}
// 2. 定义事件回调修改状态数据
setCount = () => {
// 修改状态数据
this.setState({
count: this.state.count + 1
})
}
render () {
return <button onClick={this.setCount}>{this.state.count}</button>
}
}
function App () {
return (
<>
<Counter />
</>
)
}
export default App
2. 类组件生命周期函数
概念:组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数
(1)componentDidMount:组件挂载完毕自动执行 - 异步数据获取
(2)componentWillUnmount:组件卸载时自动执行 - 清理副作用
// Class API 生命周期
import { Component, useState } from "react"
class Son extends Component {
// 声明周期函数
// 组件渲染完毕执行一次 发送网络请求
componentDidMount () {
console.log('组件渲染完毕了,请求发送起来')
// 开启定时器
this.timer = setInterval(() => {
console.log('定时器运行中')
}, 1000)
}
// 组件卸载的时候自动执行 副作用清理的工作 清除定时器 清除事件绑定
componentWillUnmount () {
console.log('组件son被卸载了')
// 清除定时器
clearInterval(this.timer)
}
render () {
return <div>i am Son</div>
}
}
function App () {
const [show, setShow] = useState(true)
return (
<>
{show && <Son />}
<button onClick={() => setShow(false)}>unmount</button>
</>
)
}
export default App
3. 类组件的组件通信
概念:类组件和 Hooks 编写的组件在组件通信的思想上完全一致
(1)父传子:通过 prop 绑定数据
(2)子传父:通过 prop 绑定父组件中的函数,子组件调用
(3)兄弟通信:状态提升,通过父组件做桥接
// Class API 父子通信
import { Component } from "react"
// 1. 父传子 直接通过prop子组件标签身上绑定父组件中的数据即可
// 2. 子传父 在子组件标签身上绑定父组件中的函数,子组件中调用这个函数传递参数
// 总结
// 1. 思想保持一致
// 2. 类组件依赖于this
// 子组件
class Son extends Component {
render () {
// 使用this.props.msg
return <>
<div>我是子组件 {this.props.msg}</div>
<button onClick={() => this.props.onGetSonMsg('我是son组件中的数据')}>sendMsgToParent</button>
</>
}
}
// 父组件
class Parent extends Component {
state = {
msg: 'this is parent msg'
}
getSonMsg = (sonMsg) => {
console.log(sonMsg)
}
render () {
return <div>我是父组件<Son msg={this.state.msg} onGetSonMsg={this.getSonMsg} /></div>
}
}
function App () {
return (
<>
<Parent />
</>
)
}
export default App