React 全家桶

文章目录

  • 前言
  • 一、React是什么?
  • 二、基础内容
      • 1. React 相关 js 库
      • 2. React 开发者调试工具
      • 3. JSX语法规则
      • 4. 模块与组件、模块化与组件化
      • 5. 类的基本知识
  • 三、React 面向组件编程
      • 1. 函数式组件
      • 2. 类式组件
      • 3. 组件实例的三个核心属性: state、refs、props
        • - state
        • - props
        • - refs (注意 ref 与 refs)
      • 4. React 中的事件处理
      • 5. 受控组件 与 非受控组件
      • 6. 高阶函数 - 函数柯理化
      • 7. 组件生命周期
      • 8. 虚拟DOM 与 DOM Diffing 算法
  • 四、React 脚手架
      • 1. 创建 react 应用
      • 2. SPA的理解
      • 3. react 脚手架项目结构
      • 4. 简单案例 hello,react!
      • 5. 样式模块化
      • 6. 配置代理
      • 7. 消息订阅 与 发布机制 PubSub
      • 8. React-router 5
        • - 路由的理解
        • - React-router 相关api
      • 8. React-router 6
      • 10. ant-design 的基本使用
      • 11. redux
      • 12. React-redux
      • 13. react 项目打包
  • 扩展
      • 1. setState
      • 2. lazyLoad
      • 3. Hooks
      • 4. Fragment
      • 5. Context
      • 6. 组件优化
      • 7. render props(相当于插槽)
      • 8. 错误边界 ErrorBoundary
  • 总结
      • 1. 组件间的通信


前言

React 全家桶:React-Router 路由库、PubSub 消息管理库、Redux 集中状态管理库、Ant-Design UI


一、React是什么?

React 是 Facebook 开发的,用于构建用户界面的开源 JavaScript 库(是一个将数据渲染为HTML视图的开源 JavaScript 库)

页面渲染步骤:

  • 1、发送请求获取数据
  • 2、处理数据(过滤、整理格式等)
  • 3、操作 DOM 呈现页面 (React 的工作)

原生 JavaScript 与 React 比较:

  • 原生JavaScript
    操作 DOM 繁琐、效率低
    直接操作 DOM 、进行大量的重绘重排
    没有组件化编码、复用率低
    命令式编码
  • React
    组件化模式、声明式编码、提高开发效率及组件复用率
    React Native 中可以使用 React 语法进行移动端开发
    高效:虚拟 DOM(不总直接操作DOM) + 优秀 Diffing 算法(最小化页面重绘)尽量减少与真实 DOM 的交互

二、基础内容

1. React 相关 js 库

  • react.js:React 核心库
  • react-dom.js: 提供操作 DOM 的 React 扩展库
  • babel.min.js:解析 JSX 语法代码转化为 JS 代码的库(es6 ⇒ es5、import 模块化、JSX ⇒ JS)

2. React 开发者调试工具

React Developer Tools

3. JSX语法规则

  • 1、定义虚拟DOM时,不要写引号
  • 2、标签中混入 JS 表达式时要用 {}

一定要注意区分:【JS表达式】 与 【JS语句(代码)】
JS 表达式: 一个表达式会产生一个值,可以放在任何一个需要值的地方:a、a+b、demo(1)、arr.map、function demo(){}
JS 代码: 控制代码走向,没有返回值:if(){}、 for(){}、switch(){case:xx}

  • 3、样式的类名指定不要用 class,要用 className
  • 4、内联样式,要用 style ={{key: value}} 的形式去写
  • 5、虚拟 DOM 只能有一个根标签
  • 6、标签必须闭合
  • 7、标签首字母
    若小写字母开头,则将该标签转为 html 中同名元素,若html中无该标签对应的同名元素,则报错
    若大写字母开头,react 渲染对应的组件,若组件没有定义,则报错
	<div id="root"></div>
	<script type="text/babel">
	   const catName = '花姐'
	   // 用 JSX 创建虚拟 DOM
	   const VDOM = (
	       <div>
	           <h2 className="name">
	               <span style={{color: "orange", fontSize: "20px"}}>{catName}</span>
	           </h2>
	       </div>
	   )
	   ReactDOM.render(VDOM, document.getElementById('root'))
	</script>

4. 模块与组件、模块化与组件化

  • 模块(JS模块)
    理解:向外提供特定功能的 js 程序,一般就是一个 js 文件
    作用:复用 js ,简化 js 的编写,提高 js 的运行效率

  • 组件
    理解:用来实现局部功能效果的代码和资源的集合(html、css、js、image等)
    作用:复用编码、简化项目编码、提高运行效率

  • 模块化:当应用的 js 都以模块来编写的,这个应用就是一个 模块化 的应用

  • 组件化:当应用都是以组件的方式实现,这个应用就是一个 组件化 的应用

5. 类的基本知识

	// 创建一个 cat 类
	class Cat{
	  // 构造器方法
	  constructor(name){
	      // 构造器中的 this 指向类的实例对象: 谁调用了new, this 指向谁
	      this.name = name
	  }
	  // 一般方法:放在了类的原型对象(_proto_)上, 供实例使用
	  // 通过 Cat 实例调用一般方法时,this 指向 Cat 实例
	  sayName(){
	      console.log(`我叫${this.name}`)
	  }
	}
	// 创建一个 cat 的实例对象
	const HJ = new Cat('花姐')
	
	// 创建一个 Minicat 类,继承于 Cat 类
	class Minicat extends Cat {
	  constructor(name, age){
	      // super 作用:调用父类的构造器函数;super 必须写在其他参数之前
	      super(name);
	      this.age = age
	  }
	
	}
	// 创建一个 Minicat 的实例对象
	const mini = new Minicat('橘宝', 2)
	
	/*
	  总结:
	      1、类中的构造器不是必须写的,要对实例进行一些初始化操作的时候,如添加属性才写;
	      2、如果 A 类继承了 B 类,且 A 类写了构造器,那 A 类中构造器的 super 是必须调用的;
	      3、类中的方法,都是放在类的原型对象上(_proto_),供实例使用
	*/

三、React 面向组件编程

1. 函数式组件

适用于 简单组件(无state) 的定义
	// 创建函数式组件
    function Demo(){
    	// babel 编译后开启了严格模式,严格模式禁止自定义的函数 this 指向 widow 
    	console.log(this);  // undefined
        return <h2>我是函数式组件</h2>
    }
    // 渲染组件到页面
    ReactDOM.render(<Demo/>, document.getElementById('root'));
    /*
      执行了 ReactDOM.render((.... 之后:
       1、React 解析组件标签,找到了 Demo 组件;
       2、发现是函数式组件的,随后调用该函数,将返回的虚拟 DOM 转为真实 DOM ,随后呈现在页面中
   */

2. 类式组件

适用于 复杂组件(有state) 的定义
	/*
      创建类式组件
          1、React.Component:React 内置类
          2、内部 render(){} 必须写,放在 Demo 原型对象上,供实例使用
    */
    class Demo extends React.Component {
        render(){
            // render 中的 this 指向 Demo 组件实例对象
            console.log(this);
            return <h2>我是类式组件</h2>
        }
    }
    // 渲染组件到页面
    ReactDOM.render(<Demo />, document.getElementById('root'))

    /*
      执行了 ReactDOM.render((.... 之后:
          1、React 解析组件标签,找到 Demo 组件;
          2、发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的 render 方法
          3、将 render 返回的虚拟 DOM 转化为真实的 DOM, 随后呈现在页面中
    */

3. 组件实例的三个核心属性: state、refs、props

- state

		// state 复杂写法
		class Weather extends React.Component {
			constructor(props){
			    super(props);
			    this.state = { isHot: false };
			    // .bind(this)用来解决 this 指向问题
			    this.changeWeather = this.changeWeather.bind(this); 
			}
			/*
			    changeWeather 放在 Weather 原型对象上,供实例使用
			    由于 changeWeather 是作为 onClick 回调,而不是通过实例调用的,是直接调用
			    类中的方法默认开启了举报的严格模式,所以方法中的 this 为 undefined
			*/
			changeWeather(){ this.setState({isHot: !this.state.isHot}) }
			render(){
			    const { isHot } = this.state;
			    return <p onClick={this.changeWeather} >今天天气{isHot}</p>
			}
		}
		// state 简写方式: 省略类构造器,函数修改 this 指向
		class Weather extends React.Component {
	        // 类中可以直接写赋值语句,给类添加属性(不能写var\let\const)
	        state =  { isHot: fasle };
	        /*
	            赋值语句的形式 + 箭头函数,修改函数 this 指向
	            箭头函数没有自己的this, 声明时会向外寻找 this,始终指向函数声明时所在的作用域下的 this 的值
	        */
	        changeWeather = () => this.setState({isHot: !this.state.isHot})
	        render(){
	            const { isHot } = this.state;
	            return <p onClick={this.changeWeather} >今天天气{isHot}</p>
	        }
	    }
	    /*
	        总结:
	            1、组件中 render 方法中的 this 为组件实例对象
	            2、组件自定义的方法中 this 为undefined, 解决方法:
	                通过函数对象的 bind(), 强制绑定this
	                箭头函数 + 赋值语句,声明方法
	            3、状态数据,通过 setState 修改
	            	this.state.isHot = true 修改 isHot 的值会成功,但是不会触发渲染
	            	需要通过 setState 修改才会触发重新渲染
	            4、标签绑定事件 onClick 要大写
	    */

