前言
最近刚学完 React,想着把笔记分享给大家,本笔记特别适合从事后端想要学习前端的人。我看视频是黑马最新的 React 视频(黑马程序员前端React18入门到实战视频教程,从react+hooks核心基础到企业级项目开发实战(B站评论、极客园项目等)及大厂面试全通关_哔哩哔哩_bilibili),个人觉得讲得还不错的。想要完整版可以私信我,如果对你有帮助的话就点个赞关注下吧。后面持续分享 Java 相关技术和笔记。
一、React 基础
1. 创建一个 react 项目
1.利用 create-react-app 工具创建一个 react 项目
npx create-react-app project-name npm start # 启动项目
2.src 目录只保留 App.js 和 index.js 文件
3.精简 App.js 和 index.js 文件
1.1 src 目录下文件的作用
index.js 是项目的入口,从这里开始运行,App 是根组件被 Index.js 导入,最后渲染到 index.html 中 root 节点上
index.js:
// 项目的核心入口 从这里开始运行 // React 必要的两个核心包 import React from 'react'; import ReactDOM from 'react-dom/client'; // 导入项目的根组件 import App from './App'; // 把 App 根组件渲染到 id 为 root 的 dom 节点上 const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <App /> );
App:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function App() { return ( <div className="App"> this is React App </div> ); } export default App;
2. jsx 基础-概念和本质
2.1 JSX 是什么?
JSX 表示在 JS 代码中编写 HTML 模板结构,是 React 中编写 UI 模板的方式
优势:
-
HTML 的声明式模板写法
-
JS 的可编程能力
JSX 是 JS 的拓展,浏览器不可直接识别,需要解析工具解析才可识别
2.2 JSX 编写 JS 代码
在 jsx 中可通过大括号 {} 识别 js 表达式,比如常见的变量、函数调用、方法调用等等
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function getName() { return 'jack'; } const count = 100; function App() { return ( <div className="App"> this is React App {/*1. 引号传递字符串*/} {'this is message'} {/*2. 识别 js 变量*/} {count} {/*3. 函数调用*/} { getName() } {/*4. 方法调用*/} {new Date().getDate()} {/*5. 使用 js 对象*/} <div style={{ color: 'red' }}>this is div</div> </div> ); } export default App;
2.3 JSX 中实现列表渲染
提示:在 JSX 中可以使用原生 JS 中 map 方法遍历渲染列表
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 const count = 100; const list = [ {id: 1001, name: 'Vue'}, {id: 1002, name: 'React'}, {id: 1003, name: 'Angular'}, ]; function App() { return ( <div className="App"> this is App {/*渲染列表*/} {list.map(item => <li key={item.id}>{item.name}</li>)} </div> ); } export default App;
注意:
-
渲染哪个结构就 return 那个
-
循环渲染记得要加上独一无二的 key(类型为 string 或 number)
2.4 JSX 实现条件渲染
在 React 中,可以通过逻辑与运算符 &&、三元表达式(?:)实现*基础的条件渲染
类似 Vue 的 v-if
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 const isLogin = true; function App() { return ( <div className="App"> {/*1. 逻辑与 &&*/} {isLogin && <span>this is span</span>} <br/> {/*2. 三元运算*/} {isLogin ? <span>is Login</span> : <span>not Login</span>} </div> ); } export default App;
2.4.1 JSX 条件渲染的 demo
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 const articleType = 3; // 0 1 3 'articleType' 的取值范围 // 定义核心函数(根据文章类型返回不同的 JSX 模板) function getArticleTemplate() { if (articleType === 0) { return <div>我是图文文章</div> } else if (articleType === 1) { return <div>我是单图文章</div> } else { return <div>我是三图文章</div> } } function App() { return ( <div className="App"> {/*调用函数渲染不同模板*/} {getArticleTemplate()} </div> ); } export default App;
显示:我是三图文章
3. React 基础事件绑定
语法:on + 事件名 = {事件处理程序/函数名},遵循驼峰命令
1.绑定事件
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function App() { const handleClick = () => { console.log('button 被点击了'); } return ( <div className="App"> <button onClick={handleClick}>click me</button> </div> ); } export default App;
2.传递事件参数 e
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function App() { // 拿到事件参数 e const handleClick = (e) => { console.log('button 被点击了', e); } return ( <div className="App"> <button onClick={handleClick}>click me</button> </div> ); } export default App;
3.传递自定义参数
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function App() { // 传递自定义参数 const handleClick = (name) => { console.log('button 被点击了', name); } return ( <div className="App"> {/*箭头函数传参*/} <button onClick={() => handleClick('jack')}>click me</button> </div> ); } export default App;
4.同时传递自定义参数和事件参数 e
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function App() { // const handleClick = () => { // console.log('button 被点击了'); // } // 拿到事件参数 e // const handleClick = (e) => { // console.log('button 被点击了', e); // } const handleClick = (name, e) => { console.log('button 被点击了', name, e); } return ( <div className="App"> {/*箭头函数传参*/} <button onClick={(e) => handleClick('jack', e)}>click me</button> </div> ); } export default App;
4. React组件
在 React 中,一个组件就是一个首字母大写的函数,内部含有组件的逻辑和 UI,渲染组件只需将组件当做标签书写即可
1.定义组件(function 定义或者箭头函数)
App.js:
function Button() { // 组件逻辑 return <button>click me</button> } const Button = () => { // 组件逻辑 return <button>click me</button> }
2.渲染组件(自闭和或成对标签)
function App() { return ( <div className="App"> {/*自闭和*/} <Button /> {/*成对标签*/} <Button></Button> </div> ); }
5. useState 基础使用
其是 React 的一个 Hook,允许我们向组件添加一个状态变量,从而控制影响组件的渲染结果
const [count, setCount] = useState(0);
count 的值不可直接修改,只能通过 setCount 修改
本质:状态变量一旦发生变化组件的视图 UI 也会变化(数据驱动视图)
特点:
-
useState 是一个函数,返回值是一个数组
-
数组的第一个参数是状态变量,第二个参数是 set 函数用来修改状态变量
-
useState 的参数将作为 count 的初始值
一个 useState 的小 demo
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 // useState 实现一个计数器按钮 import {useState} from "react"; function App() { // 1. 调用 useState 添加一个状态变量 // count 状态变量 // setCount 修改状态变量 const [count, setCount] = useState(0); // 2. 点击按钮的回调 const handleClick = () => { // 作用:1.用传入的新值修改 count // 2.重新使用新的 count 渲染 UI setCount(count + 1); }; return ( <div className="App"> <button onClick={handleClick}>{count}</button> </div> ); } export default App;
拓展 demo
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 // useState 实现一个计数器按钮 import {useState} from "react"; function App() { // 修改对象 const [form, setForm] = useState({name: 'jack'}); const changeForm = () => { // 错误写法:直接修改 // form.name = 'john'; // 正确写法:setForm 传入一个全新的对象 setForm({ ...form, name: 'john', }) }; return ( <div className="App"> <button onClick={changeForm}>修改 form {form.name}</button> </div> ); } export default App;
6. 如何修改组件样式
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 // 导入样式 import './index.css' const style = { color: 'red', fontSize: '50px', }; function App() { return ( <div className="App"> {/*行内控制*/} <span style={{color: 'red', fontSize: '50px'}}>this is span</span> <span style={style}>this is span</span> {/*通过 class 类名控制*/} <span className='foo'>this is foo</span> </div> ); } export default App;
index.css:
.foo { color: blue; }
7. B 站评论案例
7.1 列表渲染
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react"; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-18 08:15', like: 88, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '11-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] // 渲染评论列表 // 1.使用 useState 维护 list const App = () => { const [commentList, setCommentList] = useState(list); return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} <span className='nav-item'>最新</span> <span className='nav-item'>最热</span> </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text">发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => ( <div key={item.rpid} className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> <span className="delete-btn"> 删除 </span> </div> </div> </div> </div> ))} </div> </div> </div> ) } export default App
App.scss:
.app { width: 80%; margin: 50px auto; } .reply-navigation { margin-bottom: 22px; .nav-bar { display: flex; align-items: center; margin: 0; padding: 0; list-style: none; .nav-title { display: flex; align-items: center; width: 114px; font-size: 20px; .nav-title-text { color: #18191c; font-weight: 500; } .total-reply { margin: 0 36px 0 6px; color: #9499a0; font-weight: normal; font-size: 13px; } } .nav-sort { display: flex; align-items: center; color: #9499a0; font-size: 13px; .nav-item { cursor: pointer; &:hover { color: #00aeec; } &:last-child::after { display: none; } &::after { content: ' '; display: inline-block; height: 10px; width: 1px; margin: -1px 12px; background-color: #9499a0; } } .nav-item.active { color: #18191c; } } } } .reply-wrap { position: relative; } .box-normal { display: flex; transition: 0.2s; .reply-box-avatar { display: flex; align-items: center; justify-content: center; width: 80px; height: 50px; } .reply-box-wrap { display: flex; position: relative; flex: 1; .reply-box-textarea { width: 100%; height: 50px; padding: 5px 10px; box-sizing: border-box; color: #181931; font-family: inherit; line-height: 38px; background-color: #f1f2f3; border: 1px solid #f1f2f3; border-radius: 6px; outline: none; resize: none; transition: 0.2s; &::placeholder { color: #9499a0; font-size: 12px; } &:focus { height: 60px; background-color: #fff; border-color: #c9ccd0; } } } .reply-box-send { position: relative; display: flex; flex-basis: 86px; align-items: center; justify-content: center; margin-left: 10px; border-radius: 4px; cursor: pointer; transition: 0.2s; & .send-text { position: absolute; z-index: 1; color: #fff; font-size: 16px; } &::after { position: absolute; width: 100%; height: 100%; background-color: #00aeec; border-radius: 4px; opacity: 0.5; content: ''; } &:hover::after { opacity: 1; } } } .bili-avatar { position: relative; display: block; width: 48px; height: 48px; margin: 0; padding: 0; border-radius: 50%; } .bili-avatar-img { position: absolute; top: 50%; left: 50%; display: block; width: 48px; height: 48px; object-fit: cover; border: none; border-radius: 50%; image-rendering: -webkit-optimize-contrast; transform: translate(-50%, -50%); } // 评论列表 .reply-list { margin-top: 14px; } .reply-item { padding: 22px 0 0 80px; .root-reply-avatar { position: absolute; left: 0; display: flex; justify-content: center; width: 80px; cursor: pointer; } .content-wrap { position: relative; flex: 1; &::after { content: ' '; display: block; height: 1px; width: 100%; margin-top: 14px; background-color: #e3e5e7; } .user-info { display: flex; align-items: center; margin-bottom: 4px; .user-name { height: 30px; margin-right: 5px; color: #61666d; font-size: 13px; line-height: 30px; cursor: pointer; } } .root-reply { position: relative; padding: 2px 0; color: #181931; font-size: 15px; line-height: 24px; .reply-info { position: relative; display: flex; align-items: center; margin-top: 2px; color: #9499a0; font-size: 13px; .reply-time { width: 86px; margin-right: 20px; } .reply-like { display: flex; align-items: center; margin-right: 19px; .like-icon { width: 14px; height: 14px; margin-right: 5px; color: #9499a0; background-position: -153px -25px; &:hover { background-position: -218px -25px; } } .like-icon.liked { background-position: -154px -89px; } } .reply-dislike { display: flex; align-items: center; margin-right: 19px; .dislike-icon { width: 16px; height: 16px; background-position: -153px -153px; &:hover { background-position: -217px -153px; } } .dislike-icon.disliked { background-position: -154px -217px; } } .delete-btn { cursor: pointer; &:hover { color: #00aeec; } } } } } } .reply-none { height: 64px; margin-bottom: 80px; color: #99a2aa; font-size: 13px; line-height: 64px; text-align: center; }
7.2 删除功能实现
需求:
-
只有自己的评论才可以删除
-
点击删除按钮,删除当前评论,列表中不再显示
核心思路:
-
删除显示 - 条件渲染
-
删除功能 - 拿到当前项 id 以 id 为条件对评论列表做过滤
7.3 渲染 Tab + 点击高亮实现
需求:点击哪个 tab 项,哪个做高亮处理
核心思路:
点击谁就把谁的type(独一无二的标识)记录下来,然后和遍历时的每一项的type做匹配,谁匹配到就设置负责高亮的类名
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react"; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-18 08:15', like: 88, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '11-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] const App = () => { // 渲染评论列表 // 1.使用 useState 维护 list const [commentList, setCommentList] = useState(list); // 删除功能 const handleDel = (id) => { // 对 commentList 进行过滤 setCommentList(commentList.filter(item => item.rpid !== id)); }; // tab 切换功能 // 1.点击谁就把谁的 type 记录下来 // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示 const [type, setType] = useState('hot'); const handleTabChange = (type) => { console.log(type); setType(type); }; return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} {tabs.map(item => <span key={item.type} onClick={() => handleTabChange(item.type)} className={`nav-item ${type === item.type && 'active'}`}> {item.text} </span>)} </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text">发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => ( <div key={item.rpid} className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> {/*条件:user.id === item.user.id*/} {user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>} </div> </div> </div> </div> ))} </div> </div> </div> ) } export default App
7.4 排序功能
需求:点击最新,评论列表按照创建时间倒序排列(新的在前),点击最热按照点赞数排序(多的在前)
核心思路:把评论列表状态数据进行不同的排序处理,当成新值传给 set 函数重新渲染视图 UI
lodash 库
安装:
npm install lodash
引入:
import _ from 'lodash';
使用:
setCommentList(_.orderBy(commentList, 'like', 'desc'));
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react"; import _ from 'lodash'; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-20 08:15', like: 38, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '09-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] const App = () => { // 渲染评论列表 // 1.使用 useState 维护 list const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc')); // 删除功能 const handleDel = (id) => { // 对 commentList 进行过滤 setCommentList(commentList.filter(item => item.rpid !== id)); }; // tab 切换功能 // 1.点击谁就把谁的 type 记录下来 // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示 const [type, setType] = useState('hot'); const handleTabChange = (type) => { console.log(type); setType(type); // 基于列表的排序 if (type === 'hot') { // 根据点赞数量排序 // lodash setCommentList(_.orderBy(commentList, 'like', 'desc')); } else { // 根据创建时间排序 setCommentList(_.orderBy(commentList, 'ctime', 'desc')) } }; return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} {tabs.map(item => <span key={item.type} onClick={() => handleTabChange(item.type)} className={`nav-item ${type === item.type && 'active'}`}> {item.text} </span>)} </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text">发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => ( <div key={item.rpid} className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> {/*条件:user.id === item.user.id*/} {user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>} </div> </div> </div> </div> ))} </div> </div> </div> ) } export default App
8. classnams 优化类名控制
classnams 是一个简单的 JS 库,可以非常方便的通过条件动态控制 class 类名的显示
以前出现的问题:
语法:key 表示要控制的类名,value 表示条件,true 的时候类名就会显示
使用
安装:
npm install classnames
引入:
import classNames from "classnames";
用法:
<span key={item.type} onClick={() => handleTabChange(item.type)} className={classNames('nav-item', {active: type === item.type})}> {item.text} </span>)}
9. 受表单控制项
概念:使用 React 组件的状态(useState)控制表单状态
App.js
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 // 1.声明一个 react 状态 - useState // 2.核心绑定流程 // 2.1通过 value 属性绑定 react 状态 // 2.2绑定 onChange 事件,通过事件参数 e 拿到输入框最新值,反向修改 react 状态 import {useState} from "react"; function App() { const [value, setValue] = useState(); return ( <div className="App"> <input value={value} onChange={(e) => setValue(e.target.value)} type='text' /> </div> ); } export default App;
10. React 中获取DOM
在 React 中获取/操作 DOM,需要使用 useRef 钩子函数,分为两步:
-
使用 useRef 创建 ref 对象,并与 JSX 绑定
-
在 DOM 可用/DOM 渲染完毕时,通过 inputRef.current 拿到 DOM 对象
App.js:
import React, { useRef } from "react"; function App() { const inputRef = useRef(null); const showDom = () => { console.log(inputRef.current); }; const setInputValue = () => { inputRef.current.value = '新的值'; }; const focusInput = () => { inputRef.current.focus(); }; const selectInputText = () => { inputRef.current.select(); }; const addClassToInput = () => { inputRef.current.classList.add('new-class'); }; const removeClassFromInput = () => { inputRef.current.classList.remove('new-class'); }; const addEventListenerToInput = () => { inputRef.current.addEventListener('input', (event) => { console.log('Input changed:', event.target.value); }); }; return ( <div className="App"> <input ref={inputRef} type='text'/> <button onClick={showDom}>获取 dom</button> <button onClick={setInputValue}>设置值</button> <button onClick={focusInput}>聚焦</button> <button onClick={selectInputText}>选择文本</button> <button onClick={addClassToInput}>添加类</button> <button onClick={removeClassFromInput}>移除类</button> <button onClick={addEventListenerToInput}>添加事件监听器</button> </div> ); } export default App;
11.B 站评论优化
11.1 发表评论
App.scss:
.app { width: 80%; margin: 50px auto; } .reply-navigation { margin-bottom: 22px; .nav-bar { display: flex; align-items: center; margin: 0; padding: 0; list-style: none; .nav-title { display: flex; align-items: center; width: 114px; font-size: 20px; .nav-title-text { color: #18191c; font-weight: 500; } .total-reply { margin: 0 36px 0 6px; color: #9499a0; font-weight: normal; font-size: 13px; } } .nav-sort { display: flex; align-items: center; color: #9499a0; font-size: 13px; .nav-item { cursor: pointer; &:hover { color: #00aeec; } &:last-child::after { display: none; } &::after { content: ' '; display: inline-block; height: 10px; width: 1px; margin: -1px 12px; background-color: #9499a0; } } .nav-item.active { color: #18191c; } } } } .reply-wrap { position: relative; } .box-normal { display: flex; transition: 0.2s; .reply-box-avatar { display: flex; align-items: center; justify-content: center; width: 80px; height: 50px; } .reply-box-wrap { display: flex; position: relative; flex: 1; .reply-box-textarea { width: 100%; height: 50px; padding: 5px 10px; box-sizing: border-box; color: #181931; font-family: inherit; line-height: 38px; background-color: #f1f2f3; border: 1px solid #f1f2f3; border-radius: 6px; outline: none; resize: none; transition: 0.2s; &::placeholder { color: #9499a0; font-size: 12px; } &:focus { height: 60px; background-color: #fff; border-color: #c9ccd0; } } } .reply-box-send { position: relative; display: flex; flex-basis: 86px; align-items: center; justify-content: center; margin-left: 10px; border-radius: 4px; cursor: pointer; transition: 0.2s; & .send-text { position: absolute; z-index: 1; color: #fff; font-size: 16px; } &::after { position: absolute; width: 100%; height: 100%; background-color: #00aeec; border-radius: 4px; opacity: 0.5; content: ''; } &:hover::after { opacity: 1; } } } .bili-avatar { position: relative; display: block; width: 48px; height: 48px; margin: 0; padding: 0; border-radius: 50%; } .bili-avatar-img { position: absolute; top: 50%; left: 50%; display: block; width: 48px; height: 48px; object-fit: cover; border: none; border-radius: 50%; image-rendering: -webkit-optimize-contrast; transform: translate(-50%, -50%); } // 评论列表 .reply-list { margin-top: 14px; } .reply-item { padding: 22px 0 0 80px; .root-reply-avatar { position: absolute; left: 0; display: flex; justify-content: center; width: 80px; cursor: pointer; } .content-wrap { position: relative; flex: 1; &::after { content: ' '; display: block; height: 1px; width: 100%; margin-top: 14px; background-color: #e3e5e7; } .user-info { display: flex; align-items: center; margin-bottom: 4px; .user-name { height: 30px; margin-right: 5px; color: #61666d; font-size: 13px; line-height: 30px; cursor: pointer; } } .root-reply { position: relative; padding: 2px 0; color: #181931; font-size: 15px; line-height: 24px; .reply-info { position: relative; display: flex; align-items: center; margin-top: 2px; color: #9499a0; font-size: 13px; .reply-time { width: 86px; margin-right: 20px; } .reply-like { display: flex; align-items: center; margin-right: 19px; .like-icon { width: 14px; height: 14px; margin-right: 5px; color: #9499a0; background-position: -153px -25px; &:hover { background-position: -218px -25px; } } .like-icon.liked { background-position: -154px -89px; } } .reply-dislike { display: flex; align-items: center; margin-right: 19px; .dislike-icon { width: 16px; height: 16px; background-position: -153px -153px; &:hover { background-position: -217px -153px; } } .dislike-icon.disliked { background-position: -154px -217px; } } .delete-btn { cursor: pointer; &:hover { color: #00aeec; } } } } } } .reply-none { height: 64px; margin-bottom: 80px; color: #99a2aa; font-size: 13px; line-height: 64px; text-align: center; }
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-20 08:15', like: 38, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '09-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] const App = () => { // 渲染评论列表 // 1.使用 useState 维护 list const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc')); // 删除功能 const handleDel = (id) => { // 对 commentList 进行过滤 setCommentList(commentList.filter(item => item.rpid !== id)); }; // tab 切换功能 // 1.点击谁就把谁的 type 记录下来 // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示 const [type, setType] = useState('hot'); const handleTabChange = (type) => { console.log(type); setType(type); // 基于列表的排序 if (type === 'hot') { // 根据点赞数量排序 // lodash setCommentList(_.orderBy(commentList, 'like', 'desc')); } else { // 根据创建时间排序 setCommentList(_.orderBy(commentList, 'ctime', 'desc')) } }; // 发表评论 const [content, setContent] = useState(''); const handlePublish = () => { setCommentList([ ...commentList, { rpid: 4, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: content, ctime: '10-19 09:00', like: 80, } ]); }; return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} {tabs.map(item => <span key={item.type} onClick={() => handleTabChange(item.type)} className={classNames('nav-item', {active: type === item.type})}> {item.text} </span>) } </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" value={content} onChange={(e) => setContent(e.target.value)} /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text" onClick={handlePublish}>发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => ( <div key={item.rpid} className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> {/*条件:user.id === item.user.id*/} {user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>} </div> </div> </div> </div> ))} </div> </div> </div> ) } export default App
uuid 库
安装:
npm install uuid
引入:
import {v4 as uuidV4} from 'uuid' uuidV4(); // 使用
dayjs 库
安装:
npm install dayjs
引入:
import dayjs from 'dayjs' dayjs() // 使用
11.2 发表评论后清除输入框并聚焦
思路:
-
设置输入框的 useState 的 setContent 为空
-
利用 useRef 获取 dom 元素,再调用 focus 方法
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useRef, useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; import {v4 as uuidV4} from 'uuid' import dayjs from "dayjs"; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-20 08:15', like: 38, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '09-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] const App = () => { // 渲染评论列表 // 1.使用 useState 维护 list const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc')); const inputRef = useRef(null); // 删除功能 const handleDel = (id) => { // 对 commentList 进行过滤 setCommentList(commentList.filter(item => item.rpid !== id)); }; // tab 切换功能 // 1.点击谁就把谁的 type 记录下来 // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示 const [type, setType] = useState('hot'); const handleTabChange = (type) => { console.log(type); setType(type); // 基于列表的排序 if (type === 'hot') { // 根据点赞数量排序 // lodash setCommentList(_.orderBy(commentList, 'like', 'desc')); } else { // 根据创建时间排序 setCommentList(_.orderBy(commentList, 'ctime', 'desc')) } }; // 发表评论 const [content, setContent] = useState(''); const handlePublish = () => { setCommentList([ ...commentList, { rpid: uuidV4(), user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: content, ctime: dayjs(new Date()).format('MM-DD hh:mm'), like: 80, } ]); // 1.清楚输入框内容 setContent('') // 2.重新聚焦 inputRef.current.focus(); }; return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} {tabs.map(item => <span key={item.type} onClick={() => handleTabChange(item.type)} className={classNames('nav-item', {active: type === item.type})}> {item.text} </span>) } </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" value={content} onChange={(e) => setContent(e.target.value)} ref={inputRef} /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text" onClick={handlePublish}>发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => ( <div key={item.rpid} className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> {/*条件:user.id === item.user.id*/} {user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>} </div> </div> </div> </div> ))} </div> </div> </div> ) } export default App
12. 组件间通信
父传子 props
传递步骤:
1.父组件传递数据,子组件标签身上绑定属性 2.子组件接收数据,props 参数
父传子 demo:
App.js:
import React from "react"; // 父传子 // 1.父组件传递数据,子组件标签身上绑定属性 // 2.子组件接收数据,props 参数 function Son (props) { // props:对象包含了父组件传递过来的所有数据 console.log(props); return <div>this is son, father's param is {props.name}</div>; } function App() { const name = 'this is app name'; return ( <div className="App"> <Son name={name} /> </div> ); } export default App;
小 demo:
App.js:
import React from "react"; // 父传子 // 1.父组件传递数据,子组件标签身上绑定属性 // 2.子组件接收数据,props 参数 function Son (props) { // props:对象包含了父组件传递过来的所有数据 console.log(props); return <div>this is son, father's param is {props.name}</div>; } function App() { const name = 'this is app name'; return ( <div className="App"> <Son name={name} age={18} isTrue={false} list={['vue', 'react']} cb={() => console.log(123)} child={<span>this is span</span>} /> </div> ); } export default App;
注意:
父组件几乎可以给子组件传任何东西,包括布尔,数值,数组,对象和函数等
但是子组件不可修改父组件传递的属性,谁传递的谁修改
父传子 特殊的 prop children
组件包裹传递
App.js:
import React from "react"; // 父传子 // 1.父组件传递数据,子组件标签身上绑定属性 // 2.子组件接收数据,props 参数 function Son (props) { console.log(props) return <div>this is son, {props.children}</div> } function App() { return ( <div className="App"> <Son> <span>this is span</span> </Son> </div> ); } export default App;
显示:this is son,this is span
子传父
思路:子组件调用父组件中的函数并传递参数
App.js:
import React, {useState} from "react"; // 核心:在子组件中调用父组件中的函数并传递实参 function Son ({onGetSonMsg}) { const sonMsg = 'this is son msg'; return ( <div> this is son <button onClick={() => onGetSonMsg(sonMsg)}>sendMsg</button> </div> ); } function App() { const [msg, setMsg] = useState(''); const getMsg = (msg) => { console.log(msg); setMsg(msg); }; return ( <div className="App"> this is App, {msg} <Son onGetSonMsg={getMsg} /> </div> ); } export default App;
使用状态提升实现兄弟组件通信
思路:借助“状态提升”机制,通过父组件进行兄弟组件之间的数据传递
App.js:
import React, {useState} from "react"; // 1.子传父 A -> App // 2.子传父 B -> App function A ({onGetAName}) { // A 组件中的数据 const name = 'this is A name'; return ( <div> this is A component <button onClick={() => onGetAName(name)}>send</button> </div> ); } function B (props) { return ( <div> this is B component, {props.name} </div> ); } function App() { const [name, setName] = useState(''); const getAName = (name) => { console.log(name); setName(name); }; return ( <div className="App"> this is App <A onGetAName={getAName}/> <B name={name}/> </div> ); } export default App;
使用 context 机制跨层级组件通信
实现步骤:
-
使用createContext方法创建一个上下文对象Ctx
-
在顶层组件(App)中通过Ctx.Provider组件提供数据
-
在底层组件(B)中通过useContext钩子函数获取消费数据
App.js:
import React, {createContext, useContext} from "react"; // App -> A -> B // 1.createContext 方法创建一个上下文对象 const MsgContext = createContext(); // 2.在顶层组件通过 Provider 组件提供数据 // 3.在底层组件通过 useContext 钩子函数使用数据 function A () { return ( <div> this is A component <B /> </div> ); } function B () { const msg = useContext(MsgContext); return ( <div> this is B component, {msg} </div> ); } function App() { const msg = 'this is app msg'; return ( <div className="App"> <MsgContext.Provider value={msg}> this is App <A /> </MsgContext.Provider> </div> ); } export default App;
结果:
使用场景:
13. UseEffect概念理解
useEffect 用于在 React 组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送 ajax 请求,更改 DOM 等。
语法:
useEffect(() => {}, [])
参数1是一个函数,可以把它叫做副作用函数,在内部放置要执行的操作
参数2是一个数组,在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行。当是空数组时,副作用函数只会在组件渲染完毕后执行一次。
App.js:
import React, {useEffect, useState} from "react"; const URL = 'http://geek.itheima.net/v1_0/channels'; function App() { // 创建一个状态数据 const [list, setList] = useState([]); useEffect(() => { // 额外的操作 获取频道列表 async function getList() { const res = await fetch(URL); const jsonRes = await res.json(); console.log(jsonRes); setList(jsonRes.data.channels); } getList(); }, []); return ( <div className="App"> <ul> {list.map(item => <li key={item.id}>{item.name}</li>)} </ul> </div> ); } export default App;
useEffect 依赖参数说明
情况一:
function App() { // 1. 没有依赖项 初始 + 组件更新 const [count, setCount] = useState(0); useEffect(() => { console.log('副作用函数执行了'); }); return ( <div className="App"> this is app <button onClick={() => {setCount(count + 1)}}>+{count}</button> </div> ); } export default App;
情况二:
function App() { // 1. 没有依赖项 初始 + 组件更新 const [count, setCount] = useState(0); // useEffect(() => { // console.log('副作用函数执行了'); // }); // 2. 传入空数组依赖 只在初始渲染时执行 useEffect(() => { console.log('副作用函数执行了'); }, []); return ( <div className="App"> this is app <button onClick={() => {setCount(count + 1)}}>+{count}</button> </div> ); } export default App;
情况三:
import React, {useEffect, useState} from "react"; function App() { // 1. 没有依赖项 初始 + 组件更新 const [count, setCount] = useState(0); // useEffect(() => { // console.log('副作用函数执行了'); // }); // 2. 传入空数组依赖 只在初始渲染时执行 // useEffect(() => { // console.log('副作用函数执行了'); // }, []); // 3. 传入特定依赖项 初始 + 依赖项变化时执行 useEffect(() => { console.log('副作用函数执行了'); }, [count]); return ( <div className="App"> this is app <button onClick={() => {setCount(count + 1)}}>+{count}</button> </div> ); } export default App;
useEffect 清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开 启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
import React, {useEffect, useState} from "react"; function Son() { useEffect(() => { const timer = setInterval(() => { console.log('定时器执行中……') }, 1000); return () => { // 清除副作用(组件卸载时) clearInterval(timer); }; }, []); return <div>this is son</div> } function App() { const [show, setShow] = useState(true); return ( <div className="App"> {show && <Son />} <button onClick={() => setShow(false)}>卸载 Son 组件</button> </div> ); } export default App;
说明:清除副作用的函数最常见的执行时机是组件卸载时自动执行
需求:Son 组件渲染时开启一个定时器,卸载时清除它
14. 自定义 Hook 函数
概念:自定义 Hook 是以 use 开头的函数,通过自定义 Hook 函数可以用来实现逻辑的封装和复用
封装思路:
-
声明一个以 use 开头的函数
-
在函数体内封装可复用的逻辑(只要是可复用的逻辑)
-
返回状态和回调(以对象或者数据返回)
-
在哪个组件中要用到,就执行这个函数,解构出状态和回调即可使用
import React, {useEffect, useState} from "react"; // 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用 // 解决思路:自定义 hook function useToggle() { // 可复用的代码逻辑 const [value, setValue] = useState(true); const toggle = () => setValue(!value); // 哪些状态和回调函数需要在其他组件使用 return return { value, toggle } } function App() { const { value, toggle } = useToggle(); return ( <div className="App"> {value && <div>this is div</div>} <button onClick={toggle}>toggle</button> </div> ); } export default App;
15. ReactHooks 使用规则
-
只能在组件中或者其他自定义Hook函数中调用
-
只能在组件的顶层调用,不能嵌套在 if、for、其他函数中
import React, {useEffect, useState} from "react"; // 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用 // 解决思路:自定义 hook function useToggle() { // 可复用的代码逻辑 const [value, setValue] = useState(true); const toggle = () => setValue(!value); // 哪些状态和回调函数需要在其他组件使用 return return { value, toggle } } function App() { const { value, toggle } = useToggle(); return ( <div className="App"> {value && <div>this is div</div>} <button onClick={toggle}>toggle</button> </div> ); } export default App;
16. B 站评论优化
模拟请求评论接口,抽象出 Hook
实现思路:
1.使用 json-server 工具模拟接口服务,通过 axios 发送接口请求
2.使用 useEffect 调用接口获取数据
安装 json-server 库:
npm i json-server -D
安装 axios 库:
npm install axios
db.json:
{ "list": [ { "rpid": 3, "user": { "uid": "13258165", "avatar": "http://toutiao.itheima.net/resources/images/98.jpg", "uname": "周杰伦" }, "content": "哎哟,不错哦", "ctime": "10-18 08: 15", "like": 126 }, { "rpid": 2, "user": { "uid": "36080105", "avatar": "http://toutiao.itheima.net/resources/images/98.jpg", "uname": "许嵩" }, "content": "我寻你千百度 日出到迟暮", "ctime": "11-13 11: 29", "like": 88 }, { "rpid": 1, "user": { "uid": "30009257", "avatar": "http://toutiao.itheima.net/resources/images/98.jpg", "uname": "黑马前端" }, "content": "学前端就来黑马", "ctime": "10-19 09: 00", "like": 66 } ] }
改写 package.json:
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "serve": "json-server db.json --port 3004" },
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useEffect, useRef, useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; import {v4 as uuidV4} from 'uuid' import dayjs from "dayjs"; import axios from "axios"; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-20 08:15', like: 38, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '09-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] const App = () => { // 渲染评论列表 // 1.使用 useState 维护 list // const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc')); // 获取接口数据渲染 const [commentList, setCommentList] = useState([]); useEffect( () => { // 请求数据 async function getList() { // axios 请求数据 const res = await axios.get('http://localhost:3004/list'); setCommentList(res.data); } getList(); }, []); const inputRef = useRef(null); // 删除功能 const handleDel = (id) => { // 对 commentList 进行过滤 setCommentList(commentList.filter(item => item.rpid !== id)); }; // tab 切换功能 // 1.点击谁就把谁的 type 记录下来 // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示 const [type, setType] = useState('hot'); const handleTabChange = (type) => { console.log(type); setType(type); // 基于列表的排序 if (type === 'hot') { // 根据点赞数量排序 // lodash setCommentList(_.orderBy(commentList, 'like', 'desc')); } else { // 根据创建时间排序 setCommentList(_.orderBy(commentList, 'ctime', 'desc')) } }; // 发表评论 const [content, setContent] = useState(''); const handlePublish = () => { setCommentList([ ...commentList, { rpid: uuidV4(), user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: content, ctime: dayjs(new Date()).format('MM-DD hh:mm'), like: 80, } ]); // 1.清楚输入框内容 setContent('') // 2.重新聚焦 inputRef.current.focus(); }; return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} {tabs.map(item => <span key={item.type} onClick={() => handleTabChange(item.type)} className={classNames('nav-item', {active: type === item.type})}> {item.text} </span>) } </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" value={content} onChange={(e) => setContent(e.target.value)} ref={inputRef} /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text" onClick={handlePublish}>发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => ( <div key={item.rpid} className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> {/*条件:user.id === item.user.id*/} {user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>} </div> </div> </div> </div> ))} </div> </div> </div> ) } export default App;
封装评论项 Item 组件
import './App.scss' import avatar from './images/bozai.png' import {useEffect, useRef, useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; import {v4 as uuidV4} from 'uuid' import dayjs from "dayjs"; import axios from "axios"; /** * 评论列表的渲染和操作 * * 1. 根据状态渲染评论列表 * 2. 删除评论 */ // 评论列表数据 const list = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '周杰伦', }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '10-20 08:15', like: 38, }, { rpid: 2, user: { uid: '36080105', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '许嵩', }, content: '我寻你千百度 日出到迟暮', ctime: '09-13 11:29', like: 88, }, { rpid: 1, user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: '学前端就来黑马', ctime: '10-19 09:00', like: 66, }, ] // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端', } /** * 导航 Tab 的渲染和操作 * * 1. 渲染导航 Tab 和高亮 * 2. 评论列表排序 * 最热 => 喜欢数量降序 * 最新 => 创建时间降序 */ // 导航 Tab 数组 const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ] // 封装请求数据的 Hook function useGetList() { // 获取接口数据渲染 const [commentList, setCommentList] = useState([]); useEffect( () => { // 请求数据 async function getList() { // axios 请求数据 const res = await axios.get('http://localhost:3004/list'); setCommentList(res.data); } getList(); }, []); return { commentList, setCommentList }; } // 评论项组件 function Item({ item, onDel }) { return ( <div className="reply-item"> {/* 头像 */} <div className="root-reply-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" alt="" src={item.user.avatar} /> </div> </div> <div className="content-wrap"> {/* 用户名 */} <div className="user-info"> <div className="user-name">{item.user.uname}</div> </div> {/* 评论内容 */} <div className="root-reply"> <span className="reply-content">{item.content}</span> <div className="reply-info"> {/* 评论时间 */} <span className="reply-time">{item.ctime}</span> {/* 评论数量 */} <span className="reply-time">点赞数:{item.like}</span> {/*条件:user.id === item.user.id*/} {user.uid === item.user.uid && <span className="delete-btn" onClick={() => onDel(item.rpid)}>删除</span>} </div> </div> </div> </div> ) } const App = () => { // 渲染评论列表 // 1.使用 useState 维护 list // const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc')); const { commentList, setCommentList } = useGetList(); const inputRef = useRef(null); // 删除功能 const handleDel = (id) => { // 对 commentList 进行过滤 setCommentList(commentList.filter(item => item.rpid !== id)); }; // tab 切换功能 // 1.点击谁就把谁的 type 记录下来 // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示 const [type, setType] = useState('hot'); const handleTabChange = (type) => { console.log(type); setType(type); // 基于列表的排序 if (type === 'hot') { // 根据点赞数量排序 // lodash setCommentList(_.orderBy(commentList, 'like', 'desc')); } else { // 根据创建时间排序 setCommentList(_.orderBy(commentList, 'ctime', 'desc')) } }; // 发表评论 const [content, setContent] = useState(''); const handlePublish = () => { setCommentList([ ...commentList, { rpid: uuidV4(), user: { uid: '30009257', avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg', uname: '黑马前端', }, content: content, ctime: dayjs(new Date()).format('MM-DD hh:mm'), like: 80, } ]); // 1.清楚输入框内容 setContent('') // 2.重新聚焦 inputRef.current.focus(); }; return ( <div className="app"> {/* 导航 Tab */} <div className="reply-navigation"> <ul className="nav-bar"> <li className="nav-title"> <span className="nav-title-text">评论</span> {/* 评论数量 */} <span className="total-reply">{10}</span> </li> <li className="nav-sort"> {/* 高亮类名: active */} {tabs.map(item => <span key={item.type} onClick={() => handleTabChange(item.type)} className={classNames('nav-item', {active: type === item.type})}> {item.text} </span>) } </li> </ul> </div> <div className="reply-wrap"> {/* 发表评论 */} <div className="box-normal"> {/* 当前用户头像 */} <div className="reply-box-avatar"> <div className="bili-avatar"> <img className="bili-avatar-img" src={avatar} alt="用户头像" /> </div> </div> <div className="reply-box-wrap"> {/* 评论框 */} <textarea className="reply-box-textarea" placeholder="发一条友善的评论" value={content} onChange={(e) => setContent(e.target.value)} ref={inputRef} /> {/* 发布按钮 */} <div className="reply-box-send"> <div className="send-text" onClick={handlePublish}>发布</div> </div> </div> </div> {/* 评论列表 */} <div className="reply-list"> {/* 评论项 */} {commentList.map(item => <Item item={item} onDel={handleDel} />)} </div> </div> </div> ) } export default App;
17. Redux 集中状态管理工具
Redux 是 React 最常用的集中状态管理工具,类似于 Vue 中的 Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
使用步骤:
-
定义一个 Redux 函数
-
使用 createStore 方法传入 reducer 函数,生成一个 store 实例对象
-
使用 store 实例的 subscribe 方法订阅数据的变化(数据一旦变化,可以得到通知)
-
使用 store 实例的 dispatch方法提交 action 对象触发数据变化(告诉 reducer 你想怎么改数据)
-
使用 store 实例的 getState 方法获取最新的状态数据更新到视图中
Redux 与 React环境准备
配套工具
-
Redux Toolkit — 官方推荐编写 Redux 逻辑的方式,是一套工具的集合,简化书写方式
-
简化 store 配置
-
内置 immer 支持可变式状态修改
-
内置 thunk,更好的异步创建
-
-
react-redux — 用来链接 Redux 和 React 组件的中间件
-
获取状态,更新状态
-
配置基础环境
-
使用CRA快速创建React项目
npx create-react-app react-redux
-
安装配套工具
npm i @reduxjs/toolkit react-redux
-
启动项目
npm run start
使用 React Toolkit 创建 counterStore
counterStore.js:
import {createSlice} from "@reduxjs/toolkit"; const counterStore = createSlice({ // store 名称 name: 'counter', // 初始化状态 initialState: { count: 0 }, // 修改状态的方法,同步方法,支持直接修改 reducers: { increment(state) { state.count++; }, decrement(state) { state.count--; } } }); // 结构出来 actionCreator 函数 const {increment, decrement} = counterStore.actions; const reducer = counterStore.reducer; // 按需导出 actionCreator export {increment, decrement}; // 默认导出 reducer export default reducer;
src\store\index.js:
import {configureStore} from "@reduxjs/toolkit"; import counterStore from "./modules/counterStore"; const store = configureStore({ reducer: { counter: counterStore } }); export default store;
src\index.js:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import {Provider} from "react-redux"; import store from "./store"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
React 组件使用 store 中的数据
const { count } = useSelector(state => state.counter);
React 组件修改 store 中的数据
import './App.css'; import {useDispatch, useSelector} from "react-redux"; // 导入 actionCreator import {decrement, increment} from "./store/modules/counterStore"; function App() { const { count } = useSelector(state => state.counter); const dispatch = useDispatch(); return ( <div className="App"> <button onClick={() => dispatch(decrement())}>-</button> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> </div> ); } export default App;
总结:
提交 action 修改 state 值
counterStore.js:
import {createSlice} from "@reduxjs/toolkit"; const counterStore = createSlice({ // store 名称 name: 'counter', // 初始化状态 initialState: { count: 0 }, // 修改状态的方法,同步方法,支持直接修改 reducers: { increment(state) { state.count++; }, decrement(state) { state.count--; }, addToNum(state, action) { state.count += action.payload; } } }); // 结构出来 actionCreator 函数 const {increment, decrement, addToNum} = counterStore.actions; const reducer = counterStore.reducer; // 按需导出 actionCreator export { increment, decrement, addToNum }; // 默认导出 reducer export default reducer;
App.js:
import './App.css'; import {useDispatch, useSelector} from "react-redux"; // 导入 actionCreator import {addToNum, decrement, increment} from "./store/modules/counterStore"; function App() { const { count } = useSelector(state => state.counter); const dispatch = useDispatch(); return ( <div className="App"> <button onClick={() => dispatch(decrement())}>-</button> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> <button onClick={() => dispatch(addToNum(10))}>add to 10</button> <button onClick={() => dispatch(addToNum(20))}>add to 20</button> </div> ); } export default App;
Redux 异步状态操作
异步操作样板:
-
创建stor的写法保持不变,配置好同步修改状态的方法
-
单独封装一个函数,在函数内部return一个新函数,在新函数中
-
封装异步请求获取数据
-
调用同步action Creater传入异步数据生成一个action对象,并使用dispatch提交
-
-
组件中dispatch的写法保特不变
channelStore.js:
import {createSlice} from "@reduxjs/toolkit"; import axios from "axios"; const channelStore = createSlice({ // store 名称 name: 'channel', initialState: { channelList: [] }, reducers: { setChannels(state, action) { state.channelList = action.payload; } } }); // 异步请求部分 const { setChannels } = channelStore.actions; const fetchChannelList = () => { return async (dispatch) => { const res = await axios.get('http://geek.itheima.net/v1_0/channels'); dispatch(setChannels(res.data.data.channels)); }; }; export { fetchChannelList }; const reducer = channelStore.reducer; export default reducer;
index.js:
import {configureStore} from "@reduxjs/toolkit"; import counterStore from "./modules/counterStore"; import channelStore from "./modules/channelStore"; const store = configureStore({ reducer: { counter: counterStore, channel: channelStore, } }); export default store;
App.js:
import {useDispatch, useSelector} from "react-redux"; // 导入 actionCreator import {addToNum, decrement, increment} from "./store/modules/counterStore"; import {useEffect} from "react"; import {fetchChannelList} from "./store/modules/channelStore"; function App() { const { count } = useSelector(state => state.counter); const { channelList } = useSelector(state => state.channel); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchChannelList()); }, [dispatch]); return ( <div className="App"> <button onClick={() => dispatch(decrement())}>-</button> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> <button onClick={() => dispatch(addToNum(10))}>add to 10</button> <button onClick={() => dispatch(addToNum(20))}>add to 20</button> <ul> {channelList.map(item => <li key={item.id}>{item.name}</li>)} </ul> </div> ); } export default App;
18. React Router 路由
上手 demo
需求:创建一个可以切换登录和文章页的路由
实现:
npm i react-router-dom # 安装路由依赖
index.js:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from "react-router-dom"; // 1. 创建 Router 实例对象并且配置路由对应关系 const router = createBrowserRouter([ { path: '/login', element: <div>我是登录页</div> }, { path: '/article', element: <div>我是文章页</div> }, ]); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <RouterProvider router={router}></RouterProvider> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
实际开发中的 router 配置
1.创建 page 文件夹,分别创建 Login 和 Article 目录,再分别创建 index.js 文件
src/page/article/index.js:
const Article = () => { return <div>我是文章</div> }; export default Article;
src/page/login/index.js:
const Login = () => { return <div>我是登录</div> }; export default Login;
2.创建 router 文件夹,在其中创建 index.js 文件
router/index.js:
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; const router = createBrowserRouter([ { path: '/login', element: <Login /> }, { path: '/article', element: <Article /> } ]); export default router;
3.在 src/index.js 文件中引入 router 实例
src/index.js:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from "react-router-dom"; import router from './router'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <RouterProvider router={router}></RouterProvider> </React.StrictMode> ); reportWebVitals();
路由导航
声明式导航
指的是在模板中通过 <Link />
组件描述要调到哪里去,常用于 Tab 栏。类似 Vue 的 router-link
示例:
<Link to="/article">文章</Link>
编程式导航
编程式导航是指通过useNavigate
钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在 登录请求完毕之后跳转就可以选择这种方式,更加灵活
示例:
import {Link, useNavigate} from "react-router-dom"; const Login = () => { const navigate = useNavigate(); return ( <div> <div>我是登录</div> <Link to='/article'>跳转文章页</Link> <button onClick={() => navigate('/article')}>跳转文章页</button> </div> ) }; export default Login;
路由传参
searchParams 传参
// 传参 const navigate = useNavigate(); <button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button> // 拿参 const [params] = useSearchParams(); const id = params.get('id'); <div>我是文章页{id}</div>
params 传参
// 传参 const navigate = useNavigate(); <button onClick={() => navigate('/article/1001')}>params 传参</button> // 取参 const params = useParams(); const id = params.id; <div>我是文章页{id}</div>
注意 parmas 传参必须要在 router/index.js 文件的 path 加上 :id
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; const router = createBrowserRouter([ { path: '/login', element: <Login /> }, { path: '/article/:id', element: <Article /> } ]); export default router;
传参多值:
// 传参 const navigate = useNavigate(); <button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button> <button onClick={() => navigate('/article/1001/hejiajun')}>params 传参</button> // 取参 const params = useParams(); const id = params.id; const name = params.name; <div>我是文章页{id}</div> <div>我是文章页{name}</div>
router/index.js:
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; const router = createBrowserRouter([ { path: '/login', element: <Login /> }, { path: '/article/:id/:name', element: <Article /> } ]); export default router;
嵌套路由配置
大致步骤:
-
使用 children 属性配置路由嵌套关系
-
使用 <Outlet /> 组件配置二级路由渲染位置
router/index.js:
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; import Layout from "../page/layout"; import Board from "../page/board"; import About from "../page/about"; const router = createBrowserRouter([ { path: '/', element: <Layout />, children: [ { path: 'board', element: <Board /> }, { path: 'about', element: <About /> } ] }, { path: '/login', element: <Login /> }, { path: '/article/:id/:name', element: <Article /> } ]); export default router;
src/page/layout/index.js:
import {Link, Outlet} from "react-router-dom"; const Layout = () => { return ( <div> 我是一级路由 layout 组件 <Link to='/board'>面板</Link> <Link to='/about'>关于</Link> {/*配置二级路由的出口*/} <Outlet /> </div> ) }; export default Layout;
默认二级路由设置
当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置 index 属性为 true
src/router/index.js
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; import Layout from "../page/layout"; import Board from "../page/board"; import About from "../page/about"; const router = createBrowserRouter([ { path: '/', element: <Layout />, children: [ // 设置默认二级路由,一级路由被访问的时候,它也能得到渲染 { index: true, element: <Board /> }, { path: 'about', element: <About /> } ] }, { path: '/login', element: <Login /> }, { path: '/article/:id/:name', element: <Article /> } ]); export default router;
404 路由配置
场景:当浏览器输入url的路径在整个路由配置中都找不到对应的pth,为了用户体验,可以使用404兜底组件进行 渲染
两种路由模式
创建 hash 路由
import {createHashRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; import Layout from "../page/layout"; import Board from "../page/board"; import About from "../page/about"; import NotFound from "../page/notfound"; const router = createHashRouter([ { path: '/', element: <Layout />, children: [ // 设置默认二级路由,一级路由被访问的时候,它也能得到渲染 { index: true, element: <Board /> }, { path: 'about', element: <About /> } ] }, { path: '/login', element: <Login /> }, { path: '/article/:id/:name', element: <Article /> }, { path: '*', element: <NotFound /> } ]); export default router;
19. 记账本
这边没有认真记,想要学习的小伙伴可以直接看原视频好了,配合视频和笔记学习效果会更好。
环境搭建
使用CRA创建项目,并安装必要依赖,包括下列基础包
npx create-react-app react-bill
-
Redux:状态管理-@reduxjs./toolkit、react-redux
-
路由-reac-router-dom
-
时间处理-dayjs
-
class:类名处理-classnames
-
移动端组件库-antd-mobile
-
请求插件-axios
npm i @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios
配置别名路径
1.路径解析配置,把 @/ 解析为 src/ (1. npm i -D @craco/craco
2.项目根目录下创建配置文件craco.config.js
)
2.路径联想配置(VsCode),VsCode在输入@/时,自动联想出来对应的src/下的子级目录
数据 Mock
npm i -D json-server
20. UseReducer
作用:和 useState 作用类似,用来管理相对复杂的状态数据
基础用法:
-
定义一个reducer函数(根据不同的action返回不同的新状态)
-
在组件中调用useReducer,并传入reducer函数和状态的初始值
-
事件发生时,通过 dispatch 函数分派一个 action 对象(通知 reducer 要返回哪个新状态并渲染 UI)
App.js:
import React, {useReducer} from "react"; // useReducer // 1. 定义 reducer 函数,根据不同的action返回不同的新状态 // 2. 在组件中调用 useReducer,并传入 reducer 函数和状态的初始值 // 3. 调用 dispatch 通知 reducer 产生一个新状态,利用新状态更新 UI function reducer(state, action) { switch (action.type) { case 'INC': return state + 1; case 'DEC': return state - 1; case 'SET': return action.payload; default: return state; } } function App() { const [state, dispatch] = useReducer(reducer, 0); return ( <div className="App"> this is app <button onClick={() => dispatch({ type: 'DEC' })}>-</button> {state} <button onClick={() => dispatch({ type: 'INC' })}>+</button> <button onClick={() => dispatch({ type: 'SET', payload: 100 })}>update</button> </div> ); } export default App;
21. useMemo
需求:
作用:在组件每次渲染的时候缓存计算结果
useMemo(() => { // 根据 count1 返回计算的结果 }, [count1])
说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算。接受两个参数,第一个是函数,第二个是依赖项。
App.js:
import React, {useMemo, useState} from "react"; function fib(n) { console.log('计算函数执行了'); if (n < 3) { return 1; } return fib(n - 2) + fib(n - 1); } function App() { const [count1, setCount1] = useState(0); const result = useMemo(() => { return fib(count1); }, [count1]); // const result = fib(count1); const [count2, setCount2] = useState(0); console.log('组件重新渲染了'); return ( <div className="App"> this is app <button onClick={() => setCount1(count1 + 1)}>change count1: {count1}</button> <button onClick={() => setCount2(count2 + 1)}>change count2: {count2}</button> result is {result} </div> ); } export default App;
使用场景:消耗非常大的计算