React入门(B站李立超老师)

视频地址:https://www.bilibili.com/video/BV1bS4y1b7NV/
课程第一部分代码: https://pan.baidu.com/s/16hEN7j4hLDpd7NoFiS8dHw?pwd=4gxv 提取码: 4gxv
课程第二部分代码:https://pan.baidu.com/s/1mDkvLqYVz1QGTV1foz5mQg?pwd=5zir 提取码:5zir
课程第三部分代码:https://pan.baidu.com/s/1JBaN4gSST_yXgHoIPfF5uw?pwd=ws8t 提取码:ws8t

前言(三大框架的诞生)

网页是B/S架构中最重要的信息载体,用户看到的所有信息都需要在网页中呈现。像商品信息、用户信息、新闻列表等一系列的内容都需要通过网页的形式呈现出来。在传统的网站中用户每点一次链接就会加载出一个新的页面。比如用户在网站的主页中看到了一个新闻的标题,点击标题网站会跳转到一个新的页面来展示新闻的具体内容。这样就导致呈现新闻的页面是从服务器中新加载出来的,新闻和首页是两个完全独立的页面。如果本来两个页面就没有什么太大的关系这么处理当然是没有问题的,但有些使用场景中却不能这样。

在有些场景中,用户在网页中发生了一些行为,比如登录、加购物车、添加删除等操作时,这些操作的确会使网页发生变化,但这些变化往往非常细微,比如购物车图标的产品数量发生了变化,登录按钮变成了用户头像,网页中增加或减少了一条数据等。如果仅仅因为这些小的变化就刷新了整个网页是有些划不来的。刷新整个网页一来会增加用户的等待时间,二来也会增加服务器的压力。于是我们就有了局部刷新网页的技术。

所谓的局部刷新指的是,当用户和网站发生交互时,我们不再是简单的直接通过浏览器进行页面的跳转,而是通过JS中的AJAX直接通过JS向后台服务器发送请求,请求过程用户毫无感知。响应数据会通过回调函数返回给JS,而不是直接返回给用户。JS中收到响应数据后,在根据不同的结果通过DOM来完成对页面的修改。它的优点就是,请求响应过程是异步的,用户是无感的,不会影响用户的其他操作。同时,通过DOM对页面刷新时只需刷新部分页面无需整体刷新,提高了访问速度。在服务器端,服务器只需提供数据接口,无需考虑页面的渲染,在降低服务器复杂度的同时也使得服务器压力降低提高了处理请求的速度。

AJAX + DOM使得局部刷新成为了可能,但一切似乎并不是那么的完美。发送请求加载数据问题不大,但数据一旦加载过来问题就出现了。数据并不能直接在网页中显示。我们需要通过DOM将数据转换为网页的中的各种节点,这就意味着我们必须反复的操作DOM,这其中的痛苦实在是一言难尽。一来DOM操作本身十分占用系统资源一不小心就会出现卡顿。二来DOM的API十分繁复,使得各种操作并不十分的优雅。换句话说,服务器的复杂度降低了,但是前端的复杂度提高了。

于是在前端开发中就急需一个框架来帮助我们解决这个问题,使我们可以比较轻松的根据不同的数据来快速构建用户界面(UI),与此同时还要在构建过程中确保其流畅度(不能出现卡顿)。于是React、Angular、Vue这些前端框架应运而生。

React简介

React 是一个用于构建用户界面的 JavaScript 库,用来为现代的网络构建用户界面。React起源于Facebook,由Facebook的软件工程师 Jordan Walke 开发,2012年部署于 Instagram,2013年开源。除此之外,React还有React Native框架,通过它让我们可以直接使用 JavaScript 来编写原生应用。

React的特点:

  • 虚拟DOM
    -【相比较原生DOM,虚拟DOM的优点:1.api简单易学 2.解决了浏览器兼容性问题 3.实际操作上性能更好】
  • 声明式
  • 基于组件
  • 支持服务器端渲染
  • 快速、简单、易学

HelloWorld

React的常规开发方式并不是通过浏览器引入外部js脚本来使用,但在入门阶段我们暂且先使用这种方式来简单体会一下React。

使用React开发Web项目,我们需要引入两个js脚本:

react.development.js

  • react 是react核心库,只要使用react就必须要引入
  • 下载地址:https://unpkg.com/react@18.0.0/umd/react.development.js

react-dom.development.js

  • react-dom 是react的dom包,使用react开发web应用时必须引入
  • 下载地址:https://unpkg.com/react-dom@18.0.0/umd/react-dom.development.js
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Hello React</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
</head>
<body>
    <div id="root"></div>
    <script>
      const button = React.createElement('button', {
         onClick: (e) => {
            alert('你点了按钮!');
            e.stopPropagation();
         }
      }, '我是一个按钮');const box = React.createElement('div', {
          onClick: (e) => {
          alert('你点了div!');
           console.log(e);
       }
    }, '我是一个div', button);const root = ReactDOM.createRoot(document.querySelector('#root'));

    root.render(box);</script>
</body>
</html>

三个API
在这里插入图片描述

JSX(JavaScript Syntax Extension)

JSX 是 JavaScript 的语法扩展,JSX 使得我们可以以类似于 HTML 的形式去使用 JS。JSX便是React中声明式编程的体现方式。声明式编程,简单理解就是以结果为导向的编程。使用JSX将我们所期望的网页结构编写出来,然后React再根据JSX自动生成JS代码。所以我们所编写的JSX代码,最终都会转换为以调用React.createElement()创建元素的代码。

const element = <h1>Hello, world!</h1>; // React.createElement('h1', {}, 'Hello, world!')
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(element);
function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(element);

注意事项:

  1. 不要加引号
  2. 有且只有一个根标签
  3. html标签小写开头,React组件大写开头
  4. 可以使用{}插入JS表达式。(表达式:有返回值的语句。JSX也是表达式)
  5. 属性正常写(class使用className,style必须用{})
  6. 标签必须正常闭合
  7. 布尔类型、Null 以及 Undefined 将会忽略

由于JSX最终需要转换为JS代码执行,所以浏览器并不能正常识别JSX,所以当我们在浏览器中直接使用JSX时,还必须引入babel来完成对代码的编译

示例:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Hello React</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>
<div id="app"></div><script type="text/babel">
    const h1 = <h1>Hello React</h1>;
    ReactDOM.render(h1, document.querySelector('#app'));
</script>
</body>
</html>

babel下载地址:https://unpkg.com/babel-standalone@6/babel.min.js

在JSX中可以直接通过 {} 来嵌入 JS表达式,所谓的表达式指的是有返回值的语句。比如可以直接在JSX中嵌入值或变量像这样:

const name = '孙悟空';
const ele = <h1>Hello {name}</h1>;
const ele2 = <h1>Hello {孙悟空}</h1>;
const ele3 = <h1>1 + 1 = {1 + 1}</h1>;

{} 中也可以调用函数:

function fn(){
    return '哈哈';
}const ele = <h1>{fn()}</h1>

没有返回值的语句,像if、for等语句是不能出现在JSX中的!但是在if、for等里边写JSX是可以的:

let ele;
let isLogin = true;
if(isLogin){
    ele = <h1>欢迎光临!</h1>;
}else{
    ele = <h1>请登录!</h1>
}
let eles = [];for(let i=0; i<5; i++){
    eles.push(<h2>我是第{i}个h2</h2>);
}

渲染列表

现在,有这样一组数据,需要在页面中呈现:

const students = ['孙悟空', '猪八戒', '沙和尚'];

如果想直接显示,可以直接将数组插入到JSX中,JSX会自动对数组展开并显示。但这样一来数据知识直接显示,不会添加任何的结构。如果希望将数组中的元素放到一个无序列表中显示,就需要对列表做一些处理了。

<ul>
    <li>孙悟空</li>
    <li>猪八戒</li>
    <li>沙和尚</li>
</ul>

可以使用for循环对其进行处理:

const students = ['孙悟空', '猪八戒', '沙和尚'];
const items = [];for(let i=0; i<students.length; i++){
    items.push(<li>{students[i]}</li>);
}const ele = <ul>{items}</ul>

上边操作的本质其实就是根据一个旧数组students生成了一个新数组items:

const students = ['孙悟空', '猪八戒', '沙和尚'];
const items = [
    <li>孙悟空</li>,
    <li>猪八戒</li>,
    <li>沙和尚</li>,
];

这个操作我们也可以通过map()方法来完成:

const students = ['孙悟空', '猪八戒', '沙和尚'];
const items = students.map(item => <li>{item}</li>);
const ele = <ul>{items}</ul>

map()本身就是一个函数,所以我们完全可以直接将函数调用写在JSX中间:

const students = ['孙悟空', '猪八戒', '沙和尚'];
const ele = <ul>{students.map(item => <li>{item}</li>)}</ul>

很显然使用map()比使用for循环要优雅很多,所以这也是我们在React中的常用方式。

警告

输出一个列表时,无论我们使用的for循环还是map()方法,当你打开控制台时你一定会看到一行红色的内容,它大概内容是:Warning: Each child in a list should have a unique “key” prop.

Warning表示一个警告,警告表示它并不是一个特别严重的错误,但你最好把它处理了,所以当你在React项目中看到这个玩意的时候一定一定要想办法去除掉它。

为什么会报出这个警告呢?事情要往前回溯一下,我们已经知道React是通过虚拟DOM来操作元素的,而React为了提升操作的性能,在DOM发生变化时只会去修改那些发生了变化的元素,这样就大大的减少了DOM操作从而提升了页面渲染的速度。

React怎么知道哪些元素发生变化呢?其实也不难,React每次渲染都会生成一个由React元素构成的树(当然这棵树也对应着一课DOM元素构成的树,但是这里不太重要),React每次重新渲染都会生成一个新的React元素树,在页面刷新前,React会通过内部的diff算法对两个树中的React元素进行比较,并且找到那些发生变化的元素,并将他们的变化在真实的DOM元素上体现出来。

这个算法听上去很神奇,其实很简单就是按照顺序比较两个元素,第一个和第一个比,第二个和第二个比,没有变化就放过,有变化就记录。

扯了这么一长串,和上边的警告有什么关系吗?上边的例子中,我们把一个数组直接在JSX中呈现了出来。当我们第一次渲染时,列表可能是这样的:

<ul>
    <li>孙悟空</li>
    <li>猪八戒</li>
    <li>沙和尚</li>
</ul>

假使数组发生了变化(这里还没讲怎么变,想象一下吧!),页面需要重新渲染,变成了这样:

<ul>
    <li>孙悟空</li>
    <li>猪八戒</li>
    <li>沙和尚</li>
    <li>唐僧</li>
</ul>

这里我们在列表的最后增加了一个唐僧,那么前边的元素顺序是不变的,孙悟空第一个,猪八戒第二,沙和尚第三。所以这里在应用修改时只会修改最后一个li,对前边的三个不会有任何影响!性能非常棒!

但是,加入唐僧不想在最后,它想去第一个位置,像这样:

<ul>
    <li>唐僧</li>
    <li>孙悟空</li>
    <li>猪八戒</li>
    <li>沙和尚</li>
</ul>

这时问题就来了,唐僧第一,孙悟空第二…换句话说,所有元素的顺序都变了,那么React在比较元素不同时,它并不能知道孙悟空从第一变到了第二个,它会任务是唐僧替换了孙悟空,孙悟空替换了猪八戒,猪八戒替换了沙和尚,然后又增加了一个沙和尚,导致页面渲染是四个li都被重新渲染了!如果列表里有100个元素,那么就是100个都得重新渲染,性能不好!