- props

理解:每个组件都有一个 props(properties) 属性,组件标签的所有属性都保存在 props 中
作用:通过标签属性从组件外部向组件内部传递变化的数据
注意:组件内部不要修改 props 数据,会报错

        let obj ={name: '花姐'}

        // 1、函数组件使用 props,参数形式
        function Demofn(props){
            const {name} = props
            return(
                <p>{name}</p>
            )
        }
        // 对标签属性进行类型、必要性限制
        Demofn.propTypes = {
            name: PropTypes.string.isRequired, // string 表示为字符串,isRequired 表示必传
        }
        // 指定默认标签属性值
        Demofn.defaultProps = {
            name: '喵'
        }
        ReactDOM.render(<Demofn {...obj}/>, document.getElementById('root'))

        // 2、类式组件中的构造器 与 props
        class Democlass extends React.Component {
            // 构造器是都接收 props,是否传递给 super 取决于是否希望在构造器中通过 this 访问 props
            // 开发过程中一般不使用构造器
            constructor(props){
                super(props);
                console.log(this.props); // 需要调用super(props),才能在构造器中使用 this.props,否则为 undefined
            }
            // 对标签属性进行类型、必要性限制
            static propTypes = {
                name: PropTypes.string.isRequired, // string 表示为字符串,isRequired 表示必传
            }
            // 指定默认标签属性值
            static defaultProps = {
                name: '喵'
            }
            render(){
                let {name} = this.props
                return(
                    <p>{name}</p>
                )
            }
        }
        // {...obj} 扩展运算符批量传递
        ReactDOM.render(<Democlass {...obj}/>, document.getElementById('root2'))

- refs (注意 ref 与 refs)

  • 字符串形式 (不推荐使用,存在效率问题)
	class Demo extends React.Component {
         getRef = () => {
         	// this.refs.btn 是真实 DOM 
             const { btn } = this.refs
         }
         render(){
             return(
                 <button ref="btn" onClick={this.getRef}>字符串形式 ref (不推荐使用)</button>
             )
         }
     }
     ReactDOM.render(<Demo/>, document.getElementById('root'))
  • 回调函数形式(内联写法)
	class Demo extends React.Component {            
         getRef = () => {
             const { btn } = this
         }
         render(){
             return(
                 <button ref={c=> this.btn = c} onClick={this.getRef}>回调形式 ref, 内联写法</button>
             )
         }
     }
     ReactDOM.render(<Demo/>, document.getElementById('root'))

回调 ref 在页面更新过程中调用次数的问题(可以忽略,影响不大):
第一次传入 null,第二次传入 DOM,每次更新渲染时会创建一个新的函数实例,React 会清空就的 ref 并设置新的 ref
解决:通过 ref 回调函数定义成 class 的绑定函数可以解决

	class Demo extends React.Component {
		 saveBtn = (c) => { this.btn = c }
         getRef = () => { const { btn } = this }
         render(){
             return(
                 <button ref={this.saveBtn} onClick={this.getRef}>回调形式 ref, 内联写法</button>
             )
         }
     }
     ReactDOM.render(<Demo/>, document.getElementById('root'))
  • createRef 形式
    React.createRef()调用后可以返回一个容器,该容器可以存储被 ref 所标识的节点
    该容器专人专用,需要多个 ref 需要重复 React.createRef()
	class Demo extends React.Component {
       myRefs = React.createRef()
       getRef = () => {
           console.log(this.myRefs.current)
       }
       render(){
           return(
               <button ref={this.myRefs} onClick={this.getRef}>createRef 形式</button>
           )
       }
    }
    ReactDOM.render(<Demo/>, document.getElementById('root'))

4. React 中的事件处理

  • 1、通过onXxx 属性指定事件处理函数(注意大小写)
    React 使用的是自定义(合成)事件,而不是使用的原生 DOM 事件,为了更好的兼容性
    React 中的事件是通过事件委托方式处理的(委托给组件最外层的元素)

事件的执行顺序为原生事件先执行,合成事件后执行
合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用
如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行
react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用 event.preventDefault() 来阻止默认行

  • 2、通过 event.target 得到发生事件的 DOM 元素对象 – 不要过度使用 ref
    发生事件的元素跟获取数据的元素是同个时,就省略 ref ,使用event.target

5. 受控组件 与 非受控组件

受控组件:页面所有输入都由 state 控制
非受控组件: 现取现用 ref

6. 高阶函数 - 函数柯理化

  • 高阶函数
    如果一个函数符合下面2个规范中的任何一个,那么函数就是高阶函数
    1、若A函数,接收的参数是一个函数,A就可以称为高阶函数
    2、若A函数,函数调用的返回值依然是一个函数,A就可以称为高阶函数
    例:promise(() => {})、setTimeout(() => {})、arr.map(() => {}) (数据常见的方法)等

  • 函数柯里化
    通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

	function sum(a){
       return b => {
           return c =>{
               return a + b + c
           }
       }
   }
   sum(1)(2)(3)

react 表单柯里化案例:

	class Login extends React.Component {
	    state = {
	        username: '',
	        password: ''
	    }
	    handleSubmit = () => {
	        event.preventDefault();
	        const {username, password} = this.state
	    }
	    // 柯里化实现
	    saveFormData = (type) => {
	        return (event) => {
	            this.setState({[type]: event.target.value})
	        }
	    }
	    // 不使用柯里化实现
	    saveTypeData = (type, event) => {
	        this.setState({[type]: event.target.value})
	    }
	    render(){
	        return(
	            <form onSubmit={this.handleSubmit}>
	                {/* 柯里化实现 */}
	                用户名:<input type="text" name="username" onChange={this.saveFormData('username')} />
	                {/* 不使用柯里化实现 */}
	                密码:<input type="password" name="password" onChange={event => this.saveTypeData('password', event)} />
	                <button>登录</button>
	            </form>
	        )
	    }
	}

7. 组件生命周期

React 全家桶_第1张图片

  • constructor:构造函数
  • render:渲染
  • getDerivedStateFromProps:从props得到一个派生的state(17 新增),返回 null 或 状态对象
  • componentDidMount:组件挂载完毕
  • shouldComponentUpdate:控制组件是否更新 返回值 true 更新, false 不更新 (this.setState() 触发)
  • getSnapshotBeforeUpdate:组件在更新之前获取快照(17 新增)
  • componentDidUpdate:组件更新完毕
  • React.unmountComponentAtNode(document.getElementById(‘root’)): 卸载组件
  • componentWillUnmount:组件将要被卸载

componentWillMount :组件将要挂载(17 废弃)
componentWillUpdate :组件将要更新(17 废弃)(this.forceUpdate() 触发)
componentWillReceiveProp s:子组件将要接收新的 props,首次不调用(17 废弃)

旧生命周期总结:

  1. 初始化阶段(挂载时):由 ReactDOM.render() 触发初次渲染
    constructor()
    componentWillMount()
    render()
    componentDidMount()

  2. 更新阶段:由组件内部 this.setState() 或 父组件 或 render 触发
    shouldComponentUpdate()
    componentWillUpdate()
    render()
    componentDidUpdate()

  3. 卸载组件:由ReactDOM.unmountComponentAtNode() 触发
    componentWillUnmount()

新生命周期总结:

  1. 初始化阶段(挂载时):由 ReactDOM.render() 触发初次渲染
    constructor()
    getDerivedStateFromProps()
    render()
    componentDidMount()

  2. 更新阶段:由组件内部 this.setState() 或 父组件 或 render 触发
    getDerivedStateFromProps()
    shouldComponentUpdate()
    render()
    getSnapshotBeforeUpdate()
    componentDidUpdate()

  3. 卸载组件:由ReactDOM.unmountComponentAtNode() 触发
    componentWillUnmount()

常用:
componentDidMount() 在此做初始化:开启定时器,发送网络请求,订阅信息
componentWillUnmount() 在此做收尾工作:关闭定时器,取消订阅信息

getSnapshotBeforeUpdate 使用场景

	// 数据不停新增,固定滚动条位置
    class NewList extends React.Component {
         state = { newArr: [] }         
         componentDidMount() {
             setInterval(() => {
                 const { newArr } = this.state
                 const news = '新闻' + (newArr.length + 1)
                 this.setState({newArr: [...news, newArr]})
             }, 1000)
         }
         // getSnapshotBeforeUpdate 在最近渲染输出(提交到DOM节点)之前调用,能在组件发生更改之前从 DOM 中捕获一些信息
         // 返回值将作为参数传递给 componentDidUpdate
         getSnapshotBeforeUpdate(){
             return this.refs.list.scrollHeight
         }
         componentDidUpdate(preProps, preState, scrollHeight) {
             // preProps: 先前props, preState: 先前状态
             this.refs.list.scrollTop += this.refs.list.scrollHeight - scrollHeight
         }
         render(){
             return (
                 <div>
                     {
                         this.state.newArr.map((n, index) => {
                             return <div key={index}> {n} </div>
                         })
                     }
                 </div>
             )
         }
     }

