1. React概述
1.1 什么是react?
React 是一个用于构建用户界面的 JavaScript 库
用户界面:HTML页面(前端)
React 主要用来写HTML页面,或构建Web应用如果从 MVC 的角度来看,React 仅仅是视图层(V),也就是只负责视图的染,而并非提供了完整的M和C的功能
React 起源于Facebook 的内部项目,后又用来架设Instagram 的网站,并于2013年5月开源
1.1react特点
1.声明式
你只需要描述 UI(HTML)看起来是什么样,就跟写HTML一样React负责渲染 UI,并在数据变化时更新 UI
const jsx = <div className="app"> <h1>Hello React!动态变化数据:(countj</h1> </div>
2.基于组件
- 组件是 React 最重要的内容
- 组件表示页面中的部分内容
- 组合、复用多个组件,可以实现完整的页面功能
3.学习一次 随处可用
- 使用 React 可以开发Web应用
- 使用 React 可以开发移动端原生应用(react-native)
- 使用 React 可以开发VR(虚拟现实)应用(react 360)
2. React基本使用
2.1 React安装
安装命令:npm i react react-dom
- react 包是核心,提供创建元素、组件等功能
- react-dom 包提供 DOM 相关功能等
2.2 React使用
1. 引入 react 和 react-dom 两个 js 文件
2. 创建 React 元素
3. 渲染 React 元素到页面中
<div id="root"></div>
<script>
const title = React.createElement('h1',null,'hello React')
ReactDOM.render(title, document.getElementById('root'))
</script>
2.2 方法说明
React.createElement() 说明(知道)
// 返回值:React元素
// 第一个参数:要创建的React元素名称
// 第二个参数:该React元素的属性
// 第三个及其以后的参数:该React元素的子节点 const el = React.createElement('h1', { title: '标题' }, 'Hello React')
ReactDOM.render() 说明
// 第一个参数:要渲染的React元素
// 第二个参数:DOM对象,用于指定渲染到页面中的位置
ReactDOM.render(el, document.getElementById('root'))
2.3 CDN方式
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
书写react内容
</script>
3. React 脚手架的使用
3.1 React 脚手架意义
- 脚手架是开发 现代Web 应用的必备。
- 充分利用 Webpack、Babel、ESLint 等工具辅助项目开发。
- 零配置,无需手动配置繁琐的工具即可使用。
- 关注业务,而不是工具配置。
3.2 使用 React 脚手架初始化项目
1. 初始化项目,命令:npx create-react-app my-app
2. 启动项目,在项目根目录执行命令:npm start
npx 命令介绍
- npm v5.2.0 引入的一条命令
- 目的:提升包内提供的命令行工具的使用体验
- 原来:先安装脚手架包,再使用这个包中提供的命令
- 现在:无需安装脚手架包,就可以直接使用这个包提供的命令
补充说明
1. 推荐使用:npx create-react-app my-app
2. npm init react-app my-app
3. yarn create react-app my-app
- yarn 是 Facebook 发布的包管理器,可以看做是 npm 的替代品,功能与 npm 相同
- yarn 具有快速、可靠和安全的特点
- 初始化新项目:yarn init
- 安装包: yarn add 包名称
- 安装项目依赖项: yarn
- 其他命令,请参考yarn文档
3.3 在脚手架中使用 React
1. 导入 react 和 react-dom 两个包。
import React from 'react'
import ReactDOM from 'react-dom'
2. 调用 React.createElement() 方法创建 react 元素。
3. 调用 ReactDOM.render() 方法渲染 react 元素到页面中。
基础总结
React 基础
- React是构建用户界面的JavaScript库
- 使用 react 时,推荐使用脚手架方式。
- 初始化项目命令:npx create-react-app my-app 。
- 启动项目命令:yarn start(或 npm start)。
- React.createElement() 方法用于创建 react 元素(知道)。
- ReactDOM.render() 方法负责渲染 react 元素到页面中。
react(B站李立超-18)
1. hello world
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<!--引入react的核心库-->
<script src="script/react.development.js"></script>
<!--引入react的DOM库-->
<script src="script/react-dom.development.js"></script>
</head>
<body>
<div id="root"></div>
<script>
/*
* React就是用来代替DOM的
* */
// 通过DOM向页面中添加一个div
// 创建一个div
// const div = document.createElement('div'); // 创建一个dom元素
// // 向div中设置内容
// div.innerText = '我是一个div';
// // 获取root
// const root = document.getElementById('root');
// // 将div添加到页面中
// root.appendChild(div);
// 通过React向页面中添加一个div
/*
* React.createElement()
* - 用来创建一个React元素
* - 参数:
* 1. 元素名(组件名)
* 2. 元素中的属性
* 3. 元素的子元素(内容)
* */
const div = React.createElement('div', {}, '我是React创建的div'); // 创建一个React元素
// 获取根元素对应的React元素
// ReactDOM.createRoot() 用来创建React根元素,需要一个DOM元素作为参数
const root = ReactDOM.createRoot(document.getElementById('root'));
// 将div渲染到根元素中
root.render(div);
</script>
</body>
</html>
2. 3个API
- React.createElement
- ReactDOM.createRoot
-
ReactDOM.createRoot 的render方法
/*
* React.createElement()
* - 用来创建一个React元素
* - 参数:
* 1.元素的名称(html标签必须小写)
* 2.标签中的属性
* - class属性需要使用className来设置
* - 在设置事件时,属性名需要修改为驼峰命名法
* 3.元素的内容(子元素)
* - 注意点:
* React元素最终会通过虚拟DOM转换为真实的DOM元素
* React元素一旦创建就无法修改,只能通过新创建的元素进行替换
* */
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>三个API</title>
<script src="script/react.development.js"></script>
<script src="script/react-dom.development.js"></script>
</head>
<body>
<button id="btn">我是按钮</button>
<div id="root"></div>
<script>
// 创建一个React元素
const button = React.createElement('button', {
type: 'button',
className: 'hello',
onClick: () => {
alert('你点我干嘛')
}
}, '点我一下');
// 创建第一个div
const div = React.createElement('div', {}, '我是一个div', button);
// 获取根元素
const root = ReactDOM.createRoot(document.getElementById('root'));
// 将元素在根元素中显示
root.render(div);
// 获取按钮对象
const btn = document.getElementById('btn');
btn.addEventListener('click', ()=>{
// 点击按钮后,修改div中button的文字为click me
const button = React.createElement('button', {
type: 'button',
className: 'hello',
onClick: () => {
alert('你点我干嘛')
}
}, 'click me');
// 创建一个div
const div = React.createElement('div', {}, '我是一个div', button);
// 修改React元素后,必须重新对根元素进行渲染
// 当调用render渲染页面时,React会自动比较两次渲染的元素,只在真实DOM中更新发生变化的部分
// 没发生变化的保持不变
root.render(div);
});
</script>
</body>
</html>
/*
* root.render()
* - 用来将React元素渲染到根元素中
* - 根元素中所有的内容都会被删除,被React元素所替换
* - 当重复调用render()时,React会将两次的渲染结果进行比较,
* 它会确保只修改那些发生变化的元素,对DOM做最少的修改
* */
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>三个API</title>
<script src="script/react.development.js"></script>
<script src="script/react-dom.development.js"></script>
</head>
<body>
<div id="root">
</div>
<script>
// 创建一个React元素
const button = React.createElement('button', {
type: 'button',
className: 'hello',
onClick: () => {
alert('你点我干嘛')
}
}, '点我一下');
// 创建第一个div
const div = React.createElement('div', {}, '我是一个div2', button);
// ReactDOM.render(div, document.getElementById('root')); // 老版本的React中使用方法
// 获取根元素 根元素就是React元素要插入的位置
const root = ReactDOM.createRoot(document.getElementById('root'));
// 将元素在根元素中显示
root.render(div);
</script>
</body>
</html>
3. JSX
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>JSX</title>
<script src="script/react.development.js"></script>
<script src="script/react-dom.development.js"></script>
<!-- 引入babel -->
<script src="script/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!--设置js代码被babel处理-->
<script type="text/babel">
创建一个React元素 <button>我是按钮</button>
命令式编程
const button = React.createElement('button', {}, '我是按钮');
声明式编程,结果导向的编程
在React中可以通过JSX(JS扩展)来创建React元素,JSX需要被翻译为JS代码,才能被React执行
要在React中使用JSX,必须引入babel来完成“翻译”工作
const button = <button>我是按钮</button>; // React.createElement('button', {}, '我是按钮');
/*
* JSX就是React.createElement()的语法糖
* JSX在执行之前都会被babel转换为js代码
* */
const div = <div>
我是一个div
<button>我是按钮</button>
</div>;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(div);
</script>
</body>
</html>
/*
* JSX的注意事项
* 1. JSX不是字符串,不要加引号
* 2. JSX中html标签应该小写,React组件应该大写开头
* 3. JSX中有且只有一个根标签
* 4. JSX的标签必须正确结束(自结束标签必须写/)
* 5. 在JSX中可以使用{}嵌入表达式
* - 有值的语句的就是表达式
* 6. 如果表达式是空值、布尔值、undefined,将不会显示
* 7. 在JSX中,属性可以直接在标签中设置
* 注意:
* class需要使用className代替
* style中必须使用对象设置
* style={{background:'red'}}
* */
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>JSX的注意</title>
<script src="script/react.development.js"></script>
<script src="script/react-dom.development.js"></script>
<!-- 引入babel -->
<script src="script/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function fn() {
return 'hello';
}
const name = '孙悟空';
const div = <div
id="box"
onClick={() => {
alert('哈哈')
}} className="box1"
style={{ backgroundColor: "yellowgreen", border: '10px red solid' }}
>
我是一个div
<ul>
<li>列表项</li>
</ul>
<input type="text" />
<div>
{name} <br />
{1 + 1} <br />
{fn()} <br />
{NaN} <br />
</div>
</div>;
// alert(div);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(div);
</script>
</body>
</html>
4. 列表渲染
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>渲染列表</title>
<script src="script/react.development.js"></script>
<script src="script/react-dom.development.js"></script>
<!-- 引入babel -->
<script src="script/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const name = '孙悟空';
const lang = 'cn';
/*
* {} 只能用来放js表达式,而不能放语句(if for)
* 在语句中是可以去操作JSX
* */
// const div = <div>Hello {name}</div>;
let div;
if(lang === 'en'){
div = <div>hello {name}</div>;
}else if(lang === 'cn'){
div = <div>你好 {name}</div>;
}
const data = ['孙悟空', '猪八戒', '沙和尚'];
/*
<ul>
<li>孙悟空</li>
<li>猪八戒</li>
...
</ul>
[<li>孙悟空</li>, <li>猪八戒</li>, <li>沙和尚</li>]
* */
// const arr = [];
// 遍历data
// for(let i=0; i<data.length; i++){
// arr.push(<li>{data[i]}</li>);
// }
// const arr = data.map(item => <li>{item}</li>);
// 将arr渲染为一个列表在网页中显示
// jsx中会自动将数组中的元素在页面中显示
// const list = <ul>{arr}</ul>;
const list = <ul>{data.map(item => <li>{item}</li>)}</ul>;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(list);
</script>
</body>
</html>
5. 虚拟DOM
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>虚拟DOM</title>
<script src="script/react.development.js"></script>
<script src="script/react-dom.development.js"></script>
<script src="script/babel.min.js"></script>
</head>
<body>
<button id="btn">点我一下</button>
<hr>
<div id="root"></div>
<script type="text/babel">
//创建一个数据
const data = ['孙悟空', '猪八戒', '沙和尚'];
// 创建列表
const list = <ul>
{/*data.map(item => <li key={item}>{item}</li>)*/}
{data.map((item, index) => <li key={index}>{item}</li>)}
</ul>;
// 获取根元素
const root = ReactDOM.createRoot(document.getElementById('root'));
// 渲染元素
root.render(list);
/*
* 在React我们操作的元素被称为React元素,并不是真正的原生DOM元素,
* React通过虚拟DOM 将React元素 和 原生DOM,进行映射,虽然操作的React元素,但是这些操作最终都会在真实DOM中体现出来
* 虚拟DOM的好处:
* 1.降低API复杂度
* 2.解决兼容问题
* 3.提升性能(减少DOM的不必要操作)
*
* 每当我们调用root.render()时,页面就会发生重新渲染
* React会通过diffing算法,将新的元素和旧的元素进行比较
* 通过比较找到发生变化的元素,并且只对变化的元素进行修改,没有发生的变化不予处理
* */
document.getElementById('btn').onclick = function (){
// 重新渲染页面
//创建一个数据
const data = ['唐僧', '孙悟空', '猪八戒', '沙和尚'];
// 创建列表
const list = <ul>
{/*data.map(item => <li key={item}>{item}</li>)*/}
{data.map((item, index) => <li key={index}>{item}</li>)}
</ul>;
// 渲染元素
root.render(list);
/*
* 旧数据
* ul
* li>孙悟空
* li>猪八戒
* li>沙和尚
* 新数据
* ul
* li>孙悟空
* li>猪八戒
* li>沙和尚
* 比较两次数据时,React会先比较父元素,父元素如果不同,直接所有元素全部替换
* 父元素一致,在去逐个比较子元素,直到找到所有发生变化的元素为止
* 上例中,新旧两组数据完全一致,所以没有任何DOM对象被修改
*
*
* 旧数据
* ul
* li>孙悟空
* li>猪八戒
* li>沙和尚
* 新数据
* ul
* li>tom
* li>猪八戒
* li>沙和尚
*
* 上例中,只有第一个li发生变化,所以只有第一个li被修改,其他元素不变
*
* 当我们在JSX中显示数组中,数组中每一个元素都需要设置一个唯一key,否则控制台会显示红色警告
* 重新渲染页面时,React会按照顺序依次比较对应的元素,当渲染一个列表时如果不指定key,同样也会按照顺序进行比较,
* 如果列表的顺序永远不会发生变化,这么做当然没有问题,但是如果列表的顺序会发生变化,这可能会导致性能问题出现
*
*
* 旧数据
* ul
* li>孙悟空
* li>猪八戒
* li>沙和尚
* 新数据
* ul
* li>孙悟空
* li>猪八戒
* li>沙和尚
* li>唐僧
*
* 上例中,在列表的最后添加了一个新元素,并没有改变其他的元素的顺序,所以这种操作不会带来性能问题
*
*
*
* 旧数据
* ul
* li>孙悟空
* li>猪八戒
* li>沙和尚
* 新数据
* ul
* li>唐僧
* li>孙悟空
* li>猪八戒
* li>沙和尚
*
* 上例中,在列表的最前边插入了一个新元素,其他元素内容并没有发生变化,
* 但是由于新元素插入到了开始位置,其余元素的位置全都发生变化,而React默认是根据位置比较元素
* 所以 此时,所有元素都会被修改
*
* 为了解决这个问题,React为列表设计了一个key属性,
* key的作用相当于ID,只是无法在页面中查看,当设置key以后,再比较元素时,
* 就会比较相同key的元素,而不是按照顺序进行比较
* 在渲染一个列表时,通常会给列表项设置一个唯一的key来避免上述问题
* (这个key在当前列表中唯一即可)
* 注意:
* 1.开发中一般会采用数据的id作为key
* 2.尽量不要使用元素的index作为key
* 索引会跟着元素顺序的改变而改变,所以使用索引做key跟没有key是一样的
* 唯一的不同就是,控制台的警告没了
* 当元素的顺序不会发生变化时,用索引做key也没有什么问题
* 旧数据
* ul
* li(key=孙悟空)>孙悟空
* li(key=猪八戒)>猪八戒
* li(key=沙和尚)>沙和尚
* 新数据
* ul
* li(key=唐僧)>唐僧
* li(key=孙悟空)>孙悟空
* li(key=猪八戒)>猪八戒
* li(key=沙和尚)>沙和尚
* */
};
</script>
</body>
</html>
6. 手动创建React
项目结构
常规的React项目需要使用npm(或yarn) 作为包管理器来对项目进行管理。并且React官方为了方便我们的开发,为我们提供react-scripts包。包中提供了项目开发中的大部分依赖,大大的简化了项目的开发
开发步骤:
- 创建目录结构
根元素
- public
- index.html
- src
- App.js
- index.js
...
- 进入项目所在目录,并执行命令: npm init -y 或 yarn init -y
- 安装项目依赖: npm install readt react-dom react-scripts - S 或 yarn add reactreact-dom react-scripts
- 运行npx react-scripts start 启动项目 (初次启动需要输入y确认)
- 或者将react-scripts start 设置到package.json 的scripts选项中,然后通过npm start启动 (初次启动需要输入y确认)"scripts":[“start":“react-scripts start”了
7. 练习 学习记录器
js部分:
import React from "react";
import ReactDOM from "react-dom/client";
// 引入css
import "./index.css"
// 创建 JSX
const App = <div className="logs">
{/* 日志项容器 */}
<div className="item">
{/* 日期容器 */}
<div className="date">
<div className="month">四月</div>
<div className="day">19</div>
</div>
{/* 日志内容容器 */}
<div className="content">
<h2 className="desc">学习React</h2>
<div className="time">40分钟</div>
</div>
</div>
</div>
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(App)
css部分:
/*设置基本样式*/
*{
box-sizing: border-box;
}
/*设置body的样式*/
body{
background-color: #DFDFDF;
margin: 0;
}
/*设置外层容器logs的样式*/
.logs{
width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #EAE2B7;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,.2);
}
/*设置item的样式*/
.item{
/*开启弹性盒*/
display: flex;
margin: 16px 0;
padding: 6px;
background-color: #FCBF49;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,.2);
}
/*设置日期的样式*/
.date{
width: 90px;
background-color: #fff;
border-radius: 10px;
font-weight: bold;
text-align: center;
overflow: hidden;
}
/*设置月份效果*/
.month{
height: 30px;
line-height: 30px;
font-size: 20px;
color: #fff;
background-color: #D62828;
}
/*设置日期的效果*/
.day{
height: 60px;
line-height: 60px;
font-size: 50px;
}
/*设置日志内容的样式*/
.content{
flex: auto;
text-align: center;
font-weight: bold;
}
/*设置描述内容*/
.desc{
font-size: 16px;
color: #194B49;
}
/*设置学习时间*/
.time{
color: #D62828;
}
8. 组件
在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。React中定义组件的方式有两种: 基于函数的组件和基于类的组件。本书我们先看看基于函数的组件。
基于函数的组件其实就是一个会返回JSX (React元素) 的普通的JS函数,你可以这样定义:
函数组件
const App = () => {
return <div>我是App组件!</div>
};
// 导出App
export default App;
* 组件
* - React中组件有两种创建方式
* - 函数式组件
* - 函数组件就是一个返回JSX的普通
* - 组件的首字母必须是大写
* - 类组件
import React from 'react'
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
// React组件可以直接通过JSX渲染
root.render(<App />);
类组件
import React from "react";
* 类组件必须要继承React.Component
* 相较于函数组件,类组件的编写要麻烦一下,
* 但是他俩的功能是一样的
class App extends React.Component{
// 类组件中,必须添加一个render()方法,且方法的返回值要是一个jsx
render() {
return <div>我是一个类组件</div>;
}
}
// 导出App
export default App;
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
// React组件可以直接通过JSX渲染
root.render(<App/>);
9. 组件练习
组件化的方式
10. 事件
React中的事件处理类似于在HTML标签中通过属性来设置事件,像是这样:
<button οnclick="alert("你点我干嗨");">点我一下</button>
这是传统DOM中绑定事件的方式之一,onclick全都小写是事件的名字,它的值是一组JS代码,当事件触发时,JS代码便会执行。记住这一点,传统DOM中事件属性的JS代码在事件触发时执行!
React中的事件绑定是这样的:
App.js:
const App = () => { const clickHandler = (event) => { event.preventDefault(); // 取消默认行为 event.stopPropagation(); // 取消事件的冒泡 alert('我是App中的clickHandler!'); * 在React中,无法通过return false取消默认行为 * return false; * * 事件对象 * - React事件中同样会传递事件对象,可以在响应函数中定义参数来接收事件对象 * - React中的事件对象同样不是原生的事件对象,是经过React包装后的事件对象 * - 由于对象进行过包装,所以使用过程中我们无需再去考虑兼容性问题 }; return <div onClick={() => { alert('div'); }} style={{width: 200, height: 200, margin: '100px auto', backgroundColor:'#bfa'}}> {/* 在React中事件需要通过元素的属性来设置, 和原生JS不同,在React中事件的属性需要使用驼峰命名法: onclick -> onClick onchange -> onChange 属性值不能直接执行代码,而是需要一个回调函数: onclick="alert(123)" onClick={()=>{alert(123)}} */} <button onClick={() => { alert(123); }}>点我一下 </button> <button onClick={clickHandler}>哈哈</button> <br/> <a href="https://www.baidu.com" onClick={clickHandler}>超链接</a> </div> }; /* * <button onclick="alert(123)">点我一下</button> * * <button id="btn01">点我一下</button> * * document.getElementById('btn01').onclick = function(){}; * document.getElementById('btn01').addEventListener('click', function(){}, false); * * * */ // 导出App export default App;
index.js 入口js:
import ReactDOM from "react-dom/client"; import App from "./App"; import './index.css'; const root = ReactDOM.createRoot(document.getElementById('root')); // React组件可以直接通过JSX渲染 root.render(<App/>);
11. props
- 父组件在子组件标签上通过 属性={} 的方式传递给子组件
- 子组件通过 props 形参接收,通过 {props.属性名} 的方式使用
- props是只读,不能修改
初识 MDN - MDN 项目 | MDN
logs组件:
/* 日志的容器 */ import LogItem from "./LogItem/LogItem"; import './Logs.css'; const Logs = () => { // 模拟一组从服务器中加载的数据 const logsData = [ { id: '001', date: new Date(2021, 1, 20, 18, 30), desc: '学习九阳神功', time: 30 }, { id: '002', date: new Date(2022, 2, 10, 12, 30), desc: '学习降龙十八掌', time: 20 }, { id: '003', date: new Date(2022, 2, 11, 11, 30), desc: '学习JavaScript', time: 40 }, { id: '004', date: new Date(2022, 2, 15, 10, 30), desc: '学习React', time: 80 } ]; // 将数据放入JSX中 const logItemDate = logsData.map(item => <LogItem key={item.id} date={item.date} desc={item.desc} time={item.time}/>); return <div className="logs"> { logItemDate // logsData.map(item => <LogItem {...item}/> ) } </div> }; export default Logs;
logItem组件:
import React from 'react'; import MyDate from "./MyDate/MyDate"; import './LogItem.css' const LogItem = (props) => { 在函数组件中,属性就相当于是函数的参数,可以通过参数来访问 可以在函数组件的形参中定义一个props,props指向的是一个对象 它包含了父组件中传递的所有参数 console.log(props.date); return ( <div className="item"> <MyDate date={props.date}/> {/* 日志内容的容器 */} <div className="content"> {/* 如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值 我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据 */} <h2 className="desc">{props.desc}</h2> <div className="time">{props.time}分钟</div> </div> </div> ); }; export default LogItem;
MyDate组件:
import React from 'react'; import './MyDate.css'; const MyDate = (props) => { // console.log(props.date.getDate()); // 获取月份 const month = props.date.toLocaleString('zh-CN', {month:'long'}); // 获取日期 const date = props.date.getDate(); return ( <div className="date"> <div className="month"> {month} </div> <div className="day"> {date} </div> </div> ); }; export default MyDate;
12. state
/*
* 在React中,当组件渲染完毕后,再修改组件中的变量,不会使组件重新渲染
* 要使得组件可以收到变量的影响,必须在变量修改后对组件进行重新渲染
* 这里我们就需要一个特殊变量,当这个变量被修改使,组件会自动重新渲染
*
* state相当于一个变量,
* 只是这个变量在React中进行了注册,
* React会监控这个变量的变化,当state发生变化时,会自动触发组件的重新渲染
* 使得我们的修改可以在页面中呈现出来
*
* 在函数组件中,我们需要通过钩子函数,获取state
*
* 使用钩子 useState() 来创建state
* import {useState} from "react";
*
* 它需要一个值作为参数,这个值就是state的初始值
* 该函数会返回一个数组
* 数组中第一个元素,是初始值
* - 初始值只用来显示数据,直接修改不会触发组件的重新渲染
* 数组中的第二个元素,是一个函数,通常会命名为setXxx
* - 这个函数用来修改state,调用其修改state后会触发组件的重新渲染,
* 并且使用函数中的值作为新的state值
*
* */import './App.css'; import { useState } from "react"; const App = () => { console.log('函数执行了 ---> 组件创建完毕!'); const [counter, setCounter] = useState(1); // let counter = result[0]; // let setCounter = result[1]; // const [counter, setCounter] = result; /* * 当点击+时,数字增大 * 点击-时,数字减少 * */ // 创建一个变量存储数字 // let counter = 2; const addHandler = () => { // 点击后数字+1 // alert('+1'); // counter++; setCounter(counter + 1); // 将counter值修改为2 }; const lessHandler = () => { // 点击后数字-1 // alert('-1'); // counter--; setCounter(counter-1); }; return <div className={'app'}> <h1>{counter}</h1> <button onClick={lessHandler}>-</button> <button onClick={addHandler}>+</button> </div>; }; // 导出App export default App;
/*
* state
* - state实际就是一个被React管理的变量
* 当我们通过setState()修改变量的值时,会触发组件的自动重新渲染
* - 只有state值发生变化时,组件才会重新渲染
* - 当state的值是一个对象时,修改时是使用新的对象去替换已有对象
* - 当通过setState去修改一个state时,并不表示修改当前的state
* 它修改的是组件下一次渲染时state值
* - setState()会触发组件的重新渲染,它是异步的
* 所以当调用setState()需要用旧state的值时,一定要注意
* 有可能出现计算错误的情况
* 为了避免这种情况,可以通过为setState()传递回调函数的形式来修改state值
* */
import './App.css'; import { useState } from "react"; const App = () => { console.log('函数执行了 ---> 组件创建完毕!'); const [counter, setCounter] = useState(1); const [user, setUser] = useState({ name: '孙悟空', age: 18 }); const addHandler = () => { setTimeout(() => { // setCounter(counter + 1); // 将counter值修改为2 setCounter((prevCounter) => { /* * setState()中回调函数的返回值将会成为新的state值 * 回调函数执行时,React会将最新的state值作为参数传递 * */ return prevCounter + 1; }); // setCounter(prevState => prevState + 1); }, 1000); // setCounter(2); // setCounter(3); // setCounter(4); // setCounter(5); // setCounter(6); }; const updateUserHandler = () => { // setUser({name:'猪八戒'}); // 如果直接修改旧的state对象,由于对象还是那个对象,所以不会生效 // user.name = '猪八戒'; // console.log(user); // setUser(user); // const newUser = Object.assign({}, user); // newUser.name = '猪八戒'; // setUser(newUser); setUser({ ...user, name: '猪八戒' }); }; return <div className={'app'}> <h1>{counter} -- {user.name} -- {user.age}</h1> <button onClick={addHandler}>1</button> <button onClick={updateUserHandler}>2</button> </div>; }; // 导出App export default App;
13. DOM对象和ref
ref:
React中所有的操作默认都是在React元素上进行,然后再通过虚拟DOM应用到真实页面上的.这样做的好处我们不在整述。虽然如此,在React中依然为我们提供了可以直接访问原生DOM对象的方式。ref就是干这个事的。
ref是reference的简写,换句话说就是用来获取真实DOM对象的引用。咱们丑话还是要说在前边,虽然可以获取到DOM对象,但是轻易不要这么做,如果必须要获取,也尽量是读取而不要修改,如果必需要修改也要尽量减少修改的次数,总之能不用就不用。
/*
* 获取原生的DOM对象
* 1.可以使用传统的document来对DOM进行操作
* 2.直接从React处获取DOM对象
* 步骤:
* 1.创建一个存储DOM对象的容器
* - 使用 useRef() 钩子函数
* 钩子函数的注意事项:
* ① React中的钩子函数只能用于函数组件或自定义钩子
* ② 钩子函数只能直接在函数组件中调用
* 2.将容器设置为想要获取DOM对象元素的ref属性
* <h1 ref={xxx}>....</h1>
* - React会自动将当前元素的DOM对象,设置为容器current属性
*
* useRef()
* - 返回的就是一个普通的JS对象
* - {current:undefined}
* - 所以我们直接创建一个js对象,也可以代替useRef()
* - 区别:
* 我们创建的对象,组件每次重新渲染都会创建一个新对象
* useRef()创建的对象,可以确保每次渲染获取到的都是同一个对象
*
* - 当你需要一个对象不会因为组件的重新渲染而改变时,就可以使用useRef()
*
* */
import './App.css'; import { useRef, useState } from "react"; let temp; const App = () => { const h1Ref = useRef(); // 创建一个容器 const [count, setCount] = useState(1); // const h1Ref = {current:null}; // console.log(temp === h1Ref); // temp = h1Ref; const clickHandler = () => { // 通过id获取h1 const header = document.getElementById('header'); // alert(header); // header.innerHTML = '哈哈'; console.log(h1Ref); // alert(h1Ref.current === header); h1Ref.current.innerText = '嘻嘻!'; }; const countAddHandler = () => { setCount(prevState => prevState + 1); }; return <div className={'app'}> <h1 id="header" ref={h1Ref}>我是标题{count}</h1> <button onClick={clickHandler}>1</button> <button onClick={countAddHandler}>2</button> </div>; }; // 导出App export default App;
14. 类组件
父组件App.js:
向子组件User中 传递name='猪八戒' age={28} gender={'男'}
/* * Webstrom中的快捷方式: * rsc --> 函数组件(不带props) * rsi --> 函数组件(带props) * rcc --> 类组件 * */ import React, {Component} from 'react'; import './App.css'; import User from "./components/User"; class App extends Component { render() { return ( <div className="app"> <User name='猪八戒' age={28} gender={'男'}/> </div> ); } } export default App;
子组件User.js:
/*
* 类组件的props是存储到类的实例对象中,
* 可以直接通过实例对象访问
* this.props
* 类组件中state统一存储到了实例对象的state属性中
* 可以通过 this.state来访问
* 通过this.setState()对其进行修改
* 当我们通过this.setState()修改state时,
* React只会修改设置了的属性
*
* 函数组件中,响应函数直接以函数的形式定义在组件中,
* 但是在类组件中,响应函数是以类的方法来定义,之前的属性都会保留
* 但是这你仅限于直接存储于state中的属性
*
* 获取DOM对象
* 1.创建一个属性,用来存储DOM对象
* divRef = React.createRef();
* 2.将这个属性设置为指定元素的ref值
* */import React, {Component} from 'react'; class User extends Component { // 创建属性存储DOM对象 divRef = React.createRef(); // 向state中添加属性 state = { count: 0, test: '哈哈', obj: {name: '孙悟空', age: 18} }; // 为了省事,在类组件中响应函数都应该以箭头函数的形式定义 clickHandler = () => { // this.setState({count: 10}); // this.setState(prevState => { // return { // count: prevState + 1 // } // }); /*this.setState({ obj:{...this.state.obj, name:'沙和尚'} });*/ console.log(this.divRef); }; render() { // console.log(this.props); // console.log(this.divRef); return ( <div ref={this.divRef}> <h1>{this.state.count} --- {this.state.test}</h1> <h2>{this.state.obj.name} --- {this.state.obj.age}</h2> <button onClick={this.clickHandler}>点</button> <ul> <li>姓名:{this.props.name}</li> <li>年龄:{this.props.age}</li> <li>性别:{this.props.gender}</li> </ul> </div> ); } } export default User;
15. 添加card组件
logs组件:
再logs组件中引入 Card组件
Card组件:
props.children 表示组件的标签体,
import React from 'react'; import './Card.css'; const Card = (props) => { /* * props.children 表示组件的标签体 * */ // console.log(props.children); return <div className={`card ${props.className}`}>{props.children}</div>; }; export default Card;
16. 添加表单、处理表单数据、双向绑定、存储到一个state中
logsForm组件:
import React, {useState} from 'react'; import Card from "../UI/Card/Card"; import './LogsForm.css'; const LogsForm = () => { /* * 当表单项发生变化时,获取用户输入的内容 * */ // 创建三个变量,用来存储表单中的数据 // let inputDate = ''; // let inputDesc = ''; // let inputTime = 0; // const [inputDate, setInputDate] = useState(''); // const [inputDesc, setInputDesc] = useState(''); // const [inputTime, setInputTime] = useState(''); // 将表单数据统一到一个state中 const [formData, setFormData] = useState({ inputDate:'', inputDesc:'', inputTime:'' }); // 创建一个响应函数,监听日期的变化 const dateChangeHandler = (e) => { // 获取到当前触发事件的对象 // 事件对象中保存了当前事件触发时的所有信息 // event.target 执行的是触发事件的对象(DOM对象) //console.log(e.target.value); // setInputDate(e.target.value); setFormData({ ...formData, inputDate: e.target.value }); }; // 监听内容的变化 const descChangeHandler = (e) => { // 获取到当前触发事件的对象 // 事件对象中保存了当前事件触发时的所有信息 // event.target 执行的是触发事件的对象(DOM对象) //console.log(e.target.value); // setInputDesc(e.target.value); setFormData({ ...formData, inputDesc: e.target.value }); }; //监听时长的变化 const timeChangeHandler = (e) => { // 获取到当前触发事件的对象 // 事件对象中保存了当前事件触发时的所有信息 // event.target 执行的是触发事件的对象(DOM对象) //console.log(e.target.value); // setInputTime(e.target.value); setFormData({ ...formData, inputTime: e.target.value }); }; // 当表单提交时,汇总表单中的数据 /* * 在React中,通常表单不需要自行提交 * 而是要通过React提交 * */ const formSubmitHandler = (e) => { // 取消表单的默认行为 e.preventDefault(); // 获取表单项中的数据日期、内容、时长 // 将数据拼装为一个对象 const newLog = { date: new Date(formData.inputDate), desc: formData.inputDesc, time: +formData.inputTime }; // 清空表单项 setFormData({ inputDate: '', inputDesc: '', inputTime: '' }); console.log(newLog); * 提交表单后如何清空表单中的旧数据 * 现在这种表单,在React我们称为非受控组件 * * 我们可以将表单中的数据存储到state中, * 然后将state设置为表单项value值, * 这样当表单项发生变化,state会随之变化, * 反之,state发生变化,表单项也会跟着改变,这种操作我们就称为双向绑定 * 这样一来,表单就成为了一个受控组件 }; return ( <Card className="logs-form"> <form onSubmit={formSubmitHandler}> <div className="form-item"> <label htmlFor="date">日期</label> <input onChange={dateChangeHandler} value={formData.inputDate} id="date" type="date"/> </div> <div className="form-item"> <label htmlFor="desc">内容</label> <input onChange={descChangeHandler} value={formData.inputDesc} id="desc" type="text"/> </div> <div className="form-item"> <label htmlFor="time">时长</label> <input onChange={timeChangeHandler} value={formData.inputTime} id="time" type="number"/> </div> <div className="form-btn"> <button>添加</button> </div> </form> </Card> ); }; export default LogsForm;
App组件:
17. 完成添加
子组件数据传递给父组件:
- 父组件内在子组件上传递一个方法
- 子组件使用这个方法传递数据给父组件
- 父组件可以通过形参拿到传递的数据
图示:
App组件:
import Logs from "./Components/Logs/Logs"; import LogsForm from "./Components/LogsForm/LogsForm"; import './App.css'; import {useState} from "react"; const App = () => { // 数据 const [logsData, setLogsData] = useState([ { id: '001', date: new Date(2021, 1, 20, 18, 30), desc: '学习九阳神功', time: 30 }, { id: '002', date: new Date(2022, 2, 10, 12, 30), desc: '学习降龙十八掌', time: 20 }, { id: '003', date: new Date(2022, 2, 11, 11, 30), desc: '学习JavaScript', time: 40 }, { id: '004', date: new Date(2022, 2, 15, 10, 30), desc: '学习React', time: 80 } ]); /* * 下一步: * 将LogsForm中的数据传递给App组件,然后App组件,将新的日志添加到数组中! * */ // 定义一个函数 const saveLogHandler = (newLog) => { // 向新的日志中添加id newLog.id = Date.now() + ''; // console.log('App.js -->',newLog); // 将新的数据添加到数组中 // logsData.push(newLog); setLogsData([newLog, ...logsData]); }; return <div className="app"> {/*引入LogsFrom*/} <LogsForm onSaveLog={saveLogHandler}/> <Logs logsData={logsData}/> </div>; }; // 导出App export default App;
logs组件:
/* 日志的容器 */ import LogItem from "./LogItem/LogItem"; import Card from "../UI/Card/Card"; import './Logs.css'; const Logs = (props) => { * logsDate 用来存储学习的日志, * 这个数据除了当前组件Logs需要使用外,LogsForm也需要使用 * 当遇到一个数据需要被多个组件使用时,我们可以将数据放入到这些组件共同的祖先元素中 * 这样就可以使得多个组件都能方便的访问到这个数据 * * state的提升 // 模拟一组从服务器中加载的数据 // 将数据放入JSX中 const logItemDate = props.logsData.map(item => <LogItem key={item.id} date={item.date} desc={item.desc} time={item.time}/>); return <Card className="logs"> { logItemDate // logsData.map(item => <LogItem {...item}/> ) } </Card> }; export default Logs;
logsForm组件:
import React, {useState} from 'react'; import Card from "../UI/Card/Card"; import './LogsForm.css'; const LogsForm = (props) => { const [inputDate, setInputDate] = useState(''); const [inputDesc, setInputDesc] = useState(''); const [inputTime, setInputTime] = useState(''); // 创建一个响应函数,监听日期的变化 const dateChangeHandler = (e) => { setInputDate(e.target.value); }; // 监听内容的变化 const descChangeHandler = (e) => { setInputDesc(e.target.value); }; //监听时长的变化 const timeChangeHandler = (e) => { setInputTime(e.target.value); }; // 当表单提交时,汇总表单中的数据 const formSubmitHandler = (e) => { // 取消表单的默认行为 e.preventDefault(); // 获取表单项中的数据日期、内容、时长 // 将数据拼装为一个对象 const newLog = { date: new Date(inputDate), desc: inputDesc, time: +inputTime }; // 当要添加新的日志时,调用父组件传递过来的函数 props.onSaveLog(newLog); // 清空表单项 setInputDate(''); setInputDesc(''); setInputTime(''); }; return ( <Card className="logs-form"> <form onSubmit={formSubmitHandler}> <div className="form-item"> <label htmlFor="date">日期</label> <input onChange={dateChangeHandler} value={inputDate} id="date" type="date"/> </div> <div className="form-item"> <label htmlFor="desc">内容</label> <input onChange={descChangeHandler} value={inputDesc} id="desc" type="text"/> </div> <div className="form-item"> <label htmlFor="time">时长</label> <input onChange={timeChangeHandler} value={inputTime} id="time" type="number"/> </div> <div className="form-btn"> <button>添加</button> </div> </form> </Card> ); }; export default LogsForm;
18. 完成删除
logsItem组件:
监听删除方法 onDelLog
import React from 'react'; import MyDate from "./MyDate/MyDate"; import './LogItem.css' import Card from "../../UI/Card/Card"; const LogItem = (props) => { /* * props是只读的不能修改 * */ // props.desc = '嘻嘻'; // 不能修改props中的属性 // console.log(props.desc); // 删除item的响应函数 const deleteItemHandler = () => { // 临时性 const isDel = window.confirm('该操作不可恢复,确认吗?'); if (isDel){ // 删除当前的item,要删除item,其实就是要从数据的state移除指定的数据 // console.log(props.onDelLog); props.onDelLog(); } }; return ( <Card className="item"> <MyDate date={props.date}/> {/* 日志内容的容器 */} <div className="content"> {/* 如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值 我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据 */} <h2 className="desc">{props.desc}</h2> <div className="time">{props.time}分钟</div> </div> {/* 添加一个删除按钮*/} <div> <div onClick={deleteItemHandler} className='delete'>×</div> </div> </Card> ); }; export default LogItem;
App组件:
import Logs from "./components/Logs/Logs"; import LogsForm from "./components/LogsForm/LogsForm"; import './App.css'; import {useState} from "react"; const App = () => { // 数据 const [logsData, setLogsData] = useState([ { id: '001', date: new Date(2021, 1, 20, 18, 30), desc: '学习九阳神功', time: 30 }, { id: '002', date: new Date(2022, 2, 10, 12, 30), desc: '学习降龙十八掌', time: 20 }, { id: '003', date: new Date(2022, 2, 11, 11, 30), desc: '学习JavaScript', time: 40 }, { id: '004', date: new Date(2022, 2, 15, 10, 30), desc: '学习React', time: 80 } ]); /* * 下一步: * 将LogsForm中的数据传递给App组件,然后App组件,将新的日志添加到数组中! * */ // 定义一个函数 const saveLogHandler = (newLog) => { // 向新的日志中添加id newLog.id = Date.now() + ''; // console.log('App.js -->',newLog); // 将新的数据添加到数组中 // logsData.push(newLog); setLogsData([newLog, ...logsData]); }; // 定义一个函数,从数据中删除一条日志 const delLogByIndex = (index) => { setLogsData(prevState => { const newLog = [...prevState]; newLog.splice(index, 1); return newLog; }); }; return <div className="app"> {/*引入LogsFrom*/} <LogsForm onSaveLog={saveLogHandler}/> <Logs logsData={logsData} onDelLog={delLogByIndex}/> </div>; }; // 导出App export default App;
logs组件:
把App组件中的删除方法传递给 losgItem
/* 日志的容器 */ import LogItem from "./LogItem/LogItem"; import Card from "../UI/Card/Card"; import './Logs.css'; const Logs = (props) => { /* * logsDate 用来存储学习的日志, * 这个数据除了当前组件Logs需要使用外,LogsForm也需要使用 * 当遇到一个数据需要被多个组件使用时,我们可以将数据放入到这些组件共同的祖先元素中 * 这样就可以使得多个组件都能方便的访问到这个数据 * * state的提升 * * */ // 模拟一组从服务器中加载的数据 // 将数据放入JSX中 const logItemDate = props.logsData.map((item, index) => <LogItem onDelLog={()=>props.onDelLog(index)} key={item.id} date={item.date} desc={item.desc} time={item.time}/>); return <Card className="logs"> { logItemDate // logsData.map(item => <LogItem {...item}/> ) } </Card> }; export default Logs;
19. 空列表提示
logs组件:
/* 日志的容器 */ import LogItem from "./LogItem/LogItem"; import Card from "../UI/Card/Card"; import './Logs.css'; const Logs = (props) => { /* * logsDate 用来存储学习的日志, * 这个数据除了当前组件Logs需要使用外,LogsForm也需要使用 * 当遇到一个数据需要被多个组件使用时,我们可以将数据放入到这些组件共同的祖先元素中 * 这样就可以使得多个组件都能方便的访问到这个数据 * * state的提升 * * */ // 模拟一组从服务器中加载的数据 // 将数据放入JSX中 let logItemData = props.logsData.map( (item, index) => <LogItem onDelLog={() => props.onDelLog(index)} key={item.id} date={item.date} desc={item.desc} time={item.time}/> ); if (logItemData.length === 0) { logItemData = <p className="no-logs">没要找到日志!</p>; } return <Card className="logs"> { logItemData // logItemData.length !== 0 ? logItemData : <p className="no-logs">没要找到日志!</p> // logsData.map(item => <LogItem {...item}/> ) } </Card> }; export default Logs;
20. 添加 ConfirmModal
ConfirmModal组件:
import './ComfirmModal.css'; import Card from "../Card/Card"; import BackDrop from "../BackDrop/BackDrop"; const ConfirmModal = props => { return <BackDrop> <Card className="confirmModal"> <div className="confirmText"> <p>{props.confirmText}</p> </div> <div className="confirmButton"> <button onClick={props.onOk} className="okButton">确认</button> <button onClick={props.onCancel}>取消</button> </div> </Card>; </BackDrop> }; export default ConfirmModal;
logsItem组件:
import React, {useState} from 'react'; import MyDate from "./MyDate/MyDate"; import './LogItem.css' import Card from "../../UI/Card/Card"; import ConfirmModal from "../../UI/ConfirmModal/ConfirmModal"; const LogItem = (props) => { // 添加一个state,记录是否显示确认窗口 const [showConfirm, setShowConfirm] = useState(false); /* * props是只读的不能修改 * */ // props.desc = '嘻嘻'; // 不能修改props中的属性 // console.log(props.desc); // 删除item的响应函数 const deleteItemHandler = () => { // 显示确认窗口 setShowConfirm(true); }; //取消函数 const cancelHandler = () => { setShowConfirm(false); }; // 确认函数 const okHandler = () => { props.onDelLog(); }; return ( <Card className="item"> {showConfirm && <ConfirmModal confirmText="该操作不可恢复!请确认" onCancel={cancelHandler} onOk={okHandler} />} <MyDate date={props.date}/> {/* 日志内容的容器 */} <div className="content"> {/* 如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值 我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据 */} <h2 className="desc">{props.desc}</h2> <div className="time">{props.time}分钟</div> </div> {/* 添加一个删除按钮*/} <div> <div onClick={deleteItemHandler} className='delete'>×</div> </div> </Card> ); }; export default LogItem;
backDrop组件:
import React from 'react'; import './BackDrop.css'; const BackDrop = (props) => { return ( <div className="backDrop"> {props.children} </div> ); }; export default BackDrop;
21. 使用portal修改项目
createPortal方法
/*
* portal
* - 组件默认会作为父组件的后代渲染到页面中
* 但是有些情况下,这种方式会带来一些问题
* - 通过portal可以将组件渲染到页面中的指定位置
* - 使用方法:
* 1.在index.html添加一个新的元素
* 2.修改组件的渲染方式
* - 通过ReactDOM.createPortal()作为返回值创建元素
* - 参数:
* 1. jsx(修改前return后的代码)
* 2. 目标位置(DOM元素)
* */
遮罩层组件:
import React from 'react'; import './Backdrop.css'; import ReactDOM from "react-dom"; // 获取backdrop的根元素 const backdropRoot = document.getElementById('backdrop-root'); const Backdrop = (props) => { return ReactDOM.createPortal(<div className="backdrop"> {props.children} </div>, backdropRoot); }; export default Backdrop;
logItem组件:
import React, {useState} from 'react'; import MyDate from "./MyDate/MyDate"; import './LogItem.css' import Card from "../../UI/Card/Card"; import ConfirmModal from "../../UI/ConfirmModal/ConfirmModal"; const LogItem = (props) => { // 添加一个state,记录是否显示确认窗口 const [showConfirm, setShowConfirm] = useState(false); // 删除item的响应函数 const deleteItemHandler = () => { // 显示确认窗口 setShowConfirm(true); }; //取消函数 const cancelHandler = () => { setShowConfirm(false); }; // 确认函数 const okHandler = () => { props.onDelLog(); }; return ( <Card className="item"> {showConfirm && <ConfirmModal confirmText="该操作不可恢复!请确认" onCancel={cancelHandler} onOk={okHandler} />} <MyDate date={props.date}/> {/* 日志内容的容器 */} <div className="content"> {/* 如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值 我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据 */} <h2 className="desc">{props.desc}</h2> <div className="time">{props.time}分钟</div> </div> {/* 添加一个删除按钮*/} <div> <div onClick={deleteItemHandler} className='delete'>×</div> </div> </Card> ); }; export default LogItem;
22. 过滤日志
logs组件:
/* 日志的容器 */ import LogItem from "./LogItem/LogItem"; import Card from "../UI/Card/Card"; import './Logs.css'; import LogFilter from "./LogFilter/LogFilter"; import {useState} from "react"; const Logs = (props) => { // 创建一个存储年份的state const [year, setYear] = useState(2022); // 过滤数据,只显示某一年的数据 let filterData = props.logsData.filter(item => item.date.getFullYear() === year); // 创建一个修改年份的函数 const changeYearHandler = (year) => { setYear(year); }; // 将数据放入JSX中 let logItemData = filterData.map( (item, index) => <LogItem onDelLog={() => props.onDelLog(item.id)} key={item.id} date={item.date} desc={item.desc} time={item.time}/> ); if (logItemData.length === 0) { logItemData = <p className="no-logs">没要找到日志!</p>; } return <Card className="logs"> {/*引入年份的选择组件*/} <LogFilter year={year} onYearChange={changeYearHandler} /> {logItemData} </Card> }; export default Logs;
过滤组件:
import React from 'react'; const LogFilter = props => { // 创建监听change事件的响应函数 const changeHandler = e => { props.onYearChange(+e.target.value); }; return ( <div> 年份: <select onChange={changeHandler} value={props.year}> <option value="2022">2022</option> <option value="2021">2021</option> <option value="2020">2020</option> </select> </div> ); }; export default LogFilter;
App组件:
更改为id删除
import Logs from "./components/Logs/Logs"; import LogsForm from "./components/LogsForm/LogsForm"; import './App.css'; import {useState} from "react"; const App = () => { // 数据 const [logsData, setLogsData] = useState([ { id: '001', date: new Date(2021, 1, 20, 18, 30), desc: '学习九阳神功', time: 30 }, { id: '002', date: new Date(2022, 2, 10, 12, 30), desc: '学习降龙十八掌', time: 20 }, { id: '003', date: new Date(2022, 2, 11, 11, 30), desc: '学习JavaScript', time: 40 }, { id: '004', date: new Date(2022, 2, 15, 10, 30), desc: '学习React', time: 80 } ]); /* * 下一步: * 将LogsForm中的数据传递给App组件,然后App组件,将新的日志添加到数组中! * */ // 定义一个函数 const saveLogHandler = (newLog) => { // 向新的日志中添加id newLog.id = Date.now() + ''; // console.log('App.js -->',newLog); // 将新的数据添加到数组中 // logsData.push(newLog); setLogsData([newLog, ...logsData]); }; // 定义一个函数,从数据中删除一条日志 const delLogByIndex = (index) => { setLogsData(prevState => { const newLog = [...prevState]; newLog.splice(index, 1); return newLog; }); }; // 定义一个函数,从数据中删除一条日志 const delLogById = (id) => { setLogsData(prevState => { return prevState.filter(item => item.id !== id); }); }; return <div className="app"> {/*引入LogsFrom*/} <LogsForm onSaveLog={saveLogHandler}/> <Logs logsData={logsData} onDelLog={delLogById}/> </div>; }; // 导出App export default App;
23. create-react-app
为了使我们创建项目更加方便React为我们提供了一个工具 create-react-app 光看名字你应该就已经知道它的作用了
使用 create-react-app 可以快速的创建一个React项目的目录结构,并且它会自动帮助我们安装React中所有的依赖,换句话说,之前我们手动做的工作现在只需要一个命令就可以搞定了!
npx create-react-app 项目名
24. 内联样式和样式表
- 内联样式编写一个style对象,在DOM元素的style属性进行赋值
- 结合 state动态设置样式
- 通过 import 引入外部样式表
25. CSS_Module
/*
* CSS模块
* 使用步骤:
* 1.创建一个xxx.module.css
* 2.在组件中引入css
* import classes from './App.module.css';
* 3.通过classes来设置类
* className={classes.p1}
* CSS模块可以动态的设置唯一的class值
* App_p1__9v2n6
* */
import React, {useState} from 'react'; import classes from './App.module.css'; import A from "./A"; const App = () => { const [showBorder, setShowBorder] = useState(false); const clickHandler = () => { setShowBorder(true); }; return ( <div> <A/> <p className={`${classes.p1} ${showBorder ? classes.Border : ''}`}>我是一个段落</p> <button onClick={clickHandler}>点我一下</button> </div> ); }; export default App;
26. Fragment
/*
* React.Fragment
* - 是一个专门用来作为父容器的组件
* 它只会将它里边的子元素直接返回,不会创建任何多余的元素
* - 当我们希望有一个父容器
* 但同时又不希望父容器在网页中产生多余的结构时
* 就可以使用Fragment
* */
方式一:
方式二:
方式三:
使用空标签
return ( <> <div>第一个组件</div> <div>第二个组件</div> <div>第三个组件</div> </> );
27. 汉堡到家 - demo练习
Font Awesome 中文网 – | 字体图标
在需要使用的组件内引入这两个包
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";
使用示例:
<button className={classes.Sub}><FontAwesomeIcon icon={faMinus} /></button> - 引入组件 import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; - 引入图标 import {faPlus} from "@fortawesome/free-solid-svg-icons"; - 使用组件 <FontAwesomeIcon icon={faPlus}/>
Context
Context官网地址: Context – React
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
创建Context:
import React from 'react'; * Context相当于一个公共的存储空间, * 我们可以将多个组件中都需要访问的数据统一存储到一个Context中, * 这样无需通过props逐层传递,即可使组件访问到这些数据 * * 通过React.createContext()创建context * const TestContext = React.createContext({ name:'孙悟空', age:18 }); export default TestContext;
演示Context:
A组件:
import React from 'react'; import TestContext from "../store/testContext"; * 使用方式一: * 1.引入context * 2.使用 Xxx.Consumer 组件来创建元素 * Consumer 的标签体需要一个回调函数 * 它会将context设置为回调函数的参数,通过参数就可以访问到context中存储的数据 * const A = () => { return ( <TestContext.Consumer> {(ctx)=>{ return <div> {ctx.name} - {ctx.age} </div> }} </TestContext.Consumer> ); }; export default A;
B组件:
import React, {useContext} from 'react'; import TestContext from "../store/testContext"; * 使用Context方式二: * 1.导入Context * 2.使用钩子函数useContext()获取到context * useContext() 需要一个Context作为参数 * 它会将Context中数据获取并作为返回值返回 * * Xxx.Provider * - 表示数据的生产者,可以使用它来指定Context中的数据 * - 通过value来指定Context中存储的数据, * 这样一来,在该组件的所有的子组件中都可以通过Context来访问它所指定数据 * * 当我们通过Context访问数据时,他会读取离他最近的Provider中的数据, * 如果没有Provider,则读取Context中的默认数据 * const B = () => { // 使用钩子函数获取Context const ctx = useContext(TestContext); return ( <div> {ctx.name} -- {ctx.age} </div> ); }; export default B;
仓库源码:https://gitee.com/wei-shuai-lei/order-cart-react.git
28. Hooks
1. Effcet
React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。
React.StrictMode
编写React组件时,我们要极力的避免组件中出现那些会产生"副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了 React.strictMode 标签,那么React会非常"智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,我们来看看React官网的文档是如何说明的:useEffect
* useEffect是一个钩子函数,需要一个函数作为参数 * 这个作为参数的函数,将会在组件渲染完毕后执行 * 在开发中,可以将那些会产生副作用的代码编写到useEffect的回调函数中 * 这样可以避免这些代码影响带组件的渲染 * * 在useEffect() 可以传递一个第二个参数 * 第二个参数是一个数组,在数组中可以指定Effect的依赖项 * 指定后,只有当依赖项发生变化的时候,Effect才会被触发 * 通常会将Effect中使用的所有的局部变量都设置为它的依赖项 * 这样一来可以确保这些值发生变化时,可以触发 Effect * 像setState是由钩子函数 useState() 生成的 * useState() 会确保组件的每次渲染都会获得到相同 setState() 对象 * 所以setState() 方法可以不设置到依赖项中 * 如果依赖项设置了一个空数组,则意味着Effect只会在组件初始化时触发执行一次 useEffect(() => { // 降低数据过滤的次数,提高用户体验 // 用户输入完了你再过滤,用户输入过程中,不要过滤 // 当用户停止输入动作1秒后, 再查询 // 再开启一个定时器的同时,应该关掉上一个 let timer = setTimeout(() => { console.log('Effect执行了'); props.onFilter(keyword); }, 1000) // 在Effect的回调函数中,可以指定一个函数作为返回值 // 这个函数可以称其为清理函数,他会在下次Effect执行前调用 // 可以在这个函数中,做一些工作来清除上次Effect执行所带来的影响 return () => { clearTimeout(timer) } }, [keyword])
2. setState的执行流程
当我们直接在函数体中调用setState时,就会触发上述错误问题: 不是说过,当新的state 值和旧值相同时,它是不会触发组件的重新渲染的setState()的执行流程(函数组件) setCount() --> dispatchSetDate() --> 会先判断,组件当前处于什么阶段 如果是渲染阶段 --> 不会检查state 值是否相同 如果不是渲染阶段 --> 会检查state的值是否相同 - 如果值不相同,则对组件进行重新渲染 - 如果值相同,则不对值进行重新渲染 如果值相同,React在一些情况下会继续执行当前组件的渲染 但是这个渲染不会触发其子组件的渲染,这次渲染不会产生实际的效果
3. useReducer
/**
* 参数:
* reducer :整合函数
* 对于我们当前state的所有操作都应该在该函数中定义
* 该函数的返回值,会成为state的新值
* reducer 在执行时,会受到两个参数
* 1. 当前最新的state
* 2. action 它需要一个对象,在对象中会存储dispatch所发送的指令
* initialArg : state初始值,作用和useState中的值是一样的
* init :
* 返回值:
* 数组:
* 第一个参数,state 用来获取state的值
* 第二个参数,state 修改的派发器
* 通过派发器可以发送操作state的命令
* 具体的修改行为将会由另外一个函数执行
*
* */
// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
代码示例:
import React, { useEffect, useReducer, useState } from 'react'; import './App.css'; // 为了避免reducer会重复创建,通常reducer会定义到组件的外部 const countHander = (state, action) => { switch (action.type) { case 'ADD': return state + 1 case 'SUB': return state - 1 default: return state } } const App = () => { // const [count, setCount] = useState(1) // const addHander = () => { // setCount(prevState => prevState + 1) // } // const subHander = () => { // setCount(prevState => prevState - 1) // } // useReducer(reducer, initialArg, init) const [count, countDispatch] = useReducer(countHander, 1) const addHander = () => { countDispatch({ type: 'ADD' }) } const subHander = () => { countDispatch({ type: 'SUB' }) } return ( <div style={{ fontSize: 30 }}> <button onClick={subHander}>减少</button> {count} <button onClick={addHander}>增加</button> </div> ); }; export default App;
4.React.memo
React组件会在两种情况下发生重新渲染。第一种,当组件自身的state发生变化时。第二种当组件的父组件重新渲染时。第一种情况下的重新渲染无可厚非,state都变了,组件自然应该重新进行渲染。
但是第二种情况似乎并不是总那么的必要。
React.memo() 是一个高阶组件 它接收另一个组件作为参数,并且返回一个包装后的新组件 包装过的新组件就会具有缓存功能 包装过后,只有组件的props发生变化 才会触发组件的重新渲染,否则总是返回缓存中的结果 示例: (导出一个B组件) export default React.memo(B)
5. useCallback()
useCallback
- useCallback 是一个钩子函数,用来创建React 中的回调函数
- useCallback 创建的回调函数不会总在组件重新渲染时重新创建
参数:
- 回调函数
- 依赖数组
- 当依赖数组中的变量发生变化时,回调函数才会重新执行
- 如果不指定依赖数组,回调函数每次都会重新创建
- 一定要将所使用到的局部变量放到依赖数组中(除了 setState)
示例:
const B = () => { console.log('B组件执行了'); const clickHandler = useCallback(() => { setCount(prevState => prevState + num) setNum(prevState => num + 1) }, [num]) }
6.Strapi
欢迎来到 Strapi 开发人员文档!
React83_Strapi的使用_哔哩哔哩_bilibili
Strapi是什么? 官网是这么描述的“Strapi是完全使用JavaScript开发的,开源无头内容管理系统”,对于第一次接触它的同学会感觉莫名其妙,“无头内容管理系统”,什么玩意? 简单来说,strapi就是一个APl的管理系统,通过Strapi我们可以直接以网页的形式去定义自己的API、包括设置模型、权限等功能。有了strapi我们无需编写代码便可开发出功能强大的API。
创建项目:
使用npx会卡在一个压缩包,建议使用yarn
npx create-strapi-app 项目名 --quickstart yarn create strapi-app 项目名 --quickstart
7.fetch
React的主要作用是取代了原生DOM,让我们操作网页的方式变得更加简单。但是React中并没有为我们提供向服务器中发送请求的方法 (因为这本来就不是它所关注的事情)。所以在React中发送请求的方式和传统项目其实是一致的,无非就是使用浏览器自带的Ajax、Fetch或者是类似于Axios的第三方框架。这也就意味着在React中发送请求的方式其实是非常灵活的,你完全可以根据自己的习惯选择一种你喜欢的方式。
8. 数据加载提示 + 错误处理
可以添加一个 布尔值 来判断是否在加载
创建一个state 来记录错误消息
9. 自定义钩子
个人理解 所谓自定义钩子就是封装一个方法
/**
* React中的钩子函数只能在函数组件或自定钩子中调用
* 当我们需要将React 中钩子函数提取到一个公共区域时,就可以使用自定义钩子
*
* 自定义钩子其实就是一个普通函数,只是它的名字需要使用use 开头
*/
自定义hook useFetch.js自定义的useFetch.js 钩子 import React, { useState } from "react"; const useEftch = () => { const [stuData, setStuData] = useState([]) const fetchData = async () => { let res = await fetch('http://localhost:1337/api/data') if (res.ok) { const data = await res.json() setStuData(data) } } return { stuData, setStuData, fetchData } } export default useEftch
App.js
导入自定义的钩子
import React, { useCallback, useEffect, useState } from 'react'; import './App.css'; import Table from "./Table"; import useFetch from './hooks/useFetch' const App = () => { const { stuData, setStuData, fetchData } = useFetch() useEffect(() => { fetchData() }, []) const loadDataHandler = () => { fetchData() } return ( <div> <button onClick={loadDataHandler}>加载数据</button> <Table slectMethod={fetchData} list={stuData}></Table> </div> ); }; export default App;
10. Redux学习
A Predictable State Container for JS Apps是Redux官方对于Redux的描述,这句话可以这样翻译“一个专为JS应用设计的可预期的状态容器”,简单来说Redux是一个可预测的状态容器什么玩意?
状态(State)
state直译过来就是状态,使用React这么久了,对于state我们已经是非常的熟悉了。state不过就是一个变量,一个用来记录(组件)状态的变量。组件可以根据不同的状态值切换为不同的显示,比如,用户登录和没登录看到页面应该是不同的,那么用户的登录与否就应该是一个状态。再比如,数据加载与否,显示的界面也应该不同,那么数据本身就是一个状态。换句话说,状态控制了页面的如何显示。
但是需要注意的是,状态并不是React中或其他类似框架中独有的。所有的编程语言,都有状态,所有的编程语言都会根据不同的状态去执行不同的逻辑,这是一定的。所以状态是什么,状态就是一个变量,用以记录程序执行的情况。
容器(Container)
容器当然是用来装东西的,状态容器即用来存储状态的容器。状态多了,自然需要一个东西来存储,但是容器的功能却不是仅仅能存储状态,它实则是一个状态的管理器,除了存储状态外,它还可以用来对state进行查询、修改等所有操作。(编程语言中容器几乎都是这个意思,其作用无非就是对某个东西进行增删改查)
可预测(Predictable)
可预测指我们在对state进行各种操作时,其结果是一定的。即以相同的顺序对state执行相同的操作会得到相同的结果。简单来说,Redux中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作state,而不能直接修改state。这就意味着外部对state的操作都被容器所限制,对state的操作都在容器的掌控之中,也就是可预测。
总的来说,Redux是一个稳定、安全的状态管理器。
为什么是Redux?
问:不对啊?React中不是已经有state了吗?为什么还要整出一个Redux来作为状态管理器呢?
答:state应付简单值还可以,如果值比较复杂的话并不是很方便。
问:复杂值可以用useReducer嘛!
答:的确可以啊!但无论是state还是useReducer,state在传递起来还是不方便,自上至下一层一层的传递并不方便啊!
问:那不是还有context吗?
答:的确使用context可以解决state的传递的问题,但依然是简单的数据尚可,如果数据结构过于复杂会使得context变得异常的庞大,不方便维护。
Redux可以理解为是reducer和context的结合体,使用Redux即可管理复杂的state,又可以在不同的组件间方便的共享传递state。当然,Redux主要使用场景依然是大型应用,大型应用中状态比较复杂,如果只是使用reducer和context,开发起来并不是那么的便利,此时一个有一个功能强大的状态管理器就变得尤为的重要。
使用
使用Redux之前,你需要先明确一点Redux是JS应用的状态容器,它并不是只能在React使用,而是可以应用到任意的JS应用中(包括前端JS,和服务器中Node.js)。总之,凡是JS中需要管理的状态的Redux都可以胜任。
在网页中直接使用
我们先来在网页中使用以下Redux,在网页中使用Redux就像使用jQuery似的,直接在网页中引入Redux的库文件即可:
<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>网页中我们实现一个简单的计数器功能,页面长成这样:
<button id="btn01">减少</button> <span id="counter">1</span> <button id="btn02">增加</button>
我们要实现的功能很简单,点击减少数字变小,点击增加数字变大。如果用传统的DOM编写,可以创建一个变量用以记录数量,点击不同的按钮对变量做不同的修改并设置到span之中,代码像是这样:
不使用Redux:
const btn01 = document.getElementById('btn01'); const btn02 = document.getElementById('btn02'); const counterSpan = document.getElementById('counter'); let count = 1; btn01.addEventListener('click', ()=>{ count--; counterSpan.innerText = count; }); btn02.addEventListener('click', ()=>{ count++; counterSpan.innerText = count; });
上述代码中count就是一个状态,只是这个状态没有专门的管理器,它的所有操作都在事件的响应函数中进行处理,这种状态就是不可预测的状态,因为在任何的函数中都可以对这个状态进行修改,没有任何安全限制。不过就这个功能而言,这可能已经是最简单的代码了。一会我们使用了Redux,代码会变得复杂一些,但是还是那句话,这里我们只是找一个简单的场景做一个演示,Redux的真实使用场景依然是大型应用中的复杂state。
Redux是一个状态容器,所以使用Redux必须先创建容器对象,它的所有操作都是通过容器对象来进行的,创建容器的方式有多种,我们先说一种好理解的:
Redux.createStore(reducer, [preloadedState], [enhancer])
createStore用来创建一个Redux中的容器对象,它需要三个参数:reducer、preloadedState、enhancer。
reducer是一个函数,是state操作的整合函数,每次修改state时都会触发该函数,它的返回值会成为新的state。
preloadedState就是state的初始值,可以在这里指定也可以在reducer中指定。
enhancer增强函数用来对state的功能进行扩展,暂时先不理它。
三个参数中,只有reducer是必须的,来看一个Reducer的示例:
const countReducer = (state = {count:0}, action) => { switch (action.type){ case 'ADD': return {count:state.count+1}; case 'SUB': return {count:state.count-1}; default: return state } };
reducer用来整合关于state的所有操作,容器修改state时会自动调用该函数,函数调用时会接收到两个参数:state和action,state表示当前的state,可以通过该state来计算新的state。
state = {count:0}
这是在指定state的默认值,如果不指定,第一次调用时state的值会是undefined。也可以将该值指定为createStore()的第二个参数。action是一个普通对象,用来存储操作信息。将reducer传递进createStore后,我们会得到一个store对象:
const store = Redux.createStore(countReducer);store对象创建后,对state的所有操作都需要通过它来进行:
读取state:
store.getState()修改state:
store.dispatch({type:'ADD'})dipatch用来触发state的操作,可以将其理解为是想reducer发送任务的工具。它需要一个对象作为参数,这个对象将会成为reducer的第二个参数action,需要将操作信息设置到对象中传递给reducer。action中最重要的属性是type,type用来识别对state的不同的操作,上例中’ADD’表示增加操作,’SUB’表示减少的操作。
除了这些方法外,store还拥有一个subscribe方法,这个方法用来订阅state变化的信息。该方法需要一个回调函数作为参数,当store中存储的state发生变化时,回调函数会自动调用,我们可以在回调函数中定义state发生变化时所要触发的操作:
store.subscribe(()=>{ // store中state发生变化时触发 });如此一来,刚刚的代码被修改成了这个样子:
const btn01 = document.getElementById('btn01'); const btn02 = document.getElementById('btn02'); const counterSpan = document.getElementById('counter'); const countReducer = (state = {count:0}, action) => { switch (action.type){ case 'ADD': return {count:state.count+1}; case 'SUB': return {count:state.count-1}; default: return state } }; const store = Redux.createStore(countReducer); store.subscribe(()=>{ counterSpan.innerText = store.getState().count; }); btn01.addEventListener('click', ()=>{ store.dispatch({type:'SUB'}); }); btn02.addEventListener('click', ()=>{ store.dispatch({type:'ADD'}); });
修改后的代码相较于第一个版本要复杂一些,同时也解决了之前代码中存在的一些问题:
- 前一个版本的代码state就是一个变量,可以任意被修改。state不可预测,容易被修改为错误的值。新代码中使用了Redux,Redux中的对state的所有操作都封装到了reducer函数中,可以限制state的修改使state可预测,有效的避免了错误的state值。
- 前一个版本的代码,每次点击按钮修改state,就要手动的修改counterSpan的innerText,非常麻烦,这样一来我们如果再添加新的功能,依然不能忘记对其进行修改。新代码中,counterSpan的修改是在store.subscribe()的回调函数中进行的,state每次发生变化其值就会随之变化,不需要再手动修改。换句话说,state和DOM元素通过Redux绑定到了一起。
通过上例也不难看出,Redux中最最核心的东西就是这个store,只要拿到了这个store对象就相当于拿到了Redux中存储的数据。在加上Redux的核心思想中有一条叫做“单一数据源”,也就是所有的state都会存储到一课对象树中,并且这个对象树会存储到一个store中。所以到了React中,组件只需获取到store即可获取到Redux中存储的所有state。
React中使用Redux(旧的方式)
当我们需要在React中使用Redux时,我们除了需要引入Redux核心库外,还需要引入react-redux库,以使React和redux适配,可以通过npm或yarn安装:
npm install -S redux react-redux
或
yarn add redux react-redux
接下来我们尝试在Redux,添加一些复杂的state,比如一个学生的信息:
{name:'孙悟空', age:18, gender:'男', address:'花果山'}
代码:
创建reducer:
const reducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state } };
reducer的编写和之前的案例并没有本质的区别,只是这次的数据和操作方法变得复杂了一些。以SET_NAME为例,当需要修改name属性时,dispatch需要传递一个有两个属性的action,action的type应该是字符串”SET_NAME”,payload应该是要修改的新名字,比如要将名字修改为猪八戒,则dispatch需要传递这样一个对象
{type:'SET_NAME',payload:'猪八戒'}
。创建store:
const store = createStore(reducer);
创建store和前例并无差异,传递reducer进行构建即可。
设置provider:
const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App/> </Provider> );
创建store后,需要引入react-redux中提供的Provider组件,将其设置到所有组件的最外层,并且将刚刚创建的store设置为组件的store属性,只有这样才能使得Redux中的数据能被所有的组件访问到。
访问数据:
const stu = useSelector(state => state);
react-redux还为我们提供一个钩子函数useSelector,用于获取Redux中存储的数据,它需要一个回调函数作为参数,回调函数的第一个参数就是当前的state,回调函数的返回值,会作为useSelector的返回值返回,所以
state => state
表示直接将整个state作为返回值返回。现在就可以通过stu来读取state中的数据了:<p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p>
操作数据:
const dispatch = useDispatch();
useDispatch同样是react-redux提供的钩子函数,用来获取redux的派发器,对state的所有操作都需要通过派发器来进行。
通过派发器修改state:
dispatch({type:'SET_NAME', payload:'猪八戒'}) dispatch({type:'SET_AGE', payload:28}) dispatch({type:'SET_GENDER', payload:'女'}) dispatch({type:'SET_ADDRESS', payload:'高老庄'})
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {createStore} from "redux"; const reducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state } }; const store = createStore(reducer); const App = () =>{ const stu = useSelector(state => state); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={()=>{dispatch({type:'SET_NAME', payload:'猪八戒'})}}>改name</button> <button onClick={()=>{dispatch({type:'SET_AGE', payload:28})}}>改age</button> <button onClick={()=>{dispatch({type:'SET_GENDER', payload:'女'})}}>改gender</button> <button onClick={()=>{dispatch({type:'SET_ADDRESS', payload:'高老庄'})}}>改address</button> </div> </div> }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div> <Provider store={store}> <App/> </Provider> </div> );
复杂的State
上例中的数据结构已经变得复杂,但是距离真实项目还有一定的差距。因为Redux的核心思想是所有的state都应该存储到同一个仓库中,所以只有一个学生数据确实显得有点单薄,现在将数据变得复杂一些,出来学生数据外,还增加了一个学校的信息,于是state的结构变成了这样:
{ stu:{ name: '孙悟空', age: 18, gender: '男', address: '花果山' }, school:{ name:'花果山一小', address:'花果山大街1号' } }
数据结构变得复杂了,我们需要对代码进行修改,首先看reducer:
const reducer = (state = { stu: { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, school: { name: '花果山一小', address: '花果山大街1号' } }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, stu: { ...state.stu, name: action.payload } }; case 'SET_AGE': return { ...state, stu: { ...state.stu, age: action.payload } }; case 'SET_ADDRESS': return { ...state, stu: { ...state.stu, address: action.payload } }; case 'SET_GENDER': return { ...state, stu: { ...state.stu, gender: action.payload } }; case 'SET_SCHOOL_NAME': return { ...state, school: { ...state.school, name:action.payload } }; case 'SET_SCHOOL_ADDRESS': return { ...state, school: { ...state.school, address: action.payload } } default : return state; } };
数据层次变多了,我们在操作数据时也变得复杂了,比如修改name的逻辑变成了这样:
case 'SET_NAME': return { ...state, stu: { ...state.stu, name: action.payload } };
同时数据加载的逻辑也要修改,之前我们是将整个state返回,现在我们需要根据不同情况获取state,比如获取学生信息要这么写:
const stu = useSelector(state => state.stu);
获取学校信息:
const school = useSelector(state => state.school);
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {createStore} from "redux"; const reducer = (state = { stu: { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, school: { name: '花果山一小', address: '花果山大街1号' } }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, stu: { ...state.stu, name: action.payload } }; case 'SET_AGE': return { ...state, stu: { ...state.stu, age: action.payload } }; case 'SET_ADDRESS': return { ...state, stu: { ...state.stu, address: action.payload } }; case 'SET_GENDER': return { ...state, stu: { ...state.stu, gender: action.payload } }; case 'SET_SCHOOL_NAME': return { ...state, school: { ...state.school, name:action.payload } }; case 'SET_SCHOOL_ADDRESS': return { ...state, school: { ...state.school, address: action.payload } } default : return state; } }; const store = createStore(reducer); const App = () => { const stu = useSelector(state => state.stu); const school = useSelector(state => state.school); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={() => { dispatch({type: 'SET_NAME', payload: '猪八戒'}); }}>改name </button> <button onClick={() => { dispatch({type: 'SET_AGE', payload: 28}); }}>改age </button> <button onClick={() => { dispatch({type: 'SET_GENDER', payload: '女'}); }}>改gender </button> <button onClick={() => { dispatch({type: 'SET_ADDRESS', payload: '高老庄'}); }}>改address </button> </div> <hr/> <p> {school.name} -- {school.address} </p> <div> <button onClick={()=>{dispatch({type:'SET_SCHOOL_NAME', payload:'高老庄小学'})}}>改学校name</button> <button onClick={()=>{dispatch({type:'SET_SCHOOL_ADDRESS', payload:'高老庄中心大街15号'})}}>改学校address</button> </div> </div>; }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div> <Provider store={store}> <App/> </Provider> </div> );
麻烦确实是麻烦了一些,但是还好功能实现了。
多个Reducer
上边的案例的写法存在一个非常严重的问题!将所有的代码都写到一个reducer中,会使得这个reducer变得无比庞大,现在只有学生和学校两个信息。如果数据在多一些,操作方法也会随之增多,reducer会越来越庞大变得难以维护。
Redux中是允许我们创建多个reducer的,所以上例中的reducer我们可以根据它的数据和功能进行拆分,拆分为两个reducer,像是这样:
const stuReducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state; } }; const schoolReducer = (state = { name: '花果山一小', address: '花果山大街1号' }, action) => { switch (action.type) { case 'SET_SCHOOL_NAME': return { ...state, name: action.payload }; case 'SET_SCHOOL_ADDRESS': return { ...state, address: action.payload }; default : return state; } };
修改后reducer被拆分为了stuReducer和schoolReducer,拆分后在编写每个reducer时,只需要考虑当前的state数据,不再需要对无关的数据进行复制等操作,简化了reducer的编写。于此同时将不同的功能编写到了不同的reducer中,降低了代码间的耦合,方便对代码进行维护。
拆分后,还需要使用Redux为我们提供的函数combineReducer将多个reducer进行合并,合并后才能传递进createStore来创建store。
const reducer = combineReducers({ stu:stuReducer, school:schoolReducer }); const store = createStore(reducer);
combineReducer需要一个对象作为参数,对象的属性名可以根据需要指定,比如我们有两种数据stu和school,属性名就命名为stu和school,stu指向stuReducer,school指向schoolReducer。读取数据时,直接通过state.stu读取学生数据,通过state.school读取学校数据。
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {combineReducers, createStore} from "redux"; const stuReducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state; } }; const schoolReducer = (state = { name: '花果山一小', address: '花果山大街1号' }, action) => { switch (action.type) { case 'SET_SCHOOL_NAME': return { ...state, name: action.payload }; case 'SET_SCHOOL_ADDRESS': return { ...state, address: action.payload }; default : return state; } }; const reducer = combineReducers({ stu:stuReducer, school:schoolReducer }); const store = createStore(reducer); const App = () => { const stu = useSelector(state => state.stu); const school = useSelector(state => state.school); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={() => { dispatch({type: 'SET_NAME', payload: '猪八戒'}); }}>改name </button> <button onClick={() => { dispatch({type: 'SET_AGE', payload: 28}); }}>改age </button> <button onClick={() => { dispatch({type: 'SET_GENDER', payload: '女'}); }}>改gender </button> <button onClick={() => { dispatch({type: 'SET_ADDRESS', payload: '高老庄'}); }}>改address </button> </div> <hr/> <p> {school.name} -- {school.address} </p> <div> <button onClick={() => { dispatch({type: 'SET_SCHOOL_NAME', payload: '高老庄小学'}); }}>改学校name </button> <button onClick={() => { dispatch({type: 'SET_SCHOOL_ADDRESS', payload: '高老庄中心大街15号'}); }}>改学校address </button> </div> </div>; }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div> <Provider store={store}> <App/> </Provider> </div>
11. Redux Toolkit(RTK)
上边的案例我们一直在使用Redux核心库来使用Redux,除了Redux核心库外Redux还为我们提供了一种使用Redux的方式——Redux Toolkit。它的名字起的非常直白,Redux工具包,简称RTK。RTK可以帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作。
在React中使用RTK
安装,无论是RTK还是Redux,在React中使用时react-redux都是必不可少,所以使用RTK依然需要安装两个包:react-redux和@reduxjs/toolkit。
npm
npm install react-redux @reduxjs/toolkit -S
yarn
yarn add react-redux @reduxjs/toolkit
修改上边的例子
使用RTK时,reducer依然可以使用之前的创建方式不变,但是不在需要合并reducer。RTK为我们提供了一个configureStore方法,它直接接收一个对象作为参数,可以将reducer的相关配置直接通过该对象传递,而不再需要单独合并reducer。
上例中代码:
const reducer = combineReducers({ stu:stuReducer, school:schoolReducer }); const store = createStore(reducer);
修改为:
const store = configureStore({ reducer:{ stu:stuReducer, school:schoolReducer } });
configureStore需要一个对象作为参数,在这个对象中可以通过不同的属性来对store进行设置,比如:reducer属性用来设置store中关联到的reducer,preloadedState用来指定state的初始值等,还有一些值我们会放到后边讲解。
reducer属性可以直接传递一个reducer,也可以传递一个对象作为值。如果只传递一个reducer,则意味着store中只有一个reducer。若传递一个对象作为参数,对象的每个属性都可以执行一个reducer,在方法内部它会自动对这些reducer进行合并。
RTK的API
CreateAction(一般不直接用)
action是reducer中的第二个参数,当我们通过dispatch向reducer发送指令时需要手动创建action对象并传递。action中常见的属性有两个一个是type用来指定操作的类型,一个是payload用来指定要传递的数据。
RTK为我们提供了一个方法createAction,用来帮助我们创建action。
createAction(type, prepareAction?)
它的第一个参数为type,用来指定action中的type属性。第二个参数可选先忽略它。它的返回值是一个函数。我们可以这么调用:
conconst setName= createAction('ADD'); setName(); // {type: 'ADD', payload: undefined} setName('猪八戒'); // {type: 'ADD', payload: '猪八戒'}
返回值的函数我们可以调用,调用该函数后会得到一个对象,这个对象有两个属性type和payload,type属性值就是我们调用createAction传递的第一个参数,上例中type就是’ADD’。而payload属性就是我们调用该函数时传递的参数。
const add = createAction('SET_NAME'); add(); // {type: 'SET_NAME', payload: undefined} add('猪八戒'); // {type: 'SET_NAME', payload: '猪八戒'}
简单说,createAction会返回一个函数,这个函数可以用来创建固定type属性值的对象,并且这个函数的第一个参数会成为新建对象的payload属性值。
可以通过creatAction修改之前的项目:
先创建四个action函数
const setName = createAction('SET_NAME'); const setAge = createAction('SET_AGE'); const setAddress = createAction('SET_ADDRESS'); const setGender = createAction('SET_GENDER');
修改dispatch
dispatch(setName('猪八戒')); dispatch(setAge(28)); dispatch(setGender('女')); dispatch(setAddress('高老庄'));
createAction返回函数所创建的对象结构是固定的
{type:'xxx', payload:...}
,我们也可以通过向createAction传递第二个参数来指定payload的格式:const add = createAction('ADD', (name, age, gender, address) => { return { payload:{ name, age, gender, address } } }); add('沙和尚', 38, '男', '流沙河'); // {"type":"ADD","payload":{"name":"沙和尚","age":38,"gender":"男","address":"流沙河"}}
CreateReucer(一般不用)
该方法用来是创建reducer的工具方法。
createReducer(initialState, builderCallback)
参数:
initialState
—— state的初始值
builderCallback
—— 带有builer的回调函数,可以同builer来设置reducer的逻辑回调函数中会传递一个builder作为参数,通过通过builder可以将action和函数进行绑定,使用时可以通过传递指定的action来触发函数的调用。
builder有一个常用的方法addCase,addCase需要两个参数,第一个参数为action,第二个参数为回调函数。action直接传递通过createAction所创建的函数即可,第二个参数是一个回调函数,回调函数类似于reducer,第一个参数为state,第二个参数为action。但又和reducer不同,该回调函数中返回的state是一个代理对象,可以直接对该对象修改,RTK会自动完成其余操作。
示例:
// 创建action const setName = createAction('setName'); // 创建reducer const stuReducer = createReducer({ name: '孙悟空', age: 18, gender: '男', address: '花果山' }, builder => { // 通过builder将action和回调函数进行绑定 builder.addCase(setName, (state, action) => { // 这里的state是代理对象,可以直接对其进行修改 state.name = action.payload; }); } ); // 配置reducer const store = configureStore({ reducer: { stu: stuReducer, school: schoolReducer } }); // 发送指令修改name属性 dispatch(setName('猪八戒'));
无论是createAction和createReducer都不是RTK中的常用方式(要是这么写代码,可能得疯)。介绍他们只是希望你能了解一下RTK的运行方式。对于我们来创建reducer时最最常用的方式是:createSlice。
CreateSlice
createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。
createSlice(configuration object)
配置对象中的属性:
initialState
—— state的初始值
name
—— reducer的名字,会作为action中type属性的前缀,不要重复
reducers
—— reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象。示例:
const stuSlice= createSlice({ name:'stu', initialState:{ name: '孙悟空', age: 18, gender: '男', address: '花果山' }, reducers:{ setName(state, action){ state.name = action.payload } } });
createSlice返回的并不是一个reducer对象而是一个slice对象(切片对象)。这个对象中我们需要使用的属性现在有两个一个叫做actions,一个叫做reducer。
Actions
切片对象会根据我们对象中的reducers方法来自动创建action对象,这些action对象会存储到切片对象actions属性中:
stuSlice.actions; // {setName: ƒ}上例中,我们仅仅指定一个reducer,所以actions中只有一个方法setName,可以通过解构赋值获取到切片中的action。
const {setName} = stuSlice.actions;
开发中可以将这些取出的action对象作为组件向外部导出,导出其他组件就可以直接导入这些action,然后即可通过action来触发reducer。
Reducer
切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:
const store = configureStore({ reducer: { stu: stuSlice.reducer, school: schoolReducer } });
总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {configureStore, createSlice} from "@reduxjs/toolkit"; const stuSlice = createSlice({ name: 'stu', initialState: { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, reducers: { setName(state, action) { state.name = action.payload; }, setAge(state, action) { state.age = action.payload; }, setGender(state, action) { state.gender = action.payload; }, setAddress(state, action) { state.gender = action.payload; } } }); const {setName, setAge, setGender, setAddress} = stuSlice.actions; const schoolSlice = createSlice({ name: 'school', initialState: { name: '花果山一小', address: '花果山大街1号' }, reducers: { setSchoolName(state, action) { state.name = action.payload; }, setSchoolAddress(state, action) { state.address = action.payload; } } }); const {setSchoolName, setSchoolAddress} = schoolSlice.actions; const store = configureStore({ reducer: { stu: stuSlice.reducer, school: schoolSlice.reducer } }); const App = () => { const stu = useSelector(state => state.stu); const school = useSelector(state => state.school); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={() => { dispatch(setName('猪八戒')); }}>改name </button> <button onClick={() => { dispatch(setAge(28)); }}>改age </button> <button onClick={() => { dispatch(setGender('女')); }}>改gender </button> <button onClick={() => { dispatch(setAddress('高老庄')); }}>改address </button> </div> <hr/> <p> {school.name} -- {school.address} </p> <div> <button onClick={() => { dispatch(setSchoolName('高老庄中心小学')); }}>改学校name </button> <button onClick={() => { dispatch(setSchoolAddress('高老庄中心大街15号')); }}>改学校address </button> </div> </div>; }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div> <Provider store={store}> <App/> </Provider> </div> );
目前我们的代码都是编写在同一个文件中的,真实开发中还需要对代码进行拆分,不写了你们自己试一试吧,具体内容看视频。
自己尝试:
使用RTK来构建store
在store/index.js中:
自己写的使用RTK来 构建store import { configureStore, createSlice } from "@reduxjs/toolkit"; // /** * createSlice 创建 reducer 切片 ,要求传一个配置对象 * * 它需要一个配置对象作为参数,通过对象的不同属性来指定它的配置 * * */ const stuSlice = createSlice({ name: "stu", // 用来自动生成action中的type属性 initialState: { // 当前切片state的初始值 name: "孙悟空", age: 18, gender: "男", address: "花果山" }, reducers: { // 指定state的各种操作 setName(state, action) { /** * 可以通过不同的方法来指定对state的不同操作 * 两个参数: state 这个state 是一个代理对象,可以直接修改 * state.name = "猪八戒" * * */ state.name = "猪八戒" }, setAge(state, action) { state.age = 20 } } }) // 切片对象会自动的帮助我们生成 action , stuSlice内部会自动帮助我们生成actions , console.log(stuSlice.actions); // actions中存储的是slice自动生成的action创建器(函数),调用函数后会自动创建action对象 // action对象的结构 {type:name/函数名, payload} -> {type:stu/setName, payload} export const { setName, setAge } = stuSlice.actions /* const nameSet = setName('哈哈') const ageSet = setAge(30) console.log(nameSet); // {type: 'stu/setName', payload: '哈哈'} console.log(ageSet); // {type: 'stu/setAge', payload: 30} */ // 创建store 用来创建store对象, 需要一个配置对象作为参数 const store = configureStore({ /** * 1. 单个直接给值 // reducer: stuSlice.reducer * 2. 多个要写成对象 * reducer: { student: stuSlice.reducer } * * */ reducer: { student: stuSlice.reducer } }) export default store
在入口文件 index.jszhong :
使用 react-redux 中提供的 Provider, 使用 Provider包裹App 并书写 store={store}
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { Provider } from "react-redux"; import store from './store'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> );
再需要使用的store的组件中:这里是App组件
- 使用useSekector 来加载state中的数据
- 通过useDispatch() 来获取派发器对象
- 需要引入 action 里提供的方法 ,获取action构建器
import { useDispatch, useSelector } from "react-redux"; import { setName, setAge } from "./store"; function App() { // useSelector() 用来加载state中的数据 const student = useSelector((state) => { return state.student }) // 通过useDispatch() 来获取派发器对象 const dispatch = useDispatch() // 获取action构建器 const editName = () => { dispatch(setName("哈哈")) } const editAge = () => { console.log(dispatch(setAge(33))); } return ( <div className="App"> <p>{student.name}</p> <p>{student.age}</p> <p>{student.gender}</p> <p>{student.address}</p> <button onClick={editName}>修改name</button> <button onClick={editAge}>修改age</button> </div> ); } export default App;
拆分RTK
创建学生和学校的切片
import { createSlice } from "@reduxjs/toolkit"; const stuSlice = createSlice({ name: "stu", initialState: { name: "孙悟空", age: 18, gender: "男", address: "花果山" }, reducers: { setName(state, action) { state.name = action.payload }, setAge(state, action) { state.age = action.payload } } }) export const { setName: stuSetName, setAge: stuSetAge } = stuSlice.actions export const { reducer: stuReducer } = stuSlice import { createSlice } from "@reduxjs/toolkit"; // 创建学校的slice const schoolSlice = createSlice({ name: 'school', initialState: { name: "花果山一小", address: "花果山28号" }, reducers: { setName(state, action) { state.name = action.payload }, setAddress(state, action) { state.address = action.payload } } }) export const { setName: schoolSetName, setAddress: schoolSetAddress } = schoolSlice.actions export const { reducer: schoolReducer } = schoolSlice
在index中引入
import { configureStore } from "@reduxjs/toolkit"; import { stuReducer } from "./stuSlice"; import { schoolReducer } from "./schoolSlice"; // 创建store 用来创建store对象, 需要一个配置对象作为参数 const store = configureStore({ reducer: { student: stuReducer, school: schoolReducer } }) export default store
在需要使用的组件中引入: 这里以App。js为例
import { useDispatch, useSelector } from "react-redux"; // 导入action 构造器 import { stuSetName, stuSetAge } from "./store/stuSlice"; import { schoolSetName, schoolSetAddress } from "./store/schoolSlice" function App() { // useSelector() 用来加载state中的数据 const student = useSelector((state) => { return state.student }) const school = useSelector((state) => { return state.school }) // 通过useDispatch() 来获取派发器对象 const dispatch = useDispatch() // 获取action构建器 // 修改学生 const editName = () => { dispatch(stuSetName("哈哈")) } const editAge = () => { dispatch(stuSetAge(33)) } // 修改学校 const editSchoolName = () => { dispatch(schoolSetName("水帘洞")) } const editSchoolAddress = () => { dispatch(schoolSetAddress("水帘洞33号")) } return ( <div className="App"> <p>{student.name}</p> <p>{student.age}</p> <p>{student.gender}</p> <p>{student.address}</p> <button onClick={editName}>修改学生name</button> <button onClick={editAge}>修改修改age</button> <hr></hr> <p>{school.name}</p> <p>{school.address}</p> <button onClick={editSchoolName}>修改 学校 name</button> <button onClick={editSchoolAddress}>修改 学校 地址</button> </div> ); } export default App;
12.RTKQ
RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。
Web应用中加载数据时需要处理的问题:
- 根据不同的加载状态显示不同UI组件
- 减少对相同数据重复发送请求
- 使用乐观更新,提升用户体验
- 在用户与UI交互时,管理缓存的生命周期
这些问题,RTKQ都可以帮助我们处理。首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。
使用
RTKQ已经集成在了RTK中,如果我们已经在项目中引入了RTK则无需再引入其余的模块。如果你不想使用RTKQ给我们提供的发送请求的方式(简单封装过的fetch),你还需要引入一下你要使用的发送请求的工具。
创建Api切片
RTKQ中将一组相关功能统一封装到一个Api对象中,比如:都是学生相关操作统一封装到StudentApi中,关于班级的相关操作封装到ClassApi中。接下来,我们尝试创建一个简单的Api,至于数据还是我们之前所熟悉的学生数据:
studentApi.js
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react"; export const studentApi = createApi({ reducerPath:'studentApi', baseQuery:fetchBaseQuery({ baseUrl:'http://localhost:1337/api/' }), endpoints(build) { return { getStudents: build.query({ query() { return 'students' } }), } } }); export const {useGetStudentsQuery} = studentApi;
上例是一个比较简单的Api对象的例子,我们来分析一下,首先我们需要调用
createApi()
来创建Api对象。这个方法在RTK中存在两个版本,一个位于@reduxjs/toolkit/dist/query
下,一个位于@reduxjs/toolkit/dist/query/react
下。react目录下的版本会自动生成一个钩子,方便我们使用Api。如果不要钩子,可以引入query下的版本,当然我不建议你这么做。
createApi()
需要一个配置对象作为参数,配置对象中的属性繁多,我们暂时介绍案例中用到的属性:reducerPath
用来设置reducer的唯一标识,主要用来在创建store时指定action的type属性,如果不指定默认为api。
baseQuery
用来设置发送请求的工具,就是你是用什么发请求,RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装,很方便,如果你不喜欢可以改用其他工具,这里暂时不做讨论。
fetchBaseQuery
简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。配置对象中其他属性暂不讨论。
endpoints
Api对象封装了一类功能,比如学生的增删改查,我们会统一封装到一个对象中。一类功能中的每一个具体功能我们可以称它是一个端点。endpoints用来对请求中的端点进行配置。
endpoints是一个回调函数,可以用普通方法的形式指定,也可以用箭头函数。回调函数中会收到一个build对象,使用build对象对点进行映射。回调函数的返回值是一个对象,Api对象中的所有端点都要在该对象中进行配置。
对象中属性名就是要实现的功能名,比如获取所有学生可以命名为getStudents,根据id获取学生可以命名为getStudentById。属性值要通过build对象创建,分两种情况:
查询:
build.query({})
增删改:
build.mutation({})
例如:
getStudents: build.query({ query() { return 'students' } }),
先说query,query也需要一个配置对象作为参数(又他喵的是配置对象)。配置对象里同样有n多个属性,现在直说一个,query方法。注意不要搞混两个query,一个是build的query方法,一个是query方法配置对象中的属性,这个方法需要返回一个子路径,这个子路径将会和baseUrl拼接为一个完整的请求路径。比如:getStudets的最终请求地址是:
http://localhost:1337/api/
+students
=http://localhost:1337/api/students
可算是介绍完了,但是注意了这个只是最基本的配置。RTKQ功能非常强大,但是配置也比较麻烦。不过,熟了就好了。
上例中,我们创建一个Api对象studentApi,并且在对象中定义了一个getStudents方法用来查询所有的学生信息。如果我们使用react下的createApi,则其创建的Api对象中会自动生成钩子函数,钩子函数名字为useXxxQuery或useXxxMutation,中间的Xxx就是方法名,查询方法的后缀为Query,修改方法的后缀为Mutation。所以上例中,Api对象中会自动生成一个名为useGetStudentsQuery的钩子,我们可以获取并将钩子向外部暴露。
export const {useGetStudentsQuery} = studentApi;
创建Store对象
Api对象的使用有两种方式,一种是直接使用,一种是作为store中的一个reducer使用。store是我们比较熟悉的,所以先从store入手。
import {configureStore} from "@reduxjs/toolkit"; import {studentApi} from "./studentApi"; export const store = configureStore({ reducer:{ [studentApi.reducerPath]:studentApi.reducer }, middleware:getDefaultMiddleware => getDefaultMiddleware().concat(studentApi.middleware), });
创建store并没有什么特别,只是注意需要添加一个中间件,这个中间件已自动生成了我们直接引入即可,中间件用来处理Api的缓存。
store创建完毕同样要设置Provider标签,这里不再展示。接下来,我们来看看如果通过studentApi发送请求。由于我们已经将studentApi中的钩子函数向外部导出了,所以我们只需通过钩子函数即可自动加载到所有的学生信息。比如,现在在App.js中加载信息可以这样编写代码:
import React from 'react'; import {useGetStudentsQuery} from './store/studentApi'; const App = () => { const {data, isFetching, isSuccess} = useGetStudentsQuery(); return ( <div> {isFetching && <p>数据正在加载...</p>} {isSuccess && data.data.map(item => <p key={item.id}> {item.attributes.name} -- {item.attributes.age} -- {item.attributes.gender} -- {item.attributes.address} </p>)} </div> ); }; export default App;
直接调用useGetStudentsQuery()它会自动向服务器发送请求加载数据,并返回一个对象。这个对象中包括了很多属性:
- data – 最新返回的数据
- currentData – 当前的数据
- error – 错误信息
- isUninitialized – 如果为true则表示查询还没开始
- isLoading – 为true时,表示请求正在第一次加载
- isFetching 为true时,表示请求正在加载
- isSuccess 为true时,表示请求发送成功
- isError 为true时,表示请求有错误
- refetch 函数,用来重新加载数据
使用中可以根据需要,选择要获取到的属性值。写了这么多,也只写了一个Hello World。但是,良好的开端是成功的一半,这个理解了,后边的东西也就简单了!
自己尝试:
表格增删改查
student.js :
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react" import qs from 'qs' * 创建API对象 * * createApi() 用来创建RTKQ中的API对象 * RTKQ的所有功能都需要通过对象来机芯 * createApi() 需要一个对象作为参数 * const studentApi = createApi({ reducerPath: "studentApi", // API的标识,不能和其他的API 或者 reducer 重复 baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:1337/api/", }), // 指定查询的基础信息,发送请求使用的工具 tagTypes: ['student'], // 用来指定API的标签类型 endpoints(build) { /** * build 是请求的构建器,通过build来设置请求的相关信息 * * */ return { getStudents: build.query({ query(id) { // 查询接口地址是 http://localhost:1337/api/data // 请求的子路径 return 'data' }, // 用来转换响应式数据 transformResponse(baseQueryReturnValue) { // console.log(baseQueryReturnValue.data); return baseQueryReturnValue // return baseQueryReturnValue.data 可以返回其中的data数据 }, // 设置数据缓存的时间 keepUnusedDataFor: 60, // 设置数据缓存的时间,单位秒 默认60s providesTags: ( (result, error, id) => { return [{ type: 'student', id: 'LIST' }] } ) }), // 查询使用 query() // getStudentID:build.query(), // updateStudent:build.mutaion() 删除 添加 修改 都使用 mutaion() getStudentId: build.query({ query(sId) { console.log(sId); return `data?${qs.stringify({ id: sId })}` } }), delStudent: build.mutation({ query(sId) { return { url: `del?${qs.stringify({ id: sId })}`, method: 'delete' } }, }), addStudent: build.mutation({ query(obj) { return { url: 'add', method: 'post', body: obj // 默认会转JSON } }, invalidatesTags: [{ type: 'student', id: 'LIST' }] }), updateStudent: build.mutation({ query(obj) { return { url: 'edit', method: 'post', body: obj } }, invalidatesTags: ((obj) => { return [{ type: 'student', id: obj.id }] }) }), } },// endpoints 用来指定API中的各种功能,是一个方法 }) // API对象创建后, 对象中会根据各种方法自动生成对应的钩子函数 // 通过钩子函数可以向服务器发送请求 // 钩子函数的命名规则 getStudents --> useGetStudentsQuery export const { useGetStudentsQuery, useGetStudentIdQuery, useDelStudentMutation, useAddStudentMutation, useUpdateStudentMutation } = studentApi export default studentApi
index:.js
import { configureStore } from "@reduxjs/toolkit"; import { setupListeners } from "@reduxjs/toolkit/dist/query"; import studentApi from "./studentApi" const store = configureStore({ reducer: { [studentApi.reducerPath]: studentApi.reducer }, middleware: getDefaultMiddleware => { return getDefaultMiddleware().concat(studentApi.middleware) } }) setupListeners(store.dispatch) 设置以后,将会支持 refetchOnFocus refetchOnReconnect export default store
App.js
import React, { useState, useEffect } from 'react' import { useGetStudentsQuery } from "./store/studentApi" import Table from "./components/Table" function App() { const [arr, setArr] = useState([]) 调用API查询数据 这个钩子函数它会返回一个对象作为返回值,请求过程中相关数据都在该对象中存储 const { isSuccess, data } = useGetStudentsQuery() useEffect(() => { if (isSuccess) setArr(data) }, [data]) useGetStudentsQuery(null, {}) * useQuery可以接收一个对象作为第二个参数,通过该对象可以对请求进行配置 * selectFromResult: result => { * if (result.data){ * result.data = result.data.filter(item => item.attributes.age < 18) * } * return result; * } // 用来指定useQuery返回的结果 * * pollingInterval: 0 // 设置轮询的间隔,单位是毫秒,如果为0 表示不轮询 * skip:false // 设置是否跳过当前请求, 默认为false * refetchOnMountOrArgChange:false // 设置是否每次都重新加载数据 ,false正常使用缓存 ,true 每次都重载数据,数字 数据缓存的时间(秒) * refetchOnFocus:false // 是否在重新获取焦点时重载数据 需要在入口进行设置 * refetchOnReconnect:false // 是否在重新连接后重载数据 return ( <div className="App"> {isSuccess && <Table setArr={setArr} stuData={arr}></Table>} </div> ); } export default App;
13. React-Router
React Router最新版本为6,版本6和版本5之间的变化跨度比较大,讲解两个版本。
版本5
安装:
npm npm install react-router-dom@5 -S yarn yarn add react-router-dom@5
react-router使用步骤
引入react-router-dom 包
在index.js中引入BrowserRouter组件
将BrowserRouter设置为根组件
react-router 可以将url地址和组件进行映射,当用户访问某个地址时,与其对应的组件会自动挂载
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { BrowserRouter as Router } from 'react-router-dom'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Router> <App /> </Router> );
地址和组件建立映射:
import { Route } from "react-router-dom"; import Home from './components/Home' import About from './components/About' function App() { return ( <div > * 将路由和组件进行映射 * 是有Route 来映射地址和组件 * 属性: * 1. path 映射的url地址; * 2. component 映射的组件 * 3. exact 路径是完整匹配,默认值为false ( 不允许访问子路径 ) * * 当Route的路径被访问时,其对应组件就会自动挂载 * 注意: 默认情况下Route并不是严格匹配 * 只要url地址的头部和path一致,组件就会挂载,不会检查子路径 <Route exact path="/" component={Home}></Route > <Route exact path="/about" component={About}></Route > </div> ); } export default App;
Link 和 NavLink
* 在使用react-router时,一定不要使用 a标签来创建超链接
* 因为a标签创建的超链接,会自动向服务器发送请求重新加载页面
* 而我们不希望这种情况发生
*
* 可以使用Link组件来创建超链接
* NavLink和Link作用相似,只是可以指定链接激活后的样式
menu.js :
import React from 'react' import { Link, NavLink } from 'react-router-dom' import classes from './Menu.module.css' export default function Menu() { return ( <div> <ul> {/* <li><a href='/'>主页</a></li> <li><a href='/about'>关于</a></li> */} {/* <Link to="/">主页</Link> <Link to="/about">关于</Link> */} <NavLink exact // activeClassName={classes.active} activeStyle={{ textDecoration: "underline" }} to="/">主页</NavLink> <NavLink exact // activeClassName={classes.active} activeStyle={{ textDecoration: "underline" }} to="/about">关于</NavLink> </ul> </div> ) }
Menu.module.css:
a:link, a:visited { color: burlywood; text-decoration: none; } a:hover { color: black; } a:active { text-decoration: underline; }
两种Router