那么这种情况,我们就需要让React知道谁是孙悟空,谁又是唐僧。在React内部就设定了一个key属性,key属性可以作为React元素的唯一标识,和html中id类似。在创建一个列表时,我们可以为列表的每一个元素指定一个唯一的key,React就可以根据key而不是位置来比较元素,这样一来无论元素的位置如何改变,都不会导致过多的元素渲染,因为有了key以后,React就不再通过位置来比较两个元素了

上边的例子可以这样修改:

const students = ['孙悟空', '猪八戒', '沙和尚'];
const ele = <ul>{students.map(item => <li key={item}>{item}</li>)}</ul>

添加了key属性以后警告就消失了,设置key还有一些要求:

  1. key必须在当前列表的元素中是唯一的
  2. 一个元素的key最好是固定的

key不需要全局唯一,只需在当前列表中唯一即可。元素的key最好是固定的,这里直接举个反例,有些场景我们会使用元素的索引为key像这种:

const students = ['孙悟空', '猪八戒', '沙和尚'];
const ele = <ul>{students.map((item, index) => <li key={index}>{item}</li>)}</ul>

上例中,我使用了元素的索引(index)作为key来使用,但这有什么用吗?没用!因为index是根据元素位置的改变而改变的,当我们在前边插入一个新元素时,所有元素的顺序都会一起改变,那么它和React中按顺序比较有什么区别吗?没有区别!而且还麻烦了,唯一的作用就是去除了警告。所以我们开发的时候偶尔也会使用索引作为key,但前提是元素的顺序不会发生变化,除此之外不要用索引做key。

虚拟DOM

当我们通过 React 操作DOM时,比如通过 React.createElement() 创建元素时。我们所创建的元素并不是真正的DOM对象而是React元素。这一点可以通过在控制台中打印对象来查看。React元素是React应用的最小组成部分,通过JSX也就是React.createElement()所创建的元素都属于React元素。与浏览器的 DOM 元素不同,React 元素就是一个普通的JS对象,且创建的开销极小。

React元素不是DOM对象,那为什么可以被添加到页面中去呢?实际上每个React元素都会有一个对应的DOM元素,对React元素的所有操作,最终都会转换为对DOM元素操作,也就是所谓的虚拟DOM。要理解虚拟DOM,我们需要先了解它的作用。虚拟DOM就好像我们和真实DOM之间的一个桥梁。有了虚拟DOM,使得我们无需去操作真实的DOM元素,只需要对React元素进行操作,所有操作最终都会映射到真实的DOM元素上。

这不是有点多余吗?直接操作DOM不好吗?为什么要多此一举呢?原因其实很多,这里简单举几个出来。

首先,虚拟DOM简化了DOM操作。凡是用过DOM的都知道Web API到底有多复杂,各种方法,各种属性,数不胜数。查询的、修改的、删除的、添加的等等等等。然而在虚拟DOM将所有的操作都简化为了一种,那就是创建!React元素是不可变对象,一旦创建就不可更改。要修改元素的唯一方式就是创建一个新的元素去替换旧的元素,看起来虽然简单粗暴,实则却是简化了DOM的操作。

其次,解决DOM的兼容性问题。DOM的兼容性是一个历史悠久的问题,如果使用原生DOM,总有一些API会遇到兼容性的问题。使用虚拟DOM就完美的避开了这些问题,所有的操作都是在虚拟DOM上进行的,而虚拟DOM是没有兼容问题的,至于原生DOM是否兼容就不需要我们操心了,全都交给React吧!

最后,我们手动操作DOM时,由于无法完全掌握全局DOM情况,经常会出现不必要的DOM操作,比如,本来只需要修改一个子节点,但却不小心修改了父节点,导致所有的子节点都被修改。效果呈现上可能没有什么问题,但是性能上确实千差万别,修改一个节点和修改多个节点对于系统的消耗可是完全不同的。然而在虚拟DOM中,引入了diff算法,React元素在更新时会通过diff算法和之前的元素进行比较,然后只会对DOM做必要的更新来呈现结果。简单来说,就是拿新建的元素和旧的元素进行比较,只对发生变化的部分对DOM进行更新,减少DOM的操作,从而提升了性能。

创建React项目(手动)

项目结构

常规的React项目需要使用npm(或yarn)作为包管理器来对项目进行管理。并且React官方为了方便我们的开发,为我们提供react-scripts包。包中提供了项目开发中的大部分依赖,大大的简化了项目的开发。

开发步骤:
在这里插入图片描述
在这里插入图片描述

Create-React-App

上述创建React项目方式在有的人看来可能会有一些麻烦,并且在实际开发中它也不是大部分人所选择的方式。之所以在这里选择这种方式,只是我个人的一种喜好,我希望学生在入门阶段学习时,能够认识项目里的每一行代码,所以我采用了手动创建的项目的形式。

如果你不喜欢这种方式,可以自己提前预习一下create-react-app相关的内容,这也是常规的创建项目的方式。

练习1:学习记录器

通过前边课程的学习,我们已经可以使用npm去管理React项目了。接下来,我们开始尝试使用React来做一些小的练习,现在我们有如下一个设计图:
在这里插入图片描述
这个页面我称其为学习记录器,其主要作用就是记录我们每天在学习上所花费的时间。使用React开发Web项目和直接编写网页也没有太大的区别。无非就是结构、表现和行为。所以对于上述练习我们可以先尝试在index.js中定义它的结构,代码如下:
index.js

import ReactDOM from "react-dom/client";
import './index.css';

const App = <div className="logs">
    <div className="logs-item">
        <div className="logs-date">
            <div className="month">三月</div>
            <div className="day">22</div>
        </div>
        <div className="logs-item-desc">
            <h2>学习React</h2>
            <div className="logs-item-time">80分钟</div>
        </div>
    </div>
</div>;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(App);

你会发现,我们定义的结构存储到了一个名为App的变量中,然后通过root.render()将其渲染到了页面中,除此之外和编写html的结构没有任何区别。

在React中编写样式时,可以直接编写到css文件中,然后通过import直接引入,引入后Webpack会自动对代码进行打包。css代码如下:
index.css

*{
    box-sizing: border-box;
}

body{
    margin: 0;
    background-color: #dfdfdf;
}

.logs{
    width: 800px;
    background-color: #eae2b7;
    border-radius: 20px;
    margin: 20px auto;
    padding: 10px;
    box-shadow: 0 1px 8px rgba(0, 0, 0, .25);
    display: flow-root;
}

.logs-item{
    margin: 16px;
    background-color: #fcbf49;
    border-radius: 20px;
    box-shadow: 0 1px 8px rgba(0, 0, 0, .25);
    display: flex;
    align-items: center;
    padding: 10px;
    font-weight: bold;
}

.logs-date{
    width: 88px;
    border-radius: 20px;
    overflow: hidden;
    text-align: center;
}

.logs-date .month{
    height: 28px;
    line-height: 28px;
    background-color: #d62828;
    font-size: 20px;
    color: white;
}

.logs-date .day{
    background-color: #fff;
    height: 60px;
    line-height: 60px;
    font-size: 50px;
}

.logs-item-desc{
    flex: auto;
    text-align: center;
}

.logs-item-desc h2{
    margin: 10px 0;
    font-size: 20px;
    color: #003049;
}

.logs-item-time{
    color: #d62828;
}

编写这个页面时,不要求你的页面和图片里的一模一样,只要感觉对了即可,练习的主要目的还是熟练React的一些操作。

练习虽然写完了,但是你有没有发现什么问题呢?

React组件

在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。React中定义组件的方式有两种:基于函数的组件和基于类的组件。本节我们先看看基于函数的组件。

基于函数的组件其实就是一个会返回JSX(React元素)的普通的JS函数,你可以这样定义:

import ReactDOM from "react-dom/client";// 这就是一个组件
function App(){
    return <h1>我是一个React的组件!</h1>
}const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);

函数式组件主要有两个注意点:

  1. 函数名首字母大写
  2. 返回值是一个JSX(React元素)

为了使得项目结构更加的清晰,更易于维护,每个组件通常会存储到一个单独的文件中,比如上例中的App组件,可以存储到App.js中,并通过export导出。
在这里插入图片描述
在这里插入图片描述

引入样式

那么如何为React组件引入样式呢?很简单直接在组件中import即可。例如:我们打算为Button组件编写一组样式,并将其存储到Button.css中。我们只需要直接在Button.js中引入Button.css就能轻易完成样式的设置。
在这里插入图片描述

事件

在这里插入图片描述
React事件的属性值需要的是一个回调函数,函数会在触发时执行,所以上例中我写的clickHandler是没有加()的,如果加了()函数会在赋值时立刻执行,而赋值给onClick事件的将是函数的返回值undefined,这将导致事件的设置失效,这一点你可以自己试试看。

事件对象

在这里插入图片描述

Props

之前我们所定义的组件内容几乎都是固定的,组件创建的时候什么样,使用时就是什么样。但在开发时,我们往往需要的是一些动态显示的组件,换句话组件中所显示的内容必须是动态设置的。

在使用组件时,可以通过向组件传递参数的形式来向组件传递数据,这一点和JS中的函数非常相似。函数可以通过传递实参来决定函数的执行结果,组件也不例外。函数的参数如何传递我们是非常清楚的,那么组件的参数是怎么传递的呢?组件的参数需要通过属性传递,可以像这样向组件中传递参数:

<Button bgColor='red' color='white'>我是一个按钮</Button>

上边的案例中我们设置了两个属性,这些属性会被封装到一个对象中并作为参数传递给Button组件,只需要在Button组件中定义一个参数即可获取,通常这个参数我们会命名为props,像这样:

import './Button.css';
const Button = (props) => {
    return <button style={{backgroundColor:props.bgColor, color:props.color}}>{props.children}</button>;
};export default Button;

在组件内部可以通过props.xxx来访问外部传递进的属性,从而达到动态设置的目的。需要注意的是,标签体也可以设置为props的一个属性,叫做children,可以通过props.children来获取标签体的内容。

还有一点一定要记住,props中的属性是只读属性是无法修改的!

State

props中的所有属性都是不可变的,这使得React组件不能随着props的改变而改变。但在实际的开发中,我们更希望的是数据发生变化时,页面也会随着数据一起变化。React为我们提供了state用来解决这个问题。

state和props类似,都是一种存储属性的方式,但是不同点在于state只属于当前组件,其他组件无法访问。并且state是可变的,当其发生变化后组件会自动重新渲染,以使变化在页面中呈现。

现有如下组件Clock:

import React from 'react';const Clock = () => {let date = new Date().toLocaleTimeString();const clickHandler = ()=>{};return (
        <div>
            {date}
            <div>
                <button onClick={clickHandler}>刷新</button>
            </div>
        </div>
    );
};export default Clock;

现在我们希望点击按钮以后,时间可以刷新直接显示一个当前的最新日期。如果直接在clickHandler中修改date的值是无效的,像这样:

//省略const clickHandler = ()=>{
    date = new Date().toLocaleTimeString();
    console.log(date);
};//省略

state也可以被认为是一个变量,但是它的定义方式不太一样,我们以函数组件为例来介绍state的使用方式(类组件咱们后边再说)。在函数中使用state我们需要使用一种钩子(hook)函数。钩子函数可以在函数组件中“勾出”React的特性,换句话说我们要用一个函数“勾出”state。

语法:

const [state, setState] = useState(initialState);

通过钩子函数useState()勾出state,useState()中需要传递一个初始值,这个值就是你希望在变量中存储的值。函数会返回一个数组,数组中有两个元素,第一个元素是存储了值的变量,第二个元素是一个函数用来对值进行修改。比如上边的案例,可以这样修改:

import React, {useState} from 'react';const Clock = () => {const [date, setDate] = useState(new Date().toLocaleTimeString());const clickHandler = ()=>{
        setDate(new Date().toLocaleTimeString());
    };return (
        <div>
            {date}
            <div>
                <button onClick={clickHandler}>刷新</button>
            </div>
        </div>
    );
};export default Clock;

使用useState()“勾出”的变量就是一个普通变量,它里边存储了初始化的值,这个变量和其他变量没什么大区别,同样修改这个变量的值也不会对组件产生实质性的影响,所以不要尝试直接为state赋值。useState()“勾出”的函数用来修改state的值,他需要一个新的state值作为参数,调用后会触发组件的重新渲染,从而使得页面刷新,在每次的重新渲染中都会使用新的state值作为参数。

State的问题

引出问题

有了state,使得React组件可以随着某个值的改变而改变,我们无需再在某个值发生变化后重新手动对界面进行构建,React会替我们完成这些工作,大大降低了我们开发的难度。

但是state中还隐藏着一些不太容易发现的问题,现在假设我们需要开发一个计数器组件,这个组件非常简单,有一个按钮和一个数字,每点击一次按钮数字就会增加1,大概长成这个样子:
在这里插入图片描述
Counter.js

import React, {useState} from 'react';

const Counter = () => {
    const [count, setCount] = useState(1);

    const clickHandler = ()=> {
        setCount(count+1);
    }

    return (
        <div>
            <h2>{count}</h2>
            <button onClick={clickHandler}>+1</button>
        </div>
    );
};

export default Counter;

在clickHandler()中,我们调用了setCount(count+1)来对count进行更新,每次更新都是在前一次值的基础上增加1。这个代码这么写在大部分的场景下都不会带来任何的问题,但是在某些情况下就不一定了。

产生问题的原因

演示问题之前,先来说说产生这个问题的原因。在React中我们通过setState()修改状态都是异步完成的,换句话说并不是调用完setState()后状态立刻就发生变化,而是需要等上一段时间,当然这段时间不会很长。像上边的案例中state的修改虽然是异步完成的,但是由于功能比较简单,等待时间几乎可以忽略不计。但随着功能复杂度的提升,这个间隔会逐渐增多。

问题演示

假设调用setState()后1秒state的值才会真的改变,这时如果我们连续点击按钮2次,第1次点击按钮时count值是1,第2次点击速度比较快,从而两次间隔没有超过1秒,此时的count值依然是1,这就导致我点击了两次按钮,但是值只增加了1次,因为两次count+1中的count都是1。

为了演示问题,可以将上述案例的setCount()放入到一个延时调用中:

import React, {useState} from 'react';

const Counter = () => {
    const [count, setCount] = useState(1);
    const clickHandler = ()=> {
        setTimeout(()=>{
            setCount(count+1);
        }, 1000);
    }
    return (
        <div>
            <h2>{count}</h2>
            <button onClick={clickHandler}>+1</button>
        </div>
    );
};

这样一来,点击按钮后1秒setCount()才会调用,如果我们在1秒内点击按钮多次,你会发现按钮数值只会增加一次,很显然我们不希望这种情况出现。

解决问题

要解决这个问题,其实也不难,在setState()时除了直接传递一个指定值以外,React还允许我们通过一个回调函数来修改state,回调函数的返回值就是新的state的值,使用回调函数的好处是,这个回调函数会确保上一次的setState()调用完成后才被调用,同时会使用最新的state值作为回调函数的第一个参数。这样一来就有效的避免了无法正确获取上一个state值的问题。

上边案例中的 setCount(count+1);可以改成这个样子:

setCount(prevState => prevState+1);

这样一来,函数中的prevState总是上次修改后的最新state,避免再次出现点击多次按钮只修改一次的问题。总的来说,当我们修改一个state的值而需要依赖于前边的值进行计算时,最安全的方式就是通过回调函数而不是直接修改。

Ref(获取DOM对象)

React中所有的操作默认都是在React元素上进行,然后再通过虚拟DOM应用到真实页面上的。这样做的好处我们不在赘述。虽然如此,在React中依然为我们提供了可以直接访问原生DOM对象的方式。ref就是干这个事的。

ref是reference的简写,换句话说就是用来获取真实DOM对象的引用。咱们丑话还是要说在前边,虽然可以获取到DOM对象,但是轻易不要这么做,如果必须要获取,也尽量是读取而不要修改,如果必需要修改也要尽量减少修改的次数,总之能不用就不用。

下边我们来看看,如何操作,还是以函数组件为例:

import React, {useRef} from 'react';

const MyComponent = () => {

    const divRef = useRef();

    const clickHandler = () => {
        console.log(divRef);
    };

    return (
            <div ref={divRef} onClick={clickHandler}>一个div</div>
           
    );
};

export default MyComponent;

我们要获取元素的真实DOM对象,首先我们需要使用useRef()这个钩子函数获取一个对象,这个对象就是一个容器,React会自动将DOM对象传递到容器中。代码const divRef = useRef()就是通过钩子函数在创建这个对象,并将其存储到变量中。

创建对象后,还需要在被获取引用的元素上添加一个ref属性,该属性的值就是刚刚我们所声明的变量,像是这样ref={divRef}这句话的意思就是将对象的引用赋值给变量divRef。这两个步骤缺一不可,都处理完了,就可以通过divRef来访问原生DOM对象了。

useRef()返回的是一个普通的JS对象,JS对象中有一个current属性,它指向的便是原生的DOM对象。上例中,如果想访问div的原生DOM对象,只需通过divRef.current即可访问,它可以调用DOM对象的各种方法和属性,但还是要再次强调:慎用!

尽量减少在React中操作原生的DOM对象,如果实在非得操作也尽量是那些不会对数据产生影响的操作,像是设置焦点、读取信息等。

useRef()所返回的对象就是一个普通的JS对象,所以上例中即使我们不使用钩子函数,仅仅创建一个形如{current:null}的对象也是可以的。只是我们自己创建的对象组件每次渲染时都会重新创建一个新的对象,而通过useRef()创建的对象可以确保组件每次的重渲染获取到的都是相同的对象。

处理表单

表单是网页中必不可少的组件,本节课我们来看看在React中如何处理表单,首先我们先来创建一个简单的表单组件:

import React from 'react';

const MyForm = () => {
    return (
        <form>
            <div>
                用户名 <input type="text"/>
            </div>
            <div>
                密码 <input type="password"/>
            </div>
            <div>
                电子邮件 <input type="email"/>
            </div>

            <div>
                <button>提交</button>
            </div>
        </form>
    );
};

export default MyForm;

首先使用React定义表单和之前传统网页中的表单有一些区别,传统网页中form需要指定action和method两个属性,而表单项也必须要指定name属性,这些属性都是提交表单所必须的。但是在React中定义表单时,这些属性通通都可以不指定,因为React中的表单所有的功能都需要通过代码来控制,包括获取表单值和提交表单,所以这些东西都可以在函数中指定并通过AJAX发送请求,无需直接在表单中设置。

首先我们来研究一下如何获取表单中的用户所填写的内容,要获取用户所填写的内容我们必须要监听表单onChange事件,在表单项发生变化时获取其中的内容,在响应函数中通过事件对象的target.value来获取用户填写的内容。事件响应函数大概是这个样子:

const nameChangeHandler= e => {
     //e.target.value 表示当前用户输入的值
};

然后我们再将该函数设置为input元素的onChange事件的响应函数:

<div>
    用户名 <input type="text" onChange={nameChangeHandler}/>
</div>

这样一来当用户输入内容时,nameChangeHandler就会被触发,从而通过e.target.value来获取用户输入的值。通常我们还会为表单项创建一个state用来存储值:

const [inputName, setInputName] = useState(''); 
const nameChangeHandler = e => {
    //e.target.value 表示当前用户输入的值
    setInputName(e.target.value);
 };

上例中用户名存储到了变量inputName中,inputName也会设置为对应表单项的value属性值,这样一来当inputName发生变化时,表单项中的内容也会随之改变:

<div>
    用户名 <input type="text" onChange={nameChangeHandler} value={inputName}/>
</div>

如此设置后,当用户输入内容后会触发onChange事件从而调用nameChangeHandler函数,在函数内部调用了setInputName设置了用户输入的用户名。换句话说用户在表单中输入内容会影响到state的值,同时当我们修改state的值时,由于表单项的value属性值指向了state,表单也会随state值一起改变。这种绑定方式我们称为双向绑定,即表单会改变state,state也可以改变表单,在开发中使用双向绑定的表单项是最佳实践。

那么表单的提交要如何处理呢?表单提交同样需要通过事件来处理,提交表单的事件通过form标签的onSubmit事件来绑定,处理表单的方式因情况而已,但是一定要注意,必须要取消默认行为,否则会触发表单的默认提交行为:

const formSubmitHandler = e => {
    e.preventDefault();
    /*
     * username : inputName
     * password : pwdInput
     * email : inputEmail     
     * */
};

为表单绑定事件:

<form onSubmit={formSubmitHandler}>
......
</form>

如此我们便有了一个简单的表单案例,完整代码如下:

import React, {useState} from 'react';

const MyForm = () => {

    const [inputName, setInputName] = useState('');
    const [inputPwd, setInputPwd] = useState('');
    const [inputEmail, setInputEmail] = useState('');
    const nameChangeHandler = e => {
        setInputName(e.target.value);
    };

    const pwdChangeHandler = e => {
        setInputPwd(e.target.value)
    };

    const emailChangeHandler = e => {
        setInputEmail(e.target.value)
    };

    const formSubmitHandler = e => {
      e.preventDefault();
      /*
      * username : inputName
      * password : pwdInput
      * email : inputEmail
      * */
    };

    return (
        <form onSubmit={formSubmitHandler}>
            <div>
                用户名 <input type="text" onChange={nameChangeHandler} value={inputName}/>
            </div>
            <div>
                密码 <input type="password" onChange={pwdChangeHandler} value={inputPwd}/>
            </div>
            <div>
                电子邮件 <input type="email" onChange={emailChangeHandler} value={inputEmail}/>
            </div>

            <div>
                <button>提交</button>
            </div>
        </form>
    );
};
export default MyForm;

在这个案例中,表单的所有功能包括输入、显示、提交全部都由React所处理。这种表单项在React中被称为受控组件,即表单项受React控制。当然也存在有不受控组件,但那种组件使用机会少且需要通过原生DOM去操作表单,并不建议使用,所以这里便不再赘述了。

Portal

在React中,父组件引入子组件后,子组件会直接在父组件内部渲染。换句话说,React元素中的子组件,在DOM中,也会是其父组件对应DOM的后代元素。

但是,在有些场景下如果将子组件直接渲染为父组件的后代,在网页显示时会出现一些问题。比如,需要在React中添加一个会盖住其他元素的Backdrop组件,Backdrop显示后,页面中所有的元素都会被遮盖。很显然这里需要用到定位,但是如果将遮罩层直接在当前组件中渲染的话,遮罩层会成为当前组件的后代元素。如果此时,当前元素后边的兄弟元素中有开启定位的情况出现,且层级不低于当前元素时,便会出现盖住遮罩层的情况。

const Backdrop = () => {
  return <div
           style={
      {
        position:'fixed',
        top:0,
        bottom:0,
        left:0,
        right:0,
        background:'rgba(0,0,0,.3)',
        zIndex:9999
      }
    }
           >
    
  </div>
};

const Box = props => {
  return <div
           style={
      {
        width:100,
        height:100,
        background:props.bgColor
      }
    }
           >
             {props.children}
           </div>
};