8. 虚拟DOM 与 DOM Diffing 算法

  • 虚拟 DOM 中 key 的作用
    key 是虚拟 DOM 对象的表示
    当状态中的数据发生变化后,react 会根据【新数据】 生成 【新的虚拟DOM】,随后React进行【新虚拟 DOM】与【旧虚拟DOM】的 diff 比较,规则:
    1)、旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key
    - 若虚拟 DOM 中的内容没变,直接使用之前的真实 DOM
    - 若虚拟 DOM 中的内容变了,则生成新的真实 DOM ,随后替换掉页面之前的真实 DOM
    2)、旧虚拟 DOM 中未找到新虚拟 DOM 相同的 key,根据数据创建新的 DOM , 随后渲染到页面

  • 用index 作为 key 可能会引发的问题
    1)、若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没必要的DOM更新,界面没问题,效率低
    2)、如果结构中包含输入类的 DOM(input 等):会产生错误的DOM更新,界面有问题(输入类的 DOM 内容会保留)
    3)、如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用index 作为 key 是没有问题的

  • 开发中如何选择key
    1)、使用每条数据的唯一标识作为 key,例如id、手机号、学号等
    2)、如果确定只是简单的展示数据,则用 index 也是没关系的

四、React 脚手架

1. 创建 react 应用

  • 全局安装:npm install -g create-react-app
  • 切换到想要创建项目的目录,使用命令: create-react-app 项目名
  • 进入项目文件夹:cd 项目名
  • 启动项目:npm start

vacode 的 react 扩展插件:ES7+ React/Redux/React-Native snippets

2. SPA的理解

  • 单页面 Web 应用 (sing page application, SPA),单页面多组件
  • 整个应用只有一个完整的页面,页面js、css、等资源只加载一次
  • 点击页面中的链接不会刷新页面,只会做页面的局部刷新
  • 数据都需要通过 ajax 请求获取,在前端一步展现

3. react 脚手架项目结构

	public  -----  静态文件
        index.html           -----  应用主页面
        manifest.json        -----  应用加壳时的相关配置文件
        robots.txt           -----  爬虫规则文件
    src  -----  静态文件
        App.js               -----  App组件
        App.test.js          -----  用于给 App 做测试
        index.js             -----  入口文件
        reportWebVitals.js   -----  页面性能分析文件(需要 web-vitals 库的支持)
        setupTests.js        -----  用于组件测试

4. 简单案例 hello,react!

创建App组件:两种引入 React.Component 的写法

  • 第一种:import React from ‘react’
	import React from 'react'	
	// 创建并暴露app组件
	export default class App extends React.Component{
	    render() {
	        return(
	            <div>hello,react!</div>
	        )
	    }
	}
  • 第二种:import React, {Component} from ‘react’
	// 能直接拿到 Component 不是因为解构赋值, 而是因为虽然 Component 在 react 的原型上,
	// react 默认暴露了React,并且分别暴露(export)了 Component 方法
	import React, {Component} from 'react'	
	// 创建并暴露app组件
	export default class App extends Component{
	    render() {
	        return(
	            <div>hello,react!</div>
	        )
	    }
	}

创建入口文件 index.js

	import React from 'react'
	// 18版本的写法 react-dom/client
	import ReactDOM  from 'react-dom/client'
	import App from './App'	
	// 18版本的写法 ReactDOM.createRoot
	const root = ReactDOM.createRoot(document.getElementById('root'));
	root.render(
	  <React.StrictMode>
	      <App />
	  </React.StrictMode>
	);

5. 样式模块化

React 的样式是全局化,需做处理避免样式冲突,解决方法:

  • 使用 css 预编译语言,Less,Sass,Stylus 等
  • 样式模块化
    1、样式文件后缀 .css 改成 module.css;例:index.module.css
    2、使用 jsx 引入 module.css;例:import cssName from './index.module.css '; 使用 className = {cssName.title}

6. 配置代理

假设项目端口是:http://localhost:3000

  • 方式一:在 package.json 中配置 proxy
    优点:配置简单,前端请求资源时可以不加任何前缀
    缺点:只能配置一个代理,不能配置多个代理
    工作方式:上述方式配置代理,当请求了 3000 不存在的资源时,那么请求会转发给 5000 (优先匹配前端资源)

    	{
    	  "proxy": "http://localhost:5000",
    	}
    
  • 方式二:在项目根目录处创建 setupProxy.js (名字不可改)
    优点:可以配置多个代理,可以灵活的控制请求是否走代理
    缺点:配置繁琐,前端请求资源时必须加前缀

    	// 注意区分 http-proxy-middleware 版本不同写法
    	const proxy = require('http-proxy-middleware')	
    	module.exports = function(app) {
    	    app.use(
    	    	// 1、http-proxy-middleware低版本(2以下) proxy
    	        proxy('/api', { // 遇见/api前缀请求,就会触发该代理配置
    	            target: 'http://localhost:5000', // 请求转发给谁
    	            changeOrigin: true, // changeOrigin 控制服务器收到的请求头中 HOST 字段		            
    	            pathRewrite: {'^/pai': ''} // 重写请求路径(必须),去除请求前缀,保证交给服务器的是正常请求地址
    	        }),
    	        // 2、http-proxy-middleware高版本(2以上) proxy.createProxyMiddleware
    	        proxy.createProxyMiddleware('/api2',{
    				target:'http://localhost:5001',
    				changeOrigin:true,
    				pathRewrite:{'^/api2':''}
    			}),
    	    )
    	}
    	/**
    	 * changeOrigin 设置为 true 时,服务器收到的请求头 host 为:localhost:5000
    	 * changeOrigin 设置为 false 时,服务器收到的请求头 host 为:localhost:3000
    	 * changeOrigin 默认为 false, 一般会设置为 true
    	 */
    

    方法一 或 方法二 添加后,需重启项目才生效
    请求接口:http://localhost:5000/api/index/index
    假设项目端口是:http://localhost:3000
    实际请求写法可以是:

    	// 此时 url 两种值均可 "http://localhost:3000/api/index/index" 或 "/api/index/index"
    	let url = "/api/index/index"
    	axios.get(url).then(
    		response => {console.log('成功了',response.data);},
    		error => {console.log('失败了',error);}
    	)
    

7. 消息订阅 与 发布机制 PubSub

  • 1、先订阅,再发布;在需要数据的组件订阅,在发送数据的组件发布
    订阅: let 订阅名 = PubSub.subscribe(消息名, function(消息名, 数据){})
    发布: PubSub.publish(消息名,数据)

  • 2、适用于任意组件间的通讯

  • 3、要在组件的 componentWillUnmount 中取消订阅
    取消订阅:PubSub.unsubscribe(订阅名)

  • 4、react 使用 PubSub 实现搜索例子(先订阅,再发布)

    List.jsx

    	import React, { Component } from 'react'
    	import PubSub from 'pubsub-js'
    			
    	export default class List extends Component {
    	  state = { 
    	    users:[],
    	    isFirst: true, // 记录是否是第一次请求
    	    isLoading: false, // 标识是否处于加载中
    	    err: "", // 储存请求错误信息
    	  }
    	
    	  componentDidMount() {
    	    // 订阅消息
    	    this.searchPubSub = PubSub.subscribe('getsearchData', (msgName, data) => {
    	    	console.log(msgName); // getsearchData
    	    	this.setState(data)
    	    })
    	  }
    	
    	  componentWillUnmount() {
    		// 页面销毁,取消消息订阅
    		PubSub.unsubscribe(this.searchPubSub)
    	  }
    	
    	  render() {
    		const {users, isFirst, isLoading, err} = this.state
    	    return (
    	        <div className='flex-wrap'>
    	          {
    	            isFirst? <h2>欢迎使用!</h2> :
    	            isLoading ? <h2>loading....</h2> :
    	            err ? <h2>{err}</h2> :            
    	            users.map((userObj) => {
    	                return (
    	                  <div className='box' key={userObj.id}>
    	                    <img src={ userObj.avatar_url } alt="head_portrait"></img>   
    	                    <span>{ userObj.login }</span>
    	                  </div>
    	                )
    	            })
    	          }
    	        </div>
    	    )
    	  }
    	}
    

    Search.jsx

    	// Search.jsx 设置发布
    	import React, { Component } from 'react'
    	import PubSub from 'pubsub-js'
    	import axios from 'axios'	
    		
    	export default class Search extends Component {		
    	  search = () => {
    	    const { keyWordElement: {value: keyword} } = this
    	    // 发送请求前通知 list 更新状态
    	    PubSub.publish('getsearchData', {isFirst: false, isLoading: true})
    	    axios.get(`https:api.github.com/search/users?q=${keyword}`).then(response => {
    	    	// 请求成功通知 list 更新状态
    	        PubSub.publish('getsearchData', {isLoading: false, users: response.data.items, err: ""})
    	      },err => {
    	      	// 请求失败通知 list 更新状态
    	        PubSub.publish('getsearchData', {isLoading: false, err: err.message})
    	      })
    	  }
    	  render() {
    	    return (
    	      <div>
    	        <section>
    	              <h3> Search 用户</h3>
    	              <input ref={c => this.keyWordElement = c} type="text"></input>
    	              <button onClick={this.search}>搜索</button>
    	          </section>
    	      </div>
    	    )
    	  }
    	}
    

    App.jsx

    	import React, { Component } from 'react'
    	import Search from './components/Search'
    	import List from './components/List'
    	
    	export default class App extends Component {
    	    render() {
    	        return (
    	            <div>
    	                <Search />
    	                <List />
    	            </div>
    	        )
    	    }
    	}
    

