IT评测·应用市场-qidao123.com

标题: React快速上手到项目实战总篇 [打印本页]

作者: 吴旭华    时间: 2025-1-6 08:07
标题: React快速上手到项目实战总篇
React核心代价与前置知识

时刻保持对知识的渴望 家人们 开学!!!
   核心代价
  
  使用vite创建Recat项目


开发规范

使用 prettier & eslint 规范开发

  1. #eslint :
  2. npm install eslint@typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
  3. #prettier:
  4. npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev
复制代码
vite和 webpack的区别
webpack是一个非常盛行的前端打包工具 比较经典 Create-React-App 是使用webpack作为打包工具的
vite 既是构建工具 又是打包工具
vite的特点:
React JSX语法

内容 :
JSX特点:
标签



属性

和html根本相似

如下

在JSX中插入js变量


   代码案例
  

条件判定

​ 常见的if else 可以通过{}的方式实现,但是在JSX中代码一多就显得不敷实用了 以下三种方法可以解决:

比如这样:反之如果flag等于false 就不会出现hello

结果:

三元运算符:flag为判定条件 来控制标签的表现

结果:

函数:
  1. function isShowHello(){
  2.   if (flag)return <p>show hello</p>
  3.   return <p>defaultHello</p>
  4. }
复制代码

结果 :

循环


   实现
  1. const list = [
  2.   {username:'zhangsan', name:"张三"},
  3.   {username:'shuangyue', name:"双月"},
  4.   {username:'lisi', name:"李四"},
  5. ]
  6.      {/*循环*/}
  7.         <div>
  8.           {list.map(user=>{
  9.             const {username,name} = user
  10.            return <li key={username}>{name}</li>
  11.           })}
  12.         </div>
复制代码
结果:

PS : 不发起使用 index 如 :

因为我们的key 必要具有唯一性
小结实战 列表页

开发一个列表页

调整一下表现的jsx

包管这个代码结构简洁 ,然后就可以开始开发了
  1. import React from 'react';
  2. import './App1.css';
  3. function App() {
  4.     const questionList = [
  5.         {id: 'q1', title: '问卷1', isPublished: true},
  6.         {id: 'q2', title: '问卷2', isPublished: true},
  7.         {id: 'q3', title: '问卷3', isPublished: true},
  8.         {id: 'q4', title: '问卷4', isPublished: false}
  9.     ]
  10.     function edit(id) {
  11.         console.log('edit', id);
  12.     }
  13.     return (<div>
  14.         <h1>列表详情页</h1>
  15.         <div>
  16.             {questionList.map(question => {
  17.                 const {id, title, isPublished} = question;
  18.                 return <div key={id} className="list-item">
  19.                     &nbsp;
  20.                     <strong>{title}</strong>
  21.                     &nbsp;
  22.                     {isPublished ? <span style={{color: "green"}}>已发布</span> : <span>未发布</span>}
  23.                     &nbsp;
  24.                     <button onClick={() => edit(id)}>编辑问卷</button>
  25.                 </div>
  26.             })}
  27.         </div>
  28.     </div>)
  29. }
  30. export default App;
复制代码
  css
  1. .list-item {
  2.     border: 1px solid #ccc;
  3.     padding: 10px;
  4.     margin-bottom: 16px;
  5.     display: flex;
  6.     justify-content: center;
  7. }
复制代码
  结果
  

组件

react 齐备皆是组件

   组件拆分的代价和意义
  
好的组件化 逻辑是清楚的 更能提拔开发服从并且更加的雅观易读

我们可以将组件理解成一个一个的函数
使用我们之前的列表页代码 拆分成组件 list1

然后用improt的方式 引入到listdemo中

这样我们的总框架就没有那么多的代码冗余 必要修改对应的代码 只必要寻找对应的组件文件即可
属性 props


props 实在就是实现差异化组件信息转达的一种手段
   实践
  将之前循环内表现数据的div拆出来抽象成组件:QuestCard.tsx 。 CSS还是和之前的内容一样
使用 ts主要是方便传入泛型
   QuestCard.tsx
  1. import React, {FC} from "react";
  2. import './QuestCard.css'
  3. type proptype = {
  4.     id: string,
  5.     title: string,
  6.     isPublished: boolean
  7. }
  8. export const QuestCard: FC<proptype> = (props) => {
  9.     const {id, title, isPublished} = props;
  10.     function edit(id) {
  11.         console.log('edit', id);
  12.     }
  13.     return (
  14.         <div key={id} className="list-item">
  15.             &nbsp;
  16.             <strong>{title}</strong>
  17.             &nbsp;
  18.             {isPublished ? <span style={{color: "green"}}>已发布</span> : <span>未发布</span>}
  19.             &nbsp;
  20.             <button onClick={() => edit(id)}>编辑问卷</button>
  21.         </div>)
  22. }
复制代码

改造list1.jsx 这样就将表现问卷卡片抽取出来为一个独立的组件了
  1. import React from "react";
  2. import './list1.css';
  3. import {QuestCard} from "./QuestCard";
  4. export const List1 = () => {
  5.     const questionList = [
  6.         {id: 'q1', title: '问卷1', isPublished: true},
  7.         {id: 'q2', title: '问卷2', isPublished: true},
  8.         {id: 'q3', title: '问卷3', isPublished: true},
  9.         {id: 'q4', title: '问卷4', isPublished: false}
  10.     ]
  11.     return (
  12.         <div>
  13.             <h1>列表详情页</h1>
  14.             <div>
  15.                 {questionList.map(question => {
  16.                     const {id, title, isPublished} = question;
  17.                     return <QuestCard key={id} id={id} title={title} isPublished={isPublished}/>
  18.                 })}
  19.             </div>
  20.         </div>)
  21. }
复制代码
小结:

   结果
  

children

场景: 当我们把内容签到在子组件标签中时,父组件会自动的在名为 children的prop中担当内容

子组件转达父组件

顾名思义 实在就是子组件给父组件转达信息
  1. function Son({onGetSonMsg}) {
  2. //     son 中的数据
  3.     const sonMsg = 'this is son msg';
  4.     return <div>this is son
  5.         <button onClick={() => onGetSonMsg(sonMsg)}>sendMsg</button>
  6.     </div>
  7. }
  8. function AppDemo() {
  9.     const [msg, setMsg] = useState('')
  10.     const getMsg = (msg) => {
  11.         console.log(msg)
  12.         // msg = '我是信息'  这么改是无效的
  13.         setMsg(msg)
  14.     }
  15.     return <div>
  16.         this is APP Son send msg =>{msg}
  17.         <Son onGetSonMsg={getMsg}/>
  18.     </div>
  19. }
复制代码
兄弟组件转达

使用状态提拔实现兄弟组件通讯


   代码
  1. import {useState} from "react";
  2. function A({onGetAName}) {
  3.     const name = "a name"
  4.     return <div>this is A
  5.         <button onClick={() => onGetAName(name)}>send</button>
  6.     </div>
  7. }
  8. function B({pushAName}) {
  9.     return <div>this is B
  10.         {pushAName}
  11.     </div>
  12. }
  13. function AppDemo() {
  14.     const [aName, setAName] = useState('');
  15.     const getAName = (name) => {
  16.         console.log(name)
  17.         setAName(name)
  18.     }
  19.     return <div>
  20.         this is app
  21.         <A onGetAName={getAName}/>
  22.         <B pushAName={aName}/>
  23.     </div>
  24. }
  25. export default AppDemo;
  26. function A({onGetAName}) {
  27.     const name = "a name"
  28.     return <div>this is A
  29.         <button onClick={() => onGetAName(name)}>send</button>
  30.     </div>
  31. }
  32. function B({pushAName}) {
  33.     return <div>this is B
  34.         {pushAName}
  35.     </div>
  36. }
  37. function AppDemo() {
  38.     const [aName, setAName] = useState('');
  39.     const getAName = (name) => {
  40.         console.log(name)
  41.         setAName(name)
  42.     }
  43.     return <div>
  44.         this is app
  45.         <A onGetAName={getAName}/>
  46.         <B pushAName={aName}/>
  47.     </div>
  48. }
复制代码
  结果
  

React 拓展

React.memo

允许组件在Props没有改变的情况下 跳过渲染
   react组件默认的渲染机制 : 父组件重新渲染的时间子组件也会重新渲染
  1. import React, {useState} from 'react';
  2. function Son() {
  3.     console.log('子组件被重新渲染了')
  4.     return <div>this is son</div>
  5. }
  6. const ReactMemoDemo = () => {
  7.     const [, forceUpdate] = useState()
  8.     console.log('父组件重新渲染了')
  9.     return (
  10.         <>
  11.             <Son/>
  12.             <button onClick={() => forceUpdate(Math.random())}>update</button>
  13.         </>
  14.     )
  15. };
  16. export default ReactMemoDemo;
复制代码

这个时间使用 memo包裹住组件 就可以制止 但是 注意 只考虑props变化才气使用\
  1. import React, {memo, useState} from 'react';
  2. // function Son() {
  3. //     console.log('子组件被重新渲染了')
  4. //     return <div>this is son</div>
  5. // }
  6. const MemoSon = memo(function Son() {
  7.     console.log("我是子组件 我被渲染了")
  8.     return <div>this is son</div>
  9. })
  10. const ReactMemoDemo = () => {
  11.     const [, forceUpdate] = useState()
  12.     console.log('父组件重新渲染了')
  13.     return (
  14.         <>
  15.             <MemoSon/>
  16.             <button onClick={() => forceUpdate(Math.random())}>update</button>
  17.         </>
  18.     )
  19. };
  20. export default ReactMemoDemo;
复制代码

React.memo 比较机制

React会对每一个prop进行 object.is比较 返回true 表现没有变化
PS: 对于引用类型 React只关心引用是否变化
HOOKS

useState

这是React 中的一个hook 函数 它允许我们向组件添加一个状态变脸,从而控制组件的渲染结果

  1. const [msg, setMsg] = useState('')