const App = () => {
  return <div>
    
    <Box bgColor='yellowgreen'>
    <Backdrop/>
    </Box>
    <Box bgColor='orange' />
    
  </div>;
};


ReactDOM.render(
  <App/>,
  document.getElementById('root')
);

上例代码中,App组件中引入了两个Box组件,一个绿色,一个橙色。绿色组件中引入了Backdrop组件,Backdrop组件是一个遮罩层,可以在覆盖住整个网页。

现在三个组件的关系是,绿色Box是橙色Box的兄弟元素,Backdrop是绿色Box的子元素。如果Box组件没有开启定位,遮罩层可以正常显示覆盖整个页面。

在这里插入图片描述
Backdrop能够盖住页面

但是如果为Box开启定位,并设置层级会出现什么情况呢?

const Box = props => {
  return <div
           style={
      {
        width:100,
        height:100,
        background:props.bgColor,
        position:'relative',
        zIndex:1
      }
    }
           >
             {props.children}
           </div>
};

现在修改Box组件,开启相对定位,并设置了z-index为1,结果页面变成了这个样子:
在这里插入图片描述
和上图对比,显然橙色的box没有被盖住,这是为什么呢?首先我们来看看他们的结构:

<App>
    <绿色Box>
            <遮罩/>
    </绿色Box>
    <橙色Box/>
</App>

绿色Box和橙色Box都开启了定位,且z-index相同都为1,但是由于橙色在后边,所以实际层级是高于绿色的。由于绿色是遮罩层的父元素,所以即使遮罩的层级是9999也依然盖不住橙色。

问题出在了哪?遮罩层的作用,是用来盖住其他元素的,它本就不该作为Box的子元素出现,作为子元素了,就难免会出现类似问题。所以我们需要在Box中使用遮罩,但是又不能使他成为Box的子元素。怎么办呢?React为我们提供了一个“传送门”可以将元素传送到指定的位置上。

通过ReactDOM中的createPortal()方法,可以在渲染元素时将元素渲染到网页中的指定位置。这个方法就和他的名字一样,给React元素开启了一个传送门,让它可以去到它应该去的地方。

Portal的用法

  1. 在index.html中添加一个新的元素
  2. 在组件中通过ReactDOM.createPortal()将元素渲染到新建的元素中

在index.html中添加新元素:

<div id="backdrop"></div>

修改Backdrop组件:

const backdropDOM = document.getElementById('backdrop');

const Backdrop = () => {
  return ReactDOM.createPortal(
  <div
           style={
      {
        position:'fixed',
        top:0,
        bottom:0,
        left:0,
        right:0,
        zIndex:9999,
        background:'rgba(0,0,0,.3)'
      }
    }
           >
  </div>,
      backdropDOM
  );
};

如此一来,我们虽然是在Box中引入了Backdrop,但是由于在Backdrop中开启了“传送门”,Backdrop就会直接渲染到网页中id为backdrop的div中,这样一来上边的问题就解决了!嘿嘿,不错吧!

创建React项目(自动)

前边的课程中所使用的React项目均是由我们手动创建的,这种方式学习时用用还好,但是到了工作中如果依然让我们手动创建项目实在是不够优雅。为了使我们创建项目更加方便React为我们提供了一个工具create-react-app光看名字你应该就已经知道它的作用了。

使用create-react-app可以快速的创建一个React项目的目录结构,并且它会自动帮助我们安装React中所有的依赖,换句话说,之前我们手动做的工作现在只需要一个命令就可以搞定了!

使用步骤

在这里插入图片描述
接下来打开命令行,这里我使用的是windows自带的终端(windows terminal)
在这里插入图片描述
打开终端进入到刚刚创建的目录c:/Users/lilichao/Desktop/project
在这里插入图片描述
输入命令npx create-react-app react-app这里的react-app是应用的名称,可以根据实际需要设置。
在这里插入图片描述
输入命令后会自动下载插件并创建React应用,根据网络不同下载时间也不同。
在这里插入图片描述
执行完毕后,你的项目目录下会多出一个react-app目录,如果你指定的不是这个名字就去找你指定的那个。项目目录结构如下:

react-app
    ├─ node_modules
    ├─ public
	├─ favicon.ico
	├─ index.html
	├─ logo192.png
	├─ logo512.png

        ├─ manifest.json
        ├─ robots.txt
    ├─ src
        ├─ App.css
        ├─ App.js
	├─ App.test.js
	├─ index.css
	├─ index.js
	├─ logo.svg
	├─ reportWebVitals.js
        ├─ setupTests.js		
    ├─ package.json

下边我们分别来说一下每个文件(夹)的作用:

  1. Node_modules
    node的包目录,没啥可说的
  2. Public
    在这里插入图片描述
  3. index.html
<!DOCTYPE html>
<html lang="en"> <!-- 最好改成zh -->
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <!-- 收藏图标路径 %PUBLIC_URL% 表示根目录路径 -->
    <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- 完美视口 保留 -->
    <meta name="theme-color" content="#000000" /> <!-- 手机状态栏的颜色,可根据需要修改 -->
    <meta
      name="description"
      content="Web site created using create-react-app"
    /> <!-- 网页的描述,可根据需要修改 -->
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <!-- iOS主屏图标,可删 -->
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <!-- PWA配置文件路径,可删 -->
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript> <!-- JS禁用时,显示的提示文字 -->
    <div id="root"></div> <!-- 根容器 -->
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
  1. Src
    在这里插入图片描述
  2. Package.Json
    在这里插入图片描述

总结

总的来说,除了public/index.html和src/index.js必须保留外,其他的东西都是可以删除的,换句话说,之前我们手动创建的项目就是一个项目的最简状态。使用create-react-app创建React项目后,我们还要再根据自己的需要对项目的结构和代码进行修改!

React中CSS样式

现在我们已经学会了在React中通过import直接引入外部的CSS样式表,但这并不是React中使用CSS的唯一方式,这里我们简单的罗列几种React中设置样式的方式。

内联样式

在React中可以直接通过标签的style属性来为元素设置样式。style属性需要的是一个对象作为值,来为元素设置样式。

<div style={{color:'red'}}>
    我是Div
</div>

传递样式时,需要注意如果样式名不符合驼峰命名法,需要将其修改为符合驼峰命名法的名字。比如:background-color改为backgroundColor。

如果内联样式编写过多,会导致JSX变得异常混乱,此时也可以将样式对象定义到JSX外,然后通过变量引入。

样式过多,JSX会比较混乱:

const StyleDemo = () => {
    return (
        <div style={{color:'red', backgroundColor:'#bfa', fontSize:20, borderRadius:12}}>
            我是Div
        </div>
    );
};

export default StyleDemo;

可以这样修改:

import React from 'react';

const StyleDemo = () => {
    const divStyle = {color: 'red', backgroundColor: '#bfa', fontSize: 20, borderRadius: 12}

    return (
        <div style={divStyle}>
            我是Div
        </div>
    );
};

export default StyleDemo;

在内联样式中使用State

设置样式时,可以根据不同的state值应用不同的样式,比如我们可以在组件中添加一个按钮,并希望通过点击按钮可以切换div的边框,代码可以这样写:

import React, {useState} from 'react';

const StyleDemo = () => {

    const [showBorder, setShowBorder] = useState(false);

    const divStyle = {
        color: 'red',
        backgroundColor: '#bfa',
        fontSize: 20,
        borderRadius: 12,
        border: showBorder?'2px red solid':'none'
    };

    const toggleBorderHandler = ()=> {
      setShowBorder(prevState => !prevState);
    };

    return (
        <div style={divStyle}>
            我是Div
            <button onClick={toggleBorderHandler}>切换边框</button>
        </div>
    );
};

export default StyleDemo;

上例中添加一个新的state,命名为showBorder,代码是这样的const [showBorder, setShowBorder] = useState(false);当该值为true时,我们希望div可以显示一条2像素的红色边框,当为false时,我们希望div没有边框。默认值为false。

divStyle的最后一个属性是这样设置的border: showBorder?‘2px red solid’:‘none’,这里我们根据showBorder的值来设置border样式的值,如果值为true,则设置边框,否则边框设置为none。

toggleBorderHandler 是负责修改showBorder的响应函数,当我们点击按钮后函数会对showBorder进行取反,这样我们的样式就可以根据state的不同值而呈现出不同的效果了。

外部样式表

外部样式是将样式编写到外部的css文件中,然后直接通过import进行引入,上述案例修改为外部样式表是这个样子:

StyleDemo.css

.myDiv{
    color: red;
    background-color: #bfa;
    font-size: 20px;
    border-radius: 12px;
}

.redBorder{
    border: 2px red solid;
}

StyleDemo.js

import React, {useState} from 'react';
import './StyleDemo.css';

const StyleDemo = () => {

    const [showBorder, setShowBorder] = useState(false);

    const toggleBorderHandler = ()=> {
      setShowBorder(prevState => !prevState);
    };

    return (
        <div className={`myDiv${showBorder?' redBorder':''}`}>
            我是Div
            <button onClick={toggleBorderHandler}>切换边框</button>
        </div>
    );
};

export default StyleDemo;

上边的案例中,将样式编写到了外部的css文件中,然后通过import引入到了当前模块中。在JSX中通过为元素添加了class使得样式生效。同时,在设置class时使用了模板字符串,根据showBorder的值判断是否添加redBorder这个类。

上边两个案例中无论是内联样式还是外部样式表最终的实现效果都是一样的,但要是让我从这两种方式中选的话我更加倾向于选择第二种,第二种方式中将JSX和CSS分别写到了不同的文件中,我们维护起来更加的方便。

但是以这种方式使用样式也会存在一个问题,直接通过import引入的样式都是全局样式,如果不同的样式表中出现了相同的类名,会出现相互覆盖情况,这一点要尤为注意!

CSS Module

如果没有类名冲突的问题,外部CSS样式表不失为是一种非常好的编写样式的方式。为了解决这个问题React中还为我们提供了一中方式,CSS Module。

我们可以将CSS Module理解为外部样式表的一种进化版,它的大部分使用方式都和外部样式表类似,不同点在于使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一,所以使用CSS Module后,我们再也不用担心类名重复了!

使用方式
在这里插入图片描述
请看案例:

/*

StyleDemo.module.css

*/
.myDiv{
    color: red;
    background-color: #bfa;
    font-size: 20px;
    border-radius: 12px;
}
/*

StyleDemo.js

*/
import styles from './StyleDemo.module.css';

const StyleDemo = () => {
    return (
        <div className={styles.myDiv}>
            我是Div
        </div>
    );
};

export default StyleDemo;

这就是一个简单的CSS Module的案例,设置完成后你可以自己通过开发者工具查看元素的class属性,你会发现class属性和你设置的并不完全一样,这是因为CSS Module通过算法确保了每一个模块中类名的唯一性。

总之,相较于标准的外部样式表来说,CSS Module就是多了一点——确保类名的唯一,通过内部算法避免了两个组件中出现重复的类名,如果你能保证不会出现重复的类名,其实直接使用外部样式表也是一样的。

Fragment

在React中,JSX必须有且只有一个根元素。这就导致了在有些情况下我们不得不在子元素的外部添加一个额外的父元素,像是这样:

import React from 'react';

const MyComponent = () => {
    return (
        <div>
            <div>我是组件1</div>
            <div>我是组件2</div>
            <div>我是组件3</div>
        </div>
    );
};

export default MyComponent;

上边这段代码中,组件内部需要引入三个组件(使用三个div表示)。由于是三个组件,根据JSX的语法必须在三个组件的外部在套一个div,三个组件才能够正常使用,但是这个外层的div在最终的页面中没有任何的实质性作用。