8. React-router 5

react-router 的理解:react 专门用来实现一个 SPA 应用的一个插件库: 分web(用于网页:react-router-dom) 、native(原生应用开发用)、any(任何平台都可用)

- 路由的理解

一个路由就是一个映射关系(key对应value),key 为路径,value 可能是function 或 component。
路由分类(后端路由 与 前端路由):

1)、 后端路由:
	value 是 function 用来处理客户端提交的请求(后端路由即接口)			
	注册路由:router.get(path, function()=>{})			
	工作过程:node 接收到一个提交,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
   
2)、前端路由: 
		浏览器端路由 value 是component,用于展示页面内容
		注册路由:		
		工作过程:当浏览器的 path 变为 /test 时,当前路由组件就会变为Test 组件
		路由原理:
			    基石是浏览器 BOM 身上的 history(window.history), 浏览器的 history 数据结构是栈:先进后出; 用于管理浏览器历史记录等; 可用 history.listen 监听路由变化
			    history 分两种:
			    	history: H5 推出的 History Interface,利于 pushState() 和 replaceState()方法 - 兼容性差
			    	hash: 利用的是 hash 值,类似锚点,链接带#,兼容性强
			    	
	    hash 模式和 history 模式都属于浏览器自身的特性
	    vue 的 history 模式需要后端配合,如果后端不配合,刷新页面会出现404
	    hash不需要url 中的 # 后的参数,不会作为资源发送给服务器

- React-router 相关api

路由安装:

	 5版: npm  i react-router-dem@5
	 6版: npm  i react-router-dem
  • BrowserRouter、hashRouter、Route、Link

    路由器 Router 管理着路由Route, Route 要包在路由器 Router(BrowserRouter or HashRouter) 之中;整个应用只能用一个路由器管理(注意区分 路由器Router 与 路由 Route)。

    1、BrowserRouter、hashRouter 区别:

     1、底层原理不一样
     	    BrowserRouter 使用的是 H5 的 history API,不兼容 IE9 及以下版本
     	    hashRouter 使用的是 URL 的哈希值
     2、 path 表现形式不一样
     	    BrowserRouter 的路径中没有#,例如:http://localhost:3000/home
     	    HashRouter 的路径有#,例如:http://localhost:3000/#/home
     3、刷新后对路由 state 参数的影响
     	    BrowserRouter 没有任何影响,因为 state 保存在 history 对象中
     	    hashRouter 刷新后会导致路由 state 参数丢失
     4、hasRouter 可以用于解决一些路径错误相关的问题
    

    2、路由的基本使用:
    导航区用 Link 或 NavLink,展示区用 Route, 最外侧要包裹一个 BrowserRouter 或 HashRouter
    index.js 文件 设置路由器(BrowserRouter or HashRouter):

    	import {BrowserRouter} from 'react-router-dom'
    	ReactDOM.render(
    	   <BrowserRouter>
    	       <App/>
    	   </BrowserRouter>
    	,document.getElementById('root'))
    

    App.js 编写路由链接、注册路由:

    	import React, { Component } from 'react'
    	import {Link, Route} from 'react-router-dom'
    	import Home from './components/Home'
    	
    	export default class App extends Component {
    	    render() {
    	        return (
    	            <div className='main'>
    					<div className='left'>
    					    {/* 编写路由连接 - react 中靠路由连接实现切换组件*/}
    					     <Link to="/home">home</Link>
    					</div>
    					<div className='right'>
    					    {/* 注册路由 - 组件展示区 */}
    					    <Route path="/home" component={Home}/>
    					</div>
    				</div>
    	        )
    	    }
    	}
    
  • Route
    1、Route 模糊匹配 和 严格匹配(exact)

     路由匹配遵循 - 最左侧匹配原则
         模糊匹配:
         	输入的路径 必须要包含 匹配的路径,且顺序要一致,默认使用
         	例子: 
         		home 可以匹配到
         		 
         严格匹配:
         	输入的路径与匹配的路径必须一致 exact={true} 
            开启严格匹配:
            严格匹配不要随便开启,有时候开启会导致无法继续匹配多级路由           
    

    2、Route 嵌套路由(多级路由)

     1、注册子路由要写上父级路由的 path 值
     2、路由的匹配时按照注册路由的顺序进行的
     	多级路由匹配的步骤:
             首先路由匹配第一批注册的路由,例如 /home/tab 当匹配到 /home 时,就会先显示 home 组件
             随后在home 组件中继续匹配路由/home/tab, 匹配到则展示,匹配不到则跳转到 Redirect 指定的路由
    

    3、解决多级路径刷新页面样式丢失的问题

        public 中的 index.html 引入样式时:
         1、 './' 改为 '%PUBLIC_URL%/' (常用, PUBLIC_URL react 特有)
         2、 路径 './' 改为 '/'
         3、 使用 HashRouter
    

    4、路由( Link、NavLink )的两种跳转方式 push、replace

     	1、默认是 push 模式,会留下痕迹
     	2、replace 不留下痕迹
              开启 replace: 
    

    5、编程式路由导航

     不写 Link 和 NavLink,借助this.props.history对象身上的 API 对路由进行跳转、前进、后退
     利用: go、goBack、goForward、push、replace 实现路由跳转
         this.props.history.go(n)
         this.props.history.goBack()
         this.props.history.goForward()
         replace:
             this.props.history.replace(`/home/tabTow/detail/${id}`)
             this.props.history.replace(`/home/tabTow/detail?id=${item.id}`)
             this.props.history.replace(`/home/tabTow/detail`,{id})
         push: 
             this.props.history.push(`/home/tabTow/detail/${id}`)
             this.props.history.push(`/home/tabTow/detail?id=${item.id}`)
             this.props.history.push(`/home/tabTow/detail`,{id})
    

    6、向路由组件传参

     1、params 参数
           路由连接(携带参数):{item.title } 
           注册路由(声明接收): 
           组件接收参数:this.props.match.params
            
      2、search 参数
           路由连接(携带参数):{item.title}
           注册路由(无需声明,正常注册即可):
           组件接收参数:this.props.location
           备注:获取到的 search 是 urlencode 编码字符串,需要借助 querystring 解析
               import qs from 'qs'
               const {id, title} = qs.parse(search.slice(1))
                
       3、state 参数
           路由连接(携带参数):{item.title}
           注册路由(无需声明,正常注册即可):
           组件接收参数:this.props.location.state || {}
           备注:此处的 state 是路由身上的 state,不是组件的 state, 刷新也可以保留住参数
    

    7、路由组件 与 一般组件

     	1、写法不同:
             路由组件: 
             一般组件:            
     	2、存放位置不同:
              路由组件:pages
              一般路由:components            
         3、接受到的 props 不同:
              一般组件: 组件标签传递了什么,子组件 props 就收到什么
              路由组件:接受到三个属性
                  histor:{ go,  goBack,  goForward,  push,  replace }
                  location: { pathname,  search,  state }
                  match: { isExact, params, path, url } 
                  isExact: 模糊匹配 or 精准匹配
    
  • Link 和 NavLink
    Link 没有高亮效果
    NavLink 标签可以通过 activeClassName 设置高亮类名,默认值是 active

    	//  NavLink 封装
    	import React, { Component } from 'react'
        import {NavLink} from 'react-router-dom'
        export default class MyNavLink extends Component {
             render() {
                 return (
                     <NavLink activeClassName='linkactive' {...this.props} />
                 )
             }
        }
       // 封装使用
       <MyNavLink to="/home" >home</MyNavLink>
    	// 标签体内容是一个特殊的标签属性 children
    	// children 可以通过 this.props.children 获取标签体内容,再利用{...this.props}绑定到组件上
    
  • Switch
    通常情况下,path 和 component 是一一对应关系,不使用 Switch, 路由匹配后还会继续往下匹配
    Siwtch 作用:实现单一匹配 - 路由匹配到后,终止往下匹配,提高路由匹配效率

    	// 语法
    	<Switch>
             <Route path='/demo' component={Demo} />
         </Siwtch>
    
  • Redirect 重定向
    一般写在所有路由注册的最下方,当所有路由都匹配不上的时候,跳转到 Redirect 指定的路由

    	// 语法
    	<Switch>    
    		 <Route path='/demo' component={Demo} />
    		 <Redirect to="/demo" />
        </Switch>
    
  • withRouter

    只有路由组件才有 history, 一般组件没有 history,无法使用 this.props.history 的AP方法
    withRouter 可以加工一般组件,让一般组件具备路由组件所特有的 API
    withRouter 返回值是一个新组件
    语法: export default withRouter(一般组件)

    	import React, { Component } from 'react'
        import { withRouter } from 'react-router-dom'
        class Header extends Component {
            back = () => {
                this.props.history.back()
            }
            render() {
                return (
                    <div>
                        <button onClick={this.back}>回退</button>
                    </div>
                )
            }
        }
        // 暴露 withRouter 加工过的一般组件
        export default withRouter(Header)
    	
    	// 使用组件
    	<Header />
    

