Bootstrap

React学习教程

React

React前言

官方文档

  • React 18 中文文档1(国内社区):https://react.docschina.org/
  • React 18 中文文档2(官方):https://zh-hans.reactjs.org
  • React 18 英文文档:https://reactjs.org
  • React 17 英文文档:https://17.reactjs.org

react概述

  • React 是一个用于动态构建用户界面的 Js 库

  • 起源于 Facebook ,并于 2013 年 5 月开源

  • 当前最新版本是18的版本

    • 2020升级为17版本(变化特别大,添加hook语法)
    • 2022升级为18版本(变化不大)
  • React本身只关注界面,其它如:前后台交互、路由管理、状态管理等都由其它插件搞定

react特点

1.声明式

react利用JSX 语法来声明描述动态页面, 数据更新界面自动更新

React.createElement() 是命令式

声明式&命令式
  • 声明式

    只需更新数据,具体操作不需要写

    声明式不用我们亲自操作原生DOM就能去做页面的动态初始显示和更新显示,使用 JSX 语法来描述页面,只要更新状态数据界面就会自动更新

  • 命令式

    声明式的反面是命令式

    命令式即具体如何操作需要我们自己定义

2.组件化
  • 将一个较大较复杂的界面拆分成几个可复用的部分,封装成多个组件, 再组合使用
  • 组件可以被反复使用
3.一次学习 随处编写
  • 不仅可以开发 web 应用(react-dom)
  • 还可以开发原生安卓或ios应用(react-native)
4.高效
  • React虚拟DOM
  • React 虚拟 DOM Diff 算法(高效更新,更新时不是全部更新)

React基础

基本使用

步骤

在这里插入图片描述

  1. 引入两个JS文件

    ⚠注意文件引入顺序

    react-dom.development.js基于react.development.js,顺序不可改

    • react17版本

      <!-- 提供了React对象 -->
      <script src="./lib/17/react.development.js"></script>
      <!-- 提供了ReactDOM对象 -->
      <script src="./lib/17/react-dom.development.js"></script>
      
    • react18版本

      <!-- 引入react库,提供React对象 -->
      <script src="./lib/react18/react.development.js"></script>
      <!-- 引入react-dom库,提供ReactDOM对象 -->
      <script src="./lib/react18/react-dom.development.js"></script>
      
  2. 在html定义一个根容器标签

    一般id值为root或app

    <div id="root"></div>
    
  3. 创建react元素

    react元素类似html元素

    中括号代表参数是可选的,可以不传

    React.createElement(
    	type, //标签名
        [props], //包含所有标签属性的对象
        children1,
        children2... //任意多个子节点,可能是字符串,也可能是react元素
    )
    
    const element = React.createElement(
      'h1',
      {title: '你好, React!'},
      'Hello React!'
    )
    //有标签子节点,在children处创建新的react元素
    //没有属性,则传空对象{}或者null
    const element = React.createElement(
      "h1",
      { title: "你好, React!" },
      React.createElement("p", {}, "Hello1"),
      React.createElement("p", null, "Hello2")
    );
    
  4. 渲染 react 元素

    react18 与 react17 版本在渲染react元素的API上不一样

    新项目更多的使用18,而旧的项目可能用的就是17的版本

    • react17版本

      ReactDOM.render(
      	element, //react元素
          container //页面渲染元素的容器元素
      )
      
      ReactDOM.render(element, document.getElementById('root'))
      
    • react18版本

      // 4.1 创建指定了页面容器元素的root对象
      const root =  ReactDOM.createRoot(container);
      // 4.2 渲染react元素
      root.render(element)
      
      ReactDOM.createRoot(document.getElementById('root')).render(element)
      

ReactDOM中的DOM全部是大写

特殊属性-类名

在react里设置类class属性时,应写className,而不是class

class属性最终要转换为DOM元素的className属性 => 标签的class属性

const element = React.createElement(
  'h1',
  {className: "active"},
  'Hello React!'
)
React元素

React元素也称为虚拟 DOM (virtual DOM 简写vdom) 或虚拟节点(virtual Node简写vnode)

React元素是一个普通的 JS 对象,不是真实 DOM 元素对象

虚拟DOM&真实DOM
虚拟 DOM真实 DOM
差异较轻的对象(属性少)较重的对象(属性多)
占用内存少,创建快占用内存多,创建慢
关系在渲染时,虚拟DOM最终都会转换为真实DOM(通过root.render(vnode)转换)
React元素属性
{
  	type: , "h1"//标签名
  	props: {
        title: '你好, React!' //n个标签属性,
    	children: : 'Hello React!' //字符串 / vnode / vnode的数组
  	},
  	key: 1//节点的唯一标识
}
  1. 标签名 => type: “h1”

  2. 标签属性 => props: {title: ‘你好, React!’}

  3. 子节点 => props: {children: ‘Hello React!’}

  4. key => 节点的唯一标识

JSX

创建的子元素多后,React.createElement()的语法会变得复杂,使用JSX

在这里插入图片描述

const element = React.createElement(
"div",
 {},
 React.createElement("h2", { title: "北京疫情" }, "123"),
 React.createElement("p", { className: "active" }, "321")
);
ReactDOM.createRoot(document.querySelector("#root")).render(element);
基本使用

JSX 是一种JS 的扩展语法, 用来快速创建 React 元素(虚拟DOM/虚拟节点)

  1. JSX形式上类似HTML标签,且标签内部可以套JS表达式
const h1 = <h1 className="active">哈哈哈</h1> 
//可以用括号括起内容
const h1 = (<h1 className="active">哈哈哈</h1> )
  1. 浏览器无法识别JSX,需要引入babel将jsx 编译成React.createElement的形式
    • babel作用
      • es6 => es5
      • jsx => js
  • babel编译 JSX 语法的包为:@babel/preset-react
  • 运行时编译可以直接使用babel的完整包:babel.js
JSX注意点
  1. 不要加引号,否则是字符串而不是JSX

    let vnode = <p>aaa</p> ✅
    let vnode = "<p>aaa</p>" ❌
    
  2. 必须有结束标签

    <input type="text"><input type="text" /><span><span><span></span>
  3. 整个JXS只能有一个根标签

    ✅
    let vnode = (
    	<p>
        	<span>aaa</span>
        </p>
    )
    ❌
    let vnode = (
        <p>
        	<span>aaa</span>
        </p>
    	<p>bbb</p>
    )
    
  4. 空标签可以自闭合

JSX使用JS表达式

jsx表达式用于显示动态数据

JSX中使用JS表达式的语法:{表达式}

  1. {表达式}使用

    1. 标签体文本
    2. 标签属性值
  2. 可以是js表达式,但不能是js语句

    <p>---{title.toUpperCase()}----</p><p>---{title.toUpperCase();}----</p>

    ⚠有分号即为语句

  3. 基本数据中的null、undefined、布尔值在页面中不做任何显示,不会报错

  4. 可以是数组,会自动遍历数组进行显示

    <p>{[1,2,3]}</p>
    
  5. 可以是react元素对象,但不能是非react元素的js对象

    1. react元素对象可以显示

      <p>{<span>aaa</span>}</p>
    2. js对象无法显示,会报错

      例外:设置行内样式必须使用js对象

      <p>{{name:"asa",age:20}}</p>
  6. style属性值必须是一个包含样式的js对象

    <p style="color:'red'">aaa</p><p style={{color:"red"}}>aaa</p>
  7. jsx注释代码

    {/* 注释内容 */}
    

