文章目录
- 前言
- 一、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((<Demo />.... 之后:
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((<Demo />.... 之后:
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. 组件生命周期
- 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 废弃)
旧生命周期总结:
-
初始化阶段(挂载时):由 ReactDOM.render() 触发初次渲染
constructor()
componentWillMount()
render()
componentDidMount() -
更新阶段:由组件内部 this.setState() 或 父组件 或 render 触发
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate() -
卸载组件:由ReactDOM.unmountComponentAtNode() 触发
componentWillUnmount()
新生命周期总结:
-
初始化阶段(挂载时):由 ReactDOM.render() 触发初次渲染
constructor()
getDerivedStateFromProps()
render()
componentDidMount() -
更新阶段:由组件内部 this.setState() 或 父组件 或 render 触发
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate() -
卸载组件:由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/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,用于展示页面内容
注册路由:<Router path='/test' component={Test} />
工作过程:当浏览器的 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)路由匹配遵循 - 最左侧匹配原则 模糊匹配: 输入的路径 必须要包含 匹配的路径,且顺序要一致,默认使用 例子: <Link to="/home/a/b">home</Link> 可以匹配到 <Route path="/home" component={Home}/> 严格匹配: 输入的路径与匹配的路径必须一致 exact={true} 开启严格匹配:<Route exact={true} path='/demo' component={Demo}/> 严格匹配不要随便开启,有时候开启会导致无法继续匹配多级路由
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: <Link replace to='/demo'/>
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 参数 路由连接(携带参数):<Link to={`/home/tabTow/detail/${id}/${title}`}>{item.title }</Link> 注册路由(声明接收): <Route path="/home/tabTow/detail/:id/:title" component={Details} /> 组件接收参数:this.props.match.params 2、search 参数 路由连接(携带参数):<Link to={`/home/tabTow/detail?id=${item.id}&title=${item.title}`}>{item.title}</Link> 注册路由(无需声明,正常注册即可):<Route path="/home/tabTow/detail" component={Details} /> 组件接收参数:this.props.location 备注:获取到的 search 是 urlencode 编码字符串,需要借助 querystring 解析 import qs from 'qs' const {id, title} = qs.parse(search.slice(1)) 3、state 参数 路由连接(携带参数):<Link to={{pathname:'/home/tabTow/detail', state:{id:item.id,title:item.title}}}>{item.title}</Link> 注册路由(无需声明,正常注册即可):<Route path="/home/tabTow/detail" component={Details} /> 组件接收参数:this.props.location.state || {} 备注:此处的 state 是路由身上的 state,不是组件的 state, 刷新也可以保留住参数
7、路由组件 与 一般组件
1、写法不同: 路由组件: <Route path='/demo' component={Demo} /> 一般组件: <Demo /> 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、内置组件的变化:移除<Switch/>,新增了<Routes/>等
2、语法的变化:component = {Home} 变为 element = {<Home/>}等
3、新增多个hook:useParams、useNavigate、useMatch 等
4、官方明确推荐函数式组件
-
Route
语法变化 component 变为 element,属性值为标签形式
caseSensitive属性用于指定,匹配时是否区分大小写(默认false)
Route 可以嵌套使用,且可配合 useRoutes() 配置“路由表”,当需要通过<Outlet> 组件来渲染其子路由// 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/
安装: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 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({}) 或 重新渲染 <App/>
-
合并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({}) 或 重新渲染 <App />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( <React.StrictMode> <App /> </React.StrictMode> ); }) */ } 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, 给 <App/> 包裹一个 <Provider store={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 特性
-
三个常用的 Hooks
State Hook:React.useState()
Effect Hook: React.useEffect()
Ref Hook: React.useRef() -
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
-
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
-
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 属性给后代组件传递数据:
<XxxContext.Provider value={数据}> 子组件 </XxxContext.Provider>
3)、后代组件读取数据:
// 第一种方式:适用于类组件
static contextType = XxxContext // 声明接收 context
this.context // 读取context的value数据
// 第二种方式:函数组件与类组件都可以
<XxxContext.Consumer>
{
value => { 要显示的内容 } // value 就是context中的value数据
}
</XxxContext.Consumer>
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 技术,通过组件标签体传入结构 <A><B/></A>
React中:
使用 children props:通过组件标签体传入结构
使用 render props:通过组件标签属性传入结构,一般用 render 函数属性
children Props:
语法:<A><B/></A>
A组件渲染:{this.props.children }
问题:如果B组件需要 A组件内的数据,无法传递
render Props:
语法:<A rende={data => <B data={data}></B>}</A>
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 全家桶
发布评论