8. React-router 6

与 react-router 5 版本小相比,改变了什么?
	1、内置组件的变化:移除,新增了等
	2、语法的变化:component = {Home} 变为 element = {}等
	3、新增多个hook:useParams、useNavigate、useMatch 等
	4、官方明确推荐函数式组件
  • Route
    语法变化 component 变为 element,属性值为标签形式
    caseSensitive属性用于指定,匹配时是否区分大小写(默认false)
    Route 可以嵌套使用,且可配合 useRoutes() 配置“路由表”,当需要通过 组件来渲染其子路由

    	// React-router 5
    	<Route path="/home" component={Home} />
    	// React-router 6
    	<Routes>
    		// path属性用于定义路径,element 属性用于定义当前路径所对应的组件
    		<Route path="/home" element={<Home />} />
    		// 定义嵌套路由,home是以及路由,对应的路径 /home*/
    		<Route path="home" element={<Home />} >
    			<Route path="news" element={<news />} ></Route>
    			<Route path="news2" element={<news2/>} ></Route>
    		</Route>
    		// Router 也可以不写 element 属性,这是就是用于展示嵌套的路由,对应的路径是 /home/xxx
    		<Route path="home">
    			<Route path="xxx" element={<xxx/>} />
    		</Route>
    	</Routes>
    	
    
  • Routes 代替 Switch
    React-router 6 移除了 Switch,新增了Routes, Routes 跟 Switch的规则一样,匹配到一个,就不会继续向下匹配,
    Routes 要和 Route 配合使用,且必须用 Routes 包裹 Route:React-router 5 不写 Switch 不会报错,React-router 6 Route 不用 Routes 包裹会报错

    	// React-router 5
    	<Switch>
    		<Route path="/home" component={Home} />
    	</Switch>
    	// React-router 6
    	<Routes>
    		<Route path="/home" element={<Home />} />
    	</Routes>
    
  • Navigate 代替 Redirect 重定向
    Navigate 只要渲染就会引起试图的切换,to 的属性必须写,replace 属性可选

	// React-router 5
	<Switch>
		<Route path="/home" component={Home} />
		<Redirect to="/about" />
	</Switch>
	// React-router 6
	<Routes>
		<Route path="/home" element={<Home />} />
		<Route path="/" element={<Navigate to="/about" />} />
	</Routes>

页面跳转例子:

	import React,{useState} from 'react'
	import {Navigate} from 'react-router-dom'
	
	export default  function Demo(){	
	  const [sum, setSum] = useState(1)	
	  return (      
	    <div>
	      {sum === 2? <Navigate to="/home" replace={true}/> : <h3>当前的sum的值是:{sum}</h3> }
	      <button onClick={()=>setSum(2)}>点击变2</button>
	    </div>
	  )
	}
  • NavLink
    React-router 6指定高亮的类名语法修改了

    	// React-router 5
    	<NavLink activeClassName='linkactive' {...this.props} />
    	// React-router 6
    	<NavLink className={isActive => isActive ? 'linkactive' : 'link'} {...this.props} />
    	// 优化写法
    	function computedClassName(isActive){
    		return isActive ? 'linkactive' : 'link'
    	}
    	<NavLink className={computedClassName} {...this.props} />
    
  • useRoutes路由表 与 Outlet嵌套路由
    定义路由表:创建routes文件夹,生成inde.jsx文件

    import Home from './pages/Home'
    import About from './pages/About'
    import TabOne from './pages/Home/TabOne'
    import TabTow from './pages/Home/TabTow'
    import {Navigator} from 'react-router-dom'
    export default[
        {
            path: '/home',
            element: <Home />,
            // 定义子路由
            children:[{
                path: 'tabOne',
                element: <TabOne />
            },{
                path: 'tabTow',
                element: <TabTow />
            }]
        },{
            path: '/about',
            element: <About />
        },{
            path: '/',
            element: <Navigator to="/home" />
        }
    ]
    

    使用路由表 uesRoutes

    	import React from 'react'		
    	import {NavLink, uesRoutes} from 'react-router-dom'
    	import routes from './routes'	
    	export default function App() {
    	    // 根据路由表生成对应的规则
    	    const element = uesRoutes(routes)
    	    render() {
    	        return (
    	            <div className='containt'>
    	                 {/* 路由连接 */}
                         <NavLink to="/home">home</NavLink>
                         <NavLink to="/about">about</NavLink>
                         {/* 注册路由 */}
                         {element}
    	            </div>
    	        )
        }
    }
    

    Outlet嵌套路由:Home 组件展示子路由,因为App已经引入了routes.js,所以 Home作为App组件不需要引入

    import React from 'react'
    import { NavLink, Outlet } from 'react-router-dom'
    
    export default function Home() {
      return (
          <div className='containt'>
              {/* 路由连接-注意子路由不写/ */}
                <NavLink to="tabOne">home</NavLink>
                <NavLink to="tabTow">about</NavLink>
                {/* 指定路由组件呈现的位置 */}
                {Outlet}
          </div>
        )
    }
    
  • 路由传参
    1、传递 Prarams 参数,使用useParams 或 useMatch 获取

    // 路由表
    import Home from './pages/Home'
    import {Navigator} from 'react-router-dom'
    export default[
           {
           	// Prarams 参数需要占位
               path: '/home/:id/:title',
               element: <Home />,
           }{
               path: '/',
               element: <Navigator to="/home" />
           }
       ]
    
    	// 使用路由表uesRoutes
    	import React from 'react'		
    	import {NavLink, uesRoutes} from 'react-router-dom'
    	import routes from './routes'	
    	export default function App() {
    	    // 根据路由表生成对应的规则
    	    const element = uesRoutes(routes)
    	    render() {
    	        return (
    	            <div className='containt'>
    	                 {/* 路由连接- 定义参数 */}
                         <NavLink to={`/home/${id}/${title}`}>home</NavLink>
                         {/* 注册路由 */}
                         {element}
    	            </div>
    	        )
        }
    }
    

    Home 组件 接收参数

    import React from 'react'
    import { useParams, useMatch } from 'react-router-dom'
    
    export default function Home() {
    	// useParams获取 路由参数
    	const {id, title} = useParams()
    	// useMatch 获取 路由参数
    	const x = useMatch('/home/:id/:title')
    	return (
    	    <div className='containt'> </div>
    	  )
    }
    

    2、传递 search参数,使用useSearchParams 或 useLocation获取

    	import Home from './pages/Home'
    	import {Navigator} from 'react-router-dom'
    	export default[
            {
                path: '/home',
                element: <Home />,
            }{
                path: '/',
                element: <Navigator to="/home" />
            }
        ]
    
    	// 使用路由表uesRoutes
    	import React from 'react'		
    	import {NavLink, uesRoutes} from 'react-router-dom'
    	import routes from './routes'	
    	export default function App() {
    	    // 根据路由表生成对应的规则
    	    const element = uesRoutes(routes)
    	    render() {
    	        return (
    	            <div className='containt'>
    	                 {/* 路由连接- 定义参数 */}
                         <NavLink to={`/home?id=${id}&title=${title}`}>home</NavLink>
                         {/* 注册路由 */}
                         {element}
    	            </div>
    	        )
        }
    }
    

    Home 组件 接收参数

    import React from 'react'
    import { useSearchParams, useLocation } from 'react-router-dom'
    
    export default function Home() {
    	// useSearchParams获取路由参数,search.get()获取参数值
    	const [search, setSearch] = useSearchParams()
    	console.log(search.get('id'))	
    
    	// useLocation 获取参数 - 获取的是对象
    	const x = useLocation()
    	return (
    	    <div className='containt'>
    			// setSearch 用于更新参数值	
    			<button onClick={()=>setSearch('id=1&title=花姐')}>更新链接参数</button>
    		</div>
    	  )
    }
    

    3、传递 state 参数,使用useLocation获取

    	import Home from './pages/Home'
    	import {Navigator} from 'react-router-dom'
    	export default[
            {
                path: '/home',
                element: <Home />,
            }{
                path: '/',
                element: <Navigator to="/home" />
            }
        ]
    
    	// 使用路由表uesRoutes
    	import React from 'react'		
    	import {NavLink, uesRoutes} from 'react-router-dom'
    	import routes from './routes'	
    	export default function App() {
    	    // 根据路由表生成对应的规则
    	    const element = uesRoutes(routes)
    	    render() {
    	        return (
    	            <div className='containt'>
    	                 {/* 路由连接- 定义参数 */}
                         <NavLink to="/home" state={{ id, title }}>home</NavLink>
                         {/* 注册路由 */}
                         {element}
    	            </div>
    	        )
        }
    }
    

    Home 组件 接收参数

    import React from 'react'
    import { useLocation } from 'react-router-dom'
    
    export default function Home() {
    	// useLocation 获取参数 - 获取的是对象
    	const state = useLocation()
    	return (
    	    <div className='containt'></div>
    	  )
    }
    
  • 编程式路由导航 useNavigate

    	import React from 'react'		
    	import {NavLink, uesRoutes, useNavigate} from 'react-router-dom'
    	import routes from './routes'	
    	export default function App() {
    		const navigate = userNavigate()
    	    // 根据路由表生成对应的规则
    	    const element = uesRoutes(routes)
    		
    		function goToDetail(id){
    			// 子路由不能写 /
    			navigate('tabOne', {
    				repalce: false, // 不写默认false
    				// 如果传递params参数与search参数,则拼接在链接上,不写state
    				state:{id}
    			})
    			// navigate(-1), navigate(1) 实现前进后退
    		}
    	    render() {
    	        return (
    	            <div className='containt'>
    	                 {/* 路由连接- 定义参数 */}
                         <NavLink to="/home" state={{ id, title }}>home</NavLink>
                         <button onClick="goToDetail">点击跳转</button>
                         {/* 注册路由 */}
                         {element}
    	            </div>
    	        )
        }
    }
    
  • useInRouterContext()、useNavigation()返回当前的导航类型、userOutlet()呈现当前组件中渲染的嵌套路由、useResolvePath()用于解析path,search,hash值