遇到这种情况我们就非常希望能有一种方式可以引入多个组件,但又不会在最终的网页中添加多余的结构,那么我们可以定义这样一个组件:

import React from 'react';

const MyFragment = (props) => {
    return props.children;
};

export default MyFragment;

MyFragment这个组件非常简单,它会直接返回当前组件内部的所有子元素,不会产生新的元素,可以将上边组件中的外层div,修改为MyFragmet:

import React from 'react';
import MyFragment from "./MyFragment";

const MyComponent = () => {
    return (
        <MyFragment>
            <div>我是组件1</div>
            <div>我是组件2</div>
            <div>我是组件3</div>
        </MyFragment>
    );
};

export default MyComponent;

这样一来,网页中就不会出现多余的div了。

实际上在React中已经为我们提供好了一个现成的组件帮助我们完成这个工作,这个组件可以通过React.Fragment使用,上述案例,也可以修改成这样:

import React from 'react';

const MyComponent = () => {
    return (
        <React.Fragment>
            <div>我是组件1</div>
            <div>我是组件2</div>
            <div>我是组件3</div>
        </React.Fragment>
    );
};

export default MyComponent;

不过最爽的是,在React中为我们提供了一种更加便捷的方式,直接使用<></>代替Fragment更加简单:

import React from 'react';

const MyComponent = () => {
    return (
        <>
            <div>我是组件1</div>
            <div>我是组件2</div>
            <div>我是组件3</div>
        </>
    );
};

export default MyComponent;

订餐应用练习(React)—— 分析

本节课,我们来通过一个练习来熟悉巩固一下React,首先我们先来看看练习的UI界面:
在这里插入图片描述
这个练习是一个手机应用,本身并不十分复杂,但是开发之前也需要对项目的结构进行分析,分析的越清楚开发起来也就越轻松。

项目的整体架构

在这里插入图片描述
需要在根应用中引入的模块主要有三个,搜索框、食物列表和购物车.
在这里插入图片描述
搜索框的结构比较简单,是一个input文本框,且需要固定定位到屏幕的上方,它主要的作用是通过关键字对食物列表进行过滤。

食物列表主要是用来呈现用户可以够买的食品,结构略微复杂。列表中放置的是一个一个的列表项,列表项中还包含有一个可以增加减少食物的按钮。
在这里插入图片描述
购物车用来显示选中商品的数量和总价格,点击购物车后可以显示出购物清单,点击去结算后会显示结账页面。
在这里插入图片描述
项目结构的分析没有一定之规,不同的公司有不同的规范,不同的人有不同的开发经验,也许你对项目结构的分析和我不同,但这绝对不是什么太严重的问题,你可以按照自己的想法去实施,亦可紧紧跟随我的步伐,都不要紧。最重要的是你心里要有这么一个结构,结构有了开发起来便更加顺畅,出了问题也可以根据结构去定位问题发生的位置。

根据上述的分析,接下来我们可以确定一下项目的目录结构:

meal-delivery
    ├─ public
        ├─ index.html
        ├─ img
    ├─ src
        ├─ index.js
        ├─ index.css
        ├─ App.js
        ├─ App.css
        ├─ components
            ├─ Cart
            ├─ FilterMeals
            ├─ Meals
                ├─ Meal
            ├─ UI
                ├─ Backdrop
                ├─ Confirm
                ├─ Counter 

本项目中最核心的功能就是食物的列表,所有的功能包括搜索、购物车和结账页面都是依赖于食物列表,接下来从食物列表开始一步一步的做起来吧,具体步骤请看视频吧!
在这里插入图片描述

Context

在React中组件间的数据通信是通过props进行的,父组件给子组件设置props,子组件给后代组件设置props,props在组件间自上向下(父传子)的逐层传递数据。但并不是所有的数据都适合这种传递方式,有些数据需要在多个组件中共同使用,如果还通过props一层一层传递,麻烦自不必多说。
Context为我们提供了一种在不同组件间共享数据的方式,它不再拘泥于props刻板的逐层传递,而是在外层组件中统一设置,设置后内层所有的组件都可以访问到Context中所存储的数据。换句话说,Context类似于JS中的全局作用域,可以将一些公共数据设置到一个同一个Context中,使得所有的组件都可以访问到这些数据。

创建Context:

const MyContext = React.createContext(defaultValue);

React.createContext(defaultValue)用来创建一个Context对象,它需要一个初始值作为参数,这个初始值可以是一个原始值,也可以是一个JS对象。调用以后,方法将会返回一个Context对象,这个对象非常关键,当我们想在其他组件中访问Context中的数据时,必须要通过这个对象。

由于Context对象需要在不同的组件中被使用,所以通常我们会将Context对象设置到一个单独的模块中并设置为默认导出像是这样:

import React from "react";

const TestContext = React.createContext({
    name:'孙悟空',
    age:18,
    gender:'男',
    sayHello:()=>{
        alert(this.name);    
    }
});

export default TestContext;

在这个案例中我们暴露的数据比较简单,就是一个简单的JS对象,其中包含了三个属性和一个方法,并最中将产生的Context对象作为默认模块向外部导出。

如果想访问到Context中的数据,我们需要先将Context引入到当前组件中,然后通过Context对象访问其中的数据。

第一种方式,可以通过Consumer标签来访问到Context中的数据:

import React from 'react';
import TestContext from '../store/test-context';

const MyComponent = () => {

    return (
        <TestContext.Consumer>
            {(ctx)=>{
                return (
                    <ul>
                        <li>{ctx.name}</li>
                        <li>{ctx.age}</li>
                        <li>{ctx.gender}</li>
                    </ul>
                );
            }}
        </TestContext.Consumer>

    );
};

export default MyComponent;

访问Context首先我们需要引入之前创建的Context:

import TestContext from '../store/test-context';

Context对象中有一个属性叫做Consumer,直译过来为消费者,如果你了解生产消费者模式这里就比较好理解了,如果没接触过,你可以将Consumer理解为数据的获取工具。你可以将它理解为一个特殊的组件,所以你需要这样使用它:

<TestContext.Consumer>
    {(ctx)=>{
        return (
            <ul>
                <li>{ctx.name}</li>
                <li>{ctx.age}</li>
                <li>{ctx.gender}</li>
            </ul>
        );
    }}
</TestContext.Consumer>

Consumer的标签体必须是一个函数,这个函数会在组件渲染时调用并且将Context中存储的数据作为参数传递进函数,该函数的返回值将会作为组件被最终渲染到页面中。这里我们将参数命名为了ctx,在回调函数中我们就可以通过ctx.xxx访问到Context中的数据。如果需要访问多个Context可以使用多个Consumer嵌套即可。

通过Consumer使用Context实在是不够优雅,所以React还为我们提供了一个钩子函数useContext(),我们只需要将Context对象作为参数传递给钩子函数,它就会直接给我们返回Context对象中存储的数据。

import React, {useContext} from 'react';
import TestContext from '../store/test-context';

const MyComponent = () => {

    const ctx = useContext(TestContext);

    return (
        <ul>
            <li>{ctx.name}</li>
            <li>{ctx.age}</li>
            <li>{ctx.gender}</li>
        </ul>
    );
};

export default MyComponent;

像上边那样使用Context并不十分常见,因为这种方式中Context的值是写死的,并不是在组件中指定的。所以React还提供了Provider,用来在组件中指定Context值:

import React from "react";
import MyComponent from "./component/MyComponent";
import TestContext from "./store/test-context";

const App = () => {


    return <TestContext.Provider value={{name:'猪八戒', age:28, gender:'男'}}>
        <MyComponent/>
    </TestContext.Provider>;
};

export default App;

Provider译为生产者,和Consumer消费者对应。Provider会设置在外层组件中,通过value属性来指定Context的值。这个Context值在所有的Provider子组件中都可以访问。Context的搜索流程和JS中函数作用域类似,当我们获取Context时,React会在它的外层查找最近的Provider,然后返回它的Context值。如果没有找到Provider,则会返回Context模块中设置的默认值。

Effect

React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。

例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。

React.StrictMode

编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,我们来看看React官网的文档是如何说明的:

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructor, render, and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState)
  • Functions passed to useState, useMemo, or useReducer

上文的关键字叫做“double-invoking”即重复调用,这句话是什么意思呢?大概意思就是,React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:

  • 类组件的的 constructor, render, 和 shouldComponentUpdate 方法
  • 类组件的静态方法 getDerivedStateFromProps
  • 函数组件的函数体
  • 参数为函数的setState
  • 参数为函数的useState, useMemo, or useReducer

重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。

如果你无法通过浏览器正常安装React Developer Tools可以通过点击这里下载。

使用Effect

为了解决这个问题React专门为我们提供了钩子函数useEffect(),Effect的翻译过来就是副作用,专门用来处理那些不能直接写在组件内部的代码。

哪些代码不能直接写在组件内部呢?像是:获取数据、记录日志、检查登录、设置定时器等。简单来说,就是那些和组件渲染无关,但却有可能对组件产生副作用的代码。

useEffect语法:useEffect(didUpdate);
useEffect()需要一个函数作为参数,你可以这样写:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */
});

useEffect()中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。

通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。

清除Effect

组件的每次重新渲染effect都会执行,有一些情况里,两次effect执行会互相影响。比如,在effect中设置了一个定时器,总不能每次effect执行都设置一个新的定时器,所以我们需要在一个effect执行前,清除掉前一个effect所带来的影响。要实现这个功能,可以在effect中将一个函数作为返回值返回,像是这样:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */
    
    return () => {
        /* 这个函数会在下一次effect执行前调用 */
    };
});

effect返回的函数,会在下一次effect执行前调用,我们可以在这个函数中清除掉前一次effect执行所带来的影响。

限制Effect

组件每次渲染effect都会执行,这似乎并不总那么必要。因此在useEffect()中我们可以限制effect的执行时机,在useEffect()中可以将一个数组作为第二个参数传递,像是这样:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */

    return () => {
        /* 这个函数会在下一次effect执行前调用 */
    };
}, [a, b]);

上例中,数组中有两个变量a和b,设置以后effect只有在变量a或b发生变化时才会执行。这样即可限制effect的执行次数,也可以直接传递一个空数组,如果是空数组,那么effect只会执行一次。

使用Effect修改练习

在《汉堡到家》的练习中,存在着一个bug。当我们在购物车或结账界面减少商品的数量全部为0时(购物车中没有商品时)。购物车或结账页面并不能自动关闭,这里我们就可以借用Effect来解决问题。可以直接修改Cart.js直接向组件中添加如下的代码:

useEffect(() => {
    if(ctx.totalAmount === 0) {
        setShowCheckout(false);
        setShowDetails(false);
    }
}, [ctx]);

这样一来,当购物车中的商品发生变化时,就会触发useEffect,从而检查商品的总数量,如果总数量为0的话就会将购物车详情页和结账也直接隐藏。

除了Cart.js以外,FilterMeals组件也存在一个问题,首先,该组件中的表单项我们并没有使用state,所以这个组件是一个非受控组件,虽然目前看来没什么太大的问题,但是我们还是应该处理一下,因为受控组件使用时会更加的灵活,可以适用于更多的场景。其次、该组件的主要作用是过滤汉堡的列表,当用户输入关键字时它可以根据关键字的内容对食物列表进行过滤。问题正在于此,由于每次用户输入都需要过滤,这就意味着它的过滤频率过高了。举个例子,用户要输入“汉堡”这个关键字,他需要一次输入h-a-n-g-b-a-o七个字母,由于每次输入都会触发一次过滤,所以在“汉堡”打出来之前,列表完全是一个空白的状态,同时无用的过滤也对应用的性能造成了一定的影响。怎么办呢?同样可以使用Effect来解决这个问题,修改FilterMeals中的代码如下:

const [keyword, setKeyword] = useState('');

useEffect(() => {
    const timer = setTimeout(() => {
        props.onFilter(keyword);
    }, 1000);

    return () => {
        clearTimeout(timer);
    };
}, [keyword]);

const inputChangeHandler = e => {
    setKeyword(e.target.value.trim());
};

Reducer

在React的函数组件中,我们可以通过useState()来创建state。这种创建state的方式会给我们返回两个东西state和setState()。state用来读取数据,而setState()用来设置修改数据。但是这种方式也存在着一些不足,因为所有的修改state的方式都必须通过setState()来进行,如果遇到一些复杂度比较高的state时,这种方式似乎就变得不是那么的优雅。

举个例子,之前的《汉堡到家》的练习中,App.js中有一个state叫做cartData用来存储购物车数据。但是这个数据本身是比较复杂的,它包括了多个属性:

const [cartData, setCartData] = useState({
    items: [],
    totalAmount: 0,
    totalPrice: 0
});

同时购物车,也需要多个操作方法,像是添加食物、删除食物、清除购物车,而useState()只给我们提供了一个setCartData()方法,所以我们不得不在继续创建出三个不同的方法以实现出不同的功能:

const addItem = (meal) => {
    const newCart = {...cartData};
    if (newCart.items.indexOf(meal) === -1) {
        newCart.items.push(meal);
        meal.amount = 1;
    } else {
        meal.amount += 1;
    }
    newCart.totalAmount += 1;
    newCart.totalPrice += meal.price;
    setCartData(newCart);
};

const removeItem = (meal) => {
    const newCart = {...cartData};
    meal.amount -= 1;
    if (meal.amount === 0) {
        newCart.items.splice(newCart.items.indexOf(meal), 1);
    }
    newCart.totalAmount -= 1;
    newCart.totalPrice -= meal.price;
    setCartData(newCart);
};

const clearCart = () => {
    const newCart = {...cartData};
    newCart.items.forEach(item => delete item.amount);
    newCart.items = [];
    newCart.totalAmount = 0;
    newCart.totalPrice = 0;
    setCartData(newCart);
};

这三个函数定义在了App.js中,是操作cartData的三个函数。就这带来一些问题,首先,三个方法都是操作cartData的,但是它们被定义在App.js中和其他的函数混杂在了一起,维护起来并不方便。其次,三个方法并不是App.js自己调用,而是通过Context传递给其他组件调用,由于是三个函数所以我们不得不在Context中分别传递三个属性,也不方便。再有,如果后期我需要再添加新的功能,依然不可避免的要定义新的函数,并且修改Context。总之,就是各种不便利,这种不便还会随着项目复杂的提升而增加。

Reducer横空出世

为了解决复杂State带来的不便,React为我们提供了一个新的使用State的方式。Reducer横空出世,reduce单词中文意味减少,而reducer我觉得可以翻译为“当你的state的过于复杂时,你就可以使用的可以对state进行整合的工具”。当然这是个玩笑话,个人认为Reducer可以翻译为“整合器”,它的作用就是将那些和同一个state相关的所有函数都整合到一起,方便在组件中进行调用。

当然工具都有其使用场景,Reducer也不例外,它只适用于那些比较复杂的state,对于简单的state使用Reducer只能是徒增烦恼。但是由于初学,我们会先用一个简单的案例来对其进行演示,实际应用我们后边会以cartData作为演示。

和State相同Reducer也是一个钩子函数,语法如下:

const [state, dispatch] = useReducer(reducer, initialArg, init);

它的返回值和useState()类似,第一个参数是state用来读取state的值,第二个参数同样是一个函数,不同于setState()这个函数我们可以称它是一个“派发器”,通过它可以向reducer()发送不同的指令,控制reducer()做不同的操作。

它的参数有三个,第三个我们暂且忽略,只看前两个。reducer()是一个函数,也是我们所谓的“整合器”。它的返回值会成为新的state值。当我们调用dispatch()时,dispatch()会将消息发送给reducer(),reducer()可以根据不同的消息对state进行不同的处理。initialArg就是state的初始值,和useState()参数一样。

上代码:

import {useReducer, useState} from 'react';

const reducer = (state, action) => {
    switch(action.type){
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
    }
};

function App() {

    const [count, countDispath] = useReducer(reducer,1);

    return (
        <div className="App">
            {count}

            <div>
                <button onClick={()=>countDispath({type:'sub'})}>-</button>
                <button onClick={()=>countDispath({type:'add'})}>+</button>
            </div>
        </div>
    );
}

export default App;

修改购物车

修改App.js

......
const cartReducer = (state, action) => {
    const newCart = {...state};
    switch (action.type){
        case 'ADD_ITEM':
            if (newCart.items.indexOf(action.meal) === -1) {
                newCart.items.push(action.meal);
                action.meal.amount = 1;
            } else {
                action.meal.amount += 1;
            }
            newCart.totalAmount += 1;
            newCart.totalPrice += action.meal.price;
            return newCart;
        case 'REMOVE_ITEM':
            action.meal.amount -= 1;
            if (action.meal.amount === 0) {
                newCart.items.splice(newCart.items.indexOf(action.meal), 1);
            }
            newCart.totalAmount -= 1;
            newCart.totalPrice -= action.meal.price;
            return newCart;
        case 'CLEAR_CART':
            newCart.items.forEach(item => delete item.amount);
            newCart.items = [];
            newCart.totalAmount = 0;
            newCart.totalPrice = 0;
            return newCart;
        default:
            return state;
    }
};

const App = () => {

    ......

    const [cartData, cartDispatch] = useReducer(cartReducer, {
        items: [],
        totalAmount: 0,
        totalPrice: 0
    });

    ......

    return (
        <CartContext.Provider value={{...cartData, cartDispatch}}>
            <div>
                <FilterMeals onFilter={filterHandler}/>
                <Meals
                    mealsData={mealsData}
                />
                <Cart/>

            </div>
        </CartContext.Provider>
    );
};

export default App;

在其他组件中,需要操作购物车时,只需先获取CartContext然后通过ctx.cartDispath操作购物车:

const ctx = useContext(CartContext); // 加载context
ctx.cartDispatch({type:'CLEAR_CART'}); // 清空购物车
ctx.cartDispatch({type:'ADD_ITEM', meal:props.meal}); // 添加食物
ctx.cartDispatch({type:'REMOVE_ITEM', meal:props.meal}); // 删除食物

React.Memo

React组件会在两种情况下发生重新渲染。第一种,当组件自身的state发生变化时。第二种,当组件的父组件重新渲染时。第一种情况下的重新渲染无可厚非,state都变了,组件自然应该重新进行渲染。但是第二种情况似乎并不是总那么的必要。

我们来看一个demo,现有如下三个组件,分别为App组件、A组件和B组件:

App.js

const App = () => {
    const [count, setCount] = useState(1);

    const clickHandler = () => {
        setCount(prevState => prevState + 1);
    };

    return (
        <div>
            <h2>App -- {count}</h2>
            <button onClick={clickHandler}>增加</button>

            <A/>
        </div>
    );
};

A.js

const A = () => {
    const [count, setCount] = useState(1);

    const clickHandler = () => {
      setCount(prevState => prevState + 1);
    };

    return (
        <div>
            <h2>组件A -- {count}</h2>
            <button onClick={clickHandler}>增加</button>
            <B/>
        </div>
    );
};

export default A;

B.js

const B = () => {
    return (
        <div>
            <h2>组件B</h2>
        </div>
    );
};

export default B;

三个组件的引用关系为,A组件是App的子组件、B组件是A组件的子组件:App –> A –> B

当App组件发生重新渲染时,A和B组件都会发生重渲染。当A组件重新渲染时,B组件也会重新渲染。B组件中没有state,甚至连props都没有设置。换言之,B组件无论如何渲染,每次渲染的结果都是相同的,虽然重渲染并不会应用到真实DOM上,但很显然这种渲染是完全没有必要的。

为了减少像B组件这样组件的渲染,React为我们提供了一个方法React.memo()。该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。

现在对上述案例中的B组件进行如下修改:

const B = () => {
    console.log('B渲染');
    return (
        <div>
            <h2>组件B</h2>
        </div>
    );
};

export default React.memo(B);

修改后的代码中,并没有直接将B组件向外导出,而是在B组件外层套了一层函数React.memo(),这样一来,返回的B组件就增加了缓存功能,只有当B组件的props属性发生变化时,才会触发组件的重新渲染。memo只会根据props判断是否需要重新渲染,和state和context无关,state或context发生变化时,组件依然会正常的进行重新渲染。

Fetch API

React的主要作用是取代了原生DOM,让我们操作网页的方式变得更加简单。但是React中并没有为我们提供向服务器中发送请求的方法(因为这本来就不是它所关注的事情)。所以在React中发送请求的方式和传统项目其实是一致的,无非就是使用浏览器自带的Ajax、Fetch或者是类似于Axios的第三方框架。这也就意味着在React中发送请求的方式其实是非常灵活的,你完全可以根据自己的习惯选择一种你喜欢的方式。

Fetch API

Fetch是浏览器中自带的一种发送请求的方式,它是Ajax的升级版,相较于Ajax来说它使用起来更加方便,代码也更加简洁清晰。

发送get请求

fetch(resource)

使用fetch发送get请求时,只需要将请求地址作为参数即可。

fetch('http://localhost:1337/api/students')

API地址是我们前边课程中通过Strapi定义的地址。和传统的Ajax不同,fetch返回的是一个promise,所以我们不需要再去监听响应返回的事件只需在fetch后调用then()方法即可。

fetch('http://localhost:1337/api/students')
    .then(res => //res是响应信息 )

当请求发送成功后,fetch会自动调用then中的回调函数,回调函数的第一个参数就是服务器返回的响应信息,这里我们用res表示(response的简写)。res中包含了全部的响应信息,这些信息中我们比较关心的是服务器返回的json数据,所以我们还需调用res的json()方法来获取res中的json数据,json()方法同样会返回一个promise,所以我们还需要接着then,像是这样:

fetch('http://localhost:1337/api/students')
    .then(res => res.json())
    .then(data => console.log(data))

第二个then中的data,便是从res中解析到的数据,也就是服务器所发送的数据,它已经直接转换为了JS对象,数据结构和服务器中返回的数据一致:

{
    "data": [
        {
            "id": 1,
            "attributes": {
                "name": "孙悟空",
                "gender": "男",
                "age": 18,
                "address": "花果山",
                "createdAt": "2022-05-16T10:05:10.538Z",
                "updatedAt": "2022-05-16T10:05:34.201Z"
            }
        },
        {
            "id": 2,
            "attributes": {
                "name": "猪八戒",
                "gender": "男",
                "age": 28,
                "address": "高老庄",
                "createdAt": "2022-05-16T10:07:01.140Z",
                "updatedAt": "2022-05-16T10:07:01.140Z"
            }
        },
        {
            "id": 3,
            "attributes": {
                "name": "沙和尚",
                "gender": "男",
                "age": 38,
                "address": "流沙河",
                "createdAt": "2022-05-16T10:15:51.303Z",
                "updatedAt": "2022-05-16T10:15:51.303Z"
            }
        }
    ],
    "meta": {
        "pagination": {
            "page": 1,
            "pageSize": 25,
            "pageCount": 1,
            "total": 3
        }
    }
}

从服务器中加载到的数据比较多,我们使用时还需要对数据进行调整,使其变成我们需要的数据结构,我们还需要在最后调用catch,并在catch处理异常:

fetch('http://localhost:1337/api/students')
    .then(
        res => res.json())
    .then(
        data => {
            const formatData = data.data.map(item => ({
                id:item.id,
                name:item.attributes.name,
                age:item.attributes.age,
                gender:item.attributes.gender,
                address:item.attributes.address,
            }));

            console.log(formatData);
        })
    .catch(err => console.log(err));

其他类型请求

发送其他类型请求时,除了要指定请求地址外,还要传递第二个参数来设置请求的信息:

fetch(resource, init)

init对象就是用来设置请求信息的,常用的属性有method用来设置请求方法,headers用来设置请求头,body用来设置请求体。

例子:

发送delete请求:

fetch('http://localhost:1337/api/students/3',{
        method:'delete'
    })
        .then(
            res => res.json())
        .then(
            data => console.log(data))
        .catch(err => console.log(err));
};