复制代码
   修改规则
  在React 中 状态被认为是只读的 我们应该更换而不是修改 直接修改状态不会得到视图的更新
  1.     const [msg, setMsg] = useState('')
  2.     const getMsg = (msg) => {        console.log(msg)        // msg = '我是信息'  这么改是无效的        setMsg(msg)    }        //如果是对象作为参数      const [msg, setMsg] = useState({id:'122ds'})    const getMsg = (msg) => {        console.log(msg)        // msg = '我是信息'  这么改是无效的        setMsg({            ...msg,                id:'123'})    }
复制代码
useContext 组件通讯

案例 :

我们必要将app的消息转达到b
  1. const MsgContext = createContext()
  2. function A() {
  3.     return <div>this is A
  4.         <B/>
  5.     </div>
  6. }
  7. function B() {
  8.     const msg = useContext(MsgContext)
  9.     return <div>this is B from APP:{msg}
  10.     </div>
  11. }
  12. function AppDemo() {
  13.     const msg = "this is app msg"
  14.     return (<div>
  15.         <MsgContext.Provider value={msg}>
  16.             this is app
  17.             <A/>
  18.         </MsgContext.Provider>
  19.     </div>)
  20. }
复制代码
useEffect

这是React中的一个 hook 函数 ,用于在React 中创建不是由变乱引起而是由渲染自己引起的操纵,比如发送 AJAX请求 更改DOM等

   基础使用
  需求: 在组件渲染完毕后,从服务器获得列表数据展示
语法:
  1. useEffect(()=>{},[])
复制代码
  1. import {useEffect, useState} from "react";
  2. const URL = 'http://geek.itheima.net/v1_0/channels'
  3. function AppDemo() {
  4.     const [list, setList] = useState([]);
  5.     useEffect(() => {
  6.         async function getList() {
  7.             const res = await fetch(URL)
  8.             const jsonRes = await res.json()
  9.             console.log(jsonRes)
  10.             setList(jsonRes.data.channels)
  11.         }
  12.         getList()
  13.         console.log("list", list)
  14.     }, []);
  15.     return (<div>
  16.         this is app
  17.         <ul>
  18.             {list.map(item => <li key={item.id}>{item.name}</li>)}
  19.         </ul>
  20.     </div>)
  21. }
  22. export default AppDemo;
复制代码
  结果
  

   依赖项参数
  

  1. function AppDemo() {
  2.     /*1. 没有依赖项*/
  3.     const [count, setCount] = useState(0);
  4.     // useEffect(() => {
  5.     //     console.log("副作用函数执行了")
  6.     // });
  7.     /*2 传入空数组依赖*/
  8.     // useEffect(() => {
  9.     //     console.log("副作用函数执行了")
  10.     // }, []);
  11.     useEffect(() => {
  12.         console.log("副作用函数执行了")
  13.     }, [count]);
  14.     return <div>this is app
  15.         <button onClick={() => setCount(count + 1)}>+{count}</button>
  16.     </div>
  17. }
复制代码
  清除副作用
  在useEffect中编写的由渲染自己引起的对接组件外部的操纵,社区也经常把它叫做副作用操纵,我们想在组件卸载时把这个定时器清理掉,这个过程就是清理副作用
  1. import {useEffect, useState} from "react";
  2. function Son() {
  3.     useEffect(() => {
  4.         const timer = setInterval(() => {
  5.             console.log("定时器执行中...")
  6.         }, 1000)
  7.         return () => {
  8.             //     清楚副作用
  9.             clearInterval(timer)
  10.         }
  11.     }, []);
  12.     return <div>this is son</div>
  13. }
  14. function AppDemo() {
  15.     const [show, setShow] = useState(true)
  16.     return <div>this is app
  17.         {show && <Son/>}
  18.         <button onClick={() => setShow(false)}>卸载组件</button>
  19.     </div>
  20. }
  21. export default AppDemo;
复制代码
useReducer


  1. import React, {useReducer} from 'react';
  2. // 根据不同的case 返回不同的状态
  3. function reducer(state, action) {
  4.     switch (action.type) {
  5.         case 'INC':
  6.             return state + 1
  7.         case 'DEC':
  8.             return state - 1
  9.         case 'SET':
  10.             return state = action.payload
  11.         default:
  12.             return state
  13.     }
  14. }
  15. const ReducerDemo = () => {
  16.     // 使用 use reducer
  17.     const [state, dispatch] = useReducer(reducer, 0)
  18.     return (
  19.         <div>
  20.             <button onClick={() => dispatch({type: 'INC'})}>+</button>
  21.             {state}
  22.             <button onClick={() => dispatch({type: 'DEC'})}>-</button>
  23.             <button onClick={() => dispatch({type: 'SET', payload: 100})}>Set</button>
  24.         </div>
  25.     );
  26. };
  27. export default ReducerDemo;
复制代码
这个钩子相当于 一个可以有多个修改state方法的 usestate
useMemo

作用:它在每次重新渲染的时间能够缓存盘算的结果
   小案例
  
  1. import React, {useState} from 'react';
  2. function factorialOf(n) {
  3.     console.log('斐波那契函数执行了')
  4.     return n <= 0 ? 1 : n * factorialOf(n - 1)
  5. }
  6. const MemoDemo = () => {
  7.     const [count, setCount] = useState(0)
  8.     // 计算斐波那契之和
  9.     const sumByCount = factorialOf(count)
  10.     const [num, setNum] = useState(0)
  11.     return (
  12.         <>
  13.             {sumByCount}
  14.             <button onClick={() => setCount(count + 1)}>+count:{count}</button>
  15.             <button onClick={() => setNum(num + 1)}>+num:{num}</button>
  16.         </>
  17.     )
  18. };
  19. export default MemoDemo;
复制代码
useMemo 就是用来解决这种问题的
  1. import React, {useMemo, useState} from 'react';
  2. function factorialOf(n) {
  3.     console.log('斐波那契函数执行了')
  4.     return n <= 0 ? 1 : n * factorialOf(n - 1)
  5. }
  6. const MemoDemo = () => {
  7.     const [count, setCount] = useState(0)
  8.     // 计算斐波那契之和
  9.     // const sumByCount = factorialOf(count)
  10.     const sumByCount = useMemo(() => {
  11.         return factorialOf(count)
  12.     }, [count])
  13.     const [num, setNum] = useState(0)
  14.     return (
  15.         <>
  16.             {sumByCount}
  17.             <button onClick={() => setCount(count + 1)}>+count:{count}</button>
  18.             <button onClick={() => setNum(num + 1)}>+num:{num}</button>
  19.         </>
  20.     )
  21. };
  22. export default MemoDemo;
复制代码
就不会出现 点击num按钮也会触发求和方法情况了
useCallback

作用 在组件多次重新渲染的时间 缓存函数
自定义hook

暂时没有什么很好的例子 写一个比较简单的 之后再拓展
  1. import {useState} from "react";
  2. function useToggle() {
  3. // 可复用代码
  4.     const [value, setValue] = useState(true);
  5.     const toggle = () => {
  6.         setValue(!value)
  7.     }
  8.     return {value, toggle}
  9. }
  10. function AppDemo() {
  11.     const {value, toggle} = useToggle()
  12.     return <div>this is app
  13.         {value && <div>this is show Toggle</div>}
  14.         <button onClick={toggle}>Toggle</button>
  15.     </div>
  16. }
  17. export default AppDemo;
复制代码
  结果
  

点击

Redux

完备代码案例仓库 :https://gitee.com/cold-abyss_admin/react-redux-meituan
Redux是 React 最常用的会合状态管理工具,类似与VUE的pinia(vuex) 可以独立于框架运行

使用思路:

配置Redux

在React中使用redux,官方要求安装俩个其他插件-和react-redux
官方推荐我们使用 RTK(ReduxToolkit) 这是一套工具聚集 可以简化书写方式

调试工具安装

谷歌浏览器搜索 redux-devtool安装 工具
   依赖安装
  1. #redux工具包
  2. npm i @reduxjs/toolkit react-redux
  3. #调试工具包
  4. npm install --save-dev redux-devtools-extension
复制代码

store目次机构计划



快速上手

使用react+redux 开发一个计数器 认识一下技能

提交acntion传参

在reducers的同步修改方法中添加action对象参数,在调用actionCreater参数的时间转达参数,参数会被转达到action对象的payload属性上
我们继续的改造一下counterStore
action这个对象参数有个固定的属性叫payload用来吸收传参

然后 app.js 添加两个按钮 用来转达参数

   结果
  

Reudx action异步操纵

区分同步和异步action

如果action的内容是 object对象那就是同步action,如果是函数 那就是异步action
为什么我们必要异步action操纵来使用请求 ?
   例子:
  我们有两种方式可以实现 隔五分钟 上蛋炒饭
  一种是客人自己思考五分钟
  一种是客人点好 叫服务员五分钟之后上
  这个服务员就是 redux 我们刚渴望相关aciton的操纵都在redux里完成这个时间同步action就不能满足我们的需求了 所以必要使用异步action
  ​ 异步操纵的代码变化不大,我们创建store的写法保持稳定 ,但是在函数中用异步操纵的时间必要一个能异步执行函数return出一个新的函数而我们的异步操纵卸载新的函数中.
异步action中一样寻常都会调用一个同步action
   案例: 从后端获取到列表展示到页面
  新建一个文件叫做 ChannelStore.js 然后编写对应的创建代码
  1. import {createSlice} from "@reduxjs/toolkit";
  2. import axios from "axios";
  3. const  channelStore = createSlice({
  4.     name: "channel",
  5.     initialState: {
  6.         channelList:[]
  7.     },
  8.     reducers:{
  9.         setChannel(state, action){
  10.             state.channelList=action.payload
  11.         }
  12.     }
  13. })
  14. const {setChannel}= channelStore.actions
  15. // 异步请求
  16. const fetchChannelList = ()=>{
  17.     return async (dispatch)=>{
  18.         const  res = await  axios.get('http://geek.itheima.net/v1_0/channels')
  19.         dispatch(setChannel(res.data.data.channels))
  20.     }
  21. }
  22. const reducer = channelStore.reducer;
  23. export {fetchChannelList}
  24. export default reducer
复制代码
然后去store入口加入channelStore
  1. import {configureStore} from "@reduxjs/toolkit";
  2. import counterStore from "./modules/counterStore";
  3. import channelStore from "./modules/channelStore";
  4. const store = configureStore({
  5.     reducer:{
  6.         counter: counterStore,
  7.         channel: channelStore,
  8.     }
  9. })
  10. export default store
复制代码
之后就可以在app.js加入代码
  1. import {useDispatch, useSelector} from "react-redux";
  2. import {useEffect} from "react";
  3. import {fetchChannelList} from "./store/modules/channelStore";
  4. function App() {
  5.   const {channelList} = useSelector(state => state.channel);
  6. const dispatch = useDispatch()
  7.     useEffect(() => {
  8.         dispatch(fetchChannelList())
  9.     }, [dispatch]);
  10.   return (
  11.       <div className="App">
  12.           <ul>
  13.               {channelList.map(item =><li key={item.id}>{item.name}</li>)}
  14.           </ul>
  15.       </div>
  16.   );
  17. }
  18. export default App;
复制代码
  代码结果
  

redux hooks

useSelector

它的作用是吧store中的数据映射到组件中
  1. const {count} = useSelector(state => state.counter);
复制代码
这里的count实在对应的就是

useDispatch

它的作用是天生提交 action对象的dispatch函数
  1. import {useDispatch, useSelector} from "react-redux";
  2. import {inscrement,descrement} from "./store/modules/counterStore"
  3. function App() {
  4.   const {count} = useSelector(state => state.counter);
  5. const dispatch = useDispatch()
  6.   return (
  7.     <div className="App">
  8.         <button onClick={()=>dispatch(inscrement())}>+</button>
  9.         {count}
  10.         <button onClick={()=>dispatch(descrement())}>-</button>
  11.     </div>
  12.   );
  13. }
  14. export default App;
复制代码
美团点餐界面小案例

下载模板地点:
   git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git
  结果与功能列表展示

根本的思路就是使用 RTK 来做状态管理,组件负责数据渲染和操纵action

我们在store文件夹下开始配置和编写store的使用逻辑
分类渲染

先编写对应的reducer 和异步请求逻辑
   takeaway.js
  用于异步请求列表数据
  1. import {createStore} from './store';
  2. import axios from "axios";
  3. const foodsState = createStore({
  4.     name:'foods',
  5.     initialState: {
  6.         foodsList:[]
  7.     },
  8.     reducers:{
  9.         setFoodsList(state, action){
  10.             state.foodsList=action.payload
  11.         }
  12.     }
  13. });
  14. const {setFoodsList} = foodsState.actions;
  15. //异步获取部分
  16. const fetchFoodsList = () => {
  17.     return async dispatch => {
  18.     //     异步逻辑
  19.        const res =  await axios.get(' http://localhost:3004/takeaway\n')
  20.     //     调用dispatch
  21.         dispatch(setFoodsList(res.data))
  22.     }
  23. }
  24. const reducer = foodsState.reducer
  25. export {fetchFoodsList}
  26. export default reducer
复制代码
将子store管理起来 在store文件夹下编写一个index.js作为访问store的入口
  1. import {configureStore} from "@reduxjs/toolkit";
  2. import foodsReducer from './modules/takeaway'
  3. const  store= configureStore({
  4.     reducer:{
  5.         foods:foodsReducer
  6.     }
  7. })
  8. export default store
复制代码
然后将redux和react连接起来 将store 注入进去 选择根目次的index.js
  1. import React from 'react'
  2. import { createRoot } from 'react-dom/client'
  3. import { Provider } from 'react-redux'
  4. import App from './App'
  5. import store from "./store";
  6. const root = createRoot(document.getElementById('root'))
  7. root.render(
  8.     <Provider store={store}>
  9.       <App />
  10.     </Provider>
  11. )
复制代码
  编写渲染页面
  在app.js里 遵循步骤开始操纵store
  1. import NavBar from './components/NavBar'
  2. import Menu from './components/Menu'
  3. import Cart from './components/Cart'
  4. import FoodsCategory from './components/FoodsCategory'
  5. import './App.scss'
  6. import {useSelector} from "react-redux";
  7. const App = () => {
  8. // 访问store拿到数据
  9. const {foodsList} = useSelector(state => state.foods)
  10.   return (
  11.     <div className="home">
  12.       {/* 导航 */}
  13.       <NavBar />
  14.       {/* 内容 */}
  15.       <div className="content-wrap">
  16.         <div className="content">
  17.           <Menu />
  18.           <div className="list-content">
  19.             <div className="goods-list">
  20.               {/* 外卖商品列表 */}
  21.               {foodsList.map(item => {
  22.                 return (
  23.                   <FoodsCategory
  24.                     key={item.tag}
  25.                     // 列表标题
  26.                     name={item.name}
  27.                     // 列表商品
  28.                     foods={item.foods}
  29.                   />
  30.                 )
  31.               })}
  32.             </div>
  33.           </div>
  34.         </div>
  35.       </div>
  36.       {/* 购物车 */}
  37.       <Cart />
  38.     </div>
  39.   )
  40. }
  41. export default App
复制代码
  结果
  

侧边栏渲染.交互

我们必要在获取列表解构的时间 拿到属于左侧列表的数据

然后循环的展示在menu组件中 只必要把异步请求的数据放到menu组件中就可以展示侧边栏了
  1. import classNames from 'classnames'
  2. import './index.scss'
  3. import {useDispatch, useSelector} from "react-redux";
  4. const Menu = () => {
  5.   //    获取dispatch
  6.   const  dispatch = useDispatch()
  7.   // 访问store拿到数据
  8.   const {foodsList} = useSelector(state => state.foods)
  9.   const menus = foodsList.map(item => ({ tag: item.tag, name: item.name }))
  10.   return (
  11.     <nav className="list-menu">
  12.       {/* 添加active类名会变成激活状态 */}
  13.       {menus.map((item, index) => {
  14.         return (
  15.           <div
  16.             key={item.tag}
  17.             className={classNames(
  18.               'list-menu-item',
  19.               'active'
  20.             )}
  21.           >
  22.             {item.name}
  23.           </div>
  24.         )
  25.       })}
  26.     </nav>
  27.   )
  28. }
  29. export default Menu
复制代码
  结果
  

接下来编写交互操纵 使用RTK来管理activeindex

  1. import {createSlice} from '@reduxjs/toolkit';
  2. import axios from "axios";
  3. const foodsState = createSlice({
  4.     name:'foods',
  5.     initialState: {
  6.         // 商品列表
  7.         foodsList:[],
  8.      // 菜单激活值
  9.         activeIndex:0,
  10.     },
  11.     reducers:{
  12.         setFoodsList(state, action){
  13.             state.foodsList=action.payload
  14.         },
  15.         changeActiveIndex(state, action){
  16.             state.activeIndex=action.payload
  17.         }
  18.     }
  19. });
  20. const {setFoodsList,changeActiveIndex} = foodsState.actions;
  21. //异步获取部分
  22. const fetchFoodsList = () => {
  23.     return async dispatch => {
  24.     //     异步逻辑
  25.        const res =  await axios.get(' http://localhost:3004/takeaway\n')
  26.     //     调用dispatch
  27.         dispatch(setFoodsList(res.data))
  28.         console.log(res.data)
  29.     }
  30. }
  31. const reducer = foodsState.reducer
  32. export {fetchFoodsList,changeActiveIndex}
  33. export default reducer
复制代码
然后开始编写menu组件的点击结果

   代码修改 menu/index.js
  1. import classNames from 'classnames'
  2. import './index.scss'
  3. import {useDispatch, useSelector} from "react-redux";
  4. import {changeActiveIndex} from "../../store/modules/takeaway";
  5. const Menu = () => {
  6.   //    获取dispatch
  7.   const  dispatch = useDispatch()
  8.   // 访问store拿到数据
  9.   const {foodsList,activeIndex} = useSelector(state => state.foods)
  10.   const menus = foodsList.map(item => ({ tag: item.tag, name: item.name }))
  11.   return (
  12.     <nav className="list-menu">
  13.       {/* 添加active类名会变成激活状态 */}
  14.       {menus.map((item, index) => {
  15.         return (
  16.           <div
  17.               onClick={()=>dispatch(changeActiveIndex(index))}
  18.             key={item.tag}
  19.             className={classNames(
  20.               'list-menu-item',
  21.                 activeIndex===index&& 'active'
  22.             )}
  23.           >
  24.             {item.name}
  25.           </div>
  26.         )
  27.       })}
  28.     </nav>
  29.   )
  30. }
  31. export default Menu
复制代码
  结果
  当点击的时间index就会切换到对应的index上 并且在点击当前index的时间选项高亮
  


   商品列表的切换表现
  点击侧边栏的时间 菜单栏必要表现对应侧边栏index的菜单
修改 app.js菜单栏标签的表现规则就行
  1. const App = () => {
  2. //    获取dispatch
  3. const  dispatch = useDispatch()
  4.   //   异步请求数据
  5.   useEffect(() => {
  6.     dispatch(fetchFoodsList())
  7.   }, [dispatch]);
  8. // 访问store拿到数据
  9. const {foodsList,activeIndex} = useSelector(state => state.foods)
  10.   return (
  11.     <div className="home">
  12.       {/* 导航 */}
  13.       <NavBar />
  14.       {/* 内容 */}
  15.       <div className="content-wrap">
  16.         <div className="content">
  17.           <Menu />
  18.           <div className="list-content">
  19.             <div className="goods-list">
  20.               {/* 外卖商品列表 */}
  21.               {foodsList.map((item,index) => {
  22.                 return (
  23.                 index===activeIndex&&  <FoodsCategory
  24.                     key={item.tag}
  25.                     // 列表标题
  26.                     name={item.name}
  27.                     // 列表商品
  28.                     foods={item.foods}
  29.                   />
  30.                 )
  31.               })}
  32.             </div>
  33.           </div>
  34.         </div>
  35.       </div>
  36.       {/* 购物车 */}
  37.       <Cart />
  38.     </div>
  39.   )
  40. }
复制代码
添加购物车

起首找到fooditem中的food对象 一会我们使用cartlist的时间要用到 id 和count

使用 RTK管理 状态cartlist
  1. import {createSlice} from '@reduxjs/toolkit';
  2. import axios from "axios";
  3. const foodsState = createSlice({
  4.     name:'foods',
  5.     initialState: {
  6.         // 商品列表
  7.         foodsList:[],
  8.      // 菜单激活值
  9.         activeIndex:0,
  10.         // 购物车列表
  11.         cartList:[]
  12.     },
  13.     reducers:{
  14.         // 修改商品列表
  15.         setFoodsList(state, action){
  16.             state.foodsList=action.payload
  17.         },
  18.         // 更改activeIndex
  19.         changeActiveIndex(state, action){
  20.             state.activeIndex=action.payload
  21.         },
  22.         // 添加购物车
  23.         addCart(state, action){
  24.         //    通过payload.id去匹配cartList匹配,匹配到代表添加过
  25.            const  item = state.cartList.find(item=>item.id ===action.payload.id)
  26.             if (item){
  27.                 item.count++
  28.             }else{
  29.                 state.cartList.push(action.payload)
  30.             }
  31.         }
  32.     }
  33. });
  34. const {setFoodsList,changeActiveIndex,addCart} = foodsState.actions;
  35. //异步获取部分
  36. const fetchFoodsList = () => {
  37.     return async dispatch => {
  38.     //     异步逻辑
  39.        const res =  await axios.get(' http://localhost:3004/takeaway\n')
  40.     //     调用dispatch
  41.         dispatch(setFoodsList(res.data))
  42.         console.log(res.data)
  43.     }
  44. }
  45. const reducer = foodsState.reducer
  46. export {fetchFoodsList,changeActiveIndex,addCart}
  47. export default reducer
复制代码
在fooditem.jsx编写cartList触发操纵
  1. import './index.scss'
  2. import {useDispatch} from "react-redux";
  3. import {addCart} from "../../../store/modules/takeaway";
  4. const Foods = ({
  5.   id,
  6.   picture,
  7.   name,
  8.   unit,
  9.   description,
  10.   food_tag_list,
  11.   month_saled,
  12.   like_ratio_desc,
  13.   price,
  14.   tag,
  15.   count =1
  16. }) => {
  17.   const  dispatch = useDispatch()
  18.   return (
  19.     <dd className="cate-goods">
  20.       <div className="goods-img-wrap">
  21.         <img src={picture} alt="" className="goods-img" />
  22.       </div>
  23.       <div className="goods-info">
  24.         <div className="goods-desc">
  25.           <div className="goods-title">{name}</div>
  26.           <div className="goods-detail">
  27.             <div className="goods-unit">{unit}</div>
  28.             <div className="goods-detail-text">{description}</div>
  29.           </div>
  30.           <div className="goods-tag">{food_tag_list.join(' ')}</div>
  31.           <div className="goods-sales-volume">
  32.             <span className="goods-num">月售{month_saled}</span>
  33.             <span className="goods-num">{like_ratio_desc}</span>
  34.           </div>
  35.         </div>
  36.         <div className="goods-price-count">
  37.           <div className="goods-price">
  38.             <span className="goods-price-unit">¥</span>
  39.             {price}
  40.           </div>
  41.           <div className="goods-count">
  42.             <span className="plus" onClick={()=>{dispatch(addCart({
  43.               id,
  44.               picture,
  45.               name,
  46.               unit,
  47.               description,
  48.               food_tag_list,
  49.               month_saled,
  50.               like_ratio_desc,
  51.               price,
  52.               tag,
  53.               count
  54.             }))}}></span>
  55.           </div>
  56.         </div>
  57.       </div>
  58.     </dd>
  59.   )
  60. }
  61. export default Foods
复制代码
  结果
  

统计订单地域


实现思路
  1. // 计算总价
  2. const totalPrice = cartList.reduce((a, c) => a + c.price * c.count, 0)
  3. {/* fill 添加fill类名购物车高亮*/}
  4. {/* 购物车数量 */}
  5. <div onClick={onShow} className={classNames('icon', cartList.length > 0 && 'fill')}>
  6.   {cartList.length > 0 && <div className="cartCornerMark">{cartList.length}</div>}
  7. </div>
复制代码

   结果
  

   cart.jsx全部代码
  1. import classNames from 'classnames'
  2. import Count from '../Count'
  3. import './index.scss'
  4. import {useSelector} from "react-redux";
  5. import {fill} from "lodash/array";
  6. const Cart = () => {
  7.     const{cartList} = useSelector(state => state.foods)
  8.   //   计算总价
  9.    const  totalPrice = cartList.reduce((a, c) => a+c.price*c.count,0)
  10.   const cart = []
  11.   return (
  12.     <div className="cartContainer">
  13.       {/* 遮罩层 添加visible类名可以显示出来 */}
  14.       <div
  15.         className={classNames('cartOverlay')}
  16.       />
  17.       <div className="cart">
  18.         {/* fill 添加fill类名可以切换购物车状态*/}
  19.         {/* 购物车数量 */}
  20.         <div className={classNames('icon')}>
  21.           {cartList.length>0 && <div className="cartCornerMark">{cartList.length}</div>}
  22.         </div>
  23.         {/* 购物车价格 */}
  24.         <div className="main">
  25.           <div className="price">
  26.             <span className="payableAmount">
  27.               <span className="payableAmountUnit">¥</span>
  28.               {totalPrice.toFixed(2)}
  29.             </span>
  30.           </div>
  31.           <span className="text">预估另需配送费 ¥5</span>
  32.         </div>
  33.         {/* 结算 or 起送 */}
  34.         {cartList.length > 0 ? (
  35.           <div className="goToPreview">去结算</div>
  36.         ) : (
  37.           <div className="minFee">¥20起送</div>
  38.         )}
  39.       </div>
  40.       {/* 添加visible类名 div会显示出来 */}
  41.       <div className={classNames('cartPanel')}>
  42.         <div className="header">
  43.           <span className="text">购物车</span>
  44.           <span className="clearCart">
  45.             清空购物车
  46.           </span>
  47.         </div>
  48.         {/* 购物车列表 */}
  49.         <div className="scrollArea">
  50.           {cart.map(item => {
  51.             return (
  52.               <div className="cartItem" key={item.id}>
  53.                 <img className="shopPic" src={item.picture} alt="" />
  54.                 <div className="main">
  55.                   <div className="skuInfo">
  56.                     <div className="name">{item.name}</div>
  57.                   </div>
  58.                   <div className="payableAmount">
  59.                     <span className="yuan">¥</span>
  60.                     <span className="price">{item.price}</span>
  61.                   </div>
  62.                 </div>
  63.                 <div className="skuBtnWrapper btnGroup">
  64.                   <Count
  65.                     count={item.count}
  66.                   />
  67.                 </div>
  68.               </div>
  69.             )
  70.           })}
  71.         </div>
  72.       </div>
  73.     </div>
  74.   )
  75. }
  76. export default Cart
复制代码
购物车列表功能


   修改takeaway.js内容如下 :
  
  1. import {createSlice} from '@reduxjs/toolkit';
  2. import axios from "axios";
  3. const foodsState = createSlice({
  4.     name:'foods',
  5.     initialState: {
  6.         // 商品列表
  7.         foodsList:[],
  8.      // 菜单激活值
  9.         activeIndex:0,
  10.         // 购物车列表
  11.         cartList:[]
  12.     },
  13.     reducers:{
  14.         // 修改商品列表
  15.         setFoodsList(state, action){
  16.             state.foodsList=action.payload
  17.         },
  18.         // 更改activeIndex
  19.         changeActiveIndex(state, action){
  20.             state.activeIndex=action.payload
  21.         },
  22.         // 添加购物车
  23.         addCart(state, action){
  24.         //    通过payload.id去匹配cartList匹配,匹配到代表添加过
  25.            const  item = state.cartList.find(item=>item.id ===action.payload.id)
  26.             if (item){
  27.                 item.count++
  28.             }else{
  29.                 state.cartList.push(action.payload)
  30.             }
  31.         },
  32.     //     count增
  33.         increCount(state, action){
  34.             const  item = state.cartList.find(item=>item.id ===action.payload.id)
  35.             item.count++
  36.         },
  37.     //     count减
  38.         decreCount(state, action){
  39.             const  item = state.cartList.find(item=>item.id ===action.payload.id)
  40.              // 只有一项的时候将商品移除购物车
  41.             if (item.count <=1){
  42.                 state.cartList=  state.cartList.filter(item=>item.id !=action.payload.id)
  43.                 return
  44.             }
  45.             item.count--
  46.         },
  47.     //     清除购物车
  48.         clearCart(state){
  49.             state.cartList=[]
  50.         }
  51.     }
  52. });
  53. const {clearCart,decreCount,increCount,setFoodsList,changeActiveIndex,addCart} = foodsState.actions;
  54. //异步获取部分
  55. const fetchFoodsList = () => {
  56.     return async dispatch => {
  57.     //     异步逻辑
  58.        const res =  await axios.get(' http://localhost:3004/takeaway\n')
  59.     //     调用dispatch
  60.         dispatch(setFoodsList(res.data))
  61.         console.log(res.data)
  62.     }
  63. }
  64. const reducer = foodsState.reducer
  65. export {fetchFoodsList,changeActiveIndex,addCart,clearCart,decreCount,increCount}
  66. export default reducer
复制代码
购物车列表的表现和隐蔽



  1. import classNames from 'classnames'
  2. import Count from '../Count'
  3. import './index.scss'
  4. import {useDispatch, useSelector} from "react-redux";
  5. import {clearCart, decreCount, increCount} from "../../store/modules/takeaway";
  6. import {useState} from "react";
  7. const Cart = () => {
  8.     const  dispatch =useDispatch()
  9.     const{cartList} = useSelector(state => state.foods)
  10.   //   计算总价
  11.    const  totalPrice = cartList.reduce((a, c) => a+c.price*c.count,0)
  12. const[visible,setVisible]=useState(false)
  13.   return (
  14.     <div className="cartContainer">
  15.       {/* 遮罩层 添加visible类名可以显示出来 */}
  16.       <div
  17.           onClick={()=>setVisible(false)}
  18.         className={classNames('cartOverlay',visible&&'visible')}
  19.       />
  20.       <div className="cart">
  21.         {/* fill 添加fill类名可以切换购物车状态*/}
  22.         {/* 购物车数量 */}
  23.         <div onClick={()=>setVisible(cartList.length!=0)} className={classNames('icon')}>
  24.           {cartList.length>0 && <div className="cartCornerMark">{cartList.length}</div>}
  25.         </div>
  26.         {/* 购物车价格 */}
  27.         <div className="main">
  28.           <div className="price">
  29.             <span className="payableAmount">
  30.               <span className="payableAmountUnit">¥</span>
  31.               {totalPrice.toFixed(2)}
  32.             </span>
  33.           </div>
  34.           <span className="text">预估另需配送费 ¥5</span>
  35.         </div>
  36.         {/* 结算 or 起送 */}
  37.         {cartList.length > 0 ? (
  38.           <div className="goToPreview">去结算</div>
  39.         ) : (
  40.           <div className="minFee">¥20起送</div>
  41.         )}
  42.       </div>
  43.       {/* 添加visible类名 div会显示出来 */}
  44.       <div className={classNames('cartPanel',visible&&'visible')}>
  45.         <div className="header">
  46.           <span className="text">购物车</span>
  47.           <span onClick={()=>dispatch(clearCart())} className="clearCart">
  48.             清空购物车
  49.           </span>
  50.         </div>
  51.         {/* 购物车列表 */}
  52.         <div className="scrollArea">
  53.           {cartList.map(item => {
  54.             return (
  55.               <div className="cartItem" key={item.id}>
  56.                 <img className="shopPic" src={item.picture} alt="" />
  57.                 <div className="main">
  58.                   <div className="skuInfo">
  59.                     <div className="name">{item.name}</div>
  60.                   </div>
  61.                   <div className="payableAmount">
  62.                     <span className="yuan">¥</span>
  63.                     <span className="price">{item.price}</span>
  64.                   </div>
  65.                 </div>
  66.                 <div className="skuBtnWrapper btnGroup">
  67.                   <Count
  68.                       onPlus={()=>dispatch(increCount({id:item.id}))}
  69.                     count={item.count}
  70.                       onMinus={()=>dispatch(decreCount({id:item.id}))}
  71.                   />
  72.                 </div>
  73.               </div>
  74.             )
  75.           })}
  76.         </div>
  77.       </div>
  78.     </div>
  79.   )
  80. }
  81. export default Cart
复制代码
到这里redux的入门, 实践, 小案例就完成了 之后大概会更新一些关于redux底层原理的文章 会加入到此中
zustand

轻量级的状态管理工具
引入 :npm install zustand
使用一个异步请求的方式 看看怎样快速上手
  1. import React, {useEffect} from 'react';
  2. import {create} from "zustand";
  3. const URL = 'http://geek.itheima.net/v1_0/channels'
  4. const useStore = create((set) => {
  5.     return {
  6.         count: 0,
  7.         ins: () => {
  8.             // 使用参数set 参数为对象 或者方法就可以操作状态
  9.             return set(state => ({count: state.count + 1}))
  10.         },
  11.         channelList: [],
  12.         // 异步请求方式
  13.         fetchChannelList: async () => {
  14.             const res = await fetch(URL)
  15.             const jsonData = await res.json()
  16.             set({channelList: jsonData.data.channels})
  17.         }
  18.     }
  19. })
  20. const ZustandDemo = () => {
  21.     const {channelList, fetchChannelList} = useStore()
  22.     useEffect(() => {
  23.         fetchChannelList()
  24.     }, [fetchChannelList])
  25.     return (
  26.         <ul>
  27.             {channelList.map((item) => (
  28.                 <li key={item.id}>{item.name}</li>
  29.             ))}
  30.         </ul>
  31.     );
  32. };
  33. export default ZustandDemo;
复制代码
切片模式

当一个store过于大的时间 可以接纳切片的方式 进行区分 并且以一个root引入用于使用


React 路由

路由就是关键字和组件的映射关系,我们可以用关键字访问和展示对应组件
安装情况
  1. npm i react-router-dom
复制代码
快速上手 demo

需求: 创建一个可以切换登录页和文章页的路由体系
找到 index.js 创建路由实例对象
语法: 链接组件可以使jsx 也可以是导出的组件 path是访问的路径
  1. createBrowserRouter([
  2.   {
  3.     path:'/login',
  4.     element: <div>登录</div>
  5.   })
复制代码

   代码:
  index.js
  PS : 这里没有app的原因实在就是路由可以自己选择 有没有app作为入口完全看心情 之后会有路由默认设置所以不误在意
  1. const router = createBrowserRouter([{
  2.     path:'/login',
  3.     element: <div>我是登录页面</div>
  4. },{
  5.     path:'/article',
  6.     element: <div>我是文章页面</div>
  7. }
  8. ])
  9. const root = ReactDOM.createRoot(document.getElementById('root'));
  10. root.render(
  11.   <React.StrictMode>
  12.       <RouterProvider router={router}>
  13.       </RouterProvider>
  14.   </React.StrictMode>
  15. );
复制代码
  结果
  

抽象路由模块

之前的快速上手 简单的了解了一下路由的语法和使用 ,如今模拟一下日常的开发使用 ,我们必要将路由模块抽象出来

我们创建路由必要对应的文件夹 放入page文件夹下 一样寻常我们路由的文件夹还会存放一些组件必要的其他资源,内容还是刚才的内容

之后创建 router文件夹存放路由js文件

之后只必要在 根目次下的index.js中把路由引入进来 就完成了抽象结果
路由导航

路由体系中的多个路由之间必要进行路由跳转,并且在跳转的同时有大概必要转达参数进行通讯

声明式导航

声明式导航是指在代码中 通过 <Link/>标签去设置要跳转去哪里
语法 : <Linl to="/article">文章</Link>
   Login组件内容
  1. import {Link} from "react-router-dom";
  2. export const Login = () => {
  3.     return (
  4.         <div>
  5.             <div>我是登录页面</div>
  6.         <Link to="/article">文章</Link>
  7.         </div>
  8.     )
  9. }
复制代码

它实在被解析成一个a链接 指向文章页的访问地点(path)
编程式导航

编程式导航是指通过 useNavigate 钩子得到导航方法,以参数+触发变乱来控制跳转比起声明式要更加灵活
  1. import {Link, useNavigate} from "react-router-dom";
  2. export const Login = () => {
  3.    const nav =  useNavigate()
  4.     return (
  5.         <div>
  6.             <div>我是登录页面</div>
  7.         {/*    声明式*/}
  8.         <Link to="/article">文章</Link>
  9.         {/*    编程式*/}
  10.         <button onClick={()=>nav("/article")}>文章</button>
  11.         </div>
  12.     )
  13. }
复制代码
传参


useSearchParams

   代码
  Login.jsx
  1. <button onClick={()=>nav('/article?name="jack"')}>文章</button>
复制代码
Article.jsx
  1. import {useSearchParams} from "react-router-dom";
  2. export const Article = () => {
  3.     const [params] = useSearchParams()
  4.    const  name = params.get('name')
  5.     return (
  6.         <div>我是文章页面
  7.             {name}
  8.         </div>
  9.     )
  10. }
复制代码
  结果
  

useParams

这种方式类似 vue的动态路由传参,
   结果
  

嵌套路由

就是多级路由的嵌套 在开发中通常必要来回的跳转 有一级路由包罗多个二级路由等等嵌套情况
   比如下图:
  看成一个管理体系 一个一级路由包罗两个二级路由
  左侧的列表用于展示路由关键字
  右边的路由出口展示点击对应关键字出现的内容
  


案例

分别创建内容 一级路由 layout 和两个二级路由

然后编写嵌套路由必要的 router
  1. {
  2.         path: '/',
  3.         element: <Layout/>,
  4.         children: [
  5.             {
  6.                 path: 'board',
  7.                 element: <Board/>
  8.             },
  9.             {
  10.                 path: 'about',
  11.                 element: <About/>
  12.             }
  13.         ]
  14.     }
复制代码
  layout代码
  1. import {Link, Outlet} from "react-router-dom";
  2. export const Layout = () => {
  3.     return (
  4.         <div>一级路由 layout
  5.             <div><Link to="/board">面板</Link></div>
  6.             <div><Link to="/about">关于</Link></div>
  7.             <Outlet/>
  8.         </div>
  9.     )
  10. }
复制代码
  结果
  

默认二级路由

当访问的是一级路由的时间 默认的二级路由可以得到渲染
语法:
layout
  1. export const Layout = () => {
  2.     return (
  3.         <div>一级路由 layout
  4.             <div><Link to="/board">面板</Link></div>
  5.             <div><Link to="/">关于</Link></div>
  6.             <Outlet/>
  7.         </div>
  8.     )
  9. }
复制代码
router.js
  1.     {
  2.         path: '/',
  3.         element: <Layout/>,
  4.         children: [
  5.             {
  6.                 path: 'board',
  7.                 element: <Board/>
  8.             },
  9.             {
  10.                 index: true,
  11.                 element: <About/>
  12.             }
  13.         ]
  14.     }
复制代码
  结果
  

404路由

当浏览器输入的路径在路由中无法找到或者不存在 我们就必要一个可以兜底的组件 来提拔用户体验

   NOTFOUND JS
  1. export const Notfound = () => {
  2.     return (
  3.         <div>
  4.             this is NotFound Page
  5.         </div>
  6.     )
  7. }
复制代码
  router
  1. {
  2.     path: '*',
  3.     element: <Notfound/>
  4. }
复制代码
  结果
  

路由模式

各个主流框架的路由常用的路由模式有俩种,history模式和hash模式, ReactRouter分别由 createBrowerRouter 和 createHashRouter 函数负责创建
路由模式url表现底层原理是否必要后端支持historyurl/loginhistory对象 + pushState变乱必要hashurl/#/login监听hashChange变乱不必要 Hooks

useNavigate

用于编程式导航
语法:
  1.   const nav =  useNavigate()
  2.    <button onClick={()=>nav("/article")}>文章</button>
复制代码
useSearchParams

用于路由跳转的时间担当转达的参数
  1. <button onClick={()=>nav('/article?name="jack"')}>文章</button>
复制代码
这个时间我们在文章组件中编写
  1. import {useSearchParams} from "react-router-dom";
  2. export const Article = () => {
  3.     const [params] = useSearchParams()
  4.    const  name = params.get('name')
  5.     return (
  6.         <div>我是文章页面
  7.             {name}
  8.         </div>
  9.     )
  10. }
复制代码
useParams

这种方式类似 vue的动态路由传参,
极客博客

项目配置

初始化项目 这里依赖的使用:
   规范src目次
  1. -src
  2.   -apis           项目接口函数
  3.   -assets         项目资源文件,比如,图片等
  4.   -components     通用组件
  5.   -pages          页面组件
  6.   -store          集中状态管理
  7.   -utils          工具,比如,token、axios 的封装等
  8.   -App.js         根组件
  9.   -index.css      全局样式
  10.   -index.js       项目入口
复制代码

路径别名

   项目配景:在业务开发过程中文件夹的嵌套层级大概会比较深,通过传统的路径选择会比较贫困也容易出错,设置路径别名可以简化这个过程
  安装 npm i @craco/craco -D
然后创建 craco.config.js
  1. const path = require('path')
  2. module.exports = {
  3.   // webpack 配置
  4.   webpack: {
  5.     // 配置别名
  6.     alias: {
  7.       // 约定:使用 @ 表示 src 文件所在路径
  8.       '@': path.resolve(__dirname, 'src')
  9.     }
  10.   }
  11. }
复制代码
更换packge.json的启动方式 就可以使用了
  1.   "scripts": {
  2.     "start": "craco start",
  3.     "build": "craco build",
  4.     "test": "craco test",
  5.     "eject": "react-scripts eject"
  6.   }
复制代码
  配置代码编辑器识别
  在跟目次创建 jsconfig.json
  1. {
  2.   "compilerOptions": {
  3.     "baseUrl": ".",
  4.     "paths": {
  5.       "@/*": [
  6.         "src/*"
  7.       ]
  8.     }
  9.   }
  10. }
复制代码
这样就有路径提示了
安装scss

安装完之后在index.scss中写下样式查看是否安装乐成

组件库antd

组件库资助我们提拔开发服从,此中使用最广的就是antD
导入依赖: npm i antd
安装图标库: npm install @ant-design/icons --save
   测试
  1. import {Button} from "antd";
  2. function App() {
  3.     return (
  4.         <div>
  5.             this is a web app
  6.             <Button type='primary'>test</Button>
  7.         </div>
  8.     );
  9. }
  10. export default App;
复制代码
  结果
  

配置路由

   导入依赖
  
在pages中创建好对应的文件夹和组件

然后配置对应的路由文件

  1. import {createBrowserRouter} from "react-router-dom";
  2. import {Layout} from "../pages/Layout";
  3. import {Login} from "../pages/Login";
  4. const router = createBrowserRouter([
  5.     {
  6.         path: '/',
  7.         element: <Layout/>
  8.     },
  9.     {
  10.         path: '/login',
  11.         element: <Login/>
  12.     }
  13. ])
复制代码
之后使用 provider 将路由放入根文件 使用
index.js:
  1. import React from 'react';
  2. import ReactDOM from 'react-dom/client';
  3. import './index.scss';
  4. import {RouterProvider} from "react-router-dom";
  5. import router from "./router";
  6. const root = ReactDOM.createRoot(document.getElementById('root'));
  7. root.render(
  8.     <RouterProvider router={router}>
  9.     </RouterProvider>
  10. );
复制代码
配置完重启 这样基础的路由就配置好了

封装requset请求模块

因为项目中会发送很多网络请求,所以我们可以将 axios做好同一封装 方便同一管理和复用

   导入依赖
  1. npm i axios
复制代码
然后在utils中编写 request配置js
  1. import axios from 'axios'
  2. const request = axios.create({
  3.     baseURL: 'http://geek.itheima.net/v1_0',
  4.     timeout: 5000
  5. })
  6. // 添加请求拦截器
  7. request.interceptors.request.use((config) => {
  8.     return config
  9. }, (error) => {
  10.     return Promise.reject(error)
  11. })
  12. // 添加响应拦截器
  13. request.interceptors.response.use((response) => {
  14.     // 2xx 范围内的状态码都会触发该函数。
  15.     // 对响应数据做点什么
  16.     return response.data
  17. }, (error) => {
  18.     // 超出 2xx 范围的状态码都会触发该函数。
  19.     // 对响应错误做点什么
  20.     return Promise.reject(error)
  21. })
  22. export {request}
复制代码
在utils中创建 index.js 作为同一的工具类使用入口,方便管理工具类
  1. import {request} from "@/utils/request";
  2. export {request}
复制代码
登录模块

@/pages/login/index.jsx 使用 antd 创建登录页面的内容解构
  1. import './index.sass'
  2. import {Button, Card, Form, Input} from "antd";
  3. import logo from "@/assets/logo.png"
  4. export const Login = () => {
  5.     return (
  6.         <div className="login">
  7.             <Card className="login-container">
  8.                 <img className="login-logo" src={logo} alt=""/>
  9.                 {/* 登录表单 */}
  10.                 <Form>
  11.                     <Form.Item>
  12.                         <Input size="large" placeholder="请输入手机号"/>
  13.                     </Form.Item>
  14.                     <Form.Item>
  15.                         <Input size="large" placeholder="请输入验证码"/>
  16.                     </Form.Item>
  17.                     <Form.Item>
  18.                         <Button type="primary" htmlType="submit" size="large" block>
  19.                             登录
  20.                         </Button>
  21.                     </Form.Item>
  22.                 </Form>
  23.             </Card>
  24.         </div>
  25.     )
  26. }
复制代码
样式文件 index.css
  1. .login {
  2.   width: 100%;
  3.   height: 100%;
  4.   position: absolute;
  5.   left: 0;
  6.   top: 0;
  7.   background: center/cover url('~@/assets/login.png');
  8.   .login-logo {
  9.     width: 200px;
  10.     height: 60px;
  11.     display: block;
  12.     margin: 0 auto 20px;
  13.   }
  14.   .login-container {
  15.     width: 440px;
  16.     height: 360px;
  17.     position: absolute;
  18.     left: 50%;
  19.     top: 50%;
  20.     transform: translate(-50%, -50%);
  21.     box-shadow: 0 0 50px rgb(0 0 0 / 10%);
  22.   }
  23.   .login-checkbox-label {
  24.     color: #1890ff;
  25.   }
  26. }
复制代码

表单校验

使用 antd form组件中的表单校验属性来完成 表单校验

如今在login组件中加入基础的表单校验
  1.   {/* 登录表单 */}
  2.                 <Form>
  3.                     <Form.Item
  4.                         name="mobile"
  5.                         rules={[
  6.                             {
  7.                                 required: true,
  8.                                 message: '请输入11位手机号'
  9.                             }
  10.                         ]}>
  11.                         <Input size="large" placeholder="请输入手机号"/>
  12.                     </Form.Item>
  13.                     <Form.Item
  14.                         name="code"
  15.                         rules={[
  16.                             {
  17.                                 required: true,
  18.                                 message: '请输入验证码'
  19.                             }
  20.                         ]}>
  21.                         <Input size="large" placeholder="请输入验证码"/>
  22.                     </Form.Item>
复制代码

基础校验设置好之后 我们必要根据业务来计划定制校验 如

  1.                   <Form.Item
  2.                         name="mobile"
  3.                         rules={[
  4.                             {
  5.                                 required: true,
  6.                                 message: '请输入手机号'
  7.                             },
  8.                             {
  9.                                 pattern: /^1[3-9]\d{9}$/,
  10.                                 message: '请输入正确的手机号'
  11.                             }
  12.                         ]}>
  13.                         <Input size="large" placeholder="请输入手机号"/>
  14.                     </Form.Item>
复制代码
  提交数据
  继续查看官方文档 案例 内里有一个 onFinish 的回调方法 ,并且放到form组件的属性里就可以看到转达的信息了

   代码修改
  1.   const onFinish = (values) => {
  2.         console.log('Success:', values);
  3.     };
  4. <Form onFinish={onFinish} validateTrigger="onBlur"></Form>
复制代码
设置好之后我们再次点击登录按钮就可以在控制台看到转达的json信息了

使用Redux管理token

token可以作为用户表现数据 实在一样寻常我们的登录操纵就是为了获取对应账号下的token权限,这个token必要我们在前端全局化的共享 所以必要使用 redux来管理
   依赖
  1. npm i react-redux @reduxjs/toolkit
复制代码
  配置redux
  在store文件夹创建对应的文件结构

然后编写 user.js
  1. import {createSlice} from '@reduxjs/toolkit'
  2. import {request} from '@/utils'
  3. const userStore = createSlice({
  4.     name: 'user',
  5.     // 数据状态
  6.     initialState: {
  7.         token: ''
  8.     },
  9.     // 同步修改方法
  10.     reducers: {
  11.         setToken(state, action) {
  12.             state.userInfo = action.payload
  13.         }
  14.     }
  15. })
  16. // 解构出actionCreater
  17. const {setToken} = userStore.actions
  18. // 获取reducer函数
  19. const userReducer = userStore.reducer
  20. // 异步方法封装
  21. const fetchLogin = (loginForm) => {
  22.     return async (dispatch) => {
  23.         const res = await request.post('/authorizations', loginForm)
  24.         dispatch(setToken(res.data.token))
  25.     }
  26. }
  27. export {fetchLogin}
  28. export default userReducer
复制代码
在index.js配置同一管理reducer
  1. import {configureStore} from '@reduxjs/toolkit'
  2. import userReducer from './modules/user'
  3. export default configureStore({
  4.     reducer: {
  5.         // 注册子模块
  6.         user: userReducer
  7.     }
  8. })
复制代码
在src下目次中的index.js注入store
  1. import {Provider} from "react-redux";
  2. import store from "./store";
  3. const root = ReactDOM.createRoot(document.getElementById('root'));
  4. root.render(
  5.     <Provider store={store}>
  6.         <RouterProvider router={router}/>
  7.     </Provider>
  8. );
复制代码
触发登录操纵

我们使用的是黑马的后端模版 所以必要使用它提供的数据
  1. 手机号 13888888888
  2. code 246810
复制代码
输入之后就可以看到乐成的拿到了 该用户的 token

redux也乐成的保存的token数据

   登岸后的操纵
  
在login jsx中修改onfinish方法内容实现跳转
PS: 篇幅问题只展示了js代码 return中的样式就不再过多展示
  1. import './index.scss'
  2. import {Button, Card, Form, Input, message} from "antd";
  3. import logo from "@/assets/logo.png"
  4. import {useDispatch} from "react-redux";
  5. import {fetchLogin} from "@/store/modules/user";
  6. import {useNavigate} from "react-router-dom";
  7. export const Login = () => {
  8.     const dispatch = useDispatch();
  9.     const navigate = useNavigate();
  10.     const onFinish = async (values) => {
  11.         await dispatch(fetchLogin(values))
  12.         //     跳转到主页
  13.         navigate('/')
  14.         message.success('登陆成功')
  15.     };
  16. }
复制代码
  结果
  

token长期化

使用localStorage+redux管理token
编写逻辑 :先查询本地有没有 如果没有就请求,然后保存在本地
   修改reducer请求token的方法内容
  这里为什么没有用sessionStorage而是选择用localStorage呢 因为我们必要更长时间的长期化 session关闭浏览器就被清空了,之后登出的时间会显式的清除token
  1. const userStore = createSlice({
  2.     name: 'user',
  3.     // 数据状态
  4.     initialState: {
  5.         token: sessionStorage.getItem('token_key') || ''
  6.     },
  7.     // 同步修改方法
  8.     reducers: {
  9.         setToken(state, action) {
  10.             state.token = action.payload
  11.             sessionStorage.setItem('token_key', state.token)
  12.         }
  13.     }
  14. })
复制代码
封装token操纵方法

创建工具类

  1. // 封装存取方法
  2. const TOKENKEY = 'token_key'
  3. function setToken (token) {
  4.   return localStorage.setItem(TOKENKEY, token)
  5. }
  6. function getToken () {
  7.   return localStorage.getItem(TOKENKEY)
  8. }
  9. function clearToken () {
  10.   return localStorage.removeItem(TOKENKEY)
  11. }
  12. export {
  13.   setToken,
  14.   getToken,
  15.   clearToken
  16. }
复制代码
然后在入口index导入工具类
  1. import {request} from "@/utils/request";
  2. import {clearToken, getToken, setToken} from "@/utils/token";
  3. export {request, getToken, setToken, clearToken}
复制代码
  修改获取的token的代码改为使用工具类
  1. const userStore = createSlice({
  2.     name: 'user',
  3.     // 数据状态
  4.     initialState: {
  5.         token: getToken() || ''
  6.     },
  7.     // 同步修改方法
  8.     reducers: {
  9.         setToken(state, action) {
  10.             state.token = action.payload
  11.             //这里是使用别名的setToken方法 是再import setToken as _setToken
  12.             _setToken(action.payload)
  13.         }
  14.     }
  15. })
复制代码
在Axios请求中携带token

后端必要token来判定是否能够使用接口 ,所以我们必要修改request工具来让他携带token请求

在请求拦截此中拿到token并且注入token
  1. // 添加请求拦截器
  2. request.interceptors.request.use((config) => {
  3.     // 如果有token就携带没有就正常
  4.     const token = getToken()
  5.     // 按照后端的要求加入token
  6.     if (token) {
  7.         config.headers.Authorization = `Bearer ${token}`
  8.     }
  9.     return config
  10. }, (error) => {
  11.     return Promise.reject(error)
  12. })
复制代码
  测试
  

使用token做路由权限控制

在没有token的时间 不允许访问必要权限的路由

创建组件 AuthRoute

  1. // 封装高级组件
  2. //核心逻辑:根据token控制跳转
  3. import {getToken} from "@/utils";
  4. import {Navigate} from "react-router-dom";
  5. export function AuthRoute({children}) {
  6.     const token = getToken();
  7.     if (token) {
  8.         return <>{children}</>
  9.     } else {
  10.         return <Navigate to={'/login'} replace={true}/>
  11.     }
  12. }
复制代码
修改router.js
  1. import {createBrowserRouter} from "react-router-dom";
  2. import {Layout} from "../pages/Layout";
  3. import {Login} from "../pages/Login";
  4. import {AuthRoute} from "@/components/AuthRoute";
  5. const router = createBrowserRouter([
  6.     {
  7.         path: '/',
  8.         element: <AuthRoute><Layout/></AuthRoute>
  9.     },
  10.     {
  11.         path: '/login',
  12.         element: <Login/>
  13.     }
  14. ])
  15. export default router
复制代码
删除token 之后革新界面 就会被强制定向到 login
主页面

   依赖
  

用来初始化样式的第三方库
  1. npm install normalize.css
复制代码
然后将其引入到程序入门 index.js
实现步骤
   主页面模版
  1. import {Layout, Menu, Popconfirm} from 'antd'
  2. import {DiffOutlined, EditOutlined, HomeOutlined, LogoutOutlined,} from '@ant-design/icons'
  3. import './index.scss'
  4. import {Outlet, useNavigate} from "react-router-dom";
  5. const {Header, Sider} = Layout
  6. const items = [
  7.     {
  8.         label: '首页',
  9.         key: '/',
  10.         icon: <HomeOutlined/>,
  11.     },
  12.     {
  13.         label: '文章管理',
  14.         key: '/article',
  15.         icon: <DiffOutlined/>,
  16.     },
  17.     {
  18.         label: '创建文章',
  19.         key: '/publish',
  20.         icon: <EditOutlined/>,
  21.     },
  22. ]
  23. const GeekLayout = () => {
  24.     const navigate = useNavigate();
  25.     const onMenuClick = (router) => {
  26.         console.log(router)
  27.         navigate(router.key)
  28.     }
  29.     return (
  30.         <Layout>
  31.             <Header className="header">
  32.                 <div className="logo"/>
  33.                 <div className="user-info">
  34.                     <span className="user-name">冷环渊</span>
  35.                     <span className="user-logout">
  36.             <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
  37.               <LogoutOutlined/> 退出
  38.             </Popconfirm>
  39.           </span>
  40.                 </div>
  41.             </Header>
  42.             <Layout>
  43.                 <Sider width={200} className="site-layout-background">
  44.                     <Menu
  45.                         mode="inline"
  46.                         theme="dark"
  47.                         defaultSelectedKeys={['1']}
  48.                         items={items}
  49.                         onClick={onMenuClick}
  50.                         style={{height: '100%', borderRight: 0}}></Menu>
  51.                 </Sider>
  52.                 <Layout className="layout-content" style={{padding: 20}}>
  53.                     <Outlet/>
  54.                 </Layout>
  55.             </Layout>
  56.         </Layout>
  57.     )
  58. }
  59. export default GeekLayout
复制代码
  主页面样式文件
  1. .ant-layout {
  2.   height: 100%;
  3. }
  4. .header {
  5.   padding: 0;
  6. }
  7. .logo {
  8.   width: 200px;
  9.   height: 60px;
  10.   background: url('~@/assets/logo.png') no-repeat center / 160px auto;
  11. }
  12. .layout-content {
  13.   overflow-y: auto;
  14. }
  15. .user-info {
  16.   position: absolute;
  17.   right: 0;
  18.   top: 0;
  19.   padding-right: 20px;
  20.   color: #fff;
  21.   .user-name {
  22.     margin-right: 20px;
  23.   }
  24.   .user-logout {
  25.     display: inline-block;
  26.     cursor: pointer;
  27.   }
  28. }
  29. .ant-layout-header {
  30.   padding: 0 !important;
  31. }
复制代码
二级路由设置



配置二级路由

  1. const router = createBrowserRouter([
  2.     {
  3.         path: '/',
  4.         element: <AuthRoute><GeekLayout/></AuthRoute>,
  5.         children: [{
  6.             path: '/',
  7.             element: <Home></Home>
  8.         }, {
  9.             path: 'article',
  10.             element: <Article></Article>
  11.         }, {
  12.             path: 'publish',
  13.             element: <Publish></Publish>
  14.         }]
  15.     },
  16.      <!--....省略-->
复制代码
渲染对应关系
  1.      <Layout className="layout-content" style={{padding: 20}}>
  2.                     <Outlet></Outlet>
  3.                 </Layout>
复制代码
路由联动

将路由的key设置成路由的跳转地点
  1. const items = [
  2.     {
  3.         label: '首页',
  4.         key: '/',
  5.         icon: <HomeOutlined/>,
  6.     },
  7.     {
  8.         label: '文章管理',
  9.         key: '/article',
  10.         icon: <DiffOutlined/>,
  11.     },
  12.     {
  13.         label: '创建文章',
  14.         key: '/publish',
  15.         icon: <EditOutlined/>,
  16.     },
  17. ]
  18. const GeekLayout = () => {
  19.     const navigate = useNavigate();
  20.     const onMenuClick = (router) => {
  21.         console.log(router)
  22.         navigate(router.key)
  23.     }
  24.     return (
  25.         <Layout>
  26.             <!--省略-->
  27.             <Layout>
  28.                 <Sider width={200} className="site-layout-background">
  29.                     <Menu
  30.                         mode="inline"
  31.                         theme="dark"
  32.                         defaultSelectedKeys={['1']}
  33.                         items={items}
  34.                         onClick={onMenuClick}
  35.                         style={{height: '100%', borderRight: 0}}></Menu>
  36.                 </Sider>
  37.                 <Layout className="layout-content" style={{padding: 20}}>
  38.                     <Outlet/>
  39.                 </Layout>
  40.             </Layout>
  41.         </Layout>
  42.     )
  43. }
复制代码
菜单点击高亮

  ueslocation获取当前的路由位置,并且将MENU中的属性defaultSelectedKeys -> SelectedKeys内容为获取到的pathname
  1. const GeekLayout = () => {
  2.     const navigate = useNavigate();
  3.     const onMenuClick = (router) => {
  4.         console.log(router)
  5.         navigate(router.key)
  6.     }
  7.     // 获取到当前点击的路由
  8.     const location = useLocation();
  9.     const selectedKey = location.pathname;
  10.     return (
  11.         <Layout>
  12.             <Header className="header">
  13.           <!--省略-->
  14.             </Header>
  15.             <Layout>
  16.                 <Sider width={200} className="site-layout-background">
  17.                     <Menu
  18.                         mode="inline"
  19.                         theme="dark"
  20.                         SelectedKeys={selectedKey}
  21.                         items={items}
  22.                         onClick={onMenuClick}
  23.                         style={{height: '100%', borderRight: 0}}></Menu>
  24.                 </Sider>
  25.   <!--省略-->
  26.             </Layout>
  27.         </Layout>
  28.     )
  29. }
  30. export default GeekLayout
复制代码
  结果
  

展示个人信息

实现步骤
   修改 store/module/user.js
  1. import {createSlice} from '@reduxjs/toolkit'
  2. import {getToken, request, setToken as _setToken} from '@/utils'
  3. const userStore = createSlice({
  4.     name: 'user',
  5.     // 数据状态
  6.     initialState: {
  7.         token: getToken() || '',
  8.         userInfo: {}
  9.     },
  10.     // 同步修改方法
  11.     reducers: {
  12.         setToken(state, action) {
  13.             state.token = action.payload
  14.             _setToken(action.payload)
  15.         },
  16.         setUserInfo(state, action) {
  17.             state.userInfo = action.payload
  18.         }
  19.     }
  20. })
  21. // 解构出actionCreater
  22. const {setToken, setUserInfo} = userStore.actions
  23. // 获取reducer函数
  24. const userReducer = userStore.reducer
  25. // 异步方法封装
  26. const fetchLogin = (loginForm) => {
  27.     return async (dispatch) => {
  28.         const res = await request.post('/authorizations', loginForm)
  29.         dispatch(setToken(res.data.token))
  30.     }
  31. }
  32. const fetchUserInfo = () => {
  33.     return async (dispatch) => {
  34.         const res = await request.get('/user/profile')
  35.         dispatch(setUserInfo(res.data))
  36.     }
  37. }
  38. export {fetchLogin, fetchUserInfo}
  39. export default userReducer
复制代码
  主页面布局表现
  这里展示的是新增的代码 必要去修改header里的user-name的内容改为我们获取到的username
  1.     const dispatch = useDispatch()
  2.     const name = useSelector(state => state.user.userInfo.name)
  3.     useEffect(() => {
  4.         dispatch(fetchUserInfo())
  5.     }, [dispatch])
  6. <Header className="header">
  7.                 <div className="logo"/>
  8.                 <div className="user-info">
  9.                     <span className="user-name">{name}</span>
  10.                     <span className="user-logout">
  11.             <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
  12.               <LogoutOutlined/> 退出
  13.             </Popconfirm>
  14.           </span>
  15.                 </div>
  16.             </Header>
复制代码

退出登录


   绑定变乱
  在layout.jsx中找到退出相关的组件Popconfirm
  这个组件有是否确认变乱的绑定方法 onConfirm={onConfirm}
  ​ 在store文件夹下user.js的reducer中增加清除用户信息的方法
  1. // 同步修改方法
  2.     reducers: {
  3.         clearUserInfo(state) {
  4.             state.token = ''
  5.             state.userInfo = {}
  6.             clearToken()
  7.         }
复制代码
在相应变乱方法中调用方法 清除用户信息
  1.    const onConfirm = () => {
  2.         dispatch(clearUserInfo())
  3.         navigate('/login')
  4.     }
复制代码

   结果
  点击确认退出后 乐成被定向到登录页面

处理失效token

为了方便管理以及控制性能 token一样寻常都会有一个有效时间, 通常后端token失效都会返回401 所以我们可以监控后端返回的状态码 来做后续操纵 如 退出登录 或 续费token

来到 request工具类中的相应拦截器 拿到相应结果并且校验状态码是否是401
  1. request.interceptors.response.use((response) => {
  2.     // 2xx 范围内的状态码都会触发该函数。
  3.     // 对响应数据做点什么
  4.     return response.data
  5. }, (error) => {
  6.     // 超出 2xx 范围的状态码都会触发该函数。
  7.     // 401代表token失效 需要清除当前token
  8.     if (error.response.status === 401) {
  9.         clearToken()
  10.         // 这里有问题 是因为使用createBrownRouter创建的实例无法使用navigate,暂时先这么写 后续会修改
  11.         router.navigate('/login').then(() => {
  12.             window.location.reload()
  13.         })
  14.     }
  15.     return Promise.reject(error)
  16. })
复制代码
  怎样查看结果?
  在控制台将本地的token修改几位 革新就可以触发401 之后查看结果是否乐成
  主页可视化图表

使用 echarts
  1. npm i echarts
复制代码
  基础demo
  从官方文档复制个demo进来
  1. import {useEffect, useRef} from "react";
  2. import * as echarts from 'echarts'
  3. export const Home = () => {
  4.     const chartRef = useRef(null)
  5.     useEffect(() => {
  6.         // 1. 生成实例
  7.         const myChart = echarts.init(chartRef.current)
  8.         // 2. 准备图表参数
  9.         const option = {
  10.             xAxis: {
  11.                 type: 'category',
  12.                 data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  13.             },
  14.             yAxis: {
  15.                 type: 'value'
  16.             },
  17.             series: [
  18.                 {
  19.                     data: [120, 200, 150, 80, 70, 110, 130],
  20.                     type: 'bar'
  21.                 }
  22.             ]
  23.         }
  24.         // 3. 渲染参数
  25.         myChart.setOption(option)
  26.     }, [])
  27.     return (
  28.         <div>
  29.             <div ref={chartRef} style={{width: '400px', height: '300px'}}/>
  30.         </div>
  31.     )
  32. }
复制代码
封装echarts组件

将内容抽象出来,将不一样的部门抽象为参数适配

然后将图标代码提取出来 开始修改: 将title, x数据, y数据, 样式作为参数
  1. import {useEffect, useRef} from 'react'
  2. import * as echarts from 'echarts'
  3. const BarChart = ({title, xData, sData, style = {width: '400px', height: '300px'}}) => {
  4.     const chartRef = useRef(null)
  5.     useEffect(() => {
  6.         // 1. 生成实例
  7.         const myChart = echarts.init(chartRef.current)
  8.         // 2. 准备图表参数
  9.         const option = {
  10.             title: {
  11.                 text: title
  12.             },
  13.             xAxis: {
  14.                 type: 'category',
  15.                 data: xData
  16.             },
  17.             yAxis: {
  18.                 type: 'value'
  19.             },
  20.             series: [
  21.                 {
  22.                     data: sData,
  23.                     type: 'bar'
  24.                 }
  25.             ]
  26.         }
  27.         // 3. 渲染参数
  28.         myChart.setOption(option)
  29.     }, [sData, xData])
  30.     return <div ref={chartRef} style={style}></div>
  31. }
  32. export {BarChart}
复制代码
  修改home内容
  1. import {BarChart} from "@/pages/Home/components/BarChat";
  2. export const Home = () => {
  3.     return (
  4.         <div>
  5.             <BarChart
  6.                 title={'三个框架满意度'}
  7.                 xData={['Vue', 'React', 'Angular']}
  8.                 sData={[2000, 5000, 1000]}/>
  9.             <BarChart
  10.                 title={'三个框架使用数量'}
  11.                 xData={['Vue', 'React', 'Angular']}
  12.                 sData={[200, 500, 100]}
  13.                 style={{width: '500px', height: '400px'}}/>
  14.         </div>
  15.     )
  16. }
复制代码
API封装

我们必要优化项目格式, 必要将接口请求维护在一个固定的模块里,但是怎样编写每个团队都有区别 仅提供参考


  1. // 用户相关的所有请求
  2. import {request} from "@/utils";
  3. //登录请求
  4. export function loginAPI(formData) {
  5.     return request({
  6.         url: '/authorizations',
  7.         method: 'POST',
  8.         data: formData
  9.     })
  10. }
  11. // 获取用户信息
  12. export function getProfileAPI() {
  13.     return  request({
  14.         url: '/user/profile',
  15.         method: 'GET',
  16.     })
  17. }
复制代码
  修改 store中user.js的调用方式
  1. // 异步方法封装
  2. const fetchLogin = (loginForm) => {
  3.     return async (dispatch) => {
  4.         const res = await loginAPI(loginForm)
  5.         dispatch(setToken(res.data.token))
  6.     }
  7. }
  8. const fetchUserInfo = () => {
  9.     return async (dispatch) => {
  10.         const res = await getProfileAPI()
  11.         dispatch(setUserInfo(res.data))
  12.     }
  13. }
复制代码
文章发布

基础文章结构

开发三个步骤:
   静态结构
  publish/index.js
  1. import {
  2.   Card,
  3.   Breadcrumb,
  4.   Form,
  5.   Button,
  6.   Radio,
  7.   Input,
  8.   Upload,
  9.   Space,
  10.   Select
  11. } from 'antd'
  12. import { PlusOutlined } from '@ant-design/icons'
  13. import { Link } from 'react-router-dom'
  14. import './index.scss'
  15. const { Option } = Select
  16. const Publish = () => {
  17.   return (
  18.     <div className="publish">
  19.       <Card
  20.         title={
  21.           <Breadcrumb items={[
  22.             { title: <Link to={'/'}>首页</Link> },
  23.             { title: '发布文章' },
  24.           ]}
  25.           />
  26.         }
  27.       >
  28.         <Form
  29.           labelCol={{ span: 4 }}
  30.           wrapperCol={{ span: 16 }}
  31.           initialValues={{ type: 1 }}
  32.         >
  33.           <Form.Item
  34.             label="标题"
  35.             name="title"
  36.             rules={[{ required: true, message: '请输入文章标题' }]}
  37.           >
  38.             <Input placeholder="请输入文章标题" style={{ width: 400 }} />
  39.           </Form.Item>
  40.           <Form.Item
  41.             label="频道"
  42.             name="channel_id"
  43.             rules={[{ required: true, message: '请选择文章频道' }]}
  44.           >
  45.             <Select placeholder="请选择文章频道" style={{ width: 400 }}>
  46.               <Option value={0}>推荐</Option>
  47.             </Select>
  48.           </Form.Item>
  49.           <Form.Item
  50.             label="内容"
  51.             name="content"
  52.             rules={[{ required: true, message: '请输入文章内容' }]}
  53.           ></Form.Item>
  54.           <Form.Item wrapperCol={{ offset: 4 }}>
  55.             <Space>
  56.               <Button size="large" type="primary" htmlType="submit">
  57.                 发布文章
  58.               </Button>
  59.             </Space>
  60.           </Form.Item>
  61.         </Form>
  62.       </Card>
  63.     </div>
  64.   )
  65. }
  66. export default Publish
复制代码
index.scss
  1. .publish {
  2.   position: relative;
  3. }
  4. .ant-upload-list {
  5.   .ant-upload-list-picture-card-container,
  6.   .ant-upload-select {
  7.     width: 146px;
  8.     height: 146px;
  9.   }
  10. }
  11. .publish-quill {
  12.   .ql-editor {
  13.     min-height: 300px;
  14.   }
  15. }
复制代码
  结果
  

富文本编辑器

导入依赖:
  1. npm i react-quill@2.0.0-beta.2
复制代码
开发方式:
在必要放入富文本编辑器的位置放入代码
  1. //在文章头部导入需要的样式      
  2. import 'react-quill/dist/quill.snow.css'
  3. {/*富文本编辑器*/}
  4.                     <Form.Item
  5.                         label="内容"
  6.                         name="content"
  7.                         rules={[{required: true, message: '请输入文章内容'}]}
  8.                     > <ReactQuill
  9.                         className="publish-quill"
  10.                         theme="snow"
  11.                         placeholder="请输入文章内容"
  12.                     /></Form.Item>
复制代码
  结果
  

频道数据渲染


   添加apis
  

  1. import {request} from "@/utils";
  2. // 获取文章频道列表
  3. export function getChannels() {
  4.     return request({
  5.         url: '/channels',
  6.         method: 'GET'
  7.     })
  8. }
复制代码
  发布界面
  
  1.     const [channels, setChannels] = useState([]);
  2.     useEffect(() => {
  3.         async function getChannelList() {
  4.             const res = await getChannels();
  5.             setChannels(res.data.channels)
  6.         }
  7.         getChannelList()
  8.     }, []);
  9.   return (    <Form.Item
  10.                  label="频道"
  11.                  name="channel_id"
  12.                  rules={[{required: true, message: '请选择文章频道'}]}
  13.                 >
  14.                         <Select placeholder="请选择文章频道" style={{width: 300}}>
  15.                             {channels.map((item) => (
  16.                                 <Option key={item.id} value={item.id}>{item.name}</Option>
  17.                             ))}
  18.                         </Select>
  19.                </Form.Item>)
复制代码
  提交接口
  
这里由于react和富文本的兼容问题 我们必要手动的获取到富文本的内容将他放入到对应表单属性的value中
  1.     const [form] = Form.useForm();
  2.     const onFinish = (formValue) => {
  3.         console.log(formValue)
  4.     }
  5.     const onRichTextChange = (value) => {
  6.         form.setFieldsValue({content: value});
  7.     };
  8. return(
  9.            {/*富文本编辑器*/}
  10.                     <Form.Item
  11.                         label="内容"
  12.                         name="content"
  13.                         rules={[{required: true, message: '请输入文章内容'}]}
  14.                     > <ReactQuill
  15.                         className="publish-quill"
  16.                         theme="snow"
  17.                         placeholder="请输入文章内容"
  18.                         onChange={onRichTextChange}
  19.                     ></ReactQuill></Form.Item>)
复制代码
  结果
  

发布基础文章

在文章apis中新增请求方法
  1. // 提交文章表单
  2. export function createArticleAPI(data) {
  3.     return request({
  4.         url: '/mp/articles?draft=false',
  5.         method: 'POST',
  6.         data
  7.     })
  8. }
复制代码
提交表单
  1.     const onFinish = (formValue) => {
  2.         const {channel_id, content, title} = formValue
  3.         const reqData = {
  4.             content,
  5.             title,
  6.             cover: {
  7.                 type: 0,
  8.                 images: []
  9.             }, channel_id
  10.         }
  11.         //   提交数据
  12.         createArticleAPI(reqData)
  13.     }
复制代码
  结果
  

上传封面

基础上传

我们必要一个上传小组件 类似下图:

   结构代码
  将代码放入 publish组件内容标签的上面 ,

  1. import { useState } from 'react'
  2. const Publish = () => {
  3.   // 上传图片
  4.   const [imageList, setImageList] = useState([])
  5.   const onUploadChange = (info) => {
  6.       setImageList(info.fileList)
  7.   }
  8.   return (
  9.            <Form.Item label="封面">
  10.       <Form.Item name="type">
  11.         <Radio.Group>
  12.           <Radio value={1}>单图</Radio>
  13.           <Radio value={3}>三图</Radio>
  14.           <Radio value={0}>无图</Radio>
  15.         </Radio.Group>
  16.       </Form.Item>
  17.       <Upload
  18.         name="image"
  19.         listType="picture-card"
  20.         showUploadList
  21.         action={'http://geek.itheima.net/v1_0/upload'}
  22.         onChange={onUploadChange}
  23.       >
  24.         <div style={{ marginTop: 8 }}>
  25.           <PlusOutlined />
  26.         </div>
  27.       </Upload>
  28.     </Form.Item>
  29.   )
  30. }
复制代码
  结果
  

上传乐成了
切换封面类型

我们必要根据封面的是三个单选框的选项来决定是否必要表现上传图标

通过 Radio组件的onChange回调函数就可以拿到我们的对应选项 ,
这样在选择无图的时间 上传组件就会隐蔽
  1. // 记录图片上传类型选择
  2.     const [imageType, setImageType] = useState(0)
  3.     // 类型选择回调
  4.     const onTypeChange = (value) => {
  5.         setImageType(value.target.value)
  6.     }
  7. <Form.Item label="封面">
  8.          <Form.Item name="type">
  9.                  <Radio.Group onChange={onTypeChange}>
  10.                                 <Radio value={1}>单图</Radio>
  11.                                 <Radio value={3}>三图</Radio>
  12.                                 <Radio value={0}>无图</Radio>
  13.                             </Radio.Group>
  14.                         </Form.Item>
  15.                         {imageType > 0 && <Upload
  16.                             name="image"
  17.                             listType="picture-card"
  18.                             showUploadList
  19.                             action=                  {'http://geek.itheima.net/v1_0/upload'}
  20.                             onChange={onUploadChange}
  21.                         >
  22.                             <div style={{marginTop: 8}}>
  23.                             </div>
  24.                         </Upload>}
  25.                     </Form.Item>
复制代码
  结果
  无图:

有图:

这里必要注意就是我们之前的静态模版有一个默认属性 type是1 这会导致上传组件的表现有问题,改为和 state一样的 0 即可

控制上传图片的数量

我们必要控制 如:

只必要将上传绑定的type表现他的最大数量就行了,
ps: 问题 安全性不高 而且之前更换掉的图片还是会占用信息

发表带图片的文章

我们之前上传基础文章的时间 有一个属性 : cover是空白的 如今我们必要将imagelist和这个cover绑定 就可以上传封面了

   修改方法 onFinish
  1.     const onFinish = (formValue) => {
  2.         // 判断type和图片数量是否相等
  3.         if (imageList.length !== imageType) {
  4.             return message.warning('封面类型和图片数量不匹配')
  5.         }
  6.         const {channel_id, content, title} = formValue
  7.         const reqData = {
  8.             content,
  9.             title,
  10.             cover: {
  11.                 type: imageType,
  12.                 images: imageList.map(item => item.response.data.url)
  13.             }, channel_id
  14.         }
  15.         //   提交数据
  16.         createArticleAPI(reqData).then(data => {
  17.             if (data.message === 'OK') {
  18.                 message.success('文章发布成功')
  19.                 form.resetFields()
  20.                 setImageType(0)
  21.             }
  22.         })
  23.     }
复制代码
  结果
  

提交之后的信息

上传乐成
校验类型

我们必要制止 三图封面只上传了两张图片的情况 所以还必要在上传方法中增加一些判定
  1. const onFinish = (formValue) => {
  2.         // 判断type和图片数量是否相等
  3.         if (imageList.length !== imageType) {
  4.             return message.warning('封面类型和图片数量不匹配')
  5.         }
  6.         const {channel_id, content, title} = formValue
  7.         const reqData = {
  8.             content,
  9.             title,
  10.             cover: {
  11.                 type: imageType,
  12.                 images: imageList.map(item => item.response.data.url)
  13.             }, channel_id
  14.         }
  15.         //   提交数据
  16.         createArticleAPI(reqData)
  17.     }
复制代码
文章列表

放入结构

小细节:
  1. import {Link} from 'react-router-dom'
  2. // 导入资源
  3. import {Breadcrumb, Button, Card, DatePicker, Form, Radio, Select, Space, Table, Tag} from 'antd'
  4. import locale from 'antd/es/date-picker/locale/zh_CN'
  5. import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
  6. const {Option} = Select
  7. const {RangePicker} = DatePicker
  8. export const Article = () => {
  9.     // 准备列数据
  10.     const columns = [
  11.         {
  12.             title: '封面',
  13.             dataIndex: 'cover',
  14.             width: 120,
  15.             render: cover => {
  16.                 return <img src={cover.images[0] || 'img404'} width={80} height={60} alt=""/>
  17.             }
  18.         },
  19.         {
  20.             title: '标题',
  21.             dataIndex: 'title',
  22.             width: 220
  23.         },
  24.         {
  25.             title: '状态',
  26.             dataIndex: 'status',
  27.             render: data => <Tag color="green">审核通过</Tag>
  28.         },
  29.         {
  30.             title: '发布时间',
  31.             dataIndex: 'pubdate'
  32.         },
  33.         {
  34.             title: '阅读数',
  35.             dataIndex: 'read_count'
  36.         },
  37.         {
  38.             title: '评论数',
  39.             dataIndex: 'comment_count'
  40.         },
  41.         {
  42.             title: '点赞数',
  43.             dataIndex: 'like_count'
  44.         },
  45.         {
  46.             title: '操作',
  47.             render: data => {
  48.                 return (
  49.                     <Space size="middle">
  50.                         <Button type="primary" shape="circle" icon={<EditOutlined/>}/>
  51.                         <Button
  52.                             type="primary"
  53.                             danger
  54.                             shape="circle"
  55.                             icon={<DeleteOutlined/>}
  56.                         />
  57.                     </Space>
  58.                 )
  59.             }
  60.         }
  61.     ]
  62.     // 准备表格body数据
  63.     const data = [
  64.         {
  65.             id: '8218',
  66.             comment_count: 0,
  67.             cover: {
  68.                 images: [],
  69.             },
  70.             like_count: 0,
  71.             pubdate: '2019-03-11 09:00:00',
  72.             read_count: 2,
  73.             status: 2,
  74.             title: 'wkwebview离线化加载h5资源解决方案'
  75.         }
  76.     ]
  77.     return (
  78.         <div>
  79.             <Card
  80.                 title={
  81.                     <Breadcrumb items={[
  82.                         {title: <Link to={'/'}>首页</Link>},
  83.                         {title: '文章列表'},
  84.                     ]}/>
  85.                 }
  86.                 style={{marginBottom: 20}}
  87.             >
  88.                 <Form initialValues={{status: ''}}>
  89.                     <Form.Item label="状态" name="status">
  90.                         <Radio.Group>
  91.                             <Radio value={''}>全部</Radio>
  92.                             <Radio value={0}>草稿</Radio>
  93.                             <Radio value={2}>审核通过</Radio>
  94.                         </Radio.Group>
  95.                     </Form.Item>
  96.                     <Form.Item label="频道" name="channel_id">
  97.                         <Select
  98.                             placeholder="请选择文章频道"
  99.                             defaultValue="lucy"
  100.                             style={{width: 120}}
  101.                         >
  102.                             <Option value="jack">Jack</Option>
  103.                             <Option value="lucy">Lucy</Option>
  104.                         </Select>
  105.                     </Form.Item>
  106.                     <Form.Item label="日期" name="date">
  107.                         {/* 传入locale属性 控制中文显示*/}
  108.                         <RangePicker locale={locale}></RangePicker>
  109.                     </Form.Item>
  110.                     <Form.Item>
  111.                         <Button type="primary" htmlType="submit" style={{marginLeft: 40}}>
  112.                             筛选
  113.                         </Button>
  114.                     </Form.Item>
  115.                 </Form>
  116.             </Card>
  117.             {/*表格区域*/}
  118.             <Card title={`根据筛选条件共查询到 count 条结果:`}>
  119.                 <Table rowKey="id" columns={columns} dataSource={data}/>
  120.             </Card>
  121.         </div>
  122.     )
  123. }
复制代码
频道模块渲染

我们这次接纳 自定义业务hook的方式实现获取频道信息


   代码
  1. // 封装获取频道列表的逻辑
  2. import {useEffect, useState} from "react";
  3. import {getChannels} from "@/apis/article";
  4. function useChannel() {
  5. //     1. 获取频道列表的所有逻辑
  6.     const [channels, setChannels] = useState([]);
  7.     useEffect(() => {
  8.         async function getChannelList() {
  9.             const res = await getChannels();
  10.             setChannels(res.data.channels)
  11.         }
  12.         getChannelList()
  13.     }, [])
  14. //     2. 把数据导出
  15.     return {channels};
  16. }
  17. export {useChannel}
复制代码
这样就可以去改造一下之前的publish获取频道的逻辑 也可以在新的组件中直接使用频道数据
   将数据放入文章编辑中
  找到频道标签 修改options
  1.   {channels.map(item => <Option value={item.id}>{item.name}</Option>)}
复制代码
  结果
  

渲染文章列表数据


   请求方法 /apis/article.js
  1. //获取文章列表
  2. export function getArticleAPI(params) {
  3.     return request({
  4.         url: '/mp/articles',
  5.         method: 'GET',
  6.         params
  7.     })
  8. }
复制代码
  Article 组件
  1. import {Link} from 'react-router-dom'// 导入资源import {Breadcrumb, Button, Card, DatePicker, Form, Radio, Select, Space, Table, Tag} from 'antd'import locale from 'antd/es/date-picker/locale/zh_CN'import {useChannel} from "@/hooks/useChannel";import {useEffect, useState} from "react";import {getArticleAPI} from "@/apis/article";import {DeleteOutlined, EditOutlined} from "@ant-design/icons";const {Option} = Selectconst {RangePicker} = DatePickerexport const Article = () => {    // 获取频道数据    const {channels} = useChannel()    // 准备列数据    const columns = [        {            title: '封面',            dataIndex: 'cover',            width: 120,            render: cover => {                return <img src={cover.images[0] || 'img404'} width={80} height={60} alt=""/>            }        },        {            title: '标题',            dataIndex: 'title',            width: 220        },        {            title: '状态',            dataIndex: 'status',            render: data => <Tag color="green">考核通过</Tag>        },        {            title: '发布时间',            dataIndex: 'pubdate'        },        {            title: '阅读数',            dataIndex: 'read_count'        },        {            title: '评论数',            dataIndex: 'comment_count'        },        {            title: '点赞数',            dataIndex: 'like_count'        },        {            title: '操纵',            render: data => {                return (                    <Space size="middle">                        <Button type="primary" shape="circle" icon={<EditOutlined/>}/>                        <Button                            type="primary"                            danger                            shape="circle"                            icon={<DeleteOutlined/>}                        />                    </Space>                )            }        }    ]    // 获取文章列表    const [list, setList] = useState([])    useEffect(() => {        async function getList() {            const res = await getArticleAPI();            setList(res.data.results)        }        getList()    }, []);    return (        <div>            <Card                title={                    <Breadcrumb items={[                        {title: <Link to={'/'}>首页</Link>},                        {title: '文章列表'},                    ]}/>                }                style={{marginBottom: 20}}            >                <Form initialValues={{status: ''}}>                    <Form.Item label="状态" name="status">                        <Radio.Group>                            <Radio value={''}>全部</Radio>                            <Radio value={0}>草稿</Radio>                            <Radio value={2}>考核通过</Radio>                        </Radio.Group>                    </Form.Item>                    <Form.Item label="频道" name="channel_id">                        <Select                            placeholder="请选择文章频道"                            style={{width: 120}}                        >                            {channels.map(item => <Option value={item.id}>{item.name}</Option>)}
  2.                         </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={`根据筛选条件共查询到 ${list.length} 条结果:`}>                <Table rowKey="id" columns={columns} dataSource={list}/>            </Card>        </div>    )}
复制代码
文章状态

我们必要根据不同的文章状态表现不同的tag , 我们在用枚举渲染的方式实现这个多种状态的表现,
我们之前的代码中有专门控制每一列表现的数组

这里我们就可以根据 拿到的数据 利用 render属性 来渲染出来必要的tag
通过接口文档我们知道目前支持两种状态 :

   文章列表组件中添加
  
  1.     // 文章状态枚举
  2.     const status = {
  3.         1:<Tag color={"warning"}>待审核</Tag>,
  4.         2:<Tag color={"success"}>审核通过</Tag>
  5.     }
  6.    
  7.             {
  8.             title: '状态',
  9.             dataIndex: 'status',
  10.             render: data => status[data]
  11.         }
复制代码
  结果
  

文章筛选

我们必要根据 :

来筛选必要的文章
本质就是给请求列表的接口转达不同的参数
   接口文档的参数
  

  1.     // 查询筛选参数
  2.     const [reqData, setReqData] = useState(
  3.         {
  4.             status: '',
  5.             channel_id: '',
  6.             begin_pubdate: '',
  7.             end_pubdate: '',
  8.             page: 1,
  9.             per_page: 4,
  10.         }
  11.     );
复制代码
这里我们利用 useEffect的机制 维护的依赖项有变动 就会重新执行内部代码 ,拉取文章数据 所以我们必要将reqdata放入之前请求列表的参数中个,之前这个参数是没有转达的
   完备代码
  1.   // 查询筛选参数
  2.     const [reqData, setReqData] = useState(
  3.         {
  4.             status: '',
  5.             channel_id: '',
  6.             begin_pubdate: '',
  7.             end_pubdate: '',
  8.             page: 1,
  9.             per_page: 4,
  10.         }
  11.     );
  12.     const onReqFinish = (formValue) => {
  13.         // 1. 准备参数
  14.         const {channel_id, date, status} = formValue
  15.         setReqData({
  16.             status,
  17.             channel_id,
  18.             begin_pubdate: date[0].format('YYYY-MM-DD'),
  19.             end_pubdate: date[1].format('YYYY-MM-DD'),
  20.         })
  21.     }
  22.     // 获取频道数据
  23.     const {channels} = useChannel()
  24.     // 获取文章列表
  25.     const [list, setList] = useState([])
  26.     useEffect(() => {
  27.         async function getList() {
  28.             const res = await getArticleAPI(reqData);
  29.             setList(res.data.results)
  30.         }
  31.         getList()
  32.     }, [reqData]);
复制代码
  结果
  

分页实现

分页公式 : 页数 = 总数/每条数
思路 : 将页数作为请求参数重新渲染文章列表
找到文章列表对应的table标签 配置 pagination属性
   补充 维护一个count
  

在请求文章列表的时间 把这个属性放入count维护即可
  1.     useEffect(() => {
  2.         async function getList() {
  3.             const res = await getArticleAPI(reqData);
  4.             setList(res.data.results)
  5.             setCount(res.data.total_count)
  6.         }
  7.         getList()
  8.     }, [reqData]);
复制代码
  代码
  简单的分页就完成了 :

  1.             {/*表格区域*/}
  2.             <Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
  3.                 <Table rowKey="id" columns={columns} dataSource={list} pagination={{
  4.                     total: count,
  5.                     pageSize: reqData.per_page,
  6.                 }}/>
  7.             </Card>
复制代码

   根据对应的页数来请求对应文章
  pagination中使用 onchange 变乱来完成对应页数的请求
标签改动:
  1.                <Table rowKey="id" columns={columns} dataSource={list} pagination={{
  2.                     total: count,
  3.                     pageSize: reqData.per_page,
  4.                     onChange: onPageChange
  5.                 }}/>
复制代码
新增方法:
page 参数会拿到点击的对应页数 ,根据特性我们只必要改变参数 就会触发useEffect来更新数据
  1.     const onPageChange = (page) => {
  2.         setReqData({
  3.             ...reqData,
  4.             page: page
  5.         })
  6.     }
复制代码
文章删除

在 /APIS/Article.js新增请求方法
  1. //删除文章
  2. export function deleteArticleAPI(data) {
  3.     return request({
  4.         url: `/mp/articles/${data.id}`,
  5.         method: 'DELETE',
  6.     })
  7. }
复制代码
  添加静态文件
  在行数据数组中找到 操纵 添加确认组件 绑定onConfirm变乱
  1.              <Popconfirm
  2.                             title="确认删除该条文章吗?"
  3.                             onConfirm={() => delArticle(data)}
  4.                             okText="确认"
  5.                             cancelText="取消"
  6.                         >
  7.                             <Button
  8.                                 type="primary"
  9.                                 danger
  10.                                 shape="circle"
  11.                                 icon={<DeleteOutlined/>}
  12.                             />
  13.                         </Popconfirm>
复制代码
  变乱代码
  1.     const delArticle = async (data) => {
  2.         await deleteArticleAPI(data)
  3.         // 更新列表
  4.         setReqData({
  5.             ...reqData
  6.         })
  7.     }
复制代码
编辑文章

我们点击编辑按钮的时间 必要携带文章id 跳转到文章编写页面,
  1. const navigate = useNavigate();
  2. //样式代码
  3. <Button type="primary" shape="circle" icon={<EditOutlined/>} onClick={() => navigate(`/publish?id=${data.id}`)}/>
复制代码
  结果
  

载入文章数据

通过传入的id获取到文章数据 使用表单组件的实例方法 setFieldsValue填进去即可
在 /APIS/Article.js新增请求方法
  1. //获取文章数据
  2. export function getArticleById(id) {
  3.     return request({
  4.         url: `/mp/articles/${id}`,
  5.     })
  6. }
复制代码
  使用 钩子来做到革新就回填数据
  1.     // 载入文章数据
  2.     const [searchParams] = useSearchParams();
  3.     // 文章数据
  4.     const articleId = searchParams.get('id');
  5.     useEffect(() => {
  6.         async function getArticleDetail() {
  7.             const res = await getArticleById(articleId)
  8.             const {cover, ...infoValue} = res.data
  9.             form.setFieldsValue({...infoValue, type: cover.type})
  10.             setImageType(cover.type)
  11.             setImageList(cover.images.map(url => ({url})))
  12.         }
  13.         if (articleId) {
  14.             getArticleDetail()
  15.         }
  16.     }, [articleId, form])
复制代码
这里必要在 上传框加入一个属性 fileList
  1. {imageType > 0 && <Upload
  2.                             name="image"
  3.                             listType="picture-card"
  4.                             showUploadList
  5.                             action={'http://geek.itheima.net/v1_0/upload'}
  6.                             onChange={onUploadChange}
  7.                             maxCount={imageType}
  8.                             fileList={imageList}
  9.                         >
  10.                             <div style={{marginTop: 8}}>
  11.                                 <PlusOutlined/>
  12.                             </div>
  13.                         </Upload>}
复制代码
根据id 展示状态

找到 title中的发布文章 判定是否有id
  1.             <Card
  2.                 title={
  3.                     <Breadcrumb items={[
  4.                         {title: <Link to={'/'}>首页</Link>},
  5.                         {title: `${articleId ? '编辑文章' : '发布文章'}`}
  6.                     ]}
  7.                     />
  8.                 }
  9.             >
复制代码
更新文章

做完内容修改后 必要确认更新文章内容 并且校对文章数据 然后更新文章
我们必要适配url参数 因为我们的图片每个接口的转达必要的格式不同
   新增更新文章方法
  /apis/article.js
  1. // 修改文章表单
  2. export function updateArticleAPI(data) {
  3.     return request({
  4.         url: `/mp/articles/${data.id}?draft=false`,
  5.         method: 'PUT',
  6.         data
  7.     })
  8. }
复制代码
  修改 onfinish方法
  1.     const onFinish = (formValue) => {
  2.         // 判断type和图片数量是否相等
  3.         if (imageList.length !== imageType) {
  4.             return message.warning('封面类型和图片数量不匹配')
  5.         }
  6.         const {channel_id, content, title} = formValue
  7.         const reqData = {
  8.             content,
  9.             title,
  10.             cover: {
  11.                 type: imageType,
  12.                 // 编辑url的时候也需要做处理
  13.                 images: imageList.map(item => {
  14.                     if (item.response) {
  15.                         return item.response.data.url
  16.                     } else {
  17.                         return item.url
  18.                     }
  19.                 })
  20.             }, channel_id
  21.         }
  22.         //   提交数据
  23.         // 需要判断 新增和修改接口的调用
  24.         if (articleId) {
  25.             updateArticleAPI({...reqData, id: articleId}).then(data => {
  26.                 if (data.message === 'OK') {
  27.                     message.success('文章修改成功')
  28.                 }
  29.             })
  30.         } else {
  31.             createArticleAPI(reqData).then(data => {
  32.                 if (data.message === 'OK') {
  33.                     message.success('文章发布成功')
  34.                     form.resetFields()
  35.                     setImageType(0)
  36.                 }
  37.             })
  38.         }
  39.     }
复制代码
  结果
  

打包优化

CRA自带的打包命令
  1. npm run build
  2. # 静态服务器
  3. npm install -g serve
  4. #启动
  5. serve -s build
复制代码
之后就可以在项目文件夹看到

我们必要安装一个本地服务器 就可以跑起来打包好的项目了
配置路由懒加载

就是使路由在必要js的时间 才会获取 可以提高项目标首次启动时间

将路由中组件的导入方式改为lazy
  1. import {createBrowserRouter} from "react-router-dom";
  2. import {Login} from "@/pages/Login";
  3. import {AuthRoute} from "@/components/AuthRoute";
  4. import GeekLayout from "@/pages/Layout";
  5. import {lazy, Suspense} from "react";
  6. // 使用 lazy进行导入
  7. const Home = lazy(() => import("@/pages/Home"));
  8. const Article = lazy(() => import('@/pages/Article'))
  9. const Publish = lazy(() => import('@/pages/Publish'))
  10. const router = createBrowserRouter([
  11.     {
  12.         path: '/',
  13.         element: <AuthRoute><GeekLayout/></AuthRoute>,
  14.         children: [{
  15.             path: '/',
  16.             element: <Suspense fallback={'加载中'}><Home></Home></Suspense>
  17.         }, {
  18.             path: 'article',
  19.             element: <Suspense fallback={'加载中'}><Article></Article></Suspense>
  20.         }, {
  21.             path: 'publish',
  22.             element: <Suspense fallback={'加载中'}><Publish></Publish></Suspense>
  23.         }]
  24.     },
  25.     {
  26.         path: '/login',
  27.         element: <Login/>
  28.     }
  29. ])
  30. export default router
复制代码
只能看看语法了 目前有React18 不知道为什么提示我使用的不对
CDN

意义就是 加载离本地最近的服务器上的文件

Hooks

  ueslocation获取当前的路由位置
  1. // 获取到当前点击的路由
  2.     const location = useLocation();
  3.     const selectedKey = location.pathname;
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4