React
# 1. React入门
React使用虚拟DOM,不总是直接操作页面的真实DOM,以至于会比较高效- 使用
DOM Diffing算法,最小化页面重绘
# 1.1 React的基本使用
# 1.1.1 相关js库
react.js:React核心库react-dom.js:提供操作DOM的react扩展库babel.min.js:解析JSX语法转换为JS代码的库
# 1.1.2 创建虚拟DOM
jsx创建虚拟DOM⭐️
创建
index.html并准备“容器”<div id="test"></div>引入
react依赖库<!-- 引入react和核心库 --> <script type="text/javascript" src="../js/16.x/react.development.js"></script> <!-- 引入react-dom,用于支持react操作DOM --> <script type="text/javascript" src="../js/16.x/react-dom.development.js"></script> <!-- 引入babel,用于将jsx转化为js --> <script type="text/javascript" src="../js/16.x/babel.min.js"></script>编写
jsx<script type="text/babel"> // 1. 创建虚拟DOM const VDOM = <h1>Hello React</h1> // jsx不需要加引号 // 2. 渲染虚拟DOM到页面 ReactDOM.render(VDOM, document.getElementById("test")) </script>render默认为覆盖不是追加
js创建虚拟DOM
<script type="text/javascript">
const VDOM = React.createElement("h1", {id: "title"}, "Hello React")
ReactDOM.render(VDOM, document.getElementById("test"))
</script>
对于多层级的虚拟DOM,js就会显得十分繁琐,jsx挺身而出
# 1.1.3 虚拟DOM与真实DOM
React提供了一些API来创建一种 “特别” 的一般js对象const VDOM = ( <h1 id="test"> <span>Hello React</span> </h1> )上面创建的就是一个简单的虚拟
DOM对象虚拟
DOM的本质是Object类型的对象虚拟
DOM相比真实DOM属性会少很多虚拟
DOM对象最终都会被React转换为真实的DOM我们编码时基本只需要操作
react的虚拟DOM相关数据,react会转换为真实DOM变化而更新界。
# 1.2 React Jsx
# 1.2.1 简介
jsx全称JavaScript XMLreact定义的一种类似于XML的JS扩展语法本质是
React.createElement(Component, props, ...children)方法的语法糖作用:用来简化创建虚拟
DOM写法
var ele = <h1>Hello JSX</h1>它不是字符串,也不是
HTML/XML标签它最终产生的就是一个
js对象标签名任意:
HTML标签或其他标签
# 1.2.2 基本语法
定义
DOM是,不要写引号如果说标签内的内容是一个变量则需要
{}进行取值<script type="text/babel"> // 1. 创建虚拟DOM const id = "haha" const data = "Hello React" const VDOM = ( <h2 id={id}> <span>{data}</span> </h2> ) // 2.渲染虚拟DOM到页面 ReactDOM.render(VDOM, document.getElementById("test")) </script>样式的类名指定不能用
class,要用className<style> .title{ background-color: orange; width: 200px; } </style> <script type="text/babel"> const VDOM = ( <h2 className="title"> <span>"Hello React"</span> </h2> ) ReactDOM.render(VDOM, document.getElementById("test")) </script>内敛样式需要使用
{{}},且如果是font-size这样有两个单词组成的需要用驼峰命名法<script type="text/babel"> const VDOM = ( <h2> <span style={{color: 'red', fontSize: '30px'}}>"Hello React"</span> </h2> ) ReactDOM.render(VDOM, document.getElementById("test")) </script>jsx只能有一个根标签标签必须闭合
<script type="text/babel"> const VDOM = ( <h2> <input type="text" /> </h2> ) ReactDOM.render(VDOM, document.getElementById("test")) </script>首字符小写标签仅能为
html的标准标签,首字符大写是一个自定义的组件
# 1.2.3 练习
jsx动态渲染数据
表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
变量
函数。。。
语句(代码):
if语句,for语句对于
jsx内部的{}只能为表达式
<div id="test"></div>
<script type="text/babel">
// 模拟后端数据
const data = ["Angular", "React", "Vue"]
const VDOM = (
<div>
<h1>前端js框架列表</h1>
<ul>
{
data.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
ReactDOM.render(VDOM, document.getElementById("test"))
</script>
每一个虚拟DOM节点,都需要一个key用来作为唯一标识
# 1.3 模块与组件、模块化与组件化
# 1.3.1 模块
- 理解:向外部提供特定功能的
js程序,一般就是一个js文件 - 为什么要拆分模块:随着业务逻辑的增加,代码越来越多且复杂
- 作用:复用
js,简化js的编写,提高js运行效率
# 1.3.2 组件
- 理解:用来实现局部功能效果的代码和资源的集合
html/css/js/image等等 - 为什么需要组件:一个界面的功能太过复杂
- 作用:复用编码,简化项目编码,提高运行效率
# 1.3.3 模块化
当应用的js都已模块来编写的,这个应用就是一个模块化的应用
# 1.3.4 组件化
当应用是以多组件的方式实现的,这个应用就是一个组件化的应用
# 2. React面向组件开发
# 2.1 基本理解和使用
# 2.1.1 使用React开发者工具调试
安装插件React Developer Tools
# 2.1.2 组件的创建方式
函数式组件
<div id="test"></div>
<script type="text/babel">
// 1. 创建函数式组件
function MyComponent() {
return (
<h1>函数式组件,适用于简单组件的定义</h1>
)
}
// 2. 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById("test"))
</script>
函数内部的this是ubderfined,因为babel编译后开启了严格模式
render之后的过程
React解析组件标签,找到了MyComponent组件- 返现组件是使用函数定义的,随后调用该函数,将返回的虚拟
DOM转为真实DOM,随后呈现在页面中
类式组件
<div id="test"></div>
<script type="text/babel">
// 1. 创建类式组件
class MyComponent extends React.Component{
render() {
return (
<h2>我是用类定义的组件</h2>
)
}
}
// 2. 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById("test"))
</script>
render是放在了原型对象上,供实例使用
render()函数内部的this指向MyComponent的实例对象
ReactDOM.render之后的过程
React解析组件标签,找到了MyComponent组件- 发现组件是类定义的,随后创建该类的实例,并通过该实例代用原型上的
render方法 - 将
render返回的虚拟DOM转换为真实DOM,随后呈现在页面中
总结
函数式组件适用于简单组件
类式组件适用于复杂组件
# 2.2 三大核心属性
三大核心属性仅仅针对类式组件
# 2.2.1 state
理解
state是组件对象最重要的属性,值是对象(可以包含多个key-value的组合)- 组件被称为''状态机“,通过更新组件的
state来更新对应的页面显示(重新渲染组件)
强烈注意
组件中的
render方法中的this为组件实例对象组件自定义的方法中
this为undefined强制绑定
this通过函数对象的bind()箭头函数
状态数据,不能直接修改或更新
动态渲染数据
<div id="test"></div>
< type="text/babel">
// 1. 创建类式组件
class Weather extends React.Component{
// 初始化状态
state = {
isHot: true
}
render() {
return (
<h2 id="title" onClick={this.changeWeather}>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h2>
)
}
// 自动以方法要用赋值语句的形式+箭头函数
changeWeather = () => {
this.setState({isHot: !this.state.isHot})
}
}
// 2. 渲染组件到页面
ReactDOM.render(<Weather />, document.getElementById("test"))
</>
# 2.2.2 props
需求:自定义用来显示一个人员信息的组件
- 名字必须指定,且为字符串类型
- 性别为字符串类型,如果性别没有指定,默认为男
- 年龄必须指定,且为数字类型
对props的属性进行限制,属性的限制需要引入prop-types.js
// 对Person属性的类型进行限制
static propTypes = {
// string类型为字符串,isRequired必须有值
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
speak: PropTypes.func
}
// 设置属性的默认值
static defaultProps = {
sex: "待鉴定",
age: 18
}
渲染组件
render() {
return (
<ul>
<li>姓名:{this.props.name}</li>
<li>性别:{this.props.age + 1}</li>
<li>年龄:{this.props.sex}</li>
</ul>
)
}
传递props的两种方式
直接传递
ReactDOM.render(<Person name="Tom" age={18} sex="男"/>, document.getElementById("test"))...传递const p = {name: "老刘", age: 19} ReactDOM.render(<Person {...p}/>, document.getElementById("test"))
理解
- 每个组件对象都会有
props(properties)属性 - 组件标签的所有属性都保存在
props中
注意
props的属性是只读的
函数式组件使用props
function Person(props) {
return (
<ul>
<li>姓名:{props.name}</li>
<li>性别:{props.age + 1}</li>
<li>年龄:{props.sex}</li>
</ul>
)
}
ReactDOM.render(<Person name="Alcie" age={18} sex="女" />, document.getElementById("test"))
对于属性限制,限制只能写在函数的外部
# 2.2.3 refs
组件内的标签可以定义ref属性来标识自己,相当于id属性,ref指定的值都会被收集到refs属性中
字符串形式的
ref:不推荐使用render<input type="text" ref="input1" placeholder="点击按钮提示数据"/> <button onClick={this.showData} >点我提示左侧的数据</button>方法
showData = () => { alert(this.refs.input1.value) }回调函数形式的
refrender,c为当前节点对象<input type="text" ref={(c) => {this.input1 = c}} placeholder="点击按钮提示数据"/> <button onClick={this.showData} ref="btn">点我提示左侧的数据</button> <input onBlur={this.showData2} type="text" ref={c => this.input2 = c} placeholder="失去焦点提示"/>方法
showData = () => { alert(this.input1.value) } showData2 = () => { alert(this.input2.value) }对于内敛的
ref会在组件更新时调用两次,第一的参数为null,第二次的参数为当前节点,如果不想要调用两次,需要将方法绑定在类上<div> <h2>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h2> <button onClick={this.showData}>点我获取天气</button> </div>方法
state ={ isHot: false } showData = (c) => { this.setState({isHot: !this.state.isHot}) }createRef首先创建容器
myRef = React.createRef()React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点,一个容器只能存储一个节点创建
jsx结构<div> <input type="text" ref={this.myRef} placeholder="点击按钮提示数据"/> <button onClick={this.showData} ref="btn">点我提示左侧的数据</button> </div>创建方法
showData = () => { alert(this.myRef.current.value) }
# 2.2.4 三大属性与事件处理
通过
onXxx属性来指定事件处理函数React使用的是自定义(合成)事件,而不是使用原生的DOM事件----为了更好的兼容性React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)-----为了高效当发生事件的
DOM与要操作的DOM相同时,可以省略ref通过event.target得到发生事件的DOM元素对象<input onBlur={this.showData} type="text" placeholder="失去焦点提示数据"/> showData = (event) => { alert(event.target.value) }
# 2.3 收集表单数据
包含表单组件的分类
非受控组件
现用现取
class Login extends React.Component{ handleSubmit = (event) => { event.prevenDefault() // 阻止表单提交 const {username, password} = this alert(`用户名:${username.value},密码:${password.value}`) } render() { return ( <form action="http://www.baidu.com" onSubmit={this.handleSubmit}> 用户名:<input ref={c => this.username = c} type="text" name="username"/> 密码:<input ref={c => this.password = c} type="password" name="password" /> <button>登录</button> </form> ) } }受控组件:推荐使用
将需要的信息存储在
state中class Login extends React.Component{ // 初始化状态 state = { username: "", password: "" } handleSubmit = (event) => { event.prevenDefault() // 阻止表单提交 const {username, password} = this.state alert(`用户名:${username},密码:${password}`) } // 保存用户名密码到状态中 saveFormData = (dataType) => { return (event) => { this.setState({[dataType]: event.target.value}) } } render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={this.saveFormData("username")} type="text" name="username"/> 密码:<input onChange={this.saveFormData("password")} type="password" name="password" /> <button>登录</button> </form> ) } }对于
saveFormData就是一个高阶函数高阶函数:
- 若
A函数,接受的参数是一个函数,那么A就可以成为高阶函数 - 若
A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数 - 常见的高阶函数:
Promise、setTimeout、arr.map()
函数的柯里化:
- 通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编码形式
- 若
# 2.4 组件的生命周期
组件分为挂载和卸载
当组件被渲染到页面上时,称为挂载
当组件被从页面中移除时,称为卸载
// 参数为指定的容器 ReactDOM.unmountComponentAtNode(document.getElementById("test"))
# 2.4.1 生命周期理解
- 组件从创建到死亡它会经历一些特定的阶段
React中包含了一系列钩子函数(生命周期回调函数),会在特定的时刻调用- 我们定义组件时,会在特定的生命周期回调函数中做特定的工作
# 2.4.1 生命周期钩子
- 旧版本的钩子
初始化阶段:由
ReactDOM.render()触发constructorcomponentWillMount⭐️
render⭐️
compoonentDidMount:一般用于初始化的操作,订阅消息,会接受preProps和preState作为参数,新版本中还会接受getSnapshotBeforeUpdate的快照值更新阶段:由组件内部
this.setState()或者父组件重新render触发shouldConponentUpdate:仅当返回值为true是,组件允许更新,如果是forceUpdate()将会跳过验证componentWillUpdaterendercomponentDidUpdate卸载组件:由
ReactDOM.unmountComponentAtNode:()触发⭐️
componentWillUnmount:一般用于收尾工作,取消订阅
- 新版本的钩子
对于新版的钩子,由于以下三个钩子经常被误用,所以componentWillMount、componentWillReceiveProps、componentWillUpdate需要加上前缀UNSAFE_
初始化阶段:由
ReactDOM.render触发constructorgetDerivedStateFromPropsrendercomponentDidMount()更新阶段
getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate卸载组件:由
ReactDOM.umountComponentAtNode触发componentWillUnmount
# 2.5 Diffing算法
Diffing算法的最小力度是标签
# 2.5.1 key的作用
虚拟
DOM中key的作用简单来说:
key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用详细的说:当状态中的数据发生变化时,
react会根据【新数据】生成【新的虚拟DOM】,随后React进行了【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:旧虚拟
DOM中找到了与新虚拟DOM相同的key- 若虚拟
DOM内容没变,直接使用之前的真实DOM - 若虚拟
DOM内容变了,则生成新的真实DOM,随后替换掉页面之前的真实DOM
旧虚拟
DOM中未找到与新虚拟DOM相同的key,根据数据创建新的真实DOM,随后渲染到页面- 若虚拟
用
index作用作为key可能会引发的问题- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实
DOM更新 ==> 界面效果没问题,但效率低 - 如果结构中还包含输入类的
DOM:会产生错误DOM更新 ==> 界面有问题 - 如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用
index作为key是没有问题的
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实
开发中如何选择
key- 最好使用每条数据的唯一标识作为
key、比如id、手机号、身份证号、学号等唯一值 - 如果确定只是简单的展示数据,用
idnex也是可以的
- 最好使用每条数据的唯一标识作为
# 3. React脚手架
# 3.1 react脚手架
xxx脚手架: 用来帮助程序员快速创建一个基于xxx库的模板项目包含了所有需要的配置(语法检查、
jsx编译、devServer…)下载好了所有相关的依赖
可以直接运行一个简单效果
react提供了一个用于创建react项目的脚手架库:create-react-app项目的整体技术架构为:
react+webpack+ ``es6+eslint`使用脚手架开发的项目的特点: 模块化, 组件化, 工程化
# 3.2 创建项目并启动
全局安装
npm install -g create-react-app切换到想创项目的目录,使用命令
create-react-app hello-react进入项目文件夹
cd hello-react启动项目
npm start常用命令
npm start # 启动项目 npm build # 编译打包项目 npm test # 测试 npm eject # 显示配置文件
# 3.3 react脚手架项目结构
public ---- 静态资源文件夹
favicon.icon ------ 网站页签图标
index.html -------- 主页面 --重要
logo192.png ------- logo图
logo512.png ------- logo图
manifest.json ----- 应用加壳的配置文件
robots.txt -------- 爬虫协议文件
src ---- 源码文件夹
App.css -------- App组件的样式
App.js --------- App组件 --重要
App.test.js ---- 用于给App做测试
index.css ------ 样式
index.js ------- 入口文件 --重要
logo.svg ------- logo图
reportWebVitals.js
--- 页面性能分析文件(需要web-vitals库的支持)
setupTests.js
---- 组件单元测试的文件(需要jest-dom库的支持)
# 3.4 功能界面的组件化编码流程
拆分组件:拆分界面抽组件
实现静态组件:使用组件实现静态页面效果
实现动态组件
动态显示初始化数据
数据类型
数据名称
保存在哪个组价
交互(从绑定事件监听开始)
# 3.5 常用库
唯一id
安装
npm install nanoid使用
import {nanoid} from 'nanoid' // 返回一个字符串类型的id id = nanoid()类型限制
安装
npm i prop-types使用
import PropTypes from 'prop-types' static propTypes = { id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, done: PropTypes.bool.isRequired, updateTodo: PropTypes.func.isRequired }
# 3.6 状态提升
如果对于A、B两兄弟组件,状态数据需要共享,那就需要把他们的状态统一放到其父组件中,通过父组件向其传递状态
- 父组件向子组件传递数据,通过
props
子组件向父组件传递数据
如果是子组件向父组件传递数据,就需要在父组件创建函数,然后将其传递给子组件,子组件进行调用时,将数据传递给父组件
# 4. React ajax
# 4.1 理解
# 4.1.1 前置说明
React本身只关注界面,并不包含发送ajax请求的代码- 前端应用需要通过
ajax请求与后台进行交互(json数据) react应用中需要集成第三方ajax库(或自己封装)
# 4.1.2 常用的ajax请求库
jQuery比较重,如果需要另外引用不建议使用axios轻量级,建议使用- 封装
XmlHttpRequest对象的ajax promis风格- 可以用在浏览器端和
node服务器端
- 封装
# 4.2 axios
# 4.2.1 安装使用
安装
npm add axios使用
import React, { Component } from 'react' import axios from 'axios' export default class App extends Component { getStudentData = () => { axios.get('www.baidu.com') .then( res => { console.log(res.data) }, error => { console.log(error) } ) } render() { return ( <div> <button onClick={this.getStudentData}>点我获取数据</button> </div> ) } }
# 4.3 配置代理解决跨域
# 4.3.1 方法一
在package.json中追加如下配置
"proxy":"http://localhost:8080"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给8080(优先匹配前端资源)
# 4.3.2 方法二
第一步:创建代理配置文件
在src下创建配置文件:src/setupProxy.js编写
setupProxy.js配置具体代理规则:const proxy = require('http-proxy-middleware') module.exports = function(app) { app.use( proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给8080) target: 'http://localhost:8080', //配置转发目标地址(能返回数据的服务器地址) changeOrigin: true, //控制服务器接收到的请求头中host字段的值 /* changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:8080 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080 changeOrigin默认值为false,但我们一般将changeOrigin值设为true */ pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置) }), proxy('/api2', { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: {'^/api2': ''} }) ) }
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
# 4.4 消息订阅-发布机制
用于解决兄弟组件传参的问题,当然也适用于所有的组件的传参
工具库
PubSubJs安装
npm install pubsub-js使用
import PubSub from 'pubsub-js' //引入 PubSub.subscribe('delete', function(data){ }); //订阅 PubSub.publish('delete', data) //发布消息
假设有A组件想要向B组件发送数据,A组件与B组件为兄弟组件
在
B组件挂载时订阅消息import PubSub from 'pubsub-js' // 订阅消息并保存token this.token = PubSub.subscribe('messageName', (msg, data) => { console.log('收到数据') })在
A中发布消息import PubSub from 'pubsub-js' // 发布消息 PubSub.publish('messageName', {data: 123})在
B组件卸载之前应该取消订阅componentWillUnmount() { // 取消消息订阅 PubSub.unsubscribe(this.token) }
# 4.5 fetch
属于javascript的内置库,与xhr同级的内置对象
# 4.5.1 文档
https://github.github.io/fetch/
https://segmentfault.com/a/1190000003810652
# 4.5.2 特点
fetch: 原生函数,不再使用XmlHttpRequest对象提交ajax请求老版本浏览器可能不支持
# 4.5.3 相关API
GET请求fetch(url).then(function(response) { return response.json() }).then(function(data) { console.log(data) }).catch(function(e) { console.log(e) });POST请求fetch(url, { method: "POST", body: JSON.stringify(data), }).then(function(data) { console.log(data) }).catch(function(e) { console.log(e) })
# 5. React路由
# 5.1 相关理解
# 5.1.1 SPA的理解
- 单页
Web应用single page web application SPA - 整个应用只有一个完整的页面
- 点击页面中的链接不会刷新页面,只会做页面的局部更新
- 数据都需要通过
ajax请求获取,并在前端异步展现
# 5.1.2 路由的理解
什么是路由
一个路由就是一个映射关系(
key: value)key为路径,value可能是function或者component路由分类
后端路由
理解:
value是function,用来处理客户端提交的请求注册路由:
router.get(path, function(req, res))工作过程中:当
node接受到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
前端路由:
- 浏览器端路由,
value是component,用于战术页面内容 - 注册路由:
<Route path="/test" component={Test}> - 工作过程:当浏览器的
path变为/test,当前路由组件就会变为Test组件
# 5.1.3 react-router
react的一个插件库- 专业用来实现一个
SPA应用 - 基于
react的项目基本都会用到此库
# 5.2 react-router-dom相关API
安装
npm install react-router-dom
# 5.2.1 内置组件
BrowserRouter:h5特有HashRouterRouteRedirectLinkNavLink:相比Link有一个activeClassName属性,即当NavLink被点击时会追加一个类名,比如说让标签高亮的样式类名Switch
# 5.2.2 其他
history对象match对象withRouter函数
# 5.3 快速入门
- 明确好界面中的导航区、展示区
- 导航区的a标签Link标签
<Link to='/xxx> Demo </Link>'- 展示区写
Route标签路径的匹配<Route path='/xxx' component={Demo} /> <App />的最外层包裹一个<BrowserRouter>或<HashRouter>
import {Link, BrowserRouter, Route} from "react-router-dom"
<BrowserRouter>
<div>
{/* react路由链接切换组件 ---编写路由链接 */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
<div className="panel-body">
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</div>
</BrowserRouter>
# 5.3.1 路由组件和一般组件
路由组件:注册路由中引用的组件,一般放在pages文件夹下
路由组件在渲染时会接受到的props
{
"history": {
"push": "ƒ push() {}",
"replace": "ƒ replace() {}",
"go": "ƒ go() {}",
"goBack": "ƒ goBack() {}",
"goForward": "ƒ goForward() {}",
},
"location": {
"pathname": "/home",
"search": "",
},
"match": {
"path": "/home",
"url": "/home",
"params": "{}"
}
}
# 5.3.2 标签体
对于组件的标签体被收集在children属性中
# 5.3.3 解决样式丢失
当路由的层级为多级时,此时去刷新页面样式就会丢失,解决办法如下
修改样式引入的链接
<link rel="stylesheet" href="./css/bootstrap.css"> <!-- 删除点 --> <link rel="stylesheet" href="/css/bootstrap.css">修改样式引入链接
<link rel="stylesheet" href="./css/bootstrap.css"> <!-- 将点修改为%PUBLIC_URL% --> <link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">修改
BrowserRouter为HashRouter
# 5.3.4 路由匹配
模糊匹配
可以匹配成功
<NavLink to="/home/test">Home</NavLink>
<Route path="/home" component={Home}/>
不能匹配成功
<NavLink to="/home/test">Home</NavLink>
<Route path="/home/test" component={Home}/>
精准匹配
<Route path="/home/test" component={Home}/>
总结
- 模式使用的是模糊匹配
- 开启严格匹配使用
exact={true} - 严格匹配不要随便开启,需要时开启。有时开启会导致无法继续匹配二级路由
# 5.4 路由组件的使用
路由组件包括两种,react-dom-router提供的组件和Route属性中component指定的组件
# 5.4.1 NavLink
NavLink可以实现路由链接的高亮,通过activeClassName指定样式名- 标签体的内容是一个特殊的标签属性
- 通过
this.props.children可以获取标签体内容
# 5.4.2 Switch
被Switch包裹的Route可以实现只匹配第一个成功的路由
<Switch>
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Test}/>
</Switch>
# 5.4.3 Redirect
指定路由不匹配时,所要使用的路由
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Redirect to="/about" />
# 5.5 二级路由和向路由组件传递参数
# 5.5.1 二级路由
路由的匹配都是从第一个注册的路由开始,因为路由的匹配都是模糊匹配,所以二级路由一定要带上父路由的前缀
# 5.5.2 路由组件传递参数
params传递参数路由路径使用
:xxx来传递参数render() { return ( <div> <ul> { this.state.messageArr.map((msg) => { return <li key={msg.id}> {/* 向路由组件传递params参数 */} <Link to={`/home/message/detail/${msg.id}/${msg.title}`} href="xxx">{msg.title}</Link> </li> }) } </ul> <hr/> <Switch> {/* 声明接受params参数 */} <Route path="/home/message/detail/:id/:title" component={Detail} /> </Switch> </div> ) }在路由组件的
this.props.match.params中会接到传递的参数const {id, title} = this.props.match.paramssearch参数search参数就相当于query参数,以?开始,&符分割<Link to={`/home/message/detail/?id=${msg.id}&title=${msg.title}`}>{msg.title}</Link> {/* search参数无需声明接受 */} <Route path="/home/message/detail" component={Detail} />传递的参数被保存为
this.props.location.search字符串,使用querystring库的parse解析为对象import qs from 'querystring' const {id, title} = qs.parse(this.props.location.search.slice(1))state参数state参数不会展示在地址栏中<Link to={{pathname: '/home/message/detail', state: {id: msg.id, title: msg.title}}}>{msg.title}</Link> {/* state参数无需声明接受 */} <Route path="/home/message/detail" component={Detail} />传递的参数会被保存到
this.props.location.state中const {id, title} = this.props.location.state
# 5.6 push与replace
react-router-dom利用的是浏览器的history的push与replace默认为push,Link中提供属性replace={true}来进行切换到replace模式
<Link replace={true} to='xxx'>xxx</Link>
# 5.7 编程式路由导航
# 5.7.1 push与replace导航
replace手动导航<button onClick={this.replaceShow(msg.id, msg.title)}>replace查看</button> replaceShow = (id, title) => { // 实现跳转到Detail组件,且为replace跳转 return () => { this.props.history.replace(`/home/message/detail/${id}/${title}`) } }push手动导航<button onClick={this.pushShow(msg.id, msg.title)}>push查看</button> pushShow = (id, title) => { // 实现跳转到Detail组件,且为push跳转 return () => { this.props.history.push(`/home/message/detail/${id}/${title}`) } }
对于params和search不同的参数传递,使用不同的路径即可,state参数需要在replace或push的第二个参数中传递
<button onClick={this.pushShow(msg.id, msg.title)}>push查看</button>
pushShow = (id, title) => {
// push跳转,携带params参数
return () => {
this.props.history.push(`/home/message/detail`, {id: id, title: title})
}
}
# 5.7.2 goBack和goForward
goBack回退<button onClick={this.back}>回退</button> back = () => { this.props.history.goBack() }goForward前进<button onClick={this.forward}>前进</button> forward = () => { this.props.history.goForward() }go根据传递的参数的值来进行回退或前进<button onClick={this.go}>go</button> go =() => { // 回退两步 this.props.history.go(-2) }
# 5.7.3 withRouter
对于路由组件this.props上的属性history、location、match属性只能在路由组件上使用,因为一般组件没有这些属性
withRouter是react-dom-router提供的一个函数,通过withRouter包裹的一般组件称为路由组件
import {withRouter} from 'react-router-dom'
class Header extends Component {
back = () => {
this.props.history.goBack()
}
render() {
return (
<div className="page-header">
<button onClick={this.back}>回退</button>
</div>
)
}
}
export default withRouter(Header)
# 5.8 history与hash
hsitory与hash的区别即是BrowserRouter与HashRouter的区别
底层原理不一样
BrowserRouter使用的是H5的hsiroty API,不兼容IE9及以下的版本,HashRouter使用的是URL的哈希值path表现形式不一致BrowserRouter的路径中没有#,例如:localhost:3000/demo/testHashRouter的路径包含#,例如:localhost:3000/#/demo/test刷新后对路由
state参数的影响BrowserRouter没有任何影响,因为state保存在history对象中HashRouter刷新后会导致路由state参数的丢失HashRouter可以用于解决一些路径错误的问题,例如:样式丢失
# 6. ant-design组件
国内蚂蚁金服的开源react组件库
# 6.1 基本使用
安装
npm install antd使用
import React, { Component } from 'react' // 导入需要的组件 import { Button } from 'antd' import { WechatOutlined } from '@ant-design/icons' // 导入组件的样式 import 'antd/dist/antd.css' export default class App extends Component { render() { return ( <div> <Button type="primary">Primary Button</Button> <WechatOutlined /> </div> ) } }
# 6.2 按需引入样式
安装
react-app-rewired和customize-cranpm install react-app-rewired customize-cra安装
babel-plugin-importnpm install babel-plugin-import修改
package.json"scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", }根目录新建
config-overrides.jsconst { override, fixBabelImports } = require('customize-cra'); module.exports = override( fixBabelImports('import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css', }), );配置完成之后,组件不需要再引入样式
# 6.3 自定义样式
由于
antd官方使用less编写样式,要想修改样式需要安装less和less-loadernpm install less less-loader修改
config-overrides.js配置文件const { override, fixBabelImports, addLessLoader } = require('customize-cra'); module.exports = override( fixBabelImports('import', { libraryName: 'antd', libraryDirectory: 'es', style: true, }), addLessLoader({ lessOptions: { javascriptEnabled: true, // 允许使用js修改主题颜色 modifyVars: { '@primary-color': 'orange' }, // 修改主题的颜色 } }), ); // 使用less的配置 const { override, fixBabelImports, addLessLoader, adjustStyleLoaders, } = require('customize-cra'); module.exports = override( // 针对antd 实现按需打包:根据import来打包 (使用babel-plugin-import) fixBabelImports('import', { libraryName: 'antd', libraryDirectory: 'es', style: true, //自动打包相关的样式 默认为 style:'css' }), // 使用less-loader对源码重的less的变量进行重新制定,设置antd自定义主题 addLessLoader({ lessOptions: { javascriptEnabled: true, modifyVars: { '@primary-color': '#1DA57A' }, }, sourceMap:true, }), adjustStyleLoaders(({ use: [ , css] }) => { css.options.sourceMap = true; css.options.modules = { // 配置默认的样式名称规则 localIdentName:'[name]__[local]--[hash:base64:5]', getLocalIdent:(loaderContext, localIdentName, localName, options) => { // 处理antd 的样式 if (loaderContext.resourcePath.includes('node_modules')) { return localName; } } } }) )
# 7. redux
# 7.1 redux理解
redux是一个专门用于做状态管理的JS库(不是react插件库)- 它可以用在
react,angular,vue等项目中,但基本与react配合使用 - 作用:集中式管理
react应用中多个组件共享的状态
# 7.1.1 使用redux的情况
- 某个组件的状态,需要让其他组件可以随时拿到
- 一个组件需要改变另一个组件的状态
- 总体原则:能不用就不用,如果不用比较吃力才考虑使用
# 7.1.2 redux工作流程
# 7.2 redux的三个核心概念
# 7.2.1 action
动作的对象
包含2个属性
type:标识属性,值为字符串,唯一,必要属性data:数据属性,值类型任意,可选属性
例子
{type: 'ADD_STUDENT', data: {name: 'tom', age: 18}}
# 7.2.2 reducer
- 用于初始化状态、加工状态
- 加工时,根据旧的
state和action,产生新的state的纯函数
# 7.2.3 store
将
state、action、reducer联系在一起的对象如何得到此对象
安装
npm install redux使用
import {createStore} from 'redux' import reducer from './reducers' const store = createStore(reducer)此对象的功能?
getState(): 得到statedispatch(action): 分发action, 触发reducer调用, 产生新的statesubscribe(listener): 注册监听, 当产生了新的state时, 自动调用
# 7.3 基本使用
# 7.3.1 redux精简版
去除
Count组件的自身状态src下建立- src - redux - store.js - count_reducer.jsstore.js引入
redux中的createStore函数,创建一个storeimport { createStore } from 'redux' import countReducer from './count_reducer'createStore调用时要传入一个为其服务的reducer,并将返回的结果暴露出去export default createStore(countReducer)
count_reducer.jsreducer的本质是一个函数,接受preState、action返回加工后的状态reducer有两个作用:初始化状态,加工状态reducer被第一次调用时,是store自动触发的,传递的preState是undefinded完整代码
// 程序初始化时preState为undefined,赋初值为0 export default function countReducer(preState = 0, action) { // 从action对象中获取:type, data const {type, data} = action // 根据type决定如何加工数据 switch(type) { // 加 case 'increment': return preState + data // 减 case 'decrement': return preState - data default: return preState } }
在
index.js中检测store中状态的改变,一旦发生改变重新渲染<App />import React from 'react' import ReactDOM from 'react-dom' import store from './redux/store' import App from './App' ReactDOM.render(<App />, document.getElementById('root')) store.subscribe(() => { // 由于虚拟DOM的性质,状态发生改变的组件,才会重新调用render渲染 ReactDOM.render(<App />, document.getElementById('root')) })redux只负责管理状态,至于状态的改变驱动着页面的展示,要自己通知react调用renderCount组件export default class Count extends Component { // 加操作 increment = () => { const {value} = this.selectNumber store.dispatch(createIncrementAction(value * 1)) } // 减操作 decrement = () => { const {value} = this.selectNumber store.dispatch(createDecrementAction(value * 1)) } // 奇数加 incrementIfOdd = () => { const count = store.getState() if(count % 2 !== 0) { const {value} = this.selectNumber store.dispatch(createIncrementAction(value * 1)) } } // 异步加 incrementAsync = () => { const {value} = this.selectNumber setTimeout(() => { store.dispatch(createIncrementAction(value * 1)) }, 2000) } render() { return ( <div> <h1>当前求和为:{store.getState()}</h1> <select ref={c => this.selectNumber = c}> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前和为奇数再+</button> <button onClick={this.incrementAsync}>异步+</button> </div> ) } }
# 7.3.2 redux完整版
相对于精简版增加两个文件
constant.js放置由于编码疏忽写错action中的typeexport const INCREMENT = 'increment' export const DECREMENT = 'decrement'count_action.js专门用于创建action对象import {INCREMENT, DECREMENT} from './constant' export const createIncrementAction = (data) => { return {type: INCREMENT, data: data} } export const createDecrementAction = (data) => { return {type: DECREMENT, data: data} }
# 7.3.3 同步和异步action
如果一个action返回的是一个普通对象,则为同步action;如果返回一个函数,则为异步action
对于异步action需要中间件redux-thunk的支持
明确:延迟的动作不想交给组件自身,想交给
action何时需要异步
action:想要对状态进行操作,但是具体的数据靠异步任务返回具体编码:
安装
redux-thunknpm install redux-thunkstore.js中使用redux-thunk处理异步任务// 引入createStore,用于创建redux中最为核心的store对象 import { createStore, applyMiddleware } from 'redux' // 引入redux-thunk,用于支持异步action import thunk from 'redux-thunk' import countReducer from './count_reducer' export default createStore(countReducer, applyMiddleware(thunk))创建
action的函数不再返回一个一般对象,而是返回一个函数,该函数中写异步任务count_action.jsexport const createIncrementAsyncAction = (data, time) => { return (dispatch) => { setTimeout(() => { // 通知redux修改状态 dispatch(createIncrementAction(data)) }, time) } }组件调用
incrementAsync = () => { const {value} = this.selectNumber store.dispatch(createIncrementAsyncAction(value * 1, 500)) }等到异步任务有结果时,分发一个同步的
action去真正操作数据异步
action不是必须的,完全可以自己等待异步任务的结果,再去分发同步action
# 7.4 react-redux
react-redux是react官方出的一个类似于redux的状态管理插件
# 7.4.1 基本理解
- 所有的
UI组件都应该包裹在一个容器组件,它们是父子关系 - 容器组件是真正和
redux打交道的,里面可以随意的使用redux的api UI组件中不能使用任何redux的api- 容器组件会传递给
UI组件:redux中所保存的状态- 用于操作状态的方法
- 容器给
UI传递:状态、操作状态的方法,均通过props传递
# 7.4.2 基本使用
安装
npm install react-redux在
src下创建容器组件- src - containers - Count index.jsx引入相关依赖
// 引入Count的UI组件 import CountUI from '../../components/Count' // 引入action import { createIncrementAction, createIncrementAsyncAction, createDecrementAction } from "../../redux/count_action"; // 引入connect用于连接UI组件与redux import {connect} from 'react-redux'编写
mapStateToProps函数function mapStateToProps(state) { return {count: state} }mapStateToProps函数的返回的是一个对象对象中的
key就作为传递给UI组件props的key,value就作为传递给UI组件props的value——状态mapStateToProps用于传递状态编写
mapDispatchToProps函数function mapDispatchToProps(dispatch) { return { increment: (data)=> { // 通知redux执行加法 dispatch(createIncrementAction(data)) }, decrement: (data) => { dispatch(createDecrementAction(data)) }, incrementAsync: (data, time) => { dispatch(createIncrementAsyncAction(data, time)) } } }mapDispatchToProps函数的返回的是一个对象对象中的
key就作为传递给UI组件props的key,value就作为传递给UI组件props的value——操作状态的方法mapDispatchToProps用于传递操作状态的方法创建并暴露一个
Count组件export default connect(mapStateToProps, mapDispatchToProps)(CountUI)在引入
Count组件中改为,引入container中的Count并传入storeimport React, { Component } from 'react' // 引入容器组件 import Count from './containers/Count' // react-redux需要在组件中使用props传递store import store from './redux/store' export default class App extends Component { render() { return ( <div> {/* 给容器组件传递store */} <Count store={store}/> </div> ) } }容器组件优化
// 引入Count的UI组件 import CountUI from '../../components/Count' // 引入action import { createIncrementAction, createIncrementAsyncAction, createDecrementAction } from "../../redux/count_action"; // 引入connect用于连接UI组件与redux import {connect} from 'react-redux' // 创建并暴露一个Count组件 export default connect( state => ({count: state}), // mspDispatchToProps的一般写法 // dispatch => ({ // increment: (data)=> { // // 通知redux执行加法 // dispatch(createIncrementAction(data)) // }, // decrement: (data) => { // dispatch(createDecrementAction(data)) // }, // incrementAsync: (data, time) => { // dispatch(createIncrementAsyncAction(data, time)) // } // }) // mapDispatchToProps的简写,传送action,react-redux自动dispatch { increment: createIncrementAction, decrement: createDecrementAction, incrementAsync: createIncrementAsyncAction } )(CountUI)
# 7.4.3 优化
如果使用了
react-redux,那么在index.js中就不需要使用store.subscribe监控App组件的变化,因为容器组件会自动监控状态的变化Provider组件的使用使用
Provider可以自动传递store而不需要在使用容器组件的地方自己传递store修改
index.jsimport React from 'react' import ReactDOM from 'react-dom' import App from './App' import store from './redux/store' import { Provider } from 'react-redux' ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )一般将
UI组件与容器组件整合为一个文件,UI组件不必暴露,暴露容器组件
# 7.4.4 多组件数据共享
每一个组件都需要定义一个
action和reducer多个
reducer中需要在store.js中使用redux中的combineReducers合并成一个reducer// 引入createStore,用于创建redux中最为核心的store对象, combineReducers用于合并reducers import { createStore, applyMiddleware, combineReducers } from 'redux' // 引入redux-thunk,用于支持异步action import thunk from 'redux-thunk' // 引入reducers import countReducer from './reducers/count' import personReducer from './reducers/person' // 汇总reducers const allReducer = combineReducers({ count: countReducer, persons: personReducer }) export default createStore(allReducer, applyMiddleware(thunk))
# 7.4.5 redux调试工具
扩展商店安装
Redux DevTools项目安装支持库
npm install redux-devtools-extensionstore.js中引入并使用import { composeWithDevTools } from 'redux-devtools-extension' //无异步的情况 // export default createStore(allReducer, composeWithDevTools()) // 有异步的情况 export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
# 8. 项目打包
执行
npm buildnpm run build安装临时查看服务器
npm install -g serve进入打包文件夹
buildserve
# 9. react扩展
# 9.1 setState
setState更新状态有2种写法
setState(stateChange, [callback])------对象式的setStatestateChange为状态改变对象(该对象可以体现出状态的更改)callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用setState(updater, [callback])------函数式的setStateupdater为返回stateChange对象的函数。updater可以接收到state和props。callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。总结:
对象式的
setState是函数式的setState的简写方式(语法糖)使用原则:
- 如果新状态不依赖于原状态 ===> 使用对象方式
- 如果新状态依赖于原状态 ===> 使用函数方式
- 如果需要在
setState()执行后获取最新的状态数据, 要在第二个callback函数中读取
# 9.2 lazyLoad
路由组件的lazyLoad
通过
React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包const Login = lazy(()=>import('@/pages/Login'))通过
<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面<Suspense fallback={<h1>loading.....</h1>}> <Switch> <Route path="/xxx" component={Xxxx}/> <Redirect to="/login"/> </Switch> </Suspense>
# 9.3 Hooks
React Hook/Hooks是什么?Hook是React 16.8.0版本增加的新特性/新语法
- 可以让你在函数组件中使用
state以及其他的React特性
三个常用的
HookState HookReact.useState()Effect HookReact.useEffect()Ref HookReact.useRef()State HookState Hook让函数组件也可以有state状态, 并进行状态数据的读写操作语法:
const [xxx, setXxx] = React.useState(initValue)useState()说明: 参数: 第一次初始化指定的值在内部作缓存 返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数setXxx()2种写法:setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值Effect HookEffect Hook可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)React中的副作用操作: 发ajax请求数据获取 设置订阅 / 启动定时器 手动更改真实DOM语法和说明:
useEffect(() => { // 在此可以执行任何带副作用操作 return () => { // 在组件卸载前执行 // 在此做一些收尾工作, 比如清除定时器/取消订阅等 } }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行可以把
useEffect Hook看做如下三个函数的组合componentDidMount() componentDidUpdate() componentWillUnmount()Ref HookRef Hook可以在函数组件中存储/查找组件内的标签或任意其它数据语法:
const refContainer = useRef()作用:保存标签对象,功能与
React.createRef()一样
# 9.4 Fragment
使用
<Fragment><Fragment>
<></>
作用
可以不用必须有一个真实的DOM根标签了
# 9.5 Context
理解
一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信
使用
创建
Context容器对象:const XxxContext = React.createContext()渲染子组时,外面包裹
xxxContext.Provider, 通过value属性给后代组件传递数据<xxxContext.Provider value={数据}> 子组件 </xxxContext.Provider>后代组件读取数据
第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context this.context // 读取context中的value数据第二种方式: 函数组件与类组件都可以
<xxxContext.Consumer> { value => ( // value就是context中的value数据 要显示的内容 ) } </xxxContext.Consumer>
注意
在应用开发中一般不用context, 一般都用它的封装react插件
# 9.6 组件优化
Component的2个问题
只要执行setState(),即使不改变状态数据, 组件也会重新render() ==> 效率低
只当前组件重新render(), 就会自动重新render子组件,纵使子组件没有用到父组件的任何数据 ==> 效率低
效率高的做法
只有当组件的state或props数据发生改变时才重新render()
原因
Component中的shouldComponentUpdate()总是返回true
解决
办法1:
重写shouldComponentUpdate()方法
比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
办法2:
使用PureComponent
PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
注意:
只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false
不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化
# 9.7 render props
如何向组件内部动态传入带内容的结构(标签)?
Vue中:
使用slot技术, 也就是通过组件标签体传入结构 <A><B/></A>
React中:
使用children props: 通过组件标签体传入结构
使用render props: 通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性
children props
<A>
<B>xxxx</B>
</A>
{this.props.children}
问题: 如果B组件需要A组件内的数据, ==> 做不到
render props
<A render={(data) => <C data={data}></C>}></A>
A组件: {this.props.render(内部state数据)}
C组件: 读取A组件传入的数据显示 {this.props.data}
# 9.8 错误边界
# 理解:
错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面
# 特点:
只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
# 使用方式:
getDerivedStateFromError配合componentDidCatch
// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
console.log(error);
// 在render之前触发
// 返回新的state
return {
hasError: true,
}
}
componentDidCatch(error, info) {
// 统计页面的错误。发送请求发送到后台去
console.log(error, info);
}
# 9.9 组件通信方式总结
# 组件间的关系:
- 父子组件
- 兄弟组件(非嵌套组件)
- 祖孙组件(跨级组件)
# 几种通信方式:
1.props:
(1).children props
(2).render props
2.消息订阅-发布:
pubs-sub、event等等
3.集中式管理:
redux、dva等等
4.conText:
生产者-消费者模式
# 比较好的搭配方式:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)