React
2013年Facebook推出的开源的函数式编程的框架,只支持IE8及以上。
16版本之后成为React Fiber(React16版本或者说是16版本中的一些底层事件),react底层在事件循环中加入了优先级的概念,可以利用事件循环的一些碎片时间执行一些高优先级的用户交互,提高react.js使用过程中的用户体验
与Vue.js的比较:react.js灵活性更大一些,所以处理非常复杂的业务时,技术方案有更多的选择,用于复杂度比较高的项目;vue.js提供了更多的API,实现功能更简单,但是API多,所以灵活性就有一定的限制了,多用于面向用户端不太复杂的项目,当然也可以做一些大型复杂的项目
安装
//安装脚手架工具create-react-app
npm i -g create-react-app
//创建项目
create-react-app 项目名字
(
报错:
创建react项目的时候node版本太低,用nvm添加了新版本之后,执行命令create-react-app myproject报错
'create-react-app' 不是内部或外部命令,也不是可运行的程序或批处理文件。
解决方法:
可以使用npx create-react-app myproject命令(这是官网上新版本的命令),
如果还是报错,是关于npx的错误,把当前使用的node版本的路径D:\nvm\nvm-setup\installation\nvm\v16.13.2加到环境变量中去,
其他错误的话,再执行一次npx create-react-app myproject命令,
我是第一次没成功,第二次又执行一次才成功的
)
//启动项目
yarn start 或者 npm run start
//在localhost:3000地址展示,借助webpack-dev-server开启的地址
//开发完项目,最终打包
yarn build
yarn test //不用
yarn eject
//项目自动把有关webpack相关的文件都隐藏了,怕你自己修改后webpack崩了。执行这个语句,会把webpack相关文件都展示出来,例如webpack.config.js。一旦执行了这个命令,就不能再返回到隐藏状态了。
附:工程化:如果在项目中用到了像webpack这样全自动化的构建工具,你写了一段代码,它能帮你进行语法检查、代码压缩、语法转换、兼容性处理等等一系列的自动的东西
附:浏览器,按住shift点击刷新就是强制刷新
工程目录文件简介
git文件夹
git仓库
yarn.lock
yarn的一些缓存文件,能让你在下一次下载这些包的时候速度更快,其中也有项目依赖安装包的一些版本号,不要改动
README.md
项目说明文件,markdown写的,可以自己修改
package.json
代表这个脚手架工具是一个node的包文件,其实是node里的一些内容,比如项目的介绍、依赖于的第三方的包、调用的指令,可以把项目变成一个node的包
.gitignore
用git管理代码时,有一些文件不想传到git仓库上,把这些文件定义在这个文件里面
node_modules
项目依赖的第三方的包/模块,不要改动
public文件夹下————————————————
favicon.ico
网站最上面的小图标
index.html
是项目的html主文件,项目首页的html模板。
link引入favicon.ico,%PUBLIC_URL%是React脚手架的关键词写法,就代表public这个文件夹的路径,ref内部也可以用相对路径的形式,%PUBLIC_URL%也有一定的优势,在使用路由的时候能感受到
<link rel="icon" ref="%PUBLIC_URL%/favicon.ico" />
meta标签,name=“viewport”,开启理想视口,用于做移动端网页的适配
meta标签,name=“theme-color”,用于配置浏览器页签(favicon.ico图标那个位置)和地址栏的颜色,仅支持安卓手机浏览器。
meta标签,name=“description”,描述网站信息的,利于SEO
link标签,ref=“apple-touch-icon”,将手机浏览器的网页添加到手机桌面时(向一个APP一样可以点击进入网页),在桌面上展示的图标,只支持苹果手机
<link rel="apple-touch-icon" ref="%PUBLIC_URL%/logo193.png" />
link标签,rel=“manifest”,引入应用加壳时的配置文件(在html网页的套一个安卓的壳,就变成了安卓手机上的应用,生成一个.apk;套一个ios的壳,就变成了苹果手机应用),在下面的manifest.json和src/index.js部分也有讲解。
<link rel="manifest" ref="%PUBLIC_URL%/manifest.json" />
noscript标签作用是如果llq(浏览器)把script禁掉了,不支持js脚本的运行,给用户提示这个标签内的内容,用于容错的标签;
id为root的div标签是程序的主入口。
robots.txt
爬虫规则文件,什么可以被爬,什么不可以
manifest.json
配置加壳应用在手机上的权限,应用的图标等。(结合src文件夹下的index.js文件里的registerServiceWorker)如果网页可以当一个app来使用,就可以把它存储在桌面上,有一个快捷方式,可以直接进入这个网址。
src文件夹下————————————————
放的是项目的所有的源代码
index.js
是所有代码的主入口文件(webpack如果想使用,需要一个入口文件),整个程序从index.js里逐行执行,所有组件都从这里挂载。引入中没写文件名后缀,会优先寻找引入目录下的js文件。
里面引入了react、react-dom(处理React和DOM之间的相关事宜的)等第三方模块(在package.json可以看到已经安装的第三方的包)。
还引入了.css文件(react一个非常重要的设计理念: all in js,文件都可以像模块一样引入,vue、angular也是)。
还引入组件,例如<App />,组件的文件是App.js。
还引入了registerServiceWorker,一个概念叫PWA(progressive web app),通过写网页的形式写一些手机的app应用。registerServiceWorker帮助我们借助网页写手机app,加入引用了它,写了一个网页并上线到一台支持https的服务器上,我们写的网页就有这样的特性了,用户第一次连接网页需要联网,但是突然断网了,此时用户第二次访问页面,依然可以看到之前访问的页面,registerServiceWorker会把之前的网页存储在我们浏览器内,有缓存,下一次即使没有网络也可以把该网页当成一个app来使用。
自己创建的组件需要挂载到DOM节点下,ReactDOM是第三方模块,使用**ReactDOM.render()**方法就可以把我们自己写好的组件挂载到DOM节点下,第一个参数是引入的组件,第二个参数是DOM标签,第三个参数可以是registerServiceWorker(),可选。
index.js(官网最新版,和上面的文件介绍稍微有些出入)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode> //不是ES5中的严格模式,而是检查App组件和其子组件的内容是否合理
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
index.css 放一些页面通用样式,例如body {…}
logo.svg 是一个图片
reportWebVitals.js 用于记录页面上的性能的,里面用web-vitals这个库实现页面性能的检测
setupTest.js 用于做组件测试的(应用的整体测试或者一个一个模块拼在一起时一个一个的单元测试),用的是jest-dom库
App.js
App组件。项目中自己写的组件通常放在src下的components文件夹下。
App.js
//组件必须引入Component这个基类并继承它,引入用到了JSX语法,所以也要引入react
import React from 'react';
class App extends React.Component {
render() {
//组件文件中,render()方法内部是该组件最终渲染的结果,render函数return什么就展示什么内容
return (
<div>
<h1>hello</h1>
<p>111</p>
</div>
)
}
}
export default App
//另一种形式
import React, { Component } from 'react'; //使用ES6的解构赋值
class App extends Component {
...
}
App.test.js
自动化测试文件,因为做React或者Vue项目的时候,因为会涉及到一些函数式编程,所以会做一些自动化测试。
顺序是:index.js执行到需要root节点这里,去public文件夹下找index.html中的root节点(react中的webpack配置将index.js和index.html进行关联),于是App组件就渲染到页面上了,组件里面引入了样式也就生效了。
基础知识
函数式组件
<script type="text/babel">
function MyComponent() {
console.log(this);
return <h2>函数定义的组件,适用于【简单组件(没有状态state)】的定义</h2>
}
ReactDom.render(<MyComponent />, document.getElementById('test'))
</script>
附:
<script>
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
console.log(`我叫${this.name},我的年龄是${this.age}`)
}
}
const p1 = new Person('tom', 12)
console.log(p1)
p1.speak()
class Student extends Person {
constructor(name, age, grade) {
super(name, age)
this.grade = grade
}
speak() {
...
}
study() {
...
}
}
</script>
类式组件
<script type="text/babel">
class MyComponent extends React.Component {
render() {
return <h2>类定义的组件,适用于【复杂组件(有状态state)】的定义</h2>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('test'))
</script>
JSX语法
是JavaScript的语法扩展,在js文件中使用了标签的形式:使用自己创建的组件的自定义标签、render函数return内使用的自定义标签或者html标签等。
自定义标签第一个字母必须大写,区别于原始的html标签,如果小写的话,JSX语法是不支持的
返回的内容必须包含在一个大的元素内部,当然这个标签也会显示在html文档中,如果不想增加过多的标签,可以使用React提供的Fragment占位符,把需要return的内容都放在<Fragment>…</Fragment>里
import React, { Component, Fragment } from 'react'; //这里是export和export default一起引入的样式,不是React的解构赋值写法
class App extends Component {
render() {
return (
<Fragment> //占位符
<h1>hello</h1>
<p>111</p>
</Fragment>
//其实可以用空标签也能起效果,例如下面,但是Fragment标签是可以写key属性用于遍历,而空标签不可以添加任何属性。key这个标签属性是唯一的,因为Fragment最终会丢失掉,所以传其他属性没有意义,Fragment也不接收。
//<>
// <h1>hello</h1>
//</>
)
}
}
export default App
//也可直接创建并暴露App组件
export default class App extends Component {
render() {
return (
<Fragment> //占位符
<h1>hello</h1>
<p>111</p>
</Fragment>
)
}
}
如果在文件中使用了JSX的语法,一定要引入react,import React from ‘react’,不引入是没办法编译JSX语法的。
在JSX模板中循环返回一个内容的时候,返回的内容不止是一条的话(注释也算),也必须有一个最外层的包裹元素
<ul>
{
this.state.list.map((item, index) => {
return (
<div>
<TodoItem />
{/*
注释内容
*/}
</div>
)
})
}
</ul>
如果在JSX中使用js表达式或者js变量,形式是**{JavaScript表达式Expressions}**
附:js语句与js表达式
1.表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,例如这些:
(1) a
(2) a + b
(3) demo(1)
(4) arr.map()
(5) function test() {}
2.语句,例如这些
(1) if() {}
(2) for() {}
(3) switch() {case: …}
在JSX里写注释
多行注释
{\*...\*}
单行注释,不能把这三行写在一行,因为写在一行会把后面的}也认为是注释的一部分
{
//...
}
className在react中因为class这个关键词已经用于定义组件了,所以在元素或者组件上定义样式用className
<input className="input" />
不转义设置页面的内容,比如在input框内填写<h1>haha</h1>,想要输出就是一个大大的标题,而不是转义过后的带有标签的原样输出<h1>haha</h1>,可以用dangerouslySetInnerHTML,但这样会有XSS攻击的风险,但有时必须需要这样做
<li
key={index}
onClick={this.handleItemDelete.bind(this, index)}
dangerouslySetInnerHTML = {{__html : item}}
//外层花括号是JSX语法的花括号,表示里面需要一个js表达式,内层的花括号表示这个js表达式就是一个js对象
//item为不转义显示在页面的内容
>
//{item},上面已经有item了,这里之前的item就没必要写了
</li>
附:
<div style={{marginTop: '10px', marginLeft: '10px'}}>
label标签的作用是扩大点击的区域,如果想点击label标签光标自动聚焦到input框,可以用htmlFor
<label for="insertArea">输入内容</label>
<input id="insertArea" />
//这样会报警告,因为react中for认为是循环的那个for,所以如果label标签上要改为htmlFor
<label htmlFor="insertArea">输入内容</label>
响应式设计思想,react英文意思就是响应、反应
不要操作DOM,操作数据,React会感知数据的变化,自动的生成DOM
State(状态):存储组件的数据
组件内部的数据可以动态修改
this.setState()函数是更新state数据的唯一途径。react中有个immutable概念,是说state不允许我们做任何的改变,不要直接改state里面的内容,否则后面做性能优化的时候就会有问题,必须先拷贝一份,改拷贝里的内容
附:展开运算符复制数据和添加数据[…this.state.list, this.state.inputValue];splice用法
注意点:{JavaScript表达式Expressions};事件绑定的时候需要对函数的作用域进行变更this.increaseLikes = this.increaseLikes.bind(this)或者onClick={() => { this.increaseLikes() }} 还有带参数的onClick={this.handleItemDelete.bind(this, index)}
import react from "react";
class LikesButton extends React.Component {
//构造器是否接收props,是否super传递props,取决于:是否希望在构造器中通过实例this访问props。如果不接收props也不传递props,构造器中使用this.props则为undefined
constructor(props) {
super(props)//调用一次父类的构造函数
this.state = {
likes: 0,
list: ['li', 'xin']
}
// this.increaseLikes = this.increaseLikes.bind(this)
//这句和下面第二种方式二选一,是确定this指向LikeButton组件的,否则this为undefined
//其实这句是提取出来了,实际在onClick事件那里写也行,如下handleItemDelete.bind那里
//通过this.事件名 = this.事件名.bind(this),写在constructor中,这样写会节约性能
//分析:右边this是LikesButton的实例对象,引用原型对象上的increaseLikes方法,bind做了两件事,一个是生成新的函数,二是改了函数里的this,改成什么看传了什么,这里传了的是LikesButton的实例对象,所以右边返回一个函数,函数的this指的是LikesButton的实例对象。然后把这个函数放到了左侧this实例的自身,给函数起了一个名字叫increaseLikes。所以下面使用onClick={this.increaseLikes}的时候相当于使用的实例对象自身多增加的increaseLikes方法,并不会再去找原型对象上的increaseLikes方法。紧接着下面有附例子
}
increaseLikes() {
//this.setState({
// likes: ++this.state.likes //使用数据
//})
//方法有参数e,是event对象,e.target就是该方法所在的DOM节点,
//如果标签是input的话,e.target.value可以获得input框的value值
//新版react的setState是一个函数的形式,返回一个对象
this.setState(() => {
return {
likes: ++this.state.likes //以对象的形式返回
}
})
//es6函数直接简化为这样
//this.setState(() => ({
// likes: ++this.state.likes
//})
//setState更新状态时是一个异步为setState,为了性能的提升,但是异步的话,假如像这样使用e.target.value就会出现问题
///
//handleInputChange(e) {
// this.setState(() => {
// return {
// inputValue: e.target.value
// }
// })
//}
//要把e.target.value提出来
//handleInputChange(e) {
// const value = e.target.value
// this.setState(() => {
// return {
// inputValue: value
// }
// })
//}
//this.setState(prevState) //其实setState有参数prevState,是修改数据之前的数据,等价于list:[...this.state.list, this.state.like]中的this.state,所以直接换为list:[...prevState.list, prevState.like]就行,这样写更靠谱,避免我们不小心改变了state的状态
//判断setState()更新状态时异步还是同步的,主要是看执行setState的位置,
//在React控制的回调函数中(生命周期钩子,react事件监听回调)这种情况是异步的;
//在非react控制的异步回调函数中(定时器回调/原生事件监听回调/promise回调)这种情况是同步的。
}
handleItemDelete(index) {
console.log(index)
}
render() {
return (
<Fragment>
<p>{this.state.likes}</p>
<button
type="button"
//onClick={this.increaseLikes} //第一种方式,需要结合上面的this.increaseLikes = this.increaseLikes.bind(this)改变this指向,或者直接按照下面这么写
//onClick={this.increaseLikes.bind(this)}
onClick={() => {this.increaseLikes()}} //箭头函数确定this指向,第二种方式
//原生的事件绑定是onclike、onchange,在react中要使用驼峰形式onChange、onClick
//react中事件的处理
//1.通过onXxx属性指定事件处理函数(驼峰)。React使用的是自定义(合成)事件,而不是使用的原生DOM事件(为了更好的兼容性);React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)(为了高效)。
//2.通过event.target得到发生事件(例如点击、失去焦点等)的DOM元素对象(避免过度使用ref)
>
</button>
<ul>
{
this.state.list.map((item, index) => { //数组的map方法
return <li key={index}
onClick={this.handleItemDelete.bind(this, index)}
//给方法传递了参数
>
{item}
</li> //必须加key值
})
}
</ul>
</Fragment>
)
}
}
export default LikesButton
附:
<script>
function demo() {
console.log(this)
}
demo()
const x = demo.bind({a: 1, b: 2})
x()
</script>
一般会把一部分拆分为函数的形式,例如
<ul>
{ this.getTodoItem() } //这里直接执行这个函数就行了,注意这里不用this指向的设置
</ul>
getTodoItem() {
return this.state.list.map((item, index) => {
return (
<li key={index} //key应该放在循环的最外层的元素上,假如外层有个div标签,要将key加在div上
onClick={this.handleItemDelete.bind(this, index)}
>
{item}
</li>
)
})
}
setState更新状态的两种写法:
1.setState(stateChange, [callback])------对象式的setState
setChange为状态改变对象(该对象可以体现出状态的改变);callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用。
2.setState(updater, [callback])------函数式的setState
updater为返回stateChange对象的函数;updater可以接收到state和props;callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用。
对象式的setState是函数式的setState的简写方式(语法糖),他俩的使用原则(不绝对,以实际情况而定就行)是:
如果新状态不依赖原状态,使用对象方式; 如果新状态依赖于原状态,使用函数方式;
注意:如果需要在setState()执行后获取最新的状态数据,要在第二个callback函数中读取。
附:
<script>
class Person {
constructor(name, age) {
this.name = name,
this.age = age
}
study() {
console.log(this)
}
}
const p1 = new Person('tom', 18)
p1.study()
const x = p1.study
x()
</script>
<script>
class Car {
constructor(name, price) {
this.name = name
this.price = price
}
wheel = 4
static demo = 100
}
const c1 = new Car('奔驰', 800)
console.log(c1)
state = {
...
}
increaseLikes = () => {
...
}
</script>
附:展开运算符
let arr1 = [1, 2]
let arr2 = [9, 4]
console.log(...arr1)
let arr3 = [...arr1, ...arr2]
function sum(...nums) {
return nums.reduce(preValue, currentValue) => {
return preValue + currentValue
}
}
console.log(sum(1, 3, 5, 6))
let person = {name: 'tom', age: 23}
console.log(...person)
let person2 = {...person}
let person3 = {name: 'li', grade: 19}
let mergePerson = {...person, ...person3}
let person4 = {...person, age: 9}
const p = {name: 'tom', age: 16}
ReactDOM.render(<Person {...p}/>, document.getElementById('test'))
附:JS中reduce()用法
数组的reduce函数,用于数组的条件统计、条件求和、筛选最值
arr.reduce(function(prev,cur,index,arr){
...
}, init);
var arr = [3,9,4,3,6,0,9];
var sum = arr.reduce(function (prev, cur) {
return prev + cur;
},0);
var max = arr.reduce(function (prev, cur) {
return Math.max(prev,cur);
});
var newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
组件间传值
TodoList.js父组件
父组件通过属性的方式向子组件传值
this.state.list.map((item, index) => {
return (
<TodoItem
key={item} //这是循环需要的key
content={item} //父组件通过属性的方式向子组件传值,传过去的值叫content
index={index} //这是父组件向子组件传值
deleteItem = {this.handleItemDelete.bind(this)} //父组件向子组件传递方法时必须在有bind指向,因为这样在子组件内部使用deleteItem方法时,this才会指向父组件
/>
)
当组件的 state或者props发生改变的时候,自己的render函数就会重新执行; 当父组件的render函数被运行时,它的子组件的render函数都将重新被允许一次
TodoItem.js子组件
子组件通过 this.props.属性名 的方式来接受值
props是只读的,不能修改
render() {
return <div onClick={this.handleClick}>{this.props.content}</div>
//可以把this.props的值用ES6语法解构
//const { content } = this.props;
//return <div onClick={this.handleClick}>{content}</div> //这里直接用content
}
handleClick() {
this.props.deleteItem(this.props.index) //子组件调用父组件传入的方法,并使用父组件传入的参数
//实际用下面这种方式
//const { deleteItem, index } = this.props;
//deleteItem(index)
//解构赋值 在各个部位分别做解构赋值,不要全部写在一处
}
不允许子组件直接修改父组件的内容,但子组件可以调用父组件的方法,通过父组件的方法进而改变父组件里的数据
附:函数式组件因为内部this为undefined,所以不能使用state,refs,但是可以使用props,因为函数可以传参数
<script>
function Person(props) {
const {name, age} = props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
</ul>
)
}
ReactDOM.render(<Person name='tom' age={18} />, document.getElementById('test'))
</script>
react的一些相关概念和思考
是声明式代码(操作数据) ,对应的是命令式代码(直接操作Dom); 可以与其他框架并存,因为它只负责id为root标签的那部分渲染; 组件化; 单项数据流(父组件可以向子组件传值,但是子组件只能使用不能修改这个值,修改了会报错) 为了测试和开发方便,假如没有单项数据流的话,很多子组件使用同一个父组件内的值,一旦报错没办法定位哪个子组件出错; 视图层框架,只解决数据和页面渲染方面,可配合一些数据型框架redux等解决react中组件间的复杂传值问题; 函数式编程,constructor、render等等都是函数的形式,维护起来比较容易,更容易前端自动化测试
安装react developer tools开发调试工具
Chrome浏览器=》更多工具=》扩展程序=》商店=》react developer tools开发调试工具,安装后浏览器右上角有个红色的图标 红色,本地开发版本,黑色,线上版本。线上版本的代码相对于本地版本的代码,js css代码进行了压缩,也剔除了一些警告的问题,更加精悍 会在控制台最右边多个React菜单,方便进行组件结构的查询,右侧有该组件值的内容,就不需要反复console.log打印信息看内容是否符合我们的预期,只需要实时监测这个工具右侧的state的变化就可以了。当然也有其他功能
PropTypes、DefaultProps
PropTypes对参数做校验、DefaultProps定义参数的默认值
TodoItem.js组件内,引入PropTypes
import PropsTypes from 'props-types';
...
组件名.propsTypes = {
content: PropsTypes.string,
deleteItems: PropsTypes.func.isRequired,
item: PropsTypes.oneOfType([PropsTypes.number, PropsTypes.string])
arrayItem: PropsTypes.arrayOf(...)
speak: PropsTypes.func
}
组件名.defaultProps = {
constent : 'hello'
}
Class Person extends React.Component {
static propsTypes = {
...
}
static defaultProps = {
...
}
render() {
return (
...
)
}
}
官网文档有更多的关于PropTypes的校验设置
虚拟DOM
虚拟DOM本质就是js对象Object,(Vue中的虚拟DOM机制和React的虚拟DOM差不多完全一致)
1.state数据
2.JSX 模板
3.数据+模板生成虚拟DOM
(虚拟DOM就是一个js对象,可以理解为数组结构的js对象,用它来描述真实DOM)
[‘div’, {id: ‘abc’}, [‘span’, {}, ‘hello’]]
4.用虚拟DOM的结构来生成真实的DOM,来显示
<div id=‘abc’><span>hello</span> </div>
5.state发生变化
6.数据+模板生成新的虚拟DOM(极大提高了性能) [‘div’, {id: ‘abc’}, [‘span’, {}, ‘byebye’]] (js创建一个js对象很简单,但是js创建一个DOM对象性能损耗是比较大的)
7.比较原始虚拟DOM和新的虚拟DOM的区别,找到区别是span中的内容(极大提高了性能)(减少了对真实DOM的创建以及减少了真实DOM的对比,取而代之,创建的都是js对象,对比的也是js对象,js比较js对象不怎么耗性能,但是比较真实的DOM比较耗性能)
setState里面写成函数,设计成异步函数初衷是为了提高react的底层性能,比如多次修改state的时间间隔很小,只做一次虚拟DOM的修改。
Diff算法,同层比较,一层一层进行比对,以标签为最小比较单位,假如一个标签内部的内容变了但内部里还有其他标签,其他标签没变,那么其他标签还复用之前的虚拟DOM,内容生成新的虚拟DOM。同层中根据key值做比对,key值是唯一的,所以比对起来就很节省性能,所以不能用索引值index代替,因为这样原始的虚拟DOM树上的key值就没办法和新的虚拟DOM树的key值一致了,比对就必须循环一次了。
8.直接操作DOM,改变span中的内容 流程是: JSX => React.createElement(‘div’, {id: ‘abc’}, React.createElement(‘span’, {}, ‘hello’))方法 => 虚拟DOM(js对象) =>真实DOM
JSX的语句可经过babel编译为React语法,所以JSX就是语法糖,编译为React.createElement()方法
优点:
1.性能提升了,DOM的比对变成了js的比对
2.它使得跨端的应用得以实现。React Native用react 语法去写原生应用,真实DOM在浏览器端渲染是没有问题的,但在移动端的原生应用中是不存在DOM的概念的,没有虚拟DOM则没法在移动端使用,虚拟DOM是个js对象,在浏览器和原生应用中都可以被识别,在浏览器中虚拟DOM变成真实DOM,在原生应用中虚拟DOM不让它生成DOM,而生成一些原生应用的组件,就可以在移动端显示出来了
附:
问:react/vue中的key有什么作用(key的内部原理是什么)/为什么遍历列表时,key最好不要用index?
答:当状态中的数据发生变化时,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,会产生错误DOM更新,界面就会有问题。
ref直接获取DOM元素
可以使用某元素的方法中的e.target获取元素节点,也可以使用ref获取元素节点
//第一种写法,回调函数形式的ref,回调函数(满足三个特点:1.自己定义的函数 2.没调用 3.这函数最终执行了)
<input
id="insertArea"
value={this.state.inputValue}
onChange={this.handleInputChange}
//ref={(input) => {this.input = input}} //构建了一个ref引用,这个引用叫做this.input,指向input这个标签,可以直接用
//这个回调函数接收参数就是ref所在的元素节点,这里参数input也可以用别的名字。把这个节点放在了组件实例自身上,叫做input,所以就是this.input,箭头函数没有自身的this,像外找找到render,render里的this就是组件的实例对象
//render()执行,执行里面的JSX,发现ref是一个回调函数的形式,直接触发这个回调函数的执行
//直接在标签上写函数,这种形式叫内联函数
//上面那句ref简写为
ref={input => this.input = input}
//回调ref中函数调用次数的问题
//官网中有说明:如果ref回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null,然后第二次会传入参数DOM元素。这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的(意思是当数据更新,重新调用render函数时,到ref这里了,第一次不知道之前的参数是啥,为了不产生bug,将其重新置为null,执行了一遍函数,然后在以该DOM元素为参数再执行一遍这个回调函数)。通过将ref的回调函数定义成class的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的(按照正常上面那种形式写就行)。用下面的class的绑定函数的方式,ref中的回调函数就不会出现执行两次的情况,只执行一次。
//ref={this.saveInput}
/>
//和上面class的绑定函数的方式这里配套使用
//saveInput = (input) => { //还是传入DOM元素作为参数
// this.input = input
//}
//handleInputChange(e) {
// const value = e.target.value //e.target就是input元素;e.target不能放在setState里面,上面有讲解
// this.setState(() => ({
// inputValue: value
// }))
//}
//ref的写法
handleInputChange() {
const value = this.input.value
//也可用结构赋值的方式
//const { input } = this
//const value = input.value
this.setState(() => ({
inputValue: value
}))
}
//第二种写法,React.createRef,目前React最推荐的方式
class Demo extends React.Component {
myRef = React.createRef() //React.createRef调用后返回一个容器,该容器可以存储被ref所表示的节点。这个容器给了实例的myRef属性。该容器是“专人专用”的,里面只能存一个,如果多处使用却使用同一个容器,后放进去的节点就把前面的替换了。JSX中需要用到几个ref,就要写几个容器。
render() {
return (
<div>
<input ref={this.myRef} type="text" />
{/*render函数执行到input标签的时候,发现ref使用的是React.createRef的形式,把当前ref所在的节点直接存储到了容器里,此时this.myRef值为{current: input},通过this.myRef.current就可以拿到这个input节点了*/}
</div>
)
}
}
//第三种写法,字符串形式的ref,废弃了,可能在未来的版本中移除,因为用多了会产生效率问题
<input
id="insertArea"
value={this.state.inputValue}
onChange={this.handleInputChange}
ref="input1" //这样写组件实例的refs属性就等于{input1: input},键input1就是你自己定义的,值input就是所在的标签
/>
handleInputChange() {
//const value = this.refs.input1.value //this.refs.input1可以拿到input标签
//下面用的解构赋值
const { input1 } = this.refs
const value = input1.value
debugger //打断点,代码运行到这停止,附加的,和这里没关系
this.setState(() => ({
inputValue: value
}))
}
尽量别用ref(发生事件(例如点击、失去焦点等)的元素正好是要操作事件的元素(onChange={this.handleInputChange}),就可以之前的函数中event.target来代替ref的使用),因为react建议用数据驱动的形式编写代码,尽量不要直接操作DOM,有时候使用ref会出现问题,例如ref和setState合用的时候有时会出现一些坑,因为在方法中,使用setState是一个异步函数,不会立即执行,会和其他代码有个执行顺序,一些代码即使写在下面,但是是同步代码,会在setState运行之前运行的,例如下面这种情况
handleBtnClick() {
this.setState((preState) => ({
list: [...preState.list, preState],
inputValue: ''
}))
console.log(this.ul.querySelectAll('div'.length)) //想要先执行setState数据改变后再执行这句获取页面的DOM信息,结果这句是同步代码,先执行了,没达到预期效果
//this.ul是上面ref的写法
}
//可以这样改,setState有第二个参数,也是一个回调函数,执行完第一个回调函数后会执行第二个回调函数
handleBtnClick() {
this.setState((preState) => ({
list: [...preState.list, preState],
inputValue: ''
}), () => {
console.log(this.ul.querySelectAll('div'.length))
})
}
附:解构赋值的连续写法
const {value} = this.keywordElement
console.log(value)
const {keywordElement: {value}} = this
console.log(value)
console.log(keywordElement)
const {keywordElement: {value: data}} = this
console.log(data)
受控组件
页面中所有输入类的DOM(例如input,radio等),随着输入就能把输入值维护到状态state中去,需要用数据的地方直接从状态中取出来,就属于受控组件。(和Vue中的双向绑定类似)
非受控组件
表单内输入类DOM的值,数据现用现取(需要用数据的时候取节点,再取节点的value),就属于非受控组件。
项目中更偏向于受控组件,因为非受控组件有几个需要输入的元素就要写几个ref。
纯函数
一类特别的函数:只要是同样的输入(实参),必定得到同样的输出(返回)
必须遵守的约束: 不得改写参数数据; 不会产生任何副作用,例如网络请求,输入和输出设备; 不能调用Date.now()或者Math.random()等不纯的方法
redux的reducer函数必须是一个纯函数
高阶函数和函数的柯里化
state = {
username: '',
password: ''
}
saveFormDate = (dataType) => {
return (event) => {
this.setState({
[dataType]: event.target.value
})
}
}
handleSubmit = (event) => {
event.preventDefault()
const {username, password} = this.state
alert(`你输入的用户名是:${username},你输入的密码是${password}`)
}
render() {
return(
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormDate('username')} type="text" name="username" />
密码:<input onChange={this.saveFormDate('password')} type="password" name="password" />
<button>登录</button>
)
}
saveFormDate = (dataType, event) => {
this.setState({
[dataType]: event.target.value
})
}
}
用户名:<input onChange={(event) => {this.saveFormDate('username', event)}} type="text" name="username" />
<input onChange={event => this.saveFormDate('username', event)} type="text" name="username" />
附:对象通过读取变量来赋值对象的键的方式
let a = 'name'
let obj = {}
obj[a] = 'tom'
生命周期
生命周期函数指在某一时刻组件会自动执行的函数
组件初始化;组件更新;组件卸载
当组件创建的那一刻,constructor会被自动地执行,用于initialization初始化数据set props and state,但它并不是react独有的,是es6中自带的函数,所以可不算react的生命周期函数,但它和react生命周期函数没有太大区别
附:
ReactDOM.render(<Life />, document.getElementById('test'))
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
页面挂载
组件第一次被放到页面上叫做挂载。static getDerivedStateFromProps(props,state):静态方法生命周期钩子。不是给实例用的,所以要加static,给类自身用的。getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。必须有返回值,它应返回一个state状态对象来更新 state,如果返回 null 则不更新任何内容。为什么这个生命周期钩子要设计成静态方法呢?因为这样开发者就访问不到this也就是实例了,也就不能在里面调用实例方法或者setState了,用一个静态函数getDerivedStateFromProps来取代被废弃的其他几个生命周期函数,就是强制开发者在render之前只做无副作用的操作,而且能做的操作局限在根据props和state决定新的state 而已。第一个参数props是组件的props对象,第二个参数state是现在的state对象。该方法适用于罕见的用例,即state的值在任何时候都取决于props;componentDidMount(组件被挂载到页面之后,自动被执行)(常用,做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息),获取Ajax数据的代码在componentDidMount中获取,如果在render里面写Ajax代码,render会重复执行,导致代码重复执行,假如在componentWillMount里面写代码的话,虽然这个生命周期函数也只执行一次,但是写React Native或者用React做服务器端的同构等更深的技术时,可能会和以后更高端的技术产生冲突。发送Ajax请求,需要安装axios。
//安装axios
yarn add axios
//引入
import axios from 'axios'
//在componentDidMount内部获取Ajax数据
componentDidMount() {
axios.get('api/todoList').then((res) => {
this.setState(() => ({
list : [...res.data] // [...res.data]这样写比直接写res.data更好,避免一不小心把res的data数据做了修改造成的不可预知数据改变的情况
})
}).catch(() => {...})
}
组件更新
shouldComponentUpdate (nextProps, nextState) (组件被更新之前,自动被执行,需要返回一个布尔类型的返回结果) 。其他生命周期函数删掉都可以,render删掉则会报错,因为组件是继承自React.Component,内部默认内置了其他生命周期函数,唯一没有内置render函数的默认实现,所以需要我们自己定义render。父组件的render函数被执行,子组件的render函数也会被执行,即使子组件内容不变也会被执行,所以会发生性能损耗,可以在子组件中加入这个
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.content !== this.props.content) {
return true;
}else {
return false;
}
}
;getSnapshotBeforeUpdate(prevProps,prevState):保存状态快照。必须有返回值,返回一个snapshotValue(任何值都可以是快照值,比如字符串、数字等)或者null。它是用来代替componentWillUpdate生命周期钩子函数的。在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。当render阶段完成后,意味着在内存中构建的workInProgress树所有更新工作已经完成,这包括树中fiber节点的更新、diff、effectTag的标记、effectList的收集。循环effectList链表将有更新的fiber节点应用到页面上是commit阶段的主要工作。getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。此生命周期返回的任何值都将作为参数传递给componentDidUpdate(); componemtDidUpdate(prevProps,prevState, , snapshotValue)(组件更新完成之后,它会自动被执行)
强制更新
this.forceUpdate()使用之后页面强制更新一次,不改状态也能更新,和this.setState()类似使用
Unmounting
componentWillUnmount(当这个组件即将被从页面中剔除的时候,会被执行)(常用,做一些收尾的事,例如:关闭定时器、取消订阅消息)
React性能优化的点: 1.this.handleClick.bind(this)改变作用域的话,放在constructor中来做,整个函数作用域绑定的操作只会执行一次,也可以避免组件的无畏地渲染 2.react底层的setState为异步的函数,将多次数据的改变结合成一次,降低虚拟DOM的频率 3.react底层用了虚拟DOM,diff算法同层比对,key值,来提升虚拟DOM的比对速度,从而提升react的性能 4.借助shouldComponentUpdate提高组件的性能,避免无畏的组件的render函数的运行 5.获取Ajax数据在componentDidMount中获取,如果在render里面写代码,render会重复执行,导致代码重复执行,假如在componentWillMount里面写代码的话,虽然这个生命周期函数也只执行一次,但是写React Native或者用React做服务器端的同构等更深的技术时,可能会和以后更高端的技术产生冲突
Charles本地接口数据模拟
过渡动画
//CSS3的过渡动画
.show {
opacity: 1;
transistion: all 1s ease-in;
}
.hide {
opacity: 0;
}
//CSS动画效果
.show {
opacity: 1;
transistion: all 1s ease-in;
}
.hide {
animation: hide-item 1s ease-in forwards;
@keyframes hide-item {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
react-transition-group实现动画
CSSTransition组件给单个元素增加样式
App.js
yarn add react-transition-group
import { CSSTransition } from 'react-transition-group'
<CSSTransition
in = {this.state.show}
timeout = {300}
classNames = {fade}
unmountOnExit
onEntered = {(el) => {el.style.color = 'blue'}}
appear = {true}
>
<div>hello</div>
</CSSTransition>
style.css
.fade-enter, .fade-appear{
opacity: 0;
}
.fade-enter-active, .fade-appear-active{
opacity: 1;
transition: opacity 2s ease-in;
}
.fade-enter-done{
opacity: 1;
color: res;
}
.fade-exit{
opacity: 1;
}
.fade-exit-active{
opacity: 0;
transition: opacity 2s ease-in;
}
.fade-exit-done{
opacity: 0;
}
Transition相对于CSSTransition是更底层的一个组件,当CSSTransition一些动画实现不了的时候,可以查一下Transition这个api
TransitionGroup 组件给多个元素增加样式
App.js
import { CSSTransition, TransitionGroup } from 'react-transition-group'
<TransitionGroup> //外层
{
this.state.map((item, index) => {
return (
//内层,在每个具体的标签上还要加CSSTransition,和上面差不多的各种设置,没有了in={},把循环的key不放在之前的标签上,放在CSSTransition设置上
<CSSTransition
timeout = {300}
classNames = {fade}
unmountOnExit
onEntered = {(el) => {el.style.color = 'blue'}}
appear ={true}
key={index} //注意循环的key要写在这里,不写在下面的div标签上
>
<div>{item}</div>
</CSSTransition>
)
})
}
</TransitionGroup>
antd(Ant Design)UI框架–蚂蚁金服
//安装antd
yarn add antd
TodoList.js
import React, { Component } from 'react';
import 'antd/dist/antd.css'; //引入antd的css样式文件,安装之后在node_modules里,但是这是所有的样式,如果只是使用几个组件,就没必要引入全部样式,所以要进行按需引入
import { Input, Button } from 'antd'; //input button list等组件怎么用,看官网的各部分代码介绍
class TodoList extends Component {
render() {
return (
<div style={{marginTop: '10px', marginLeft: '10px'}}>
<div>
<Input placehoder='todo info' style={{width: '300px', marginRight: '10px'}} />
<Button type="primary">提交</Button>
</div>
</div>
)
}
}
export default TodoList;
按需引入样式
在antd官网找到3.x版本(这个介绍的更详细)=>在create-react-app中使用,往下翻到高级配置,进行配置后就不需要import ‘antd/dist/antd.css’; 了
自定义主题(需要先配置上面的按需引入样式)
antd是用less写的样式,要自定义主题,在antd官方在less文件里所写的主题颜色,把这个颜色改掉。按照官网上设置会有一个报错,是因为less-loader升级了,把javascriptEnabled和modifyVars放在lessOptions:{…}里就行了
在刚刚的高级配置再往下翻,有个自定义主题,
UI框架还有ElementUI for React(饿了么)、vantUI(适合移动端,有赞团队)、material-ui(国外的)
附:plugin是插件的意思
状态提升(lifting state up)
真实项目中多个组件经常会分享相同的数据,这种情况下最好将这份共享的状态提升到最近的父组件上进行管理,这种做法称为状态提升
单向数据流(自上而下的数据流top-down data flow)
和双向绑定的区别:
单项数据流比双向绑定要写更多的模板代码,比如要用回调函数的方式;好处是可以更快和寻找定位bug,每个组件自己才能操作属于自己的状态数据,也可以使用自定义逻辑来拒绝或者更改用户的输入
Context
Props属性是由上到下单项传递的,很多时候中间组件是不需要这个数据的,而Context提供了在组件中共享此类值的方法,而不必通过组件的每个层级显示地传递props,设计目的是共享那些对于组件来说全局的数据,但是不要因为仅仅为了避免在几个层级下的组件传递props而使用context,context的.js文件本身不是一个component而是object,所以文件放在src层级下,不放在components文件夹下
在应用开发中一般用context,一般都用它的封装react插件,例如react-redux
context.js //context对象就创建完毕了,必须放在祖辈组件和孙子组件都能获取到的地方
import React from 'react'
//创建Context容器对象,ThemeContext大写的原因是组件标签必须大写
const ThemeContext = React.createContext()
export default ThemeContext
App.js里,祖父组件
import ThemeContext from '../theme-context/context.js'
<ThemeContext.Provider value={...}>
包裹子组件并且根据value传入数据
</ThemeContext.Provider>
在子孙组件中。父子组件用props最方便,祖孙组件间采用这种方式
import ThemeContext from '../theme-context/context.js'
<ThemeContext.Comsumer>
{
value => {
return value.name
}
}
</ThemeContext.Comsumer>
static contextType = ThemeContext
render() {
return(
)
}
Redux数据层框架
Redux = Reducer + Flux
Flux是官方推出的最原始的辅助React使用的数据层框架,有些缺点比如数据依赖的问题,升级到Redux
redux是一个专门用于做状态管理的JS库(不是react插件库),可以用在react、vue、angular等项目中,但基本与react配合使用,作用:集中式管理react应用中多个组件共享的状态,使用场景:1.某个组件的状态,需要让其他组件可以随时拿到(共享)2.一个组件需要改变另一个组件的状态(通信) 使用原则:能不用就不用,如果不用比较吃力才考虑使用
Redux Flow图
//安装redux
yarn add redux
src文件夹下创建一个store文件夹,里再创建index.js,里面存放store代码
index.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
store文件夹下的reducer.js返回的是一个函数
reducer.js
const defaultState = {
inputValue: '12',
list: [4, 5, 6]
};
export default (state = defaultState, action) => {return state;}
reducer初始化状态,被第一次调用时是store自动触发的,传递的preState是undefined,传递的action是{type:@@REDUX/INIT_随机数};其他的是加工状态,加工时根据旧的state和action,产生新的state的纯函数。
附:数字的字符串变为数字,可以乘以1,例如’2’*1为数字2
附:箭头函数返回一个对象
const a = b => {}
const a = b => ({})
const a = b => {return {}}
TodoList.js
import store from './store';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = store.getState();
}
}
Redux-DevTools–Redux中间件
安装Redux DevTools插件,Chrome浏览器更多工具=》扩展程序=》应用商店搜索Redux DevTools
使用方法(最新版):比下面的这种简单
//安装
yarn add redux-devtools-extension
index.js
import { createStore } from 'redux';
import reducer from './reducer';
import {composeWithDevtools} from 'redux-devtools-extension'
const store = createStore(reducer, compostWithDevtools(applyMiddleware(thunk)));
export default store;
这样就可以使用redux-devtools了。下面的操作是之前的方式
在控制台会有Redux菜单,点击instructions,按照提示在创建store的第二个参数添加代码后才能使用。数据一目了然
import {createStore } from 'redux';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_ETENSION__&&window.__REDUX_DEVTOOLS_ETENSION__()
)
创建action并dispatch
TodoList.js
handleInputChange(e) {
const action = {
type: 'change_value',
value: e.target.value
}
store.dispatch(action)
}
store会把接收到的action命令直接传递给reducer
reducer.js
export default (state = defaultState, action) => {
if (action.type === 'change_value') {
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue == action.value;
return newState;
}
return state;
}
TodoList.js
import store from './store';
class TodoList extends Component {
constructor(props) {
super(props);
this.state = store.getState();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChang = this.handleStoreChang.bind(this);
store.subscribe(this.handleStoreChang());
}
handleStoreChange() {
this.setState(store.getState());
}
}
异步action和同步action
对象类型的action叫做同步action,函数类型的action叫做异步action,异步action中一般都会调用同步action,异步action不是一个必须要用的东西,把异步的代码放在某组件的index.js文件中执行方法的里面写也行
ActionTypes的拆分
store文件夹下新建actionTypes.js用于ActionTypes的拆分
actionTypes.js
export const CHANGE_VALUE = 'change_value';
需要在组件文件TodoList.js、reducer.js文件和下面的actionCreator.js文件内import { 常量名 } from来引入常量,替换掉之前的action的types,例如change_value。目的在于假如你把action的types值拼错了,可以直接找到报错来源,之前的action的type用字符串的那种方式假如拼错不会报错,但是却没有应有的使用效果,处理bug但是找它很费劲。
actionCreator统一创建action
store文件夹下创建一个actionCreators.js
目的:提高代码可维护性,前端的自动化测试也会很方便
actionCreators.js
import { CHANGE_VALUE } from './store/actionTypes'
export const getInputChangeAction = (value) => {
type: CHANGE_VALUE,
value
}
TodoList.js
handleInputChange(e) {
const action = {
type: 'change_value',
value: e.target.value
}
store.dispatch(action)
}
import { getInputChangeAction } from './store/actionCreators'
handleInputChange(e) {
const action = getInputChangeAction(e.target.value);
store.dispatch(action)
}
redux设计和使用时有三个基本原则:
store是唯一的;
只有store能够改变自己的内容,而不是reducer,store把数据给了reducer,reducer把数据进行深拷贝后处理复制的这一份数据,再把处理后的数据返回给store,store是自己改变自身的内容的;
Reducer必须是纯函数(纯函数:给定固定的输入,不管在任何时刻都会有固定的输出,假如函数里有setTimeout这种异步的操作或者new Date这种和日期相关的内容,就不算纯函数了,而且纯函数不会有任何的副作用,比如不能对传入的store的state的数据做修改)
UI组件(傻瓜组件)和容器组件(聪明组件)
UI组件主要负责页面的渲染,容器组件主要负责页面的逻辑
src文件夹下创建TodoListUI.js,把TodoList.js里关于页面渲染的部分全部放在这里,是一个TodoListUI组件
在TodoList.js里import这个TodoListUI组件,把需要引入的部分转移到TodoListUI.js中
父子组件间传值,父组件向子组件传递带有参数的方法时,不能用之前的this.handleItemDelete.bind(this, index)了,父组件还是用this.handleItemDelete = this.handleItemDelete.bind(this),handleItemDelete() = {this.handleItemDelete()}父组件通过属性方式向子组件传值,子组件要用函数的形式来写,如下把index传给父组件
<List
renderItem={(item, index) => (
<List.Item onClick = {() => {this.props.handleItemDelete(index)}}>{item}</List.Item>
)}
/>
无状态组件
一个组件内部只有一个render函数的时候就可以定义为无状态组件,就是一个函数,一般用在UI组件这种负责页面渲染不负责逻辑的情况
优点:性能比较高,因为它就是一个函数,里面没有任何的生命周期函数,也不会生成真正的组件的实例,而其他组件是一个类,里面还会有生命周期函数,执行起来远比一个函数形式的组件多得多
TodoListUI.js
import ...
const ToDoList = ( props ) => {
return (
)
}
Redux中异步逻辑的拆分
Redux-thunk
(react是Facebook的,redux是其他团队的,redux-thunk是Facebook的,方便更加舒服地在react中使用redux)
Redux-thunk–Redux中间件–在action中写函数–进行ajax请求发送 redux-thunk将axios异步请求或者复杂的逻辑放在action中进行处理
//安装redux-thunk
yarn add redux-thunk
在store文件夹下的index.js文件中使用,通过Redux创建store时要用中间件
index.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(reducer, applyMiddleware(thunk))
import {createStore, applyMiddleware, compose } from 'redux';
const componentEnhancers = windows.__ReDUX_DEVTOOLS_EXTENSION ?window.__ReDUX_DEVTOOLS_EXTENSION():compose
const enhancer = componentEnhancers(applyMiddleware(thunk));
const store = createStore(reducer, enhancer);
export default store;
之前actionCreator返回的内容是一个对象,使用redux-thunk之后返回的内容不仅仅可以是一个对象了,还可以是一个函数,函数里面返回异步axios请求的数据,函数执行结束再action传给store。不应该将异步代码放在声明周期函数里,容易使得异步代码越来越多,组件就会越来越大,所以要放在actionCreators这里更合适
之前是这样的TodoList.js
componentDidMount() {
axios.get('./list.json').then((res) => {
const data = res.data;
const action = initListAction(data);
store.dispatch(action)
}
}
现在是这样的
TodoList.js
componentDidMount() {
const action = getToDoList();
store.dispatch(action);
}
actionCreates.js
import axios from 'axios'
export const getToDoList = () => {
return (dispatch) => {
axios.get('./list.json').then((res) => {
const data = res.data;
const action = initListAction(data);
dispatch(action)
})
}
}
当是函数时能够接收到store的dispatch方法 getToDoList还需要在组件文件的componentDidMount内执行action,dispatch,也就是函数执行结束才把action传给store 这样做有益于生命周期函数内部代码的简洁性和自动化测试 redux中间件的中间指的是action和store中间,中间件其实就是对store的dispatch的一个升级 中间件比较火的有redux-thunk(处理异步操作,把异步操作放在action中) redux-logger redux-saga(也是处理异步操作的,单独把异步的操作拆分出来放在另外的文件中管理)
Redux Data Flow
Redux的标准流程:View在Redux中会派发一个action,action通过Store的dispatch方法派发给store,store接收action和之前的state一起传给reducer,reducer返回一个新的数据给store,store改变自己的state
Redux中间件指的是action和store中间,action通过dispatch方法传递给store,所以action和store中间就是dispatch方法,中间件就是对dispatch方法的封装或者说对dispatch方法的升级,升级后的dispatch接收到的action是对象的话直接把action传递给store,如果接收到的action是函数的话,会先执行action这个函数,执行完之后需要调用store的时候,这个函数再调用store
还有其他Redux的中间件,比如Redux-logger可以记录action每次派发的日志,也是对dispatch进行升级,每次派发action的时候,把action通过console.log打印在控制台中;Redux-saga也是解决Redux中异步问题的中间件,不同于Redux-thunk把异步操作放在action中进行处理,而是单独把异步逻辑拆分出来放到单独的文件进行管理,
Redux-saga
中间件指的是action和store的中间,
详细讲解在github上搜redux-saga
//安装redux-saga
yarn add redux-saga
store下的index.js
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
import toDoSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const componentEnhancers = windows.windows.__ReDUX_DEVTOOLS_EXTENSION ? window.windows.__ReDUX_DEVTOOLS_EXTENSION() :compose
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);
sagaMiddleware(toDoSaga);
export default store;
创建sagas.js文件单独存放异步逻辑,把异步的代码都写在saga.js文件中,根据官网代码写,saga.js内部使用的是generator函数,经过saga在创建store文件内底层配置后,一旦dispatch后,reducer内和saga.js内都可以接收到派发的action,在saga.js内部处理异步函数
sagas.js
import { takeEvery, put } from 'redux-saga/effects';
import { GET_INIT_LIST } from './actionTypes';
import { initListAction } from './actionCreators';
import axios from 'axios';
function* getInitList() {
try{
const res = yield axios.get('/list.json');
const action = initListAction(res.data);
yield put(action);
}catch(e) {
console.log("list.json网络请求失败");
}
}
function* mySaga() {
yield takeEvery(GET_INIT_LIST, getInitList);
}
export default mySaga;
TodoList.js
componentDidMount() {
const action = getInitList();
store.dispatch(action);
}
actionCreators.js
import { GET_INIT_LIST } from './actionTypes';
export const getInitList = () => ({
type: GET_INIT_LIST
})
export const initListAction = (value) => ({
type: INIT_LISST_ACTION,
value
})
actionTypes.js
export const GET_INIT_LIST = 'getInitList';
redux-saga远比redux-thunk复杂的多,内置api非常多,由于它的这种拆分为另一个文件,处理非常大型项目时感觉更优于redux-thunk,redux-thunk使用更加简单
React-Redux
UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。 容器组件:负责和redex通信,将结果交给UI组件。
容器组件是UI组件的父组件,通过props传值
附:
上面的监测store.subscribe()可以放在主入口文件index.js中 只要redux里发生任何的变化,App组件就重新调用一下render(),即使只有一个地方改了也会重新调用一次,但不会有太大的效率问题,因为虚拟DOM的diffing算法
import store from './redux/store'
ReactDOM.render(<App/>, document.getElementById('root'))
store.subscribe(() => {
ReactDOM.render(<App/>, document.getElementById('root'))
})
第三方模块,在React中更加方便地使用Redux,将组件和store做连接
//安装react-redux
yarn add react-redux
index.js–项目的入口文件
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';
import { Provider } from 'react-redux';
import store from './store';
const App = (
<Provider store={store}>
<ToDoList/>
...其他组件
</Provider>
);
ReactDOM.render(App, document.getElementById('root'));
TodoList.js
constructor(props) {
super(props);
this.state = store.getState();
}
import { connect } from 'react-redux';
class TodoList extends Component {
render() {
return (
<div>
<div>
<input value={this.props.inputValue} onChange={this.props.changeInputValue()} />
<button>提交</button>
</div>
</div>
)
}
}
const mapStateToProps = (state) =>{
return {
inputValue: state.inputValue
}
}
const mapDispatchToProps = (dispatch) => {
return {
changeInputValue = (e) => {
const action = {
type: 'change_input_value',
value: e.target.value
}
dispatch(action);
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ToDoList);
export default connect(
state => ({count: state}),
{
jia: createIncrementAction,
jian: createDecrementAction,
jiaAsync: createIncrementAsyncAction
}
)(ToDoList)
代码变得更加精简
render() {
const { inputValue, changeInputValue } = this.props;
}
发布订阅机制PubSubJS
yarn add pubsub-js
import PubSub from 'pubsub-js'
componentDidMount() {
this.token = PubSub.subscribe('MY TOPIC', mySubscriber);
}
var mySubscriber = function(msg, data) {
...
}
componentWillUnmount() {
PubSub.unsubscribe(this.token)
}
PubSub.publish('MY TOPIC', 'hello world');
fetch(了解)
jQuery(容易产生回调地狱)和axios(Promise风格)都是用XMLHTTPRequest对象提交ajax请求,而windows内置的fetch不用xhr也能发ajax请求,而且本身也是Promise风格的,fetch用的关注分离思想(把一个总体进行拆分,分别关注是否成功)
fetch使用率不高,因为兼容性问题,老版本可能不支持
附:promise可以统一处理错误,就是链状的.then可以在最后只写一个.catch用于处理报错情况
实战技巧
附:为了分清组件的.js文件和普通的.js文件,组件的.js文件首字母要大写,还可以将组件的.js文件改为.jsx文件,文件的小图标就变了,和普通的.js文件的图标就不一样了。项目的总的组件App.js可以不改。
附:在文件引入的时候,可以省略的是.js或者.jsx这个文件后缀;如果在组件文件夹下的组件名为index.js或者index.jsx,引入的时候直接引入到整个组件文件夹名就可以,index.js或index.jsx可以省略,也能找到。怎么使用看情况,也可以直接在组件文件夹下的组件用组件.js,看公司的规定。
功能界面的组件化编码流程(通用)
1.拆分组件:拆分界面,抽取组件
2.实现静态组件:使用组件实现静态页面效果
3.实现动态组件
? 3.1动态显示初始化数据
? 3.1.1数据类型
? 3.1.2数据名称
? 3.1.3保存在哪个组件
? 3.2交互(从绑定事件监听开始)
组件的props传入多个属性
<Item id={todo.id} name={todo.name} done={todo.done} />
//像这样的给组件的props传入多个属性,如果有几十个属性,很麻烦
//直接这样写
<Item {...todo} />
//{...todo}用这种语法直接将todo对象的属性全部传给Item组件的props
附:
//input标签的checked属性设置之后,在页面不能通过点击进行修改是否被选中,如果想要修改必须调用onchange方法
<input type="checkbox" checked={true} onChange={...} />
//如果是defaultChecked属性被设置了,可以在页面通过鼠标点击进行修改,但是defaultChecked只在第一次起作用,页面刚呈现那一次起作用,checked始终以最后一次为主
<input type="checkbox" defaultChecked={true} />
//类似的还有defaultValue和value
附:
handleKeyUp = (event) => {
//解构赋值获取keyCode, target
const {keyCode, target} = event
//判断是否是回车按键
if(keyCode !== 13) return
//内容不能为空,这里用去除字符串空格后判断一下
if(target.value.trim() === '') {
alert('输入不能为空')
}
...
}
//键盘的点击事件是onkeyup
<input onKeyUp={this.handleKeyUp} type="text" placeholder="输入任务名称" />
//鼠标有移入和移出的事件
<li onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<label>
<input type="checkbox" defaultChecked={done} />
<span>{name}</span>
</label>
</li>
//input标签的回调函数中可以通过event.target.value拿到里面的值,如果类型是checkbox的话,可以通过event.target.checked拿到是否勾选的布尔值
附:uuid、nanoid
//对于需要自己设定一个id值的地方可以使用uuid,会返回一个全球独一无二的字符串
//安装
yarn add uuid
//uuid库有点大,可以使用nanoid这个库
//安装
yarn add nanoid
//引入
import {nanoid} from 'nanoid'
//使用,执行这个函数会返回一个唯一的字符串
id = nanoid()
附:状态在哪里,操作状态的方法就在哪里,例如状态state在App组件内,那么操作状态的所有方法都要放在App组件内
附:数组的filter函数
const todo = [2, 3, 5, 3]
const newTodo = todo.filter((item) => {
return item !== 3
})
附:因为react的JSX中只能写js表达式,所以不能写if语句,可以用三元表达式实现if语句效果,而且三元表达式还可以嵌套使用
附:删除对象的属性delete
let obj = {a: 1, b: 4}
delete obj.a
console.log(obj)
附:在react中如果想用confirm(‘确定删除吗?’),直接写会报错,应该用window.confirm(‘确定删除吗?’),用户点击之后会返回一个布尔值,
axios获取数据,脚手架代理配置
第一种方法:
在package.json文件里添加代理
{
{
},
"proxy": "http://localhost:5000"
}
优点:配置简单,前端请求资源时可以不加任何前缀
缺点:只能配置一个服务器代理地址,假如要从多个服务器上获取数据,这种方法就不可以了
工作方式:当请求了3000不存在的资源时,那么该请求会转发给5000(优先匹配前端资源)
第二种方法:
在项目的src文件夹下创建setupProxy.js文件,文件名不能改动,文件里不能用ES6的语法写,要用CJS写。这个文件不是给前端代码用于执行的,react脚手架会找到这个文件,把这个文件加到webpack配置里面,webpack里面都是CJS语法。
axios.get('http://localhost:3000/api1/students').then()
axios.get('/api1/students').then()
setupProxy.js
优点:可以配置多个代理,可以灵活的控制请求是否走代理(上图,如果不写/api1,就不走5000的代理,如果不写/api2,就不走5001的代理)。
缺点:配置繁琐,前端请求资源是必须加前缀
样式的模块化
Hello文件夹下放置Hello组件index.jsx,样式组件放置全局内有类名相同时冲突,可以样式文件为index.module.css,然后在index.jsx中引入时这样写
import React, {Component} from 'react'
import hello from './index.module.css'
//之前引入时这样的
//import './index.css'
export default class Hello extends Component {
render() {
return <h2 className={hello.title}>Hello, React!</h2>
{/*这里要这样写hello.title,之前是<h2 className="title">Hello, React!</h2>*/}
}
}
也可以用下面的styled-components避免类名冲突的问题
styled-components
.css文件一旦在一个文件中引入了,是全局生效的,可能会造成不同组件内的css样式冲突 styled-components对样式进行管理,组件自己的样式只对自己生效
yarn add styled-components
index.js
import './style.js';
全局样式的书写
style.js
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
import { injectGlobal } from 'styled-components';
injectGloabl`
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
`
reset.css
使用reset.css进行样式统一,因为不同浏览器内核对标签的默认样式是不同的
把reset.css的代码粘贴到上面全局样式的style.js的injectGlobal中,设置全局样式
在src文件夹下创建common文件夹,用于存放像Header这样的公共组件,common下创建header文件夹,文件夹内创建index.js来书写Header组件;创建style.js书写样式
index.js
import React, { Component } from 'react';
import {
HeaderWrapper,
Logo
} from './style';
class Header extends Component {
constructor(props) {
super(props);
focused: false;
}
render() {
return (
<HeaderWrapper>
<Logo href='/' />
<NavItem className="left active">首页</Navitem>
<NavItem className="left">
<i className="iconfont"></i>
</Navitem>
<NavItem className="right">下载</Navitem>
<NavSearch className={this.state.focused ? focused : ""}></NavSearch>
</HeaderWrapper>
)
}
}
export default Header;
style.js
import styled from 'styled-components';
import logoPic from '../../statics/logo.png';
export const HeaderWrapper = styled.div`
position: relative;
height: 56px;
background: red;
`;
export const Logo = styled.a`
position: absolute;
top: 0;
left: 0;
display: block;
width: 100px;
height: 56px;
background: url(${logoPic}); //``字符串引入变量的写法
background-size: contain; //图片刚好占满区域
`;
export const Logo = styled.a.attrs({
href: '/'
})`
...
`;
export const NavItem = styled.div`
position: relative;
line-height: 56px;
padding: 0 15px;
font-size: 17px;
color: #333;
&.left { //NavItem组件有left这个class的话
float: left;
}
&.right {
float: right;
color: #969696;
}
&.active {
color: #ea6fja;
}
.iconfont { //NavItem下的iconfont类的样式
position: absolute;
top: 0;
left: 0;
}
`;
export const NavSearch = styled.input.attrs({
placehoder: '搜索'
})`
width: 160px;
height: 38px;
...
box-sizing: border-box;
&..placeholder { //对组件NavSearch下面的placeholder的样式进行设置
color: #999;
}
&.focused {
width: 200px;
}
`;
bootstrap一个CSS插件
//安装
install bootstrap --save
//在index.js文件引入
import 'bootstrap/dist/css/bootstrap.min.css'
路径是这样的,public文件夹下有css文件夹(内部有bootstrap.css)和favicon.ico和index.html
脚手架内置个服务器,就是通过webpack dev server开启的服务器,react脚手架里通过webpack配置的public文件夹就是localhost:3000这台内置服务器的根路径,开启服务器运行项目目的是为了得到一个最真实的开发场景,项目上线都是通过主机名加端口号的形式访问的
地址栏输入localhost:3000/css/bootstrap.css就可以访问到public/css/bootstrap.css这个文件了,假如请求的东西不存在,就会访问public/index.html文件,这个文件其实就是一个兜底文件,如果你访问的东西不存在,就把这个给你。如果只访问localhost:3000,就会把public/index.html给你
样式丢失
路由路径是多级的结构<Route path=‘/lala/about’ exact component={Home}></ Route>,页面刷新就会样式丢失(shift + 浏览器刷新按钮,是强制刷新,不走缓存按钮),因为浏览器认为多级路径中的/lala也是3000中的路径,获取文件的时候localhost:3000/css/bootstrap.css地址就变为localhost:3000/lala/css/bootstrap.css,获取不到就会把public/index.html给你,样式就丢失了
解决方法:
1.(常用)在index.html引入样式<link rel=“stylesheet” href=“./css/bootstrap.css” />中的.去掉,变为<link rel=“stylesheet” href=“/css/bootstrap.css” />就好了,因为加.了就是相对路径了,从当前文件夹出发找文件,认为多级路径中的/lala也是3000中的路径,就会加上路径上多的lala/css/bootstrap.css,不加.的意思是从localhost:3000下面取东西,也就是public,进而public下的css/bootstrap.css
2.(常用,但是适用于React脚手架工具)<link rel=“stylesheet” href=“./css/bootstrap.css” />换成<link rel=“stylesheet” href=“%PUBLIC_URL%/css/bootstrap.css” />,%PULIC_URL%代表的就是public的绝对路径
3.(少见)<link rel=“stylesheet” href=“./css/bootstrap.css” />不变,使用路由的地方将BrowserRouter改为HashRouter,地址栏就是localhost:3000/#/lala/about这个路由,该地址发请求的时候,自动忽略#后面的东西,因为#后面的东西都是哈希值,请求的时候不带/lala,以在index.html中的href=“./css/bootstrap.css” 为主
在src文件夹下创建static文件夹用于存放图片等静态资源
iconfont
.css .eot .svg .ttf .woff这五个文件有用
在static文件夹下创建iconfont文件夹,把这五个放进去。
.css文件内有四个iconfont开头的前面需要加上./相对路径,中间的base64文件不需要加相对路径,将后面几个没用的class删掉,只剩下两大段(@font-size和.iconfont这两部分),为了全局生效,把.css文件改为.js文件,最上面引入injectGlobal,import { injectGlobal } from “styled-components”;并把这两块包含在injectGlobal`…`中。在入口文件index.js中import引入这个iconfont.js文件
在组件的中插入iconfont图标<i className=“iconfont”>&16进制;</i>
一个reducer.js存放太多数据可能造成代码的不可维护,把一个reducer拆成多个reducer再进行整合。和header相关的数据放在header中去,在header文件夹下创建一个store文件夹,再创建一个reducer.js
在总的store文件夹下reducer.js内这样写
import { combineReducers } from 'redux';
import headerReducer from '../common/header/store/reducer';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
header文件夹下的index.js
const mapStateToProps = (state) => {
return {
focused: state.header.focused;
}
}
在总的reducer.js内引入小的reducer.js时路径会非常的长,在header下的store下创建一个index.js,相当于header的store部分的一个出口文件,引入同目录下的reducer.js。也就是总的store/reduce.js通过引用header/store/index.js间接地引用header里的reducer.js
header/store/index.js
import reducer from './reducer.js';
export { reducer };
在总的reducer.js内部里,就可以这样地址简化了
import { reducer as headerReducer } from '../common/header/store';
将Header组件需要所有的action放在header/store下的actionCreators.js中集中写
在header/store/index.js
import * as actionCreators from './store/actionCreators';
dispatch(actionCreators.searchFocus());
把action需要的常量字符串都放在header/store/contants.js(或者命名为actionTypes.js)中,在actionCreators.js和reducer.js中都引入
header/store/actionCreators.js
import * from contants from './contants';
在header文件夹下store下的index.js中引入同目录下的actionCreator.js和contants.js,让index.js成为一个出口文件
header/store/index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as contants from './contants';
export { reducer, actionCreator, contants };
在header/index.js文件中,把引入actionCreators这里换一下
import * as actionCreators from './store/actionCreators';
import { actionCreators } from './store';
immutable.js
header/store/reducer.js内部state是不允许修改的,要用原始的state表达方法,要时刻警惕state不被修改,会存在风险,所以要变成immutable对象管理store中的数据
immutable.js是一个第三方模块,可以生成一个immutable对象,
yarn add immutable
header/store/reducer.js
import * as constants from './constants';
import { fromJS } from 'immutable';
const defaultState = fromJS{(
focused: false;
});
export default (state=defaultState, action) = {
if(action.type == 'constants.SEARCH_FOCUS') {
return state.set('focused', true);
}
if() {
}
return state;
}
header/index.js
const mapStateToProps = (state) => {
return {
focused: state.header.get('focused');
}
}
redux-immutable
上面既使用了.又使用了get,这样写格式不统一,所以将总的reducer.js的state也换成immutable对象
yarn add redux-immutable;
在总的store文件夹下reducer.js内这样写
import { combineReducers } from 'redux-immutable';
import headerReducer from '../common/header/store/reducer';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
header/index.js
const mapStateToProps = (state) => {
return {
focused: state.getIn(['header', 'focused']);
}
}
如果想要添加模拟的数据文件,在public文件夹下创建api文件夹,放在这里。可以在网址栏通过输入对应路径进行查看
一个函数内部可以由多个dispatch(action),都会派发
项目中使用Redux-thunk发送Ajax数据,获取到数据之后存到store里,再在页面展示
header/store/reducer.js
const defaultState = fromJS({
focused: false,
list: []
})
header/store/reducer.js内
return state.set('list', action.data);
在header/store/actionCreator.js内部axios获取data的时候也要变为immutable形式的
import { fromJS } from 'immutable';
const changeList = (data) => ({
type: constants.CHANGE_LIST,
data: fromJS(data),
totalPage: Math.ceil(data.length / 10)
})
export const getList = () => {
return (dispatch) => {
axios.get('/api/headerList.json').then((res) => {
const data = res.data;
dispatch(changeList(data.data));
}).catch(() => {
console.log('error');
})
}
}
immutable形式的数据也有map方法
this.props.list.map((item) => {
return <SearchItem key={item}>{item}</SearchItem>
})
header/store/reducer.js内部可以用switch精简繁多的if代码
export default (state = defaultState, action) => {
switch (action.type) {
case constants.SEARCH_FOCUS :
return state.merge({
focused: true,
totalPage: action.totalPage
})
case ...
default:
return state;
}
}
换一批的实现,数据的处理方式(7-14)和图标点击旋转的实现(7-15)
异步请求问题(7-16)
一个immutable数组list转化为普通数组jsList
jsList = list.toJS();
代码简化
if(list.length === 0) {
dispatch(actionCreators.getList());
}
(list.length === 0)&&dispatch(actionCreators.getList());
React路由
React和Vue都是SPA(single page app)单页面应用,应用只有一个完整的页面(.html),点击页面中的链接不会刷新页面,只会做页面的局部刷新,数据都需要通过ajax请求获取,并在前端异步展现。
前端路由就是点击页面某处,地址栏的地址变化,被捕捉到然后将对应的组件展现到页面上。利用的是BOM的history。
基本使用:
1.明确好界面的导航区、展示区
2.导航区的a标签改为Link标签
3.展示区写Route标签进行路径的匹配
4.<App />的最外侧包裹一个<BrowserRouter>或<HashRouter>
react-router-dom
//安装
yarn add react-router-dom
在总的App.js里
import { BrowserRouter, Route, Switch } from 'react-router-dom'; //BrowserRouter代表的是路由, Route代表的是一个一个的路由规则
//react-router-dom中的Router有两种,一种是BrowserRouter,一种是HashRouter(地址栏的路径是中带#的,localhost:3000/#/home,#后面的是哈希值,特点是#后面的哈希值不会作为资源发送给服务器的),如果用HashRouter的话,只是地址栏路径多个#/,别的路由设置和BrowserRouter一样
class App extends Component {
render() {
return {
<Provider store={store}>
<div> //Provider标签里的元素都要放在一个标签下
<Header /> //Header组件是公共组件,在哪个路由中都需要
<BrowserRouter> //这里面的内容要使用路由了
//BrowserRouter标签里的元素都要放在一个标签下
<Switch> //匹配到一个路由后不会继续往下匹配了,如果不用Switch,匹配一个路由后还会继续匹配下一个路由,所以多个路由的时候再Switch包裹起来,一个路由的时候不用Switch
//注册路由
<Route Path='/' exact render{() => <div>home</div>}></Route>
//render()函数作为属性,表示要渲染什么
//路径Path的值只要输入的url包含,就会展示该路由效果,例如localhost:3000/detail首页和详情页的内容都会展示出来,exact的意思是路径不能包含,只能完全一致。其实exact是exact={true}的缩写,叫严格匹配,不使用exact叫模糊匹配。而且模糊匹配只能是应该匹配到的放在最前面,放在中间也不行。
//使用exact的原则是如果不使用也不耽误页面的呈现,那就不开启严格匹配,如果不开启严格会引发一些问题,再开启。因为有些时候开启严格匹配的话导致无法继续匹配二级路由。
<Route Path='/detail' exact render{() => <div>detail</div>}></Route>
</Switch>
</BroeserRouter>
</div>
</Provider>
}
}
}
在src目录下创建pages文件夹,代表这个项目有多少页面,里面创建home和detail文件夹,代表有两个页面,里面分别创建index.js,里面创建组件
在总的App.js中引入
import Home from './pages/home';
<Route path='/' exact component={Home}></ Route>
组件路由的懒加载
index.jsx
//防止第一次加载页面的时候所有组件都直接加载过来,组件太多容易卡顿,可以用组件的懒加载
import React, { Component, lazy, Suspense} from 'react'
...
//之前是这样的
//import Home from './Home'
const Home = lazy(() => import('./Home')) //lazy函数里面要用函数形式
//Suspense需要用的组件这里不能用懒加载,必须要用的时候早期就已经加载回来了
import Loading from './Loading'
//注册路由这里要用Suspense包裹
//必须添加Suspense,用于当懒加载的组件还没有返回的时候,页面展示的内容,可以有两种方式,如下。
//<Suspense fallback={<h2>加载中...</h2>}
<Suspense fallback={<loading />}
//注册路由
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</Suspense>
BrowserRouter和HashRouter的区别
1.底层原理不一样:
BrowserRouter使用的是H5的history API(这个history不是this.props.history,这个history是React的封装,BrowserRouter使用的是H5自带的一个,React对它进行二次封装),不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
BrowserRouter没有任何影响,因为state保存在history对象中。
HashRouter刷新后会导致路由state参数的丢失。因为它没有使用history这个API
4.HashRouter可以用于解决一些路径错误相关的问题,例如样式丢失问题。
路由组件和一般组件
1.写法不同
这里的Home组件叫路由组件,需要靠路由匹配上才展示的;正常使用<Home />这种样式的组件叫做一般组件
2.存放位置不同
路由组件不应该放在src/components文件夹下,应该放在src/pages文件夹下
3.接收到的props不同(路由组件和一般组件最大的区别)
一般组件写组件标签时传递了什么就能收到什么,如果属性不传值,this.props为空对象;而路由组件会收到路由器给传递的三个最重要的porps信息,分别为history(go goBack goForward push replace)
location(pathname search state)
match(params path url)
把Home页面拆分为多个组件,home文件夹下有个大的index.js和style.js和一个compoments文件夹,component文件夹下有多个组件文件List.js Topic.js等,将这些小组件引入到Home下的index.js里进行显示。这些小组件功能很少很单一,避免过度设计,将样式直接在Home下的style.js下设计就行
Home文件夹下创建store文件夹,下面有index.js和red1ucer.js,将这个小的reducer.js引入到同目录下的index.js内,使得出口文件都是index.js,再由index.js传到最外层的大的reducer.js内
最外层的reducer.js
import { reducer as homeReducer } from '../pages/home/store';
const reducer = combineReducers({
header: headerReducer,
home: homeReducer
});
export default reducer;
嵌套路由(二级路由)
每当点击一个路由的时候,都从最开始注册的路由重新匹配,继续到嵌套路由中进行组件内部下一级路由的匹配
localhost:3000/home/news
App.jsx
<Route path="/home" component={Home} />
<Route path="/detail" component={Detail} />
<Redict to="/home" />
//如果这里用严格模式exact就不能继续匹配二级路由了,因为没匹配上
src/pages/Home/index.jsx(Home文件夹下除了index.jsx,还有News和Messages文件夹是Home组件的子组件)
<MyLink to="/home/news">News</MyLink>
<MyLink to="/home/messages">Messages</MyLink>
//注册路由
<Route path="/home/news" component={News} />
<Route path="/home/messages" component={Messages} />
<Redict to="/home/news" /> //重定向,当地址只是localhost:3000/home时,上个代码框内的路由匹配后,到这里,/home/news和/home/messages都不匹配,直接重定向
图片地址的组件–style-componnts的一个语法 组件Recommend.js中
<RecommendItem imgUrl="地址" />
style.js中
export const RecommendItem = styled.div`
background: url(${(props) => props.imgUrl});
`;
JSON格式
键值对除了数字和布尔值,都需要双引号包裹,例如字符串
异步获取不同的文件
axios.get('/api/homeList.json?page=' + page).then((res) =>{...});
执行函数直接将页面返回顶部
handleScrollTop () {
window.scrollTo(0, 0);
}
附:
const list = document.getElementByClassName('list')[0]
list.scrollTop = 30px
list.scrollHeight
window监听滚动
window.addEventListener('scroll', this.props.函数名);
当前距离页面顶部的距离(滚动页面的话值会越来越大)
document.documentElemnt.scrollTop
对于window上操作的事件方法,页面移除时也一定要在window上移除
componentWillUnmount() {
window.removeEventListener('scroll', this.props.函数名);
}
PureComponent
对于多个组件的connect和store进行连接,一旦store内部修改了,那么每个组件都会重新渲染,也就是每个组件的render都会重新执行,解决这个问题可以使用之前的shouldComponentUpdate,react内部有PureComponent解决这个问题,PureComponent底层自己有shouldComponentUpdate。只要把之前的引入包括使用Component的地方都改为PureComponent,项目中一般使用PureComponent来优化。
import React, { PureComponent } from 'react';
因为使用了immutable.js这个框架,PureComponent和immutable.js能完美结合,但是如果不使用immutable.js就使用PureComponent的话,那么会遇到一些底层问题的坑,所以如果不使用immutable.js,最好自己写一个shouldCompomentUpdate来进行代码优化,不要使用PureComponent
export default Class Parent extends PureComponent {
state = {carName: '奔驰'}
changeCar = () => {
//this.setState({carName: '迈巴赫'}) //这里的{carName: '迈巴赫'}和上面的{carName: '奔驰'}是两个对象
const obj = this.state
obj.carName = '迈巴赫'
setState(obj) //这里的obj和上面的{carName: '奔驰'}是一个对象,只是引用不同,PureComponent底层做了一个浅对比,不去看对象里面的东西变没变,只去分析这个obj是不是原来的对象,如果是的话组件就不会更新了,这也是数组不应该用push、unshift等方法来改变获得新数组的原因。
}
}
Link----react-router-dom,路由链接
单页面应用,只加载一次html文件,优点是当从首页里点击到详情页的时候也不会出现页面加载卡顿的情况 单页面应用就不能用<a href=‘/detail’>…</a>,这种,每次跳转到详情页就会重新发送请求 要使用Link标签,其实该标签在页面上展示的话会转化为a标签,to属性会变为href属性
import { Link } from 'react-router-dom';
<Link to='/detail'>...</Link>
注意:使用Link标签的组件必须放在总的App.js文件的<BrowserRouter></BrowserRouter>内部,<BrowserRouter>相当于一个路由器,一个页面只能使用一个路由器,使用多个的话没办法沟通,所以直接把整个<App />组件包裹起来
index.js
import {BrowserRouter} from 'react-router-dom'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
使用bootstrap.css,如果点击Link就改变样式,相当于点击后增加一个active类,此时就不能用Link了,要用NavLink标签
import { NavLink } from 'react-router-dom';
<NavLink activeClassName="active" className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/about">About</NavLink>
<NavLink activeClassName="lala" className="list-group-item" to="/about">About</NavLink>
MyNavLink.js一般标签
//如果有很多NavLink的话,出了to属性内容和标签体About内容不一样,其他都一样,太冗余了,可以封装一个自己的标签叫MyNavLink
//一个好的封装要和想要封装的标签的样式一致,比如NavLink标签是含有标签体的,那么咱们封装的标签页也最好不是自闭合标签,也是要有标签体的
//标签体也是属于标签属性的,props属性是children,值为标签体,就是this.props.children就可以拿到标签体
import React, {Component} from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
const {children} = this.props
return (
<NavLink activeClassName="lala" className="list-group-item" {...this.props} />
//就不用像下面这样写标签体了,{...this.props}已经包含to="/about" a={1} b={2}这些属性以及children属性了
//<NavLink activeClassName="lala" className="list-group-item" {...this.props}>{this.props.children}</NavLink>
)
}
}
App.jsx
<MyNavLik to="/about" a={1} b={2}>About</MyNavLik>
<MyNavLik to="/home">Home</MyNavLik>
向路由组件传递params参数
//1路由链接(携带参数),和Nodejs的express的params参数一样
<Link to={`/home/messages/${mesgObj.id}/${mesgObj.title}`}>Messages</Link>
//2注册路由(声明接收)
<Route path="/home/news/:id/:title" component={News} /> //News组件内部this.props就能接收传递过来的id和title
//在News组件中接收this.props.match.params.id,就能获取id值
附:数组的find方法
News组件的index.jsx
const DetailData = [
{id:'01', content: 'nihao'},
{id:'02', content: 'nio'},
{id:'03', content: 'niha'},
]
const {id, title} = this.props.match.params
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
})
向路由组件传递search参数
//1路由链接(携带参数),和nodejs的express的query参数一样
<Link to={`/home/messages/?id=${mesgObj.id}&title=${mesgObj.title}`}>Messages</Link>
//2注册路由(search参数无需声明接收,正常注册路由即可)
<Route path="/home/news" component={News} />
News组件的index.jsx
import qs from 'querystring'
let obj = {
name: 'tom',
age: 18
}
qs.stringify(obj)
let str = 'name=tom&age=18'
qs.parse(str)
const {search} = this.props.location
const {id, title} = qs.parse(search.slice(1))
向路由组件传递state参数(注意:是路由的state,不是组件内部的state)
//1路由链接(传递的参数不在地址栏中显示,Link标签的to属性的值不是字符串了,而是对象)
//即使地址栏是localhost:3000/home/news,没有参数,刷新的话,也不会丢掉参数,因为一直在用BrowserRouter,它一直在维护浏览器的API,history.xxx
<Link to={{pathname: '/home/news', state: {id: mesObj.id, title: mesObj.title}}}>Messages</Link>
//其实params和search的to这里也可以写成对象,就是把字符串作为pathname的值就可以了,不过完全没有必要变得那么麻烦,所以就以字符串的形式就可以
//2注册路由(state参数无需声明接收,正常注册路由即可)
<Route path="/home/news" component={News} />
News组件的index.jsx
const {id, title} = this.props.location.state || {}
路由有两种模式,默认的是push模式,就是每次页面的点击,都相当于在栈中进行记录(A=>B=>C),点击浏览器回退按钮,页面依次回退并把栈顶弹出(放在另一个栈中记录用于之后的前进一步);而replace模式,是你点击页面,页面跳转之后,把这条记录直接替换掉之前的栈顶记录(A=>B=>C,再点击D后,栈中为A=>B=>D,下一次单击回退时退回到B中),所以,假如项目中全是replace模式,那么浏览器的前进和后退图标就一直是灰色的
//开启replace模式
<Link replace to={`/home/messages/${mesgObj.id}/${mesgObj.title}`}>Messages</Link>
//其全写方式为下面这样,和严格匹配exact一样
<Link replace={true} to={`/home/messages/${mesgObj.id}/${mesgObj.title}`}>Messages</Link>
编程式路由导航
其他标签也可以像Link标签一样进行路由跳转
用原生属性,例如this.props.history.push(path, state) this.props.history.replace(path, state) 回退this.props.history.goBack() 前进this.props.history.goForward() this.props.history.go(n) n为正整数,就前进n步,n为负整数,就后退n步
附:获取当前路由路径的方式有this.props.location.pathname(比如"/about")this.props.match.path this.props.match.url
withRouter让一般组件也能用上路由组件的API
只有路由组件才有history location match,就是上面的这些
Header/index.jsx
import React, {Component} from 'react'
import {withRouter} from 'react-router-dom' //withRouter是一个函数
class Header extends Component {
back = () => {
this.props.history.goBack() //可以使用路由组件的history了
}
render() {
return (
<div>
<h2>Header</h2>
<button onClick={this.back}>回退</button>
</div>
)
}
}
export default withRouter(Header) //withRouter能够接收一般组件,然后就在一般组件的身上加上路由组件所特有的history location match,withRouter的返回值是一个新组件
给后端传递详情页码的参数
第一种方式:
动态路由
根据路由参数的不同获取不同页面的信息
List.js组件
...
<Link to={'/detail/' + item.get('id')}></ Link>
App.js
<Route path='/detail/:id' exact component={Detail} />
在跳转到的页面的Detail组件的index.js中可以通过this.props.match.params.id获取这个id值
index.js–再异步获取数据
componentDidMount({
this.props.getDetail(this.props.match.params.id);
}
const mapDispatch = (dispatch) => ({
getDetail(id) {
dispatch(actionCreators.getDetail(id))
}
})
actionCreators.js–Detail组件的
axios.get('/api/detail.json?id=' + id).then()
第二种实现方式(不推荐)
需要手动解析参数值
List.js组件
...
<Link to={'/detail?id=' + item.get('id')}></ Link>
App.js
<Route path='/detail' exact component={Detail} />
在跳转到的页面的Detail组件的index.js中可以通过this.props.location.search获取到?id=2,需要手动提取这个参数2,之后给后端传递参数就和第一种方式一样了
innerRef
使用ref获取数据时如果为styledComponent时是没有办法拿到内容的,将ref改为innerRef就可以获取标签了
在登录页面需要把账号和密码给后台
index.js–Login组件
<Input placehoder='账号' innerRef={(input) => {this.account = input}} />
<Input placehoder='密码' type='password' innerRef={(input) => {this.password = input}} />
<Button onClick={() => this.props.login(this.account, this.password)}>登录</Button>
const mapDispatch = (dispatch) => ({
login(accountElem, passwordElem) {
dispatch(actionCreators.login(accountElem.value, passwordElem.value))
}
})
actionCreators.js
axios.get('api/login.json?account=' + account + '&password=' + password).then()
重定向–react-router-dom的Redirect
登录退出用loginStatus这个布尔值进行设置,最开始登录页面,loginStatus为false,最上面显示登录字样,点击登录字样,跳转到登录页面,输入用户名密码,点击登录按钮,loginStatus变为true,跳转到首页,上面变为退出字样,点击退出,loginStatus为false,上面变为登录字样,点击登录字样,跳转到登录页面
header/index.js
loginStatus ? 退出标签(点击修改loginStatus这个布尔值) : 登录标签(点击跳转到登录页面的Link标签)
login/index.js
import Redirect from 'react-router-dom';
const { loginStatus } = this.props;
if(!loginStatus) {
}
else{
return <Redirect to='/' />
}
异步组件
react-loadable
访问首页时只加载首页的代码,访问其他页面时只加载该页面代码,好处是加载速度快,使用异步组件实现
yarn add react-loadable
detail文件夹下创建loadable.js文件,下面怎么引入看组件的语法
import React from 'react';
import Loadable from 'react-loadable';
const LoadableComponemt = Loadable({
loader: () => import(./)
loading() {
return <div>正在加载</div>
}
});
export default () => <LoadCompoment />
在最外层的App.js中引入
import Deatil from './pages/detail'
import Deatil from './pages/detail/loadable.js'
通过上面的操作就把detail组件变成了异步组件
但是这样detail下的index.js就没办法直接获取路由的内容了,this.props.match.params.id就会报错,因为App.js中引入的loadable.js不是Route路由标签直接对应的组件,可以这样解决
react-router-dom的withRouter
detail/index.js中引入
import { withRouter } from 'react-router-dom';
export default connect(mapState, mapDispatch)(withRouter(Detail));
附:Chrome浏览器的FeHelper插件自动帮我们整理json格式的文件
可以用XAMPP进行后端的数据模拟
npm run build 生成的build文件夹里的文件就是前端项目上线需要的文件,把这些文件复制一份,放在后端服务器生成的api文件夹同级,api内放的是前端代码需要异步获取的各种.json数据文件,此时运行后端接口localhost:8080简写为localhost就可以在浏览器展示页面效果了
第三方的库serve
把你快速搭建一条服务器
npm i serve -g
//你想以哪个文件夹作为根路径,例如使用上面打包后的build文件夹,serve build
serve 文件夹
Hooks
1.React Hook/Hooks是什么?
Hook是React 16.8.0版本增加的新特性/新语法,可以让你在函数组件中使用state以及其他的React特性
2.三个常用的Hook
State Hook: React.useState() Effect Hook: React.useEffect() Ref Hook: React.useRef()
注意:函数组件中this是undefined
State Hook: React.useState() 函数组件中可以使用状态并且可以更新状态
import React from 'react'
function Demo() { //Demo组件就如Class形式的render,会调用n+1次,初次渲染调用一次,以后状态每次改变都渲染一次
//const a = React.useState() //React.useState()是个数组,数组中只包含两个元素,第一个就是状态,第二个是用于更新状态的方法
//React.useState(0)状态中count的初始值是0
const [count, setCount] = React.useState(0) //数组的解构赋值
//React底层进行了处理,第一次调用这句的时候,就把count存了下来,等以后Demo重新渲染的时候,这句也执行了,但不会因为再次执行把上一次渲染得到的count值覆盖掉
const [name, setName] = React.useState('tom')
//加的回调
function add() {
//第一种写法,setXxx(newValue)
setCount(count + 1) //count + 1是新返回的newState的值
//第二种写法,里面是一个函数,setXxx(value => newVlaue)
//setCount(count => count + 1)
}
function changeName() {
setName('Li')
}
return (
<div>
<h2>当前求和为:{count}</h2>
<h2>我的名字是:{name}</h2>
<button onClick={add}>点我+1</button>
<button onClick={changeName}>点我改名</button>
</div>
)
}
export default Demo
Effect Hook: React.useEffect() 可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子),React中的副作用操作:发ajax请求数据获取;设置订阅/启动定时器;手动更改真实DOM
import React from 'react'
import ReactDOM from 'react-dom'
function Demo() {
const [count, setCount] = React.useState(0)
//当参数只是一个函数,在组件挂载和更新的时候都执行,自此可以执行任何带副作用操作。当状态改变的时候,React底层就会帮你调用这个函数,监测所有的状态的改变。相当于componentDidMount和componentDidUpdate。
//React.useEffect(() => {
// console.log('@@@')
//})
//没传第二个参数相当于监测所有状态,第二个参数是空数组[],那就是谁也不监测,只在组件挂载的时候执行一次第一个参数那个函数,相当于componentDidMount
//React.useEffect(() => {
// setInterVal(() => {
// setCount(count + 1)
// }, 1000)
//}, [])
React.useEffect(() => {
let timer = setInterVal(() => {
setCount(count + 1)
}, 1000)
return () => { //React.useEffect第一个参数返回时的函数相当于componentWillUnmount,可以在这里清除定时器/取消订阅等
clearInterval(timer)
}
}, [])
//数组中有状态,如这里,相当于只监测count。相当于conponentDidMount和监测count值。监测多个值[count, name]
//React.useEffect(() => {
// console.log('@@@')
//}, [count])
function add() {
setCount(count + 1)
}
//卸载组件的回调,组件卸载了必须把定时器也关掉,否则会报错
function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmount}>卸载组件</button>
</div>
)
}
export default Demo
Ref Hook: React.useRef() 可以在函数组件中存储/查找组件内的标签或者其他任意数据
import React from 'react'
function Demo() {
//类组件中使用ref的对象的方式用的是React.createRef(),这里其他语法一样,也是“专人专用”
const myRef = React.useRef()
//提示输入的回调
show() {
alert(myRef.current.value) //和类组件一样要用.current获取数据
}
return (
<div>
<input type="text" ref="myRef" /> {/*这里用ref对象的方式写的*/}
<button onClick={show}>点我提示数据</button>
</div>
)
}
export default Demo
useMemo
useMemo是针对一个函数,是否多次执行 useMemo主要用来解决使用React hooks产生的无用渲染的性能问题 在方法函数,由于不能使用shouldComponentUpdate处理性能问题,react hooks新增了useMemo
useMemo使用 如果useMemo(fn, arr)第二个参数匹配,并且其值发生改变,才会多次执行执行,否则只执行一次,如果为空数组[],fn只执行一次
举例说明:
第一次进来时,控制台显示rich child,当无限点击按钮时,控制台不会打印rich child。但是当改变props.name为props.isChild时,每点击一次按钮,控制台就会打印一次rich child
export default () => {
let [isChild, setChild] = useState(false);
return (
<div>
<Child isChild={isChild} name="child" />
<button onClick={() => setChild(!isChild)}>改变Child</button>
</div>
);
}
let Child = (props) => {
let getRichChild = () => {
console.log('rich child');
return 'rich child';
}
let richChild = useMemo(() => {
//执行相应的函数
return getRichChild();
}, [props.name]);
return (
<div>
isChild: {props.isChild ? 'true' : 'false'}<br />
{richChild}
</div>
);
}
renderProps
组件如果不写成自闭合标签,那么标签体的内容要通过this.props.children拿到,相当于组件标签的children属性
//在A的父组件中
<A>hello</A>
//在A组件中。这种叫做childrenProps,通过组件标签传入结构,但如果子组件想要获取父组件里的数据就没办法做到,要用到下面的,叫renderProps。他俩都是React中向组件内部动态传入带内容的结构/标签。Vue中使用slot插槽技术,也就是通过组件标签体传入结构<A><B/><A/>
{this.props.children} //hello
//有两种方式成为父子组件
//一种就是上面的那种
//在A的父组件中
<A>
<B />
</A>
//在A组件中通过{this.props.children}展示B组件的内容
//第二种就是普通的情况,在A的父组件中使用<A />,在A组件中使用<B />
//使用第一种方式,A组件如果想要给B组件传递数据。这种叫做renderProps,通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性
import React, { Component } from 'react'
export default class Parent extends Component {
render() {
return (
<div>
<h3>我是Parent组件</h3>
{/*这种方式如果没有下面这三行,单看A和B组件看不出来其关系。往下看
<A>
<B />
</A>
*/}
{/*给A标签添加一个render属性,属性值为一个函数,函数返回值为B组件,就可以给B组件传值了。传什么值在A组件中设置。叫renderProps的原因是这个render不是组件中的render,而是标签属性里的render,是给A的props里添加属性render,这个render名字不是必须交render,叫其他也可以,但一般都叫render,职场习惯,*/}
<A render={(name) => <B name={name}/>}/>
</div>
)
}
}
class A extends Component {
state = {name: 'tom'}
render() {
//解构赋值
const {name} = this.state
return (
<div>
<h3>我是A组件</h3>
{/*这里传递给B值。这种方式优点在于,这里预留一个位置,这个位置放哪个组件先不确定,通过上面A标签中返回的是哪个组件,这里就展示哪个组件,这就是Vue中的插槽技术,<A><B/><A/>*/}
{this.props.render(name)}
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
{/*B标签就收到了A组件传递的name值*/}
<h3>我是B组件,{this.props.name}</h3>
</div>
)
}
}
错误边界ErrorBoundary
不要让一个子组件的错误(例如子组件返回的服务器端的数据不是想要的格式)导致整个页面都出不来,边界的意思就是把错误控制在一定范围之内,用错误边界,需要在容易发生错误的组件的父组件内部设置
注意:只能捕获后代组件的生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成时间、定时器中产生的错误;只适用于生产环境,不适用于开发环境
Parent.jsx
import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
state = {
hasError: '' //用于表示子组件是否产生错误
}
//如果Parent的子组件出现了任何的报错,都会执行这个钩子,并携带错误信息
static getDerivedStateFromError(error) {
return {hasError: error}
}
//加上这个更加详细
componentDidCatch() { //组件在渲染的整个过程中,由于子组件出现问题引发错误,就会调用这个钩子
console.log('统计错误次数,反馈给服务器,用于通知编码人员进行bug的解决')
}
render() {
return(
<div>
<h2>我是Parent组件</h2>
{this.state.hasError ? <h2>当前网络不稳定,稍后再试</h2> : <Child />}
</div>
)
}
}
组件通讯方式总结
通讯方式:
1.props
children props和render props
2.消息订阅-发布
有很多库,例如pubs-sub(用于js)、event(主要在C#中应用)等
3.集中式管理
redux、dva等
4.conTexts生产者-消费者模式
组件间关系及与通讯方式比较好的搭配:
父子组件:props
兄弟组件(非嵌套组件):消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)
|