10. ant-design 的基本使用

官网: https://ant.design/index-cn
安装: npm i antd
antd 提倡按需加载模块,使用相关 api 时,需要注意引入的文件名
antd 适用于搭建后台管理系统
按需引入样式:官方配置文档-高级配置

11. redux

redux 是一个专门用于做状态管理的JS库,可以用在 react、angular、 vue等项目中,但是基本与 react 配合使用

官网: https://www.redux.org.cn/
安装:npm i redux
redux开发者工具:谷歌安装扩展 redux-devtools,还要在项目安装 redux-devtools-extension 库,并在 store.js 引用:

		import {composeWithDevTools} from 'redux-dextools-extension'
		
		export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
  • 作用: 集中式管理 react 应用中多个组件共享的状态(独立于所有组件之外)

  • 使用场景
    某个组件的状态,需要让其他组件可以随时拿到(共享)
    一个组件需要改变另外一个组件的状态(通信)
    总体原则:能不用就不用,如果不用比较吃力才考虑使用

  • redux 工作流程React 全家桶_第2张图片

     React Components: 触发动作
    
  • 三个核心概念 action、reducer、Store

     action(动作对象):
     	1、同步 action
     		当 action 的值为一般对象是为同步 action,包含两个属性:
     			type:标识属性,值为字符串,唯一,必要属性
     			data:数据属性,值任意属性,可选属性
     	2、异步 action
     		当 action 的值为函数时,为异步 action,异步 action 中一般都会调用同步 action			        		
     
     reducer:
         reducer 的本质是一个函数,接收 preState, action, 返回加工后的新 state 
         reducer 有两个作用:初始化状态,加工状态
         reducer 被第一次调用时,是 store 自动触发的,传递的 preState 是 undefined
     
     Store:
         将state、action、reducer 联系在一起的对象(不做数据处理)
         如何得到此对象?
             import { createStore } from 'redux'
             import reducer from './reducers'
             const store = createStore (reducer )
         此对象的功能:
         	getState():得到 state 
         	dispatch(action):分发 action,触发 reducer 调用。产生新的 state
         	subscribe(listener):注册监听,当产生了新的 state 时,自动调用
         			在组件的 componentDidMount 中或在 index.js 中通过 store.subscribe() 检测 store 中状态的改变
         			一旦改变重新调用 this.setState({}) 或 重新渲染 
    
  • 合并Reducers
    每个reducer只负责管理全局state中它负责的一部分。每个reducer的state参数都不同,分别对应它管理的那部分state数据
    Redux 提供了combineReducers()来合并所有的reducer

    	/**
    	 * 该文件用于汇总所有的reducer为一个总的reducer
    	 * combineReducers 用于汇总多个reducer
    	 */		
    	 import { combineReducers} from 'redux'		
    	// 引入为 count 组件服务的 reducer
    	import count from './count'
    	import personObj from './person'		
    	// 汇总所有的reducer 变为一个总的reducer
    	export default combineReducers({
    	    count,
    	    personObj
    	})
    

redux 实践:实现运算

  • store.js
    引入 redux 中的 createStore 函数,创建一个 store, createStore 调用时要传入一个为其服务的 reducer
    记得暴露 store 对象

    	/**
    	 * 该文件专门用于暴露一个 store 对象,整个应用只有一个 store 对象
    	 */	
    	// 引入 createStore, 专门用于创建 redux 最核心的 store 对象, applyMiddleware 用于执行中间件
    	import {createStore, applyMiddleware} from 'redux'
    	// 引入为 count 组件服务的 reducer
    	import countReducer from './count_reducer'
    	// 引入 redux-thunk 用于支持异步 action 
    	import thunk from 'redux-thunk' 
    	
    	export default createStore(countReducer, applyMiddleware(thunk))
    
  • count_reducer.js
    reducer 的本质是一个函数,接收 preState, action,返回加工后的状态
    reducer 有两个作用:初始化状态,加工状态
    reducer 被第一次调用时,是 store 自动触发的,传递的 preState 是 undefined

    	/**
    	 * 该文件是用于创建一个 Count 组件服务的 reducer, reducer 的本质就是一个函数
    	 * reducer 是一个纯函数,相同输入得到相同结果
    	 * reducer 函数会接到两个参数,分别为:之前的状态(preState)、动作对象(action)
    	 */
    	import {INCREMENT} from './constant'
    	
    	export default function countReducer(preState = 0, action) {
    	    const {type, data} = action
    	    // 根据 type 决定如何加工数据
    	    switch (type) {
    	        case INCREMENT:
    	            return preState + data
    	        default:
    	            return preState
    	    }
    	}
    
  • count_action.js
    专门用于创建 action 对象

    	/**
    	 * 该文件专门为 Count 组件生成 action 对象
    	 * 当 action 的值为对象时,为同步 action, action({type,data})
    	 * 当 action 的值为函数时,为异步 action, action(function()) 异步 action 中一般都会调用同步 action,异步 action 不是必要的
    	 */
    	import {INCREMENT} from './constant'
    	// 同步 action
    	export const createIncrementAction = data => ({type: INCREMENT, data})
    	// 异步 action : 注意异步action 里面调用 dispatch 的写法
    	export const createIncrementAsyncAction = (data, time) => {
    	    return (dispatch)=>{
    	        setTimeout(()=>{
    	            dispatch(createIncrementAction(data))
    	        }, time)
    	    }
    	}
    
  • constant.js
    放置容易写错 action 中的 type 值

    	/**
    	 * 该模块用于定义action 对象中 type 类型的常量值
    	 */			
    	export const INCREMENT = 'increment'
    
  • CountRedux.jsx
    使用 action 的组件,注意 store.subscribe 的写法
    在组件的 componentDidMount 中或在 index.js 中通过 store.subscribe() 检测 store 中状态的改变
    一旦改变重新调用 this.setState({}) 或 重新渲染

    	import React, { Component } from 'react'
    	// 引入 store 用于获取 redux 中保存的状态
    	import store from '../redux/store'
    	// 引入 actionCreator, 用于创建 action 对象
    	import {createIncrementAction, createIncrementAsyncAction} from '../redux/count_action'
    	
    	export default class CountRedux extends Component {
    
    	    componentDidMount(){
    	        // 检测 redux 中状态的变化,只要变化,就调用 setState 触发 render 刷新
    	        store.subscribe(()=> {
    	            this.setState({})
    	        })	
    	        /**
    	            store.subscribe 也可以写在入口文件 index.js - 一劳永逸
    	            store.subscribe(()=> {
    	                root.render(    );
    	            })
    	         */
    	    }
    	
    	    increment = ()=> {
    	        let {value} = this.selectNumber
    	        // 通知 redux 操作 同步
    	        store.dispatch(createIncrementAction(value*1))
    	    } 
    	
    	    incrementAsync = ()=> {
    	        let {value} = this.selectNumber
    	        // setTimeout(()=>{
    	        //     store.dispatch(createIncrementAction(value*1))
    	        // }, 1000)
    	        // 异步 action,与上面的 setTimeout  效果一致
    	        store.dispatch(createIncrementAsyncAction(value*1, 1000))       
    	    }
    	
    	    render() {
    	        return (
    	        <div>
    	            <div>当前求和为: { store.getState() }</div>
    	            <select ref={c => this.selectNumber = c}>
    	                <option value="1">1</option>
    	                <option value="2">2</option>
    	            </select>
    	            <button onClick={this.increment}>+</button>
    	            <button onClick={this.incrementAsync }>异步+</button>
    	        </div>
    	        )
    	    }
    	}	
    

12. React-redux