条件渲染

1.满足条件显示界面1,否则显示界面2

需求1: 如果loading为真显示提示loading界面, 否则显示结果页面

let loading = false
let vdom
1.if else
if (loading) {
  vdom = <h2>正在加载中...</h2>
} else {
  vdom = <p>结果页面</p>
}
2.三目表达式
vdom = loading ? <h2>正在加载中222...</h2> : <p>结果页面222</p>
2.只有满足条件才显示界面

不满足条件也不会报错,只是不显示界面

&&

需求2: 只有loading为真才显示界面

vdom = loading && <h3>loading...</h3>

列表渲染

利用react会自动遍历数组元素显示的特性

  1. 根据数组生成标签数据 arr.map()

    <ul>{arr.map((item) => <li>{item}</li>)}</ul>
    

⚠不是花括号,是大括号

  1. 列表中的vnode都需要有一个唯一的key属性值
const arr = [
  { id: 1, name: "React" },
  { id: 3, name: "Vue" },
  { id: 5, name: "小程序" },
];
const vdom = (
  <div>
    <h2>前端框架课程列表</h2>
    <ul>
      {courses.map((c) => ( //⚠不是花括号,是大括号
        <li key={c.id}>{c.name}</li>
          //⚠此处的li是JSX,而不是html
      ))}
    </ul>
  </div>
);
ReactDOM.createRoot(document.getElementById("root")).render(vdom);
script>

样式处理

行内样式
  • style属性值必须是一个包含样式的js对象
  • 样式属性名使用小驼峰命名法 camelCase
  • 如果样式是数值,可以省略px单位
<h2 style={{color: 'red', fontSize: 30}}>React style</h2>
<h2 style={{color: 'red', fontSize: '30px'}}>React style</h2>
类名样式
  • 必须用className,不能用class
  • 比起行内样式更推荐使用类名添加css样式,效率更高些
<h2 className="title">React class</h2>

事件处理

绑定事件

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • 命名采用小驼峰式(camelCase),比如:onClick、onFocus 、onMouseEnter
  • 使用 JSX 语法时需要传入一个函数作为事件处理函数,而不是一个字符串(用大括号包裹,而不是引号)
const div = <div onClick={事件函数}></div>
React事件特点

React事件不是原生DOM事件,而是合成事件(即包装原生事件产生的对象)

  1. React事件有原生事件的所有重要属性+新属性nativeEvent(原生事件对象)

  2. react事件与原生事件基本都是对应关系,一个例外——表单事件的change事件

    1. react事件的change监听在输入过程中触发
      1. 因为react事件的onChange事件内部绑定的是原生input事件
    2. 而原生是在失去焦点才触发
  3. 处理好了浏览器的兼容性问题

    React 根据 W3C 规范来自定义的合成事件,屏蔽了底层浏览器的细节差异,保证了行为的一致性

  4. React事件阻止事件默认行为

    return false 不起作用

    e.preventDefault()
    

React脚手架

使用官方react脚手架原因:

  • JSX 转 JS 和 ES6 转 ES5 语法运行时编译效率低

  • webpack打包环境搭建复杂且没有质量保证, 效率低

使用脚手架创建react项目

使用脚手架工具 create-react-app 来创建React项目

方式一 npm全局下载

全局下载后(只需下载一次),再通过其命令创建项目

这种方式下载版本不一定是最新的,推荐学习时使用

npm i create-react-app -g
create-react-app 项目名称
方式二 npx最新下载

利用 npx 临时下载最新的,同时运行其命令创建项目

在公司开发时,最好使用npx,而不用全局下载的方式 =》 能保证用的是最新的