发送post请求:

fetch('http://localhost:1337/api/students', {
    method: 'post',
    headers: {
        "Content-Type": 'application/json'
    },
    body: JSON.stringify({
        data: {
            name: '沙和尚',
            age: 38,
            gender: '男',
            address: '流沙河'
        }
    })
})
    .then(
        res => res.json())
    .then(
        data => console.log(data))
    .catch(err => console.log(err));

发送put请求:

 fetch('http://localhost:1337/api/students/5', {
     method: 'put',
     headers: {
         "Content-Type": 'application/json'
     },
     body: JSON.stringify({
         data: {
             age: 48,
         }
     })
 })
     .then(
         res => res.json())
     .then(
         data => console.log(data))
     .catch(err => console.log(err));

自定义钩子

随着练习功能的增多,我们编写的React代码变得越来越复杂。像是上节课中我编写的React代码,仅仅是增加了一个加载数据的功能,我们就需要向App.js中引入了三个state和一个钩子函数:

const [stuData, setStuData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);\
useEffect(()=>{
    const fetchData = async () => {
        try{
            setLoading(true);
            setError(null);
            const res = await fetch('http://localhost:1337/api/students');
            if(res.ok){
                const data = await res.json();
                setStuData(data.data);
            }else{
                throw new Error('数据加载失败!');
            }
        }catch (e){
            setError(e);
        }finally {
            setLoading(false);
        }
    };
    fetchData();
}, []);

随着这种代码的增多App.js中的代码会变得越来越多,难以维护。所以我们就迫切的需要一个东西可以将这些代码存储起来,一来可以降低单个文件中的代码数量,二来也可以让这些代码方便在多个组件中复用。

但是问题就来了,这些代码无论是state还是effect其实都是钩子函数,但是钩子函数又不是说随便在哪都能写的,这要怎么处理呢?

我们在刚刚介绍钩子函数时就说过,钩子函数只能运行在函数组件或自定义钩子中,所以要提取这些代码我们只有一个选择,那就是自定义钩子。

自定义钩子是个什么玩意?它其实一点也不神秘,自定钩子就是一个普通的函数。普通函数怎么定义,它就怎么定义。但是它又不那么普通,因为钩子函数的名字必须以use开头,使用use开头后,React就能自动识别出它是一个钩子函数,这样才会以钩子函数的方式去处理它。

使用钩子

  1. 创建一个函数,命名为useXxx
  2. 在函数中正常调用React中的各种钩子
  3. 在组件中引用钩子

这个步骤看着是不是特别草率,但事实其实就这么一回事。当函数使用use开头后,React就允许我们在其中调用React的钩子函数,所以我们就可以通过自定义的钩子函数,将组件中的涉及到钩子的代码封装起来,方便调用。

将上述代码修改为自定义钩子:

src/hooks/useFetch.js

import {useEffect, useState} from "react";

const useFetch = (url) => {

    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    async function fetchData(){
        try{
            setLoading(true);
            setError(null);

            const res = await fetch('http://localhost:1337/api/students');
            if(!res.ok){
                throw new Error('数据加载失败!');
            }
            const data = await res.json();
            setData(data.data);
        }catch (e){
            setError(e);
        }finally {
            setLoading(false);
        }
    }
    return {data, loading, error, fetchData};
};

export default useFetch;

App.js调用钩子

const {data:stuData, loading, error, fetchData} = useFetch();

useEffect(()=>{
    fetchData();
}, [])

这样一来,将App.js中发送请求相关的钩子都编写到useFetch中,并将App中会用到的变量作为返回值返回,而作为App来说,只需要调用useFetch,即可获取到stuData、loading、error等数据以及fetchData函数,这样一来大大简化了App中的代码,同时使得其他组件也可以通过useFetch来发送请求加载数据。

等等,好像还有个问题,我们将请求地址在useFetch中写死了,难道我们只向一个地址发请求吗?这样做不合适吧?当然不合适,我们可以将钩子中那些会发生的变化的值作为参数传递,比如请求地址,如此一来我们就可以向任意地址发送请求啦!但是这里我我就不写了,自己试一试吧!

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>
);

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 Query

RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。

Web应用中加载数据时需要处理的问题:

  1. 根据不同的加载状态显示不同UI组件
  2. 减少对相同数据重复发送请求
  3. 使用乐观更新,提升用户体验
  4. 在用户与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()它会自动向服务器发送请求加载数据,并返回一个对象。这个对象中包括了很多属性:
在这里插入图片描述
使用中可以根据需要,选择要获取到的属性值。写了这么多,也只写了一个Hello World。但是,良好的开端是成功的一半,这个理解了,后边的东西也就简单了!

React-Router

使用React这些工具所编写的项目通常都是单页应用(SPA)。单页应用中,整个应用中只含有一个页面,React会根据不同的状态在应用中显示出不同的组件。但是我们之前所编写应用还存在着一个问题,整个应用只存在一个页面,一个请求地址,这就使得用户只能通过一个地址访问应用,当我们点击组件中的不同链接时应用的地址是不会发生变化的。这又有什么问题呢?由于应用只有一个地址,所以我们通过该地址访问应用时,总会直接跳转到应用的首页。如此一来,我们便不敢随意的刷新页面,因为一旦刷新页面便直接跳转到首页。在对页面进行分享时,也只能分享网站的首页,而不能分享指定的页面。

怎么办呢?难道我们要将一个页面拆分为多个页面吗?很明显不能这么做,这么做以后应用的跳转便脱离了React的控制,增加应用的复杂度,提高了项目维护的成本。

为了解决这个问题,我们需要引入一个新的工具React Router,React Router为我们提供一种被称为客户端路由的东西,通过客户端路由可以将URL地址和React组件进行映射,当URL地址发生变化时,它会根据设置自动的切换到指定组件。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过客户端路由进行切换。

React Router最新版本为6,版本6和版本5之间的变化跨度比较大,我们的课程会分别讲解两个版本。

版本5

安装:

npm

npm install react-router-dom@5 -S

yarn

yarn add react-router-dom@5

HelloWorld:

import ReactDOM from "react-dom/client";
import {BrowserRouter, Link, Route, Switch} from "react-router-dom";


const Home = () => {
    return <div>这是首页</div>;
};

const About = () => {
    return <div>关于我们,其实没啥可说的</div>
};


const App = () => {
    return <div>
        <ul>
            <li>
                <Link to="/home">首页</Link>
            </li>
            <li>
                <Link to="/about">关于</Link>
            </li>
        </ul>
        <Switch>
            <Route path="/home" component={Home} />
            <Route path="/about" component={About} />
        </Switch>
    </div>;
};


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <BrowserRouter>
        <App/>
    </BrowserRouter>
);

解析

  1. React-Router-Dom包
    react router适用于web和原生项目,我们在web项目中使用,所以需要引入的包是react-router-dom。

  2. BrowserRouter组件
    和Redux类似,要使得路由生效,需要使用Router组件将App组件包裹起来。这里我们选择的是BrowserRouter,除了BrowserRouter外还有其他的Router,暂时我们只介绍BrowserRouter。

案例中,BrowserRouter我们是这样使用的:

import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <BrowserRouter>
        <App/>
    </BrowserRouter>
);

实际开发中,也可以为BrowserRouter起一个别名Router,这样一来我们在切换Router时,只需要修改引用位置,而不需要修改其他代码,像是这样:

import {BrowserRouter as Router, Link, Route, Switch} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Router>
        <App/>
    </Router>
);
  1. Route组件
    route组件是路由的映射组件,通过该组件将url地址和React组件进行映射,映射后当url地址变为指定地址时指定的组件就会显示,否则不显示。
<Route path="/home" component={Home} />
<Route path="/about" component={About} />

上例中,路径/home和组件进行了映射,路径/about和组件进行了映射。当访问http://localhost:3000/about时,about组件会自动渲染显示,访问http://localhost:3000/home时,home组件会自动渲染显示。

Route组件可以设置以下几个属性
在这里插入图片描述
path

用来设置要映射的路径,可以是一个字符串或字符串数组。字符串用来匹配单个路径,数组可以匹配多个路径。看一个数组的例子:

<Route path={["/about", "/hello"]}>
    <About/>
</Route>

使用数组映射后,当我们访问数组中的路径时都会使组件挂载。设置路径时也可以在路径中设置参数,比如:/student/:id其中id就是一个参数,可以动态的传递:id的值,换句话说/student/1或/student/2,都会触发组件挂载。

设置动态参数后,在组件的内部可以使用useParams()钩子来读取参数:

const Student = () => {
    const {id} = useParams();
    return <div>学生id:{id}</div>
};

......
<Route path="/student/:id">
    <Student/>
</Route>
......

exact

路由的匹配默认并不是完整匹配,这意味着路由映射的地址是/home时,只要我们访问的路径是以/home开头就会触发组件的挂载,默认情况下是不会检查路由的子路径的。比如:/home/hello、/home/abc等都会导致home组件挂载。

exact属性用来设置路由地址是否完整匹配,它需要一个布尔值,默认为false,就像上边的情况。如果设置为true,那么只有地址和path完全一致时,组件才会挂载。

<Route path="/home" exact>
    <Home/>
</Route>

这样一来只有访问/home时,home组件才会挂载,差一个字母都不行哦!

strict

布尔值,默认值为false。false时,会匹配到以/结尾的路径。比如:path设置为/home默认情况下/home/也会导致组件挂载。设置为true时,以/结尾的路径不会被匹配。

component

设置路径匹配后需要挂载的组件。作用和Route的标签体类似。

<Route path="/home" component={Home}/>

和标签体指定组件不同,如果通过component属性指定组件,React Router会自动向组件中传递三个参数match、location和history。

match

对象,表示请求匹配的路径信息,其中包含四个属性:
在这里插入图片描述
location

对象,表示浏览器地址栏的信息,请求完整路径、查询字符串等,可能具有的属性:
在这里插入图片描述
history

对象,用来读取和操作浏览器的历史记录(页面跳转)等功能,属性:
在这里插入图片描述
render

render也是Route组件中的属性,和component类似,也用来指定路径匹配后需要挂载的组件。只是render需要的是一个回调函数作为参数,组件挂载时,render对应的回调函数会被调用,且函数的返回值会成为被挂载的组件。render的回调函数中会接收到一个对象作为参数,对象中包含三个属性,即match、location和history,我们可以根据需要选择是否将其传递给组件。

<Route path="/student/:id" render={routeProps => <Student {...routeProps}/>} />

children

children实际上就是组件的组件体,设置方式有两种一个是通过组件体设置,一个是通过children属性设置。它的值也有两种方式,一种直接传递组件,这样当路径匹配时组件会自动挂载。一种是传递一个回调函数,这样它和render的特点是一样的。