react-redux 是由 react 官方提供,redux不是 react 官方提供
react-redux 实际也是通过 redux 库创建 store ,再由 react-redux 的 connect 函数创建容器组件,并由组件容器与store交互,而不是ui组件自身去触发
注意: React-redux 的store.js、action.js、reducer.js、constant.js 创建,以及合并多个redux都与 redux 一样

	1、所有的 UI 组件都应该包裹一个容器组件,他们是父子关系
	2、容器组件是真正和 redux 打交道的,里面可以随意的使用 redux 的 api
	3、UI 组件不能使用任何 redux 的 api
	4、容器组件会传给 UI 组件:1)、redux 中所保存的状态,2)、用于操作状态的方法
	5、UI 组件放在 component,容器组件放在 containers
	6、备注:容易给 UI 组件传递:状态、操作状态的方法,均通过 props 传递

基本使用:

1、容器组件和ui组件整合一个文件
 	UI 组件:不能使用任何 redux 的 api,只负责页面的呈现、交互等,在 UI 组件中通过 this.props.key 读取和操作状态
    容器组件: 由 react-redux 的 connect 函数创建,负责和 redux 通信,将结果与操作状态的方法传递给 UI 组件
    创建一个容器组件:
     	connect(mapStateToProps,mapDispatchToProps)(UI 组件)
        mapStateToProps:映射状态,返回值是一个对象
        mapDispatchToProps: 映射操作状态的方法,返回值是一个对象, 可以简写成一个对象
        简写语法:
        	connect(
                 state => {key: value}, // 映射状态
                 {key: xxxAction} // 映射操作状态的方法
             )(UI组件)
	// 整合容器组件和ui组件示例代码
	import React, { Component } from 'react'	
	// 引入 connect 用于连接 ui 组件 与 redux
	import {connect} from 'react-redux'
	// 引入 action 方法	
	import {increment, incrementAsync} from '../../redux/action/count'
	// 此处的 ui 组件不暴露
	class Count extends Component {	
	    increment = ()=> {
	        let {value} = this.selectNumber
	        this.props.increment(value*1)
	    } 	
	    incrementAsync = ()=> {
	        let {value} = this.selectNumber     
	        this.props.incrementAsync(value*1, 2000)
	    }	
	    render() {
	        return (
	        <div>
	            <h2>我是Count组件</h2>
	            <div>总人数为: {this.props.presonCount},当前求和为: { this.props.count }</div>
	            <select ref={c => this.selectNumber = c}>
	                <option value="1">1</option>
	                <option value="2">2</option>
	            </select>
	            <button onClick={this.increment}>+</button>
	            <button onClick={this.incrementAsync}>异步加</button>
	        </div>
	        )
	    }
	}
	
	/** 引入 connect 生成一个 Count 容器组件,并暴露 */
	export default connect(
		// mapStateToProps 的简写
	    state => ({ count: state.count, presonCount: state.personObj.length }), 
	    // mapDispatchToProps 一般写法
	    // dispatch => ({
	    //     increment: number => dispatch(increment(number)),
	    //     incrementAsync: (number, time) => dispatch(incrementAsync(number, time))
	    // })
	    // mapDispatchToProps 的简写
	    {
	        increment,
	        incrementAsync
	    })(Count)
 2、无需自己给容器组件传递 store, 给  包裹一个  即可
 		使用react-redux 后也不用再自己检测redux 中状态的改变,容器组件可以自动完成这个工作
	import React from 'react'
	import ReactDOM  from 'react-dom/client'
	import App from './App'
	
	// Provider 用于批量给组件传递 store
	import store from './redux/store';
	import {Provider} from 'react-redux'
	
	const root = ReactDOM.createRoot(document.getElementById('root'));
	root.render(
	  <React.StrictMode>
	    {/* 此处需要用 Provider 包裹 App, 目的是让 App 的所有后代容器组件都能接收到 store  */}
	    <Provider store={store}>
	      <App />
	    </Provider>
	  </React.StrictMode>
	);

13. react 项目打包

打包后可以借助serve库运行打包好的文件
安装: npm i serve -g
到打包好的index目录运行:serve


扩展

1. setState

setState 更新状态的2种写法:
    1、setState(stateChange, [callback]) 对象式的 setState
        1)、stateChange 为状态改变对象(该对象可以体现出状态的更改)
        2)、callback 是可选的回调函数,它在状态更新完毕,界面也更新后(render调用后)才被调用
    2、setState(updater, [callback]) 函数式的 setState
        1)、updater 为返回 stateChange 对象的函数
        2)、updater 可以接收到 state 和 props 
        3)、callback 是可选的回调函数,它在状态更新、界面也更新后(render调用后)才被调用
    总结:
        对象式的 setSate 是函数式的 setState 的简写方式(语法糖)
        使用原则:
            如果新状态不依赖原状态,使用对象方式
            如果新状态依赖原状态,使用函数方式
            如果需要在 setState() 执行后获取最新的状态数据,要在第二个 callback 函数中读取

2. lazyLoad

路由组件的 lazyLoad
1、通过react的lazy函数配合import()函数动态加载路由组件 ===》 路由组件代码会被分开打包

	const Login = Lazy(()=>import('@/pages/Login'))

2、通过指定在加载得到路由打包文件前显示一个自定义 loading 界面

	<Suspense fallback={<h1>loading...</h1>}>
	    <Switch>
	        <Route path='/xxx' componet={Xxx}>
	        <Redirect to="login">
	    </Switch>
	</Suspense>

3、 标签的 fallback 可以传入一个组件

	// 注意写在 Suspense fallback 中的 Loading 组件不能使用懒加载
    import Loading from '../Loading'
    <Suspense fallback={<Loading/>}>
        <Switch>
            <Route path='/xxx' componet={Xxx}>
            <Redirect to="login">
        </Switch>
    </Suspense>

3. Hooks

Hook是React 16.8.0版本增加的新特性/新语法,可以在函数组件中使用 state 以及其他的 React 特性

  1. 三个常用的 Hooks
    State Hook:React.useState()
    Effect Hook: React.useEffect()
    Ref Hook: React.useRef()

  2. State Hook

    State Hook 能让函数组件也可以有 state 状态,并进行状态数据的读写操作
    语法:const [xxx, setXxxx] = React.useState(initValue)
    useState()说明:
        参数:第一次初始化指定的值在内部作缓存
        返回值:包含2个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数
    setXxx 2种写法:
        setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
        setXxx(value => newValue): 参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
    
    	import React from 'react'		
    	function Demo(){
    	  const [count, setCount] = React.useState(0)
    	  const [name, setName] = React.useState('橘宝')		
    	  function add() {
    	    // 第一种写法
    	    // setCount(count + 1)
    	    // 第二种写法
    	    setCount(count => count+1)
    	  }		
    	  function changeName() {
    	    setName('花姐')
    	  }		
    	  return (      
    	    <div>
    	      <h2>当前数字为{count}, 名字:{name}</h2>
    	      <button onClick={add}>1</button>
    	      <button onClick={changeName}>改名</button>
    	    </div>
    	  )
    	}		
    	export default Demo
    
  3. Effect Hook

    Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
    React 中的副作用操作:发送ajax请求、设置订阅/启动定时器、手动更改真实DOM
    语法和说明:
            React.useEffect(()=>{
                // 在此处可以执行任何带副作用操作, 相当于 componentDidMount 与 componentDidUpdate比如页面初始化,加载数据等
                return()=>{
                    // 在组件卸载前执行,相当于 componentWillUnmount,可以做一些收尾工作,比如清除定时器,取消订阅等
                }
            }, [stateVlaue]) 
            // 如果指定的是[],回调函数只会在第一次render() 后执行
            // 如果不指定,则会监听所有的 state 更新,并每次更新都执行,如果传对应的state,则监听对应的state	            
    可以把 useEffect Hook 看做是三个函数的结合:
        componentDidMount()、
        componentDidUpdate()、
        componentWillUnmount()
    
    	import React from 'react'
    	import root from '../../index'		
    	function Demo(){		
    	  const [count, setCount] = React.useState(0)		
    	  React.useEffect(()=> {
    	    // 此处返回的函数体相当于 componentDidMount 与 componentDidUpdate
    	    let timer = setInterval(()=>{
    	      // 注意此处的setCount需要写函数形式,否则拿不到count
    	      setCount(count => count+1)
    	    }, 1000)
    	    // 此处返回的函数相当于 componentWillUnmount
    	    return () => {
    	      clearInterval(timer)
    	    }
    	  }, [])
    	  		
    	  function unmount() {
    	    // 注意 18 版本的卸载,与之前的区别,需要在入口文件中,将 root 暴露
    	    root.unmount()
    	  }		
    	  return (      
    	    <div>
    	      <h2>当前数字为{count}</h2>
    	      <button onClick={unmount}>卸载组件</button>
    	    </div>
    	  )
    	}		
    	export default Demo
    
  4. Ref Hook

    Ref Hook 可以在含糊组件中储存/查找组件内的标签或任意其他数据
    语法:const refContainer = useRef()
    作用:保存标签对象,功能与 React.createRef() 一样
    
    	import React from 'react'	
    	
    	function Demo(){		
    	  const myRef = React.useRef()		
    	  function show() {
    	    console.log(myRef.current.value)
    	  }		
    	  return (      
    	    <div>
    	      <input type="text" ref={myRef}/>
    	      <button onClick={show}>打印数据</button>
    	    </div>
    	  )
    	}
    	export default Demo
    