npx create-react-app 项目名称
npx 做的事情:
  1. 先全局下载 create-react-app
  2. 执行 create-react-app 命令, 创建 react 项目
  3. 自动将 create-react-app 从全局中删除掉(保证每次都下载最新的

最后cd到项目文件夹,npm strart开始项目

⚠引入React & ReactDOM

import React from "react";
//react18
import ReactDOM from "react-dom/client";
//react17
import ReactDOM from "react-dom";
安装chrome调试工具

安装谷歌插件React Developer Tools

功能

  • 访问react项目时,插件图标会亮
  • 多了调试选项: Components,查看应用中组件组成和各个组件的相关数据(props/state)

国内安装谷歌插件

  • 极简插件: https://chrome.zzzmh.cn/#/index
  • 解压出chrome插件文件: .crx文件
  • 进入谷歌扩展程序列表页面
  • 将crx文件拖拽到列表页面,当页面显示安装时再松手

React组件

类组件目前不怎么用,都用函数组件

组件与组件化
组件
  1. 组件:实现一个局部功能界面的所有代码的集合(jsx(js+html)、css、img)

    相当于jsx(js+html)、css、img组合而成的标签

    • 因此使用时也是标签的形式

      比如APP是一个组件,使用时==<APP/>==即可

    • 虽然函数组件返回的是一个函数,但不能由我们自己调用,而是由react来调用,所以不是APP()而是<APP/>

  2. 一个组件中可能会用到多个js模块

  3. 优点:可以复用

    一个React应用就是由多个React组件组合而成的

  4. 一个组件可以有内部动态数据(state状态), 也可以接收外部传入的动态数据(props)

组件化

组件化是一种项目的编码方式

将界面的功能界面拆分成多个组件来实现

定义组件的方式
定义注意点
  1. 组件名首字母必须大写

    react以组件名来区分自定义组件元素和一般元素

  2. 组件必须有返回值

    返回内容就是组件呈现的结构

    返回值为null,表示不渲染任何内容(undefined、布尔值也一样,但一般用null)

  3. return的根标签只能有一个

    export default function App(){
      //js代码写在return外面
     console.log("aaa")
      return (
        <div>
          <img src={logo} alt="" />
          <p>123</p>
        </div>
      )
    }
    
1.函数组件
// 定义组件
function App() {
  // return null  表示不渲染任何内容
  return <div>App</div>
}

// 渲染组件标签
ReactDOM.createRoot(document.querySelector('#root')).render(<App />)
  1. 函数组件在组件标签渲染时调用,但不会new产生实例对象

    类组件在组件渲染时会new产生实例对象

  2. 函数组件没有state状态,但可以通过hooks语法提供

2.类组件
// 定义类组件
import React from "react"
class App extends React.Component {
  render () {
    return <div>App Component</div>
  }
}

// 渲染组件标签
ReactDOM.createRoot(document.querySelector('#root')).render(<App />)
  1. 类组件继承React.Component父类,从而使用Component类中提供的方法或属性
  2. 类组件中必须声明一个render函数,来返回组件界面的虚拟DOM元素
  3. 类组件在组件标签渲染时调用,new 产生实例对象
  4. 类组件有state状态

react便捷操作vscode插件

在这里插入图片描述

  • 快速创建类组件rcc

    react class component

  • 快速创建函数组件rfc

    react class component

类组件状态state
概述
  1. 状态(state)是内部动态数据 ,是组件内部的私有数据,只能在组件内部使用

  2. 函数组件又叫无状态组件,类组件又叫有状态组件

    函数组件没有state,只能根据外部传入的数据(props)动态渲染

    类组件有自己的state数据,一旦更新state数据, 组件界面就会自动更新

  3. 组件对象state属性的属性值为对象,可以在state对象中保存多个数据

    state = {
    	name:"asa",
        age:20
    }
    
使用
初始化state
  1. 方式一:推荐

    state = {xxx: value}
    
  2. 方式二:在构造器中初始化

    1. 子类有构造器必须调用super()
    2. 初始化state
    constructor () {
      // 1.必须调用super()
      super() 
      // 2.初始化state
      this.state = {
         xxx: value
    }
    
读取state
this.state.xxx
更新state

使用方法**setState**进行更新

this.setState({xxx: newValue})

⚠不能使用this.state.xxx = newValue进行更新,页面并不会更新显示

实例
class StateTest extends React.Component {
  state = {
    count: 0,
    xxx: 'abc'
  }
  render () {
    // 读取state数据
    const {count} = this.state
    return <div onClick={() => {
      // 通过setState()更新state => 界面会自动更新
      this.setState({
        count: count + 1
      })
    }}>点击的次数: {count}</div>
  }
}
事件回调this问题

为了提高代码的性能和阅读性,最好把事件函数定义在return的组件界面结构外面

而要读取类组件的state,this.state.xxx的this必须指向组件对象

但在严格模式下,事件函数的this指向undefined

事件回调都不是组件对象调用的,是事件触发后直接调用的

handleCLick () {
  this.setState({
    count: this.state.count + 1
  })
}
<button onClick={this.handleCLick}>有问题</button><br/>
解决办法

方式一和方式二都利用了箭头函数的this穿透问题

  1. 方式一:将事件函数定义为箭头函数

    handleCLick2 = () => {
      this.setState({
        count: this.state.count + 3
      })
    }
    <button onClick={this.handleCLick2}>方式一</button>
    
  2. 方式二:用箭头函数包裹事件函数

    <button onClick={() => this.handleCLick()}>方式二</button>
    //这种方法可以传递参数
    <button onClick={(e) => this.handleCLick(e, 'abc')}>方式二</button>
    
  3. 方式三:bind更改事件函数this指向

    <button onClick={this.handleCLick.bind(this)}>方式三</button>
    

方式的选择:

  1. 一般用箭头函数方式,编码简洁
  2. 如果要参数,用包裹箭头函数方式
组件props
概述
  1. props是外部传入动态数据

    组件是封闭的,接收外部数据通过 props 来实现,

    props是父组件向子组件传递的数据

  2. props可以给组件传递任意类型的数据

  3. props是只读对象,子组件只能读取属性的值,不要修改props

  4. 如果父组件传入的是动态state数据,那一旦父组件更新state数据,子组件也会更新

使用
父组件传递

父向子传入数据:给组件标签添加属性

<APP name1="asa" name2={"asa"} age1="20" age2={20}/>
//name1、name2、age1为字符串
//age2为数字

//一次传递多个属性
<Child {...obj}>
子组件读取
  1. 函数组件:通过参数props接收数据
  2. 类组件:通过this.props接收数据
更新props

更新只能在父组件更新,而不在子组件更新

如果父组件传入的是动态的state数据,那一旦父组件更新state数据,子组件也会更新

实例
  • 父页面

    import { Son1, Son2 } from "./son";
    
    export default class Classb extends Component {
      state= {
        user:{
          name:"asa",
          age:20
        }  
      }
      fn=()=>{
        this.setState({user:{name:"oooo",age:10000}})
      }
      render() {
        let {name,age} = this.state.user
        return (
          <div>
            father:
            name:{name}  age:{age}
            <Son1 name={name} age={age}></Son1>
            <Son2 name={name} age={age}></Son2>
            //简洁传法: <Son2 {...user}></Son2>
            <button onClick={this.fn}>更新</button>
          </div>
        )
      }
    }
    
  • 子页面 son.jsx

    export  class Son1 extends Component {
      render() {
        let {name,age}=this.props;
        return (
          <div>Son1:name:{name}  age:{age}</div>
        )
      }
    }
    
    export  function Son2(props) {
      const {name,age} = props;
      return (
        <div>Son2:name:{name}  age:{age}</div>
      )
    }
    
props校验&默认值-了解

不怎么用

函数组件函数体外指定

类组件的类体内指定

props校验

指定并检查接收prop的类型和必要性

  1. 导入 prop-types 包 (内置包

    import PropTypes from 'prop-types'
    
  2. 指定校验

    //函数组件
    FunPropsCheck.propTypes = {
      myName: PropTypes.string.isRequired, 
      age: PropTypes.number,
    }
    //类组件
    static propTypes = {
      myName: PropTypes.string.isRequired,
      age: PropTypes.number,
    }
    //myName属性: 字符串类型, 必须有
    //age属性: 数值类型, 不是必须的
    
props默认值

如果prop没有传入,指定默认值是多少

//函数组件
FunPropsCheck.defaultProps = {
  age: 0
}
//类组件
static defaultProps = {
  age: 0
}
//age属性:默认值为0
类组件生命周期
生命周期图

生命周期图谱:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
在这里插入图片描述

生命周期三大阶段
挂载阶段

流程:constructor => render => componentDidMount

触发:render()渲染组件元素

更新阶段

流程:render => componentDidUpdate

触发:setState() 、forceUpdate()、 组件接收到新的props

卸载阶段

流程:componentWillUnmount

触发:不再渲染组件、条件渲染不再满足条件

生命周期钩子
constructor
  • 只执行一次:创建组件对象挂载时第一个调用
  • 用于初始化state属性或其它实例属性方法(可以简写到类体中)
render
  • 执行多次:挂载调用一次 + 每次state/props更新都会调用
  • 用于返回要初始显示或更新显示的虚拟DOM界面

render的调用(高效)

  • 第一次调用render => 得到虚拟DOM =>生成真实DOM
  • 重新调用render => 得到新虚拟DOM => 进行新旧虚拟DOM差异比较 => 最小化的真实DOM的更新
componentDidMount
  • 执行一次:在第一次调用render且组件界面已显示之后调用
  • 用于初始执行一个异步操作:发ajax请求/启动定时器等
componentDidUpdate
  • 执行多次:组件界面更新(真实DOM更新)之后调用
  • 用于数据变化后, 就会要自动做一些相关的工作(比如: 存储数据/发请求)
componentWillUnmount
  • 执行一次:在组件卸载前调用
  • 用于做一些收尾工作,如:清除定时器

React Hooks

官方hook

  1. 基础 Hook
  1. 额外的 Hook
概述
  • Hook 是 React 16.8 的新增特性。
  • Hook也叫钩子,本质就是函数,能让你使用 React 组件的状态和生命周期函数…
  • Hook语法基本代替了类组件的语法,可以在不编写类组件的情况下使用 state 以及其他的 React 特性
Hook规则
  1. 只在最顶层使用 Hook,不要在条件或循环中
    1. hook按照创造hook的顺序排列
  2. 只在React组件函数内部中调用 Hook,不要在组件函数外部调用
useState()❗

用来定义状态数据

可以多次调用,产生多个状态数据

1.初始执行useState
useState(初始值)

所做的事:

  1. React创建一个state数据,将state数据初始值定为传入的参数
  2. 同时定义一个更新state数据的函数
  3. 返回一个数组:[state数据,更新state数据的函数]
useState()以数组形式返回
const [xxx, setXxx] = useState(initValue)
//定义一个数据
const [name, setName] = useState("asa")
//定义多个数据
//1.方式一:调用2次useState()
const [name, setName] = useState("asa")
const [age, setAge] = useState(20)
//2.方式二:使用对象???
const [name, setName] = useState("asa")
2.更新state数据

更新内部的状态数据,并触发组件的更新,即重新调用组件函数(重新渲染组件)

setXxx的2种调用方式
  1. setXxx(newValue)

    更新为指定某个值时,这种方式更占优势,如点击直接变成5

  2. setXxx(value => newValue)

    • value是内部保存的状态数据值,总是内部存储的最新值
    • newValue指定更新后的值
    • 更适合用于基于原数据更新数据的情况,如每次加一

    在异步函数里必须使用这种方式,因为val总是内部存储的最新值

    详见useEffect的调用一注意点

setXxx的"异步"问题❗

参考

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
3.再次执行useState

返回内部存储的状态数据和更新函数的数组

实例
//需要引入useState方法
import React, { useState } from 'react';

export default function App() {
  const [name,setName] = useState("asa")
  return (
    <div>
      <p>{name}</p>
      <button onClick={()=>setName(name+"o")}>click1</button>
      <button onClick={()=>setName(value => value+"o")}>click2</button>
    </div>
  )
}
useEffect()

模拟类组件的生命周期,相当于componentDidMount、componentDidUpdate、componentWillUnmount的组合

用于执行一些带副作用的操作(异步、数据存储、更新DOM,更新状态数据)

可以在一个组件中多次使用

模拟周期说明
useEffect(() => {}, [])componentDidMount只在初始化执行一次
可以在回调中执行异步操作:启动定时器、发ajax请求等
没有监视任何数据变化
useEffect(() => {})componentDidMount + componentDidUpdate1.初始化执行一次
2.任意更新后都会执行
useEffect(() => {}, [xxx])componentDidMount + xxx更新的componentDidUpdate初始化执行一次
2.xxx更新后执行
useEffect(() => { return () => {} }, [])componentWillUnmountreturn的函数中的内容为卸载前调用执行
调用一
useEffect(() => {}, [])

相当于componentDidMount

只在初始化执行一次

可以在回调中执行异步操作: 启动定时器,发ajax请求

⚠注意点

在这里插入图片描述

useRef()

用于获取标签对象或自定义数据

获取标签对象
  1. 创建一个ref容器

    const inputRef = useRef();
    
  2. 将ref容器交给标签

    <input ref={inputRef}/>
    
  3. 通过ref容器获取标签

const input = inputRef.current
export default function App() {
  const inputRef = useRef();
  function fn(){
    const input = inputRef.current
    alert(input.value)
  }
  return (
    <div>
      <input type="text" ref={inputRef}/>
      <button onClick={fn}>获取input输入值</button>
    </div>
  )
}
获取自定义数据

ref用来存储标识,更新数据后可立即获取,无需等到重新渲染组件函数后才能获取最新数据

  1. 创建ref容器

    const myRef = useRef(初始值)
    
  2. 存储数据

    myRef.current = 数据
    
  3. 读取数据

    myRef.current
    
export default function App() {
  const msgRef = useRef("asa");
  const timer
  //创建msgRef容器,并设置msgRef初始值为asa,若此处不传初始值,读取时为undefined
  return (
    <div>
      <button onClick={()=>{
        msgRef.current = "ooo"
      }}>更改msgRef值为ooo</button>
      <button onClick={()=>{
        alert(msgRef.current)
      }}>读取并弹窗msgRef值</button>
      <button onClick={()=>{
        msgRef.current = "bbb"
      }}>更改msgRef值为bbb</button>
    </div>
  )
}

useState和useRef存储数据的区别

  1. useState

    用于显示在界面上,会更新

  2. useRef

    不用于显示在界面上,仅开发人员自己存储数据使用(如定时器的id等

useContext()

读取context提供的数据

收集表单数据

受控组件&非受控组件都是包含表单的组件

  • 非受控组件无法进行表单校验(只能提交时才能校验)

  • 受控组件可以实现实时表单校验(在输入过程中就进行校验)

非受控组件

输入的数据不与state数据关联同步,需要手动读取表单元素的值

借助于 useRef,使用原生 DOM 方式来获取表单元素值

export default function App() {
  const submit = (e) => {
    e.preventDefault();
    let name = nameRef.current.value;
    alert(name);
  };
  const nameRef = useRef();
  return (
    <form>
      用户名: <input type="text" ref={nameRef} /> <br />
      <input type="submit" value="登 陆" onClick={submit} />
    </form>
  );
}
受控组件

输入数据与状态数据同步,双向绑定:

优势:受控组件可以实时表单校验(在输入过程中就进行校验)

实现步骤

  1. 在 state 中添加一个数据,作为表单元素的value值
  2. 给表单元素绑定 change 事件,将表单元素的值设置为 state 的值
export function App2() {
  const submit = (e) => {
    e.preventDefault();
    alert(name);
  };
  const [name, setName] = useState();
  const fn = (e) => {
    setName(e.target.value);
  };
  return (
    <form onSubmit={submit}>
      用户名: <input type="text" value={name} onChange={fn} /> <br />
      <input type="submit" value="登 陆" />
    </form>
  );
}

组件化编码

基本流程
1.拆分组件
  1. 拆分界面,定义组件
  2. 为每个组件分别创建一个文件夹用来存储jsx和css代码
2.实现静态组件界面

在组件中编写html、css

(可以先用一个静态页面实现,再拆分到每个组件中)

3.实现动态组件
  1. 动态初始化显示

    1. 设计状态数据

      1. 数据的类型/结构
      2. 数据名称
      3. 数据存在于哪个组件:看该state数据是什么组件需要
        • 只有一个组件需要——存于该组件本身
        • 多个组件需要——存于这些组件的父组件
    2. 传递给相应组件进行显示

      通过标签属性props进行传递

  2. 实现交互功能

    如:添加/删除/勾选

    • state数据在哪个组件,更新状态数据的行为/函数就定义在哪个组件

    • 如果子组件需要更新父组件的state,将更新state的函数通过props传递给子组件调用

    • 步骤

      1. 在父组件中定义更新状态数据的函数
      2. 通过props将函数传递给子组件
      3. 子组件中调用接收的函数去更新父组件的状态数据
案例

在这里插入图片描述

功能描述

  1. 动态显示初始列表
  2. 添加一个 todo
  3. 删除一个 todo
  4. 反选一个 todo
  5. todo 的全部数量和完成数量
  6. 全选/全不选 todo
  7. 删除完成的 todo
  8. todo列表持久化
实现步骤
  1. 拆分组件:拆分界面,定义组件

在这里插入图片描述

组件通讯

组件通讯

组件间的数据交互,一个组件向另一个组件发送数据

组件间的关系

  • 父子
  • 祖孙
  • 兄弟或其它
props通讯

通过标签属性传递数据进行通讯

缺点

无法跨层通讯,只能逐层多次传递

传递方式
  • 父向子:通过子组件标签传递非函数属性

  • 子向父:通过子组件标签传递函数属性

  • 祖向孙:通过子组件标签传递非函数属性(逐层多次传递)

  • 孙向祖:通过子组件标签传递函数属性(逐层多次传递)

  • 兄弟间:借助父组件进行通讯

    将state数据定义在父组件,再将state数据和更新state数据的函数分别传递给兄弟子组件使用

context通讯

context可以实现跨层直接通信

传递方式
  • 祖向孙
  • 孙向祖
使用
  1. 创建一个context容器对象

    const context = React.createContext()
    
  2. 最外层组件向后代组件提供数据

    import context from './context'
    
    <context.Provider value={要传递的数据}>
      <Child1 /> 
    </context.Provider>
    
  3. 后代组件读取数据

    import context from './context'
    const data = useContext(context)
    
pubsub-js

消息发布订阅的js库

实现任意组件间的直接通讯

配置
安装
npm i pubsub-js
引入
import PubSub from 'pubsub-js'
订阅消息
PubSub.subscribe('消息名', (msgName, data) => {})
  • 参数
    • msgName:消息名,没用
    • data:发布的数据
  • 返回值
    • 返回一个标识token,用于取消订阅
    • token为唯一的
发布消息
PubSub.publish('消息名', 数据)
取消订阅
PubSub.unsubscribe(token/消息名)
  1. token

    token即订阅消息时返回的token

    通过token取消订阅,只能取消一个订阅

  2. 消息名

    由于一个消息可以被多个订阅

    通过消息名取消订阅,可能取消多个订阅

Ajax

测试接口
  • 接口1: https://api.github.com/search/repositories?q=re&sort=stars
  • 接口2: https://api.github.com/search/users?q=a
说明

在 react 项目开发中,基本都是使用 axios 库来发送 ajax请求与后台通信

发送Ajax的情况
  1. 初始化发一次请求

    在useEffect(() => {}, [])中发送 
    
  2. 在事件回调函数中发送请求

  3. 在某个state或props数据发生改变后发送请求

    useEffect(() => {}, [数据]) 中发送
    

⚠effect函数不能是async函数

需要用箭头函数包裹一层

()=>(
	async function(
		await axios.get()
	)
)
react配置代理

react/vue的开发环境中有一个专门处理ajax跨域的代理服务器:webpack-dev-server

  • webpack-dev-server的作用

    将项目代码在内存中打包,生成内存中的打包文件,并启动服务运行项目,就可以通过虚拟地址来访问项目

  • http-proxy-middleware

    • webpack-dev-server中的一个代理服务器工具包
    • 作用:将对当前项目的所有404的请求转发给指定的跨域的后台接口
  • react配置代理

    在package.json中追加配置,当请求了当前域不存在的资源时,会将该请求转发给proxy设置的域名进行请求

"proxy": "url"

//当请求了当前域不存在的资源时,将请求转发给4000端口
"proxy": "http://localhost:4000"

在这里插入图片描述

⚠此时需要跨域请求的url,前面不应该带域名

axios.get{
 url:"http://localhost:3000/serach/users"url:"/serach/users"}

⚠不能配置多个代理

Fragment

Fragment是原生DOM中,内存中用来保存多个DOM节点对象的容器

如果将Fragment添加到页面中,其本身不会进入页面但它的所有子节点会进行页面

可以使用Fragment作为根标签(前提:根标签没有样式)

Fragment可简写为空标签==<>jsx代码</>==

function Hello(){
  return (
    // 这样就只会渲染h1和p
    <React.Fragment>
      <h1>fragment</h1>
      <p>hello react</p>
    </React.Fragment>
  ) 
}

js补充

高阶函数&函数柯里化
高阶函数

参数是函数或返回值是函数的函数

  1. 参数是函数
    1. 数组遍历api
    2. Promise及其的then、finally等
    3. setInterval & setTimeout
  2. 返回值是函数
    1. bind
    2. 返回函数的闭包
函数柯里化

柯里化函数是一种特别的高阶函数

将一次传递参数的方式变为多次传递参数,函数就会嵌套返回,整个函数即称为柯里化函数

柯里化函数特点——懒计算

外层调用时先不进行计算,最内层的函数调用时才进行计算处理

function fn1(a, b, c) {
  return a + b + c;
}
console.log(fn1(1, 2, 3));
function fn2(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}
console.log(fn2(1)(2)(3));
纯函数&副作用函数
纯函数

多次调用传入相同的参数,结果一样

函数执行不影响外部环境:外部变量/外部存储/远程存储/异步

function fn(a,b){
    return a+b;
}
副作用函数

也称为不纯的函数

副作用函数的执行会影响周围环境,影响其他人

let c = 3;
function fn(a,b){
    //1.使用了外部数据
    return a+b+c;
    //2.更新了外部数据
    c = 5
    //3.存储了数据
    session.setItem("key",val)
    //4.异步操作
}
区别并实现bind和call
区别
callbind
都是用来改变函数中的this的
call立即调用bind只有当调用bind返回的函数时才会调用
选择看函数是否是需要立即调用, 如果不是,只能用bind
实现
实现bind
Function.prototype.bind = function (thisObj, ...args) {
  console.log('bind')
  // bind中的this是原函数
  return (...args2) => {
    return this.call(thisObj, ...args, ...args2)
  }
}
实现call
Function.prototype.call = function (thisObj, ...args) {
  if(thisObj===undefined || thisObj===null) {
    thisObj = window
  }

  // 将原函数添加为thisObj的方法
  thisObj.tempFn = this
  // 通过thisObj来调用方法
  const result = thisObj.tempFn(...args)
  // 删除这个方法
  delete thisObj.tempFn

  return result
}

路由工具-React Router6

前言

单页应用

SPA (single page application):单页应用

  • 整个应用只有一个整体页面,页面切换只是局部页面更新
  • 点击路由链接切换页面时,本身不发请求,进行纯前台的DOM更新
  • 优点:加载数据少,基本不会出现切换页面时空白的情况
路由

路由:一套 key: value 的映射组合

  • 后端路由:path 与处理请求的回调及请求方式的映射组合

    app.get('/getUser', () => {})
    
  • 前台路由:path 与 路由组件的映射组合

    <Route path='/about' element={<About/>}></Route>
    
路由工具概述

React Router有三个不同的包:

  • react-router:路由的核心库,提供了很多的:组件、钩子。
  • react-router-dom:开发网页端使用的路由库
  • react-router-native:开发移动端使用的路由库

路由基本使用

路由组件一般放在pages或views文件夹中

安装
npm i react-router-dom
基本使用
  1. 使用路由器标签包裹App

    ReactDOM.createRoot(document.getElementById("root")).render(<BrowserRouter><App /></BrowserRouter>);
    
  2. 定义路由导航链接及文本

    <NavLink to="/about">About</NavLink>
    <NavLink to="/home">Home</NavLink>
    

    ⚠NavLink会给当前所在的路由链接添加一个类名: active

  3. 注册渲染路由组件

    <Routes>
      <Route path="/about" element={<About/>} />
      <Route path="/home" element={<Home/>} />
      <Route path="/" element={<Navigate to="/home"/>} />
      {/* <Navigate to="/home"/> 位于/时自动重定向到/home */}
    </Routes>
    
路由表

路由表:应用中所有路由信息的数组

将路由的注册信息集合在一个js模块进行单独管理,不与组件混在一起

定义路由表
const routes = [
	{path: '/xxx', element: <Xxx/>},
	{path: '/xxx', element: <Navigate to="/xxx"/>}
]
根据路由表注册路由

利用useRoutes根据路由表注册路由,显示路由组件

{
	useRoutes(routes)
}
嵌套路由
  1. 定义子路由导航链接(和路由主无差

    <NavLink to="news">News</NavLink>
    <NavLink to="/home/msg">Msg</NavLink>
    
  2. 在路由表中注册子路由

    1. 子路由需写在父路由的children键中
    2. 子路由的path有2种写法
      1. path:"/父路由/子路由"
      2. path:"子路由"
    {
      path: "/home",
      element: <Home />,
      children: [
        {
          path: "news",
          element: <News />,
        },
        {
          path: "/home/message",
          element: <Message />,
        },
      ],
    },
    
  3. 使用<Outlet />渲染显示子路由

内置组件

组件总览
功能
BrowserRouter用于包裹整个应用<APP/>
HashRouter用于包裹整个应用<APP/>
Link点击修改url,跳转至对应路由,不发送网络请求
NavLink1. 点击修改url,跳转至对应路由,不发送网络请求
2. 对应路由的NavLink添加特殊类名-active
Routes & Route注册渲染路由组件
必须要用包裹
Navigate重定向url
Outlet渲染子路由
BrowserRouter & HashRouter区别❗
BrowserRouterHashRouter
前台路由路径不带#
http://loacalhost:3000/home
带#
http://loacalhost:3000/#/home
请求urlhttp://loacalhost:3000/homehttp://loacalhost:3000
始终请求根路径
在某个路由路径下刷新浏览器携带路由路径进行请求,服务器没有对应资源(index.html)返回,就会404

但由于react内部设置了webpack配置:
devServer: {hostoryApiFallback: true}
当404时,给浏览器返回index界面(fallback 备用计划)
不会携带hash路径,浏览器总是请求项目根路径,服务器返回对应的index界面
引入静态文件1.相对路径./引入静态文件
http://loacalhost:3000/home/css/style.css
在子路由刷新时文件会加载错误

2.最好使用/,保证通过根路径发送请求
相对路径./引入静态文件,不会出错
http://loacalhost:3000/css/style.css
(因为HashRouter始终以根路径发送请求)

内置hooks

说明
useRoutes()根据路由表,动态创建和
useRoutes(routes)
[useParams()](#方式一: params)获取路由的params参数
let { id } = useParams()
[useSearchParams()](#方式二: query)获取or更新路由的query参数
1. 返回一个数组[query参数, 更新query参数的方法]
2. 读取参数 search.get('key')
[useLocation()](#方式三: state)获取当前location信息
const x = useLocation()
useNavigate()实现编程式导航

路由组件间通讯(传参)

方式一: params
  1. 定义路径时指定params参数占位

    {
      path: "detail/:id/:title",
      element: <Detail />,
    }
    
  2. 定义路径链接,指定to携带params参数

    <Link to=`detail/1/post1`>post1</Link>
    
    <Link to={`detail/${item.id}/${item.title}`}>
    	{item.title}
    </Link>
    
  3. 目标路由组件中读取参数

    const {id,title} = useParams();
    

    useParams()返回一个params参数对象

方式二: query

query参数也称为search参数

⚠query参数传参不需要指定占位

  1. 定义路径链接,指定to携带query参数

    <Link to=`detail?id=1&title=post1`>
        
    <Link to={`detail/?id=${item.id}&title=${item.title}`}>
    	{item.title}
    </Link>
    
  2. 目标路由组件中读取参数

    const [serch,setSearch] = useSearchParams();
    const id = serch.get("id");
    const title = serch.get("title");
    

    useSearchParams()返回一个数组[search参数, 更新search参数的方法]

    1. 读取参数:search.get("key")
    2. 更新参数:setSearch('id=008&title=哈哈')
方式三: state
  • state默认值为null
  • state参数可以携带任意类型的数据
  • state参数不会出现在地址栏,但刷新还在

⚠使用前提:使用<BrowserRouter/>包裹

  1. 定义路径链接,携带state参数

    <Link to="detail" state=`detail/1/post1`>post1</Link>
    
    <Link to="detail" state={`detail/${item.id}/${item.title}`}>
    	{item.title}
    </Link>
    
  2. 目标路由组件中读取参数

    通过useLocation()获取location信息中的state

    const x = useLocation()
    const {state: {id, title, content}} = x
    return (
      <>
        <li>消息编号:{id}</li>
        <li>消息标题:{title}</li>
      </>
    )
    

    打印案例中的x(location信息)

    {
    hash: "",
    key: "ah9nv6sz",
    pathname: "/login",
    search: "?name=zs&age=18",
    state: {id: '001', title: 'm1', content: 'abcd'}
    }
    
传参方式选择
  • 一般情况下,优先选择params参数
  • 参数数据较多时,使用state参数,前提是必须使用<BrowserRouter/>

路由导航/跳转

路由导航/跳转编码模式
声明式

通过路由链接进行路由跳转

<Link to="/home">home</Link>
编程式

手动调用路由跳转函数进行路由跳转

利用**useNavigate()**返回一个跳转函数用来实现编程式导航:

  1. 获取跳转函数

    const navigate = useNavigate()
    
  2. 调用跳转函数

    navigate(path:"路由path", {配置项})
    
    1. path中可以携带params参数和query参数
    2. state参数需写在配置项中
路由导航/跳转方式
push模式(默认replace模式
跳转路由,记录当前路由并跳转到新路由跳转路由,替换当前路由路径为新路由
有历史记录,能回退到上一个路由没有历史记录,不能回退到上一个路由
push模式

默认为push模式

replace模式

切换为replace模式

声明式

当属性值是true时,可以省略属性值

<Link replace/>
编程式
navigate(path, {replace: true})

路由懒加载

默认所有路由组件都会被打包到一起,初始访问就会全部加载 => 初始要加载的打包比较大

路由懒加载:路由组件仅在需要时才发送请求加载

开始只加载(请求)当前路由所对应的打包文件,其它路由组件的打包文件只有初始访问时才加载

解决-在路由表注册路由时操作:

  1. lazy懒加载动态引入组件

    import {lazy} from 'react'
    // 懒加载动态引入组件
    const About = lazy(() => import('../pages/About'))
    

    动态引入

    import( '模块路径')
    
    • 返回promise,promise的value为模块对象
    • 被引入的模块会被单独打包
  2. 注册路由组件时,需要通过Suspense来指定Loading界面

    由于一开始组件不加载会导致报错,通过Suspense来指定备用界面,避免加载组件时报错

    import {lazy} from 'react'
    
    const routes = [
      {
        path: "/about",
        element: (
          <Suspense fallback={<h2>loading...</h2>}>
            <About />
          </Suspense>
        ),
      },
    ]
    

状态管理工具1-Redux

  • Redux:集中式状态管理工具
  • 官方文档: https://cn.redux.js.org/
  • 做项目不用Redux
  • 下载:npm i redux

redux三大原则

  1. 单一数据源
  2. State是只读的
  3. 使用纯函数来执行产生新state

三大核心概念

  • action:提出想法
  • reducer:实现想法
  • store:管理state数据和reducer,与组件进行通信
Action

action是一个原生js对象,描述要执行的行为和条件数据

type需大写

{type:"INCREMENT", payload:2}
Action creator

使用函数来创建 action,将该函数统称为 action creator

// action creator
const increment = (num) => ({ type: 'INCREMENT', payload: num })
// 创建action
increment()
Reducer

reducer是一个函数

接收state和action,计算返回新的state

const counterReducer = (state={count: 0}, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {count: state.count + action.payload}
    case 'DECREMENT':
      return {count: state.count - action.payload}
    default: // ⚠必须默认返回当前state
      return state
  }
}

⚠不要直接修改 state,而要产生新的 state返回

⚠必须默认返回当前state

⚠reducer必须是一个纯函数

Store

Redux的state 仓库,一个redux应用只有一个store

一般回专门创建一个store文件夹用来存放

作用
  1. 存储state数据,并提供更新state数据的方法
  2. store是组件与redux通信的桥梁
创建store

内部调用reducer函数,,产生初始状态值

import {createStore} from 'redux';
const store = createStore(reducer函数)
读取state
store.getState().count
更新state

根据传入的action对象, 调用reducer函数产生新的state

store.dispatch(Action)
//1.直接传入action
store.dispatch({type: 'INCREMENT', payload: 1})
//2.通过action创建器传入action
const incrementAction = (num) => ({type: 'INCREMENT', payload: num})
store.dispatch(incrementAction(1))
监听state变化

监听state变化,读取最新的state数据,从而去更新组件

store.subscribe(() => {//state变化时执行该回调函数})

⚠redux的state数据更新之后,不会自动更新组件,需使用store.subscribe()来监听数据更新

Redux基本使用

import {createStore} from 'redux'

// reducer函数: 根据老的state和指定的action, 返回一个新的state
const countReducer = (state={count: 1}, action) => {
  console.log('countReducer', state, action)
  switch (action.type) {
    case 'INCREMENT':
      // state.count += action.payload // 不能直接修改
      return {count: state.count + action.payload}  // 载荷 数据
    case 'DECREMENT':
      return {count: state.count - action.payload}
    default:
      return state
  }
}

// 创建store
const store = createStore(countReducer) // 内部会调用reducer函数, 产生初始状态值

// 读取状态值
console.log(store.getState().count) // 1

// 在index.js中监视状态数据的变化,重新渲染页面
store.subscribe(() => {
  root.render(<App/>)
})

// action creator  action对象的创建器(工厂函数)
const incrementAction = (num) => ({type: 'INCREMENT', payload: num}) 
const decrementAction = (num) => ({type: 'DECREMENT', payload: num}) 


// 更新状态数据
store.dispatch(incrementAction(2)) // => countReducer调用产生新的状态 => 更新状态 => 调用监视的回调
store.dispatch(decrementAction(3)) // => countReducer调用产生新的状态 => 更新状态 => 调用监视的回调

工具

调试工具
  1. 安装谷歌扩展调试工具:Redux DevTools

  2. 项目配置

    1. 下载

      npm i redux-devtools-extension -D
      
    2. 引入并启用redux调试工具

      import {composeWithDevTools} from 'redux-devtools-extension'
      
      // 创建store时, 启用redux调试工具
      const store = createStore(counter, composeWithDevTools())
      
异步工具

Redux-thunk

用于实现异步数据请求的redux中间件

状态管理工具二-Mobx

  • 官网:https://cn.mobx.js.org/

  • Mobx调试工具:MobX Developer Tools

前言

概述

Mobx是简单、可扩展的状态管理工具

作用: 对多组件共享的状态数据进行集中式的统一管理

三大核心概念
observable

状态数据属性

action

更新状态数据的方法

computed

基于状态数据计算属性

  • 计算属性多次读取,只会计算一次(内部在第一次计算后,会缓存结果

  • 什么时候重新计算:依赖数据发生改变后,再次读取时重新计算

在这里插入图片描述

将对象中的属性、方法和计算属性变为响应式

方式一: 简洁写法(多用
makeAutoObservable(this)
方式二: 原始写法
makeObservable(this, {
count: observable, // 标识count是一个响应式的可观察的属性
oddOrEven: computed, // 标识oddOrEven是一个响应式的计算属性
increment: action, // 标识increment是一个响应式的行为函数
})
Mobx vs. Redux
MobxRedux
侵入性
无模板代码,非常简洁

有严格的工作流程,需要写大量模板代码
store个数多个store,更方便单个store
自身是否支持异步自身支持异步自身不支持异步,需要中间件处理异步
状态数据是否可变状态数据是响应式的
可直接对状态数据进行任意修改,变化更方便
强调状态数据不可变
不能直接更新状态数据,只能产生并返回一个新状态数据
适合项目简单、规模不大的应用约束性强,更适合大型多人协作开发

⚠复杂的应用也不是说不能用,需要合理的理清业务逻辑关系,合理的使用

基本使用

安装
npm i mobx mobx-react
使用
  1. 定义管理状态数据的store

    1. 为模块创建一个js文件,用于配置store

    2. 定义类, 在类中定义状态属性、更新状态属性的方法、计算属性

      ⚠Mobx不建议在action函数外直接更新状态数据——runInAction

    3. 向外暴露类的实例对象

      暴露的是实例对象new class(),而不是类本身

    4. 引入makeAutoObservable并在构造器调用,将当前对象变为响应式对象

    5. [引入autorun并在构造器调用,监听依赖数据的改变]

    //引入makeAutoObserverable,[autorun]
    import { autorun, makeAutoObservable } from "mobx";
    //定义类
    class TodosStore {
      //定义状态属性
      todos = [];
      //定义计算属性
      get doneCount() {
        return todos.length;
      }
      //构造器中调用makeAutoObservable,[autorun]
      constructor() {
        //将当前counter对象变为响应式对象
        makeAutoObservable(this);
        //监听依赖数据的改变
        autorun(() => {
          localStorage.setItem("todos", JSON.stringify(this.todos));
        });
      }
      //定义更新状态属性的方法
      addItem = (todo) => {
        this.todos.unshift(todo);
      };
    }
    //向外暴露类的实例对象
    export default new TodosStore();
    
  2. 读取状态数据组件使用

    读取状态显示: TodosStore.todos

  3. 更新状态数据组件使用

    更新状态数据: TodosStore.更新方法

    ⚠最好不要解构store中的函数,会改变this的指向

  4. 若组件需渲染store中的内容

    1. 引入observer高阶组件函数,,监听mobx的变化,一旦变化就重新渲染组件
    2. 使用observer包裹需要渲染store内容的组件
    3. 将组件变为一个观察者export default observer(函数组件名)
    import { observer } from "mobx-react";
    import React from 'react'
    
    function List() {
      return (
        <>组件</>
      )
    }
    export default observer(List)
    
runInAction

Mobx不建议在action函数外直接更新状态数据

  • 异步操作更新状态数据会收到警告

    addItem = (todo) => {
      setTimeout(()=>{
        this.todos.unshift(todo);     
      },1000)
    };
    
  • 解决:使用runInAction包裹异步函数中更新状态数据的操作

    import {runInAction} from 'mobx'
    runInAction(() => {更新状态数据})
    
    addItem = (todo) => {
      setTimeout(()=>{
        runInAction(() => {
          this.todos.unshift(todo);    
        })
      },1000)
    };
    
高阶组件

高阶组件用于关联mobx

高阶组件不是组件,是函数,是特殊的高阶函数

成为高阶组件的条件
  • 接收的参数是组件函数
  • 返回值是一个新的组件函数

监听属性变化-了解

autorun
import {autorun} from 'mobx'
autorun(() => {
  //依赖数据以及依赖数据的内部数据更新时执行
})
执行autorun

autorun只需调用执行一次(即绑定监听一次),后面会自动监听依赖数据更新

推荐做法

放在constructor中

当实例化一个对象时,会自动调用执行一次constructor里的代码

autorun的执行
  1. 初始化调用一次
  2. 数据更新时调用

⚠autorun是深度监听
依赖数据以及依赖数据的内部数据更新时都触发(todos或todos.id改变时)

reaction

初始不调用,依赖数据更新时调用

不是深度监视,只在依赖数据本身更新时触发(只有todos改变时)

import {reaction} from 'mobx'
reaction(
  () => 指定数据,
  (newValue, oldValue) => {
    //指定数据本身更新时执行;
  }
)
autorun vs. reaction
autorunreaction
都是用于监视状态数据变化的一旦被监视数据改变了,自动调用回调函数
调用情况1.初始化调用一次
2.依赖数据更新时调用
1.初始不调用
2.依赖数据更新时调用
监听类型深度监视
1.依赖数据变化
2.依赖数据的内部数据变化时都触发
不是深度监视
只在依赖数据本身变化时触发

axios扩展

axios请求封装函数直接调用
  • 创建api文件夹的文件,将axios请求都放进去,封装成函数导出

  • 使用时直接调用函数

  • 命名以req开头,便于识别其为axios请求函数

/* 
包含n个接口请求函数的模块
每个接口返回一个promise对象
*/
import axios from 'axios'

export const reqSearchUsers = (keyword) => axios({
  url: '/search/users',
  params: {q: keyword}
})

TS版React项目环境

区别create-react-app与vite

create-react-appvite
开发环境运行启动速度
需要对所有项目代码进行打包处理

直接运行服务器,运行时动态按需打包处理
修改配置文件后是否需要重新运行需要重新运行自动重新加载运行
内部实现使用语言js语言,运行相对慢go语言,运行速度快

使用vite创建react项目

  • vite官网: https://cn.vitejs.dev/
  • 创建react项目
# 创建项目
npm create vite@latest
# 快捷创建名为vite-react的ts版的react项目
npm create vite@latest vite-react -- --template react-ts
  • 启动项目
# 下载依赖
cd vite-react
npm i
# 运行
npm run dev

配置模块路径别名

目标
  • 引入模块时避免 …/的编码: 比如用@来对应src
  • 在使用@开始写模块路径时, vscode能提示补全
配置别名
  • vite.config.ts

    ⚠为了ts能识别,需安装包npm i @types/node -D

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import path from 'path' // npm i @types/node -D
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [react()],
    
      // 配置别名和省略扩展名
      resolve: {
        alias: {
          "@": path.resolve(__dirname, 'src')
        },
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
      },
    })
    
    
  • tsconfig.json

    "compilerOptions": {
      ...
      ...
    
      "baseUrl": "./",
      "paths": {
        "@/*": ["src/*"]
      }
    },
    

改造为ts版本

  1. js => ts
  2. jsx => tsx(文件中有标签、jsx的语法的,必须为tsx
  3. 添加必要的类型约束
    1. 路由: 通过RouteObject来约束路由表中路由配置
    2. mobx:
      • 为状态数据定义数据类型接口,并进行约束
        • useState
      • 为action函数的参数指定相应的类型
    3. 组件:
      • 为接收的props参数定义类型约束 注意不能给解构的形参变量指定类型
        • 不能单独约束props参数中的某一个键值对,需要对props整体进行约束
        • function Item({todo}: {todo: Todo}){}
      • 为事件回调的event对象定义类型约束 不能用原生事件的类型,得用react内置的事件类型
        • import type {KeyboardEvent} from ‘react’
    4. ajax请求
      • 为响应数据定义对应的interface/type
      • 为axios指定请求成功的响应数据结构:
        • 响应拦截器成功回调返回的是response.data: axios.get<any, 响应体数据类型>()
        • 响应拦截器成功回调不是返回response.data: axios.get<响应体数据类型>()

UI组件库

UI组件库:封装了一些通用的界面功能效果的组件, 简化开发编码

流行的React UI 组件库

  • 国内-Ant Design:https://ant-design.antgroup.com/index-cn
  • 国外-Material UIhttps://www.mdui.org/design/

antd下载

npm i antd

//目前最新版本为5,开发版本一般用4
//样式需要引用antd的样式合集,见使用
npm i antd@4

组件按需打包

问题
  • antd默认已经实现了组件的按需要打包
  • 但默认没有实现样式的按需打包
解决

利用 vite-plugin-style-import 实现 antd样式的按需打包

  1. 下载工具包
npm i -D vite-plugin-style-import #自动打包组件样式

npm i -D consola  # 内部使用consola

npm i -D less # antd内部使用less编写的样式
  1. 配置: vite.config.ts
...
...
import { createStyleImportPlugin, AntdResolve } from 'vite-plugin-style-import'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    ...
    ...
    // 自动打包antd组件对应的样式配置1
    createStyleImportPlugin({
      resolves: [AntdResolve()]
    })
  ],

  // 自动打包antd组件对应的样式配置2
  css: {
    preprocessorOptions: {
      less: {
        // 支持内联 JavaScript
        javascriptEnabled: true,
      },
    }
  },
  ...
  ...
})
  1. 不再引入antd的样式文件
// import 'antd/dist/antd.css'

自定义主题

问题

antd提供的默认主色是蓝色, 而我们项目需要的主色调可能是其它颜色, 比如绿色(#25b864)

解决

通过vite给antd配置新的主色

css: {
  preprocessorOptions: {
    less: {
      // 支持内联 JavaScript
      javascriptEnabled: true,
      // 重写 less 变量,自定义主题样式
      modifyVars: {
        '@primary-color': '#25b864',
      },
    },
  }
},

配置好后要删除node_modules下的.vite

否则,主色不会改变

因为.vite中缓存着前面打包的antd, 默认不进行重新打包

;