直接设置组件:<Route path="/student/:id" children={<Student/>} />

<Route path="/student/:id">
    <Student/>
</Route>

传递回调函数:

<Route path="/student/:id" children={routeProps => <Student {...routeProps}/>} />
<Route path="/student/:id">
    {routeProps => <Student {...routeProps}/>}
</Route>

需要注意的时,当children接收到的是一个回调函数时,即使路径没有匹配组件也会被挂载到页面中(没有使用Switch标签的情况下),这一特性可以在一些特殊应用场景下发挥作用。如果不希望出现路径不匹配时组件被挂载的情况,最好选择使用render来代替。

  1. Switch组件
    Switch组件是Route组件的外部容器,可以将Route组件放入到Switch组件中。放入Switch组件中后,匹配路径时会自动自上向下对Route进行匹配,如果匹配到则挂载组件,并且一个Switch中只会有一个Route被挂载。如果将Route组件单独使用,那么所有的路径匹配的Route中的组件都会被挂载。

  2. Link组件
    Link组件作用类似于a标签(超链接),并且Link组件在浏览器中也会被渲染为超链接。但是Link组件生成的链接点击后只会修改浏览器地址栏的url,并不会真的向服务器发送请求。这种方式有利于组件的渲染,所以在开发中应该使用Link组件而不是超链接。

其他组件

  1. HashRouter组件
    除了BrowserRouter以外,react router中还为我们提供了HashRouter,它是干什么用的呢?其实很简单,当我们使用BrowserRouter时,路径会直接根据url地址进行跳转,也就是我们在使用应用时在浏览器的地址栏看到的地址就和我们正常去访问网页一样。

但是,HashRouter不是这样,使用HashRouter时,组件的跳转不再是以完整的url形式,而是通过url地址中的hash值进行跳转(url地址中#后的内容为hash值)。
在这里插入图片描述
为什么会有这两种Router呢?首先,你要明确我们的项目在开发完成后需要进行构建,构建后的代码需要放到服务器中,以供用户访问。服务器无非就是Nginx或Apache这些东西,服务器的主要功能是将url地址和网页进行映射。传统web项目中,每一个页面都对应一个文件,当用户访问/index.html时,服务器会自动返回根目录下的index.html。当用户访问/about.html时,服务器会返回根目录下about.html。换句话说url和文件的映射都是由服务器来完成的。

但是React项目不同,React项目所有的页面都是通过React进行渲染构建的。项目中只存在一个index.html没有那么多的页面(所以才叫单页应用)。当浏览器地址发生变化时,比如用户访问/about时,此时是不需要服务器介入的,react router会自动挂载对应的组件。

当我们将React项目部署到服务器时,如果直接访问根目录,请求会直接发送给index.html。这个页面我们是有的,所以此时不会有任何问题。用户访问页面后,点击页面后的连接切换到不同的组件也没有问题,因为页面并没有真的发生跳转,而是通过react router在内存中完成了模拟跳转。但是,当我们刷新某个路由或直接通过浏览器地址栏访问某个路由时,比如:http://localhost:3000/about,此时请求会发送给服务器,服务器会寻找名为about的资源(此时并没有经过React)。显然找不到这个资源,于是返回404。

这样一来,我们的项目只能够通过首页访问,然后点击链接跳转,刷新和直接通过路由访问都是不行的,一旦进行这些操作就会出现404。

怎么办呢?两种解决方式:

  1. 使用HashRouter,HashRouter通过hash地址跳转,而服务器不会处理hash地址,这样地址就会交由React处理,路由便可正常跳转。缺点是url地址上总会多出一个#,但不妨碍使用。
  2. 修改服务器映射规则,将所有的请求交给React处理,禁止服务器自动匹配页面。以nginx为例,可以将nginx.conf中的配置信息修改如下:
location / {
    root   html;
    try_files $uri /index.html;
}

两种方式都可以解决404的问题,具体采用那种方案,需要根据你自己项目的实际情况选择。

  1. NavLink组件
    特殊版本的Link,可以根据不同的情况设置不同的样式。

属性:
在这里插入图片描述
3. Prompt组件
prompt组件可以在用户离开页面前弹出提示。

属性:
在这里插入图片描述
4. Redirect组件
将请求重定向到一个新的位置,经常用来进行权限的处理。例如:当用户已经登录时则正常显示组件,用户没有登录时则跳转到登录页面。

{isLogin && <SomeAuthComponent/>}
{!isLogin && <Redirect to={"/login"}></Redirect>}

上例中,如果isLogin的值为true,表示用户已经登录,若用户登录,则挂载对应组件。若isLogin值为false,则挂载Redirect组件触发重定向,重定向会使得路径跳转到登录页面。

属性:
在这里插入图片描述
5. 钩子函数
在这里插入图片描述

版本6

安装:

npm

npm install react-router-dom@6 -S

yarn

yarn add react-router-dom@6

HelloWorld

import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter as Router, Link, Route, Routes } from ‘react-router-dom’;

const Home = ()=>{
  return <div>首页</div>
};

const About = () => {
  return <div>关于</div>
};

const App = () => {
  return <div>App
    <ul>
      <li>
        <Link to=/>home</Link>
      </li>
      <li>
        <Link to=/about”>about</Link>
      </li>
    </ul>

    <Routes>
      <Route path=/” element={<Home/>}/>
      <Route path=/about” element={<About/>}/>
    </Routes>
  </div>;
};

const root = ReactDOM.createRoot(document.getElementById(‘root’));

root.render(
  <Router>
    <App />
  </Router>
);

Routes组件
和版本5不同,6中的Route组件不能单独使用,而是必须要放到Routes组件中。简言之Routes就是一个存放Route的容器。

Route组件
Route作用和版本5的一样,只是变得更简单了,没有了那么多复杂的属性,并且Route组件必须放到Routes中,当浏览器的地址发生变化时,会自动对Routes中的所有Route进行匹配,匹配到的则显示,其余Route则不再继续匹配。可以将Route当成是一个类似于if语句的东西,路径(path)匹配则其中的组件便会被渲染。
在这里插入图片描述
Outlet组件
Outlet组件用来在父级路由中挂载子路由。

在版本6中Route组件是可以嵌套的,可以通过嵌套Route来构建出嵌套路由,像这样:

<Route path='/students' element={<StudentList/>}>
    <Route path=':id' element={<Student/>}/>
</Route>

上例中,Route嵌套后,如果访问/students则会挂载StudentList组件,如果访问/students/:id则会自动在StudentList组件中对Student组件进行挂载。在StudentList组件中就可以使用Outlet来引用这些被挂载的组件。

const StudentList = () => {
    return <div>
        学生列表
        <Outlet/>
    </div>
};

Link组件
和版本5的类似,具体区别看视频

NavLink组件
和版本5的类似,具体区别看视频

Navigate
类似于版本5中的Redirect组件,用来跳转页面,具体看视频

部分钩子函数
在这里插入图片描述

关于Hook

关于React中的钩子函数,我们已经非常熟悉了。钩子函数的功能非常的强大,而它的使用又十分简单。关于钩子函数的使用,我们只需记住两点:

  1. 钩子只能在React组件和自定义钩子中使用
  2. 钩子不能在嵌套函数或其他语句(if、switch、white、for等)中使用

React中自带的钩子函数

在这里插入图片描述

UseMemo

useMemo和useCallback十分相似,useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果。在组件中,会有一些函数具有十分的复杂的逻辑,执行速度比较慢。闭了避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果,像是这样:

const result = useMemo(()=>{
    return 复杂逻辑函数();
},[依赖项])

useMemo中的函数会在依赖项发生变化时执行,注意!是执行,这点和useCallback不同,useCallback是创建。执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。

UseImperativeHandle

在React中可以通过forwardRef来指定要暴露给外部组件的ref:

const MyButton = forwardRef((props, ref) => {
    return <button ref={ref}>自定义按钮</button>
});

上例中,MyButton组件将button的ref作为组件的ref向外部暴露,其他组件在使用MyButton时,就可以通过ref属性访问:

<MyButton ref={btnRef}/>

通过useImperativeHandle可以手动的指定ref要暴露的对象,比如可以修改MyButton组件如下:

const MyButton = forwardRef((props, ref) => {

    useImperativeHandle(ref,()=> {
        return {
            name:'孙悟空'
        };
    });

    return <button>自定义按钮</button>
});

useImperativeHandle的第二个参数是一个函数,函数的返回值会自动赋值给ref(current属性)。上例中,我们将返回值为{name:‘孙悟空’},当然返回孙悟空没有什么意义。实际开发中,我们可以将一些操作方法定义到对象中,这样可以有效的减少组件对DOM对象的直接操作。

const MyButton = forwardRef((props, ref) => {

    const btnRef = useRef();

    useImperativeHandle(ref,()=> {
        return {
            setDisabled(){
                btnRef.current.disabled = true;
            }
        };
    });

    return <button ref={btnRef}>自定义按钮</button>
});

const App = () => {
    
    const btnRef = useRef();

    const clickHandler = () => {
        btnRef.current.setDisabled();
    };

    return <div>
        <MyButton ref={btnRef}/>
        <button onClick={clickHandler}>点击</button>
    </div>;
};

UseLayoutEffect

useLayoutEffect的方法签名和useEffect一样,功能也类似。不同点在于,useLayoutEffect的执行时机要早于useEffect,它会在DOM改变后调用。在老版本的React中它和useEffect的区别比较好演示,React18中,useEffect的运行方式有所变化,所以二者区别不好演示。

useLayoutEffect使用场景不多,实际开发中,在effect中需要修改元素样式,且使用useEffect会出现闪烁现象时可以使用useLayoutEffect进行替换。

在这里插入图片描述

UseDebugValue

用来给自定义钩子设置标签,标签会在React开发工具中显示,用来调试自定义钩子,不常用。

UseDeferredValue

useDeferredValue用来设置一个延迟的state,比如我们创建一个state,并使用useDeferredValue获取延迟值:

const [queryStr, setQueryStr] = useState('');
const deferredQueryStr = useDeferredValue(queryStr);

上边的代码中queryStr就是一个常规的state,deferredQueryStr就是queryStr的延迟值。设置延迟值后每次调用setState后都会触发两次组件的重新渲染。第一次时,deferredQueryStr的值是queryStr修改前的值,第二次才是修改后的值。换句话,延迟值相较于state来说总会慢一步更新。

延迟值可以用在这样一个场景,一个state需要在多个组件中使用。一个组件的渲染比较快,而另一个组件的渲染比较慢。这样我们可以为该state创建一个延迟值,渲染快的组件使用正常的state优先显示。渲染慢的组件使用延迟值,慢一步渲染。当然必须结合React.memo或useMemo才能真正的发挥出它的作用。

UseTransition

当我们在组件中修改state时,会遇到复杂一些的state,当修改这些state时,甚至会阻塞到整个应用的运行,为了降低这种state的影响,React为我们提供了useTransition,通过useTransition可以降低setState的优先级。

useTransition会返回一个数组,数组中有两个元素,第一个元素是isPending,它是一个变量用来记录transition是否在执行中。第二个元素是startTransition,它是一个函数,可以将setState在其回调函数中调用,这样setState方法会被标记为transition并不会立即执行,而是在其他优先级更高的方法执行完毕,才会执行。

除了useTransition外,React还直接为为我们提供了一个startTransition函数,在不需要使用isPending时,可以直接使用startTransition也可以达到相同的效果。

UseId

生成唯一id,使用于需要唯一id的场景,但不适用于列表的key。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值