4. Fragment

	import React, { Component, Fragment } from 'react'	
	export default class Demo extends Component {
	  render() {
	    return (
	      <Fragment key={1}>
	        <div>Fragment 标签可以代表外层根节点,并且会被解析,页面渲染时不显示</div>
	        <div>Fragment 可以设置key值</div>
	      </Fragment>
	    )
	  }
	}

5. Context

用于组件间通信方式,常用于【祖组件】与【后代组件】间通信
注意:在应用开发中,一般不用context,一般用来封装 react 插件

  1)、创建 Context 容器对象
      const XxxContext = React.createContext()
      
  2)、渲染子组件时,外面包裹 XXXContext.Provider,通过 Value 属性给后代组件传递数据:
       子组件 
      
  3)、后代组件读取数据:      
      // 第一种方式:适用于类组件
      static contextType = XxxContext // 声明接收 context
      this.context // 读取context的value数据
      
      // 第二种方式:函数组件与类组件都可以
      
          {
              value => { 要显示的内容  }   // value 就是context中的value数据
          }
      
	import React, { Component } from 'react'

	const MyContext = React.createContext()
	const {Provider, Consumer} = MyContext
	
	export default class A extends Component {
	    state = {username:'花姐'}
	    render() {
	        let {username} = this.state
	        return (
	            <div>
	                <p>我是A组件,我的用户名是: {username}</p>
	                <Provider value={{username, age: 5}}>
	                    <B/>
	                </Provider>
	            </div>
	        )
	    }
	}
	
	class B extends Component {
	    render() {
	        return (
	            <div>
	                <p>我是B组件</p>
	                <C/>
	                <D/>
	            </div>
	        )
	    }
	}
	
	// 类式接收context
	class C extends Component {
	    // 声明接收context
	    static contextType = MyContext
	    render() {
	        const {username, age} = this.context
	        return (
	            <div>
	                <p>我是类式 C 组件,A组件的用户名是:{username}, {age}</p>
	            </div>
	        )
	    }
	}
	
	// 函数接收context
	function D() {
	    return (
	        <div>
	            <p>我是函数式 D 组件,A组件的用户名是:
	                <Consumer>
	                    {
	                        value => `${value.username}, ${value.age}`
	                    }
	                </Consumer></p>
	        </div>
	    )
	}

6. 组件优化

Component 的2个问题
        1)、只要执行了setState,即使不改变状态数据,组件也会重新render()
        2)、即使子组件没有用到父组件的任何数据,只要父组件重新render(),就会自动重新render子组件 ==> 效率低
    原因:
        Component中的 shouldComponentUpdate()总是返回 true
    效率高的做法:
        只有当组件的 state 或 props 数据发生改变时才重新render()

解决:
方法1:重写 shouldComponentUpdate() 方法,比较新旧 state 或 props 数据,如果有变化才返回 true,如果没有返回false

	import React, { Component } from 'react'
	
	export default class Parent extends Component {
	
	    state = {catName: '花姐'}
	
	    changeCat = ()=> {
	        this.setState({catName: '橘宝'})
	    }
	
	    shouldComponentUpdate(nextProps, nextState){
	        if(this.state.catName === nextState.catName) return false
	        return true
	    }
	
	    render() {
	        console.log('Parent---Chils')
	        return (
	            <div>
	                <h3>Parent,{this.state.catName}</h3>
	                <button onClick={this.changeCat}>改名</button>
	                <Chils catName="橘宝"/>
	            </div>
	        )
	    }
	}
	
	class Chils extends Component {
	
	    shouldComponentUpdate(nextProps, nextState){
	        if(this.props.catName === nextProps.catName) return false
	        return true
	    }
	
	    render() {
	        console.log('render---Chils')
	        return (
	            <div>
	                <h3>Chils</h3>
	            </div>
	        )
	    }
	  }

方法2:,使用PureComponent,PureComponent 重写了 shouldComponentUpdate(),只有sate或props数据有变化才返回true

注意:
       只是进行 state 与 props 数据的浅比较,如果只是数据对象内部变了,返回false
       不要直接修改 state 数据,而是要产生新数据
项目中一般用 PureComponent 来优化
	import React, { PureComponent } from 'react'

	export default class Parent extends PureComponent {
	
	    state = {catName: '花姐'}
	
	    changeCat = ()=> {
	        this.setState({catName: '橘宝'})
	    }
	
	    render() {
	        console.log('Parent---Chils')
	        return (
	            <div>
	                <h3>Parent,{this.state.catName}</h3>
	                <button onClick={this.changeCat}>改名</button>
	                <Chils catName="橘宝"/>
	            </div>
	        )
	    }
	}
	
	class Chils extends PureComponent {
	
	    render() {
	        console.log('render---Chils')
	        return (
	            <div>
	                <h3>Chils</h3>
	            </div>
	        )
	    }
	}

7. render props(相当于插槽)

向组件内部动态传入带内容的结构

vue 中:
	使用slot 技术,通过组件标签体传入结构 
React中:
	使用 children props:通过组件标签体传入结构
	使用 render props:通过组件标签属性传入结构,一般用 render 函数属性
children Props:
	语法:
	A组件渲染:{this.props.children }
	问题:如果B组件需要 A组件内的数据,无法传递
render Props:
	语法: }
	A组件:{this.props.render(内部state数据)}
	B组件:读取A组件传入的数据显示 {this.props.data}
	import React, { Component } from 'react'
	export default class Parent extends Component {
	    render() {
	        return (
	            <div>
	                <p>我是Parent组件</p>
	                // 组件的标签属性 render 返回一个组件
	                <ChilsA render={name => <ChilsB name={name} />} />
	            </div>
	        )
	    }
	}	
	class ChilsA extends Component {
	    state = {name: 'ChilsA'}
	    render() {
	        return (
	            <div>
	                <p>我是ChilsA组件</p>
	                // 使用props.render 渲染传入的组件
	                {this.props.render(this.state.name)}
	            </div>
	        )
	    }
	}	
	class ChilsB extends Component {
	    render() {
	        return (
	            <div>
	                <p>我是ChilsB组件,{this.props.name}的子组件</p>
	            </div>
	        )
	    }
	}

8. 错误边界 ErrorBoundary

错误边界 Error boundary :用来捕获后代组件错误,渲染出备用页面
特点:
    只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:
    getDerivedStateFromError 配合 componentDidCatch
        // 当Parent的子组件出现报错稍后,会触发 getDerivedStateFromError 调用,并携带错误信息
	    static getDerivedStateFromError(error){
	        return {hasError: error}
	    }
	    // 子组件渲染错误会调用-给后台做出反馈
	    componentDidCatch(){
	        console.log('统计错误此处,反馈给服务器,用于通知编码人员进行bug的解决')
	    }
	import React, { Component } from 'react'

	export default class Parent extends Component {
	
	    state = {
	        hasError:'', // 用于标识子组件是否产生错误
	    }
	    // 当Parent的子组件出现报错稍后,会触发 getDerivedStateFromError 调用,并携带错误信息
	    static getDerivedStateFromError(error){
	        return {hasError: error}
	    }
	
	    // 子组件渲染错误会调用-给后台做出反馈
	    componentDidCatch(){
	        console.log('统计错误此处,反馈给服务器,用于通知编码人员进行bug的解决')
	    }
	    
	    render() {
	        return (
	            <div>
	                <p>我是Parent组件</p>
	                {this.state.hasError? <h2>当前网络不稳定,请稍后再试</h2>: <ChilsA />}
	            </div>
	        )
	    }
	}
	
	class ChilsA extends Component {
	    state = {list:'123'}
	    render() {
	        return (
	            <div>
	                <p>我是ChilsA组件</p>
	                {
	                    this.state.list.map((item,index) => {
	                        return <div key={index}>{item.name}</div>
	                    })
	                }
	            </div>
	        )
	    }
	}

总结

1. 组件间的通信

组件间的关系: 父子组件、兄弟组件(非嵌套组件)、祖孙组件(跨级组件)
几种通信方式:

1、props:
	1)、children props
			父传子: 通过标签属性传递,子组件通过 props 获取
			子传父:props+回调的⽅式,父组件通过 props 传递给子组件一个函数,子组件调用这个函数
	2)、render props
			父传子: 通过组件标签属性 render  传入结构, 父组件通过 {this.props.render(内部state数据)} 渲染
			子组件: {this.props.data} 读取数据
	状态提升:某些组件的 state 放在它们共同的父组件 state 中
2、消息订阅 - 发布:
		发布者发布事件(PubSub.publish()),订阅者监听事件(PubSub.subscribe())并做出反应,我们可以通过引⼊event 模块进⾏通信 -- 适用于任意组件间的通讯
3、集中式管理:
	借助 Redux 或者 Mobx 等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态。
4、conText :
	生产者-消费者模式
	Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过

比较好的搭配方式:

父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅发布、集中式管理、conText(开发用的少,封装插件用的多)

你可能感兴趣的:(react.js)