概述
一、React文件的创建(不使用脚手架进行文件的创建过程)
1.1、这里的React文件是一个html文件,或者是一个js文件,需要安转相关依赖
- React:是React的核心包
- React-dom:是React操作dom的扩展包
- babel:是将React文件中的jsx代码转换成js代码
- jsx(JavaScript XML),是js的一种扩展,其中可以直接书写html语句,和js脚本
- 安转相关依赖:npm install react react-dom babel --save
1.2、什么是JSX
- 全称:JavaScript XML,是js的一种扩展语法
- jsx的语法规则
- 定义虚拟DOM时不要加引号,否则会被认为定义的是一个字符串
- 标签中混入JS表达式时需要使用{}
<script> let idName = "root" let html = <div id={idName}></div> {/*将js中的变量写入jsx标签中时需要将其包裹在一个大括号内*/} </script>
- 在jsx标签中使用class时不能直接使用这个属性,因为在ES6中class是类的意思,在这里需要使用className属性代替class
- 如果要写行内样式时,需要使用{{}}的形式,这里的样式是以键值对的形式传入的,所以在传递时用的是对象,注意这里的样式如果是复杂形式的需要以小驼峰的形式写入,其中的属性值也要以字符串形式书写,比如:
style = {{fontSizer = "20px"}}
- 在jsx中有且只能有一个根标签
- 标签必须闭合,单标签必须在后面加斜线,双标签必须闭合
- 标签首字母规则
(1) 如果标签首字母是小写,则会在解析时将其解析成html标签,并且比对是否存在改标签,不存在就会报错
(2) 如果标签首字母是大写,则会认为这个标签是一个React中组件去进行渲染,如果没有这个组件就会报错
1.3、如何将jsx代码显示在视图呢?
- 这里使用的是ReactDom中的render方法
- 使用:ReactDom.render(jsx代码,挂载点使用原生操作获取元素)
- jsx代码:
<div><h1>这是显示在页面上的文字,React第一个页面</h1></div>
- 这里还可以写成React.createElement(标签名,标签属性,标签内容)
- 挂载点:document.getElementById(“ReactDome”)
<body>
<script src="这里是直接将相关的包进行引入.js,注意路径"></script>
<div id="reactDemo"> </div>
//挂载点,简单说就是一个容器
<script type="text/babel">
{/*
1.这里一定要写type="text/bable"
2.如果不写下方声明的额Com就会报错,因为它默认解释的是js而不是jsx
*/}
let Com = <h1> hello react </h1>
{/*
1.创建虚拟DOM,这里写的标签必须*闭合*否则会报错
2.最后会将这行代码通过babel解析成下面的那种形式
3.这里的Com虚拟DOM是什么呢?
a) 本质:它只是一个普通的Object对象,不是什么高大上的东西
b) 虚拟DOM就是一个介于真实DOM和js之间的一个缓存,之后会通过diff算法将它渲染成真实的DOM,最终会被React转换为真实的DOM渲染在页面之上
c) 与真实DOM的区别:虚拟DOM身上的方法属性很少,如果是在控制台输出真实DOM会是以html标签的形式进行一个打印输出
d) 简单一句话就是:虚拟DOM "轻",真实DOM "重",原因就是虚拟DOM是React内部使用,没有必要有那么多的属性和方法
*/}
let Com = createElement("h1",{ id : "title" },"你好 react")
{/*
1.这种方式在开发的时候是一定不会使用的,只需要知道就可以
2.这里的第三个参数如果还想写一个标签会在写一个createElement会很麻烦
*/}
ReactDom.render(<Com/>,doucment.getElementById("reactDemo"))
{/*将所创建的虚拟DOM通过render渲染到页面中*/}
</script>
</body>
// 注意:这里创建的组件或者是虚拟DOM元素只能有且只有一个根元素,如果在jsx中进行换行操作薛跃将jsx代码包裹在一个()中书写
1.4、JSX中动态渲染数据
- 首先要明确的是动态渲染数据,那么这个值是不能写死的,
- 在jsx中书写js表达式,那么什么是js表达式呢?js语句又是什么呢?在jsx中的{}中可以写那种呢?
- js的表达式:表达式有以下几种形式,简单来说可以被变量接收到值的都是表达式
(1) a
(2) a + b
(3) add(1)
(4) arr.map()
(5) function dome() {}
这些都是表达式,都可以赋值给一个变量并拿到值 - js的语句:简单来说就是js中的逻辑语句
(1) if(){}
(2) for(){}
(3) switch(){}
- js的表达式:表达式有以下几种形式,简单来说可以被变量接收到值的都是表达式
- 在{}中直接书写一个数组的话
- React会直接进行一个循环遍历其中的每一项数据
- 这里会将数组元素一行遍历出来,这样一定不会是我们想要的得到的样式
- 使用
Array.map((item,index)=>{})
进行处理数组并且返回一个新的数组,不过这里需要注意如果每一项循环出来的是一个带有标签的元素,那么一定要在这个标签之中添加一个key属性,用来对标签进行唯一标识
- 在{}中写的是一个对象的话,不会变量到页面之中
二、React中的重要定义
2.1、什么是模块
- 简单理解就是,一个向外提供特定功能的一个js文件
- 使用模块化的原因:使代码进行一个复用,减少代码的复杂度
- 简化代码,提高js的运行效率
- 这里拆分的是JS
2.2、什么是组件
- 就是将实现除一个区域内的所有代码拆分成一个独立的文件,在使用时改变其中呢内容即可
- 实现一个界面的重复使用
- 简化项目的编码,增加编码的复用,提高运行的效率
- 这里拆分的是一个界面所有与之相关的html、css、js、img、font、音频视频等
2.3、什么是模块化
- 简单来说就是使用模块来开发的项目就是模块化
2.4、什么是组件化
- 简单来说就是使用组件来开发的项目就是组件化,也就是说一个复杂页面是有多个简单的组件拼接成的
2.5、组件
2.5.1、函数式组件(无状态组件)
- 函数式组件首先它是一个函数
function demo(){ }
- 组件:是将所有相关的html、css、js、等文件进行拆分,所以这里需要有返回值,返回值是jsx代码
function demo(){ //声明一个函数组件 return <h1>这是一个函数式组件</h1> } //将这个函数组件渲染到页面之中 //错误写法: ReactDom.render(demo,document.getElementById("root")) //但是这样写会报错,这里render方法的第一个参数需要是一个标签,同时这个标签如果是一个组件的话需要首字母大写,否则它将会同html中的标签进行一个比对 ReactDom.render(<Demo/>,document.getElementById("root")) //这里要注意的是既然这个标签首字母已经大写了,那么对应的函数也需要大写,并且一定要闭合标签
- 在React中在执行
ReactDom.render(<Demo/>,document.getElementById("root"))
时都做了什么呢?- React解析组件标签,并找到对应的Demo组件
- 发现组件是一个函数是组件,紧接着会去执行这个函数,并将返回的虚拟DOM对象转换为真实的DOM对象,随后呈现在页面之中
- 关于函数式组件中的this问题
- 这里的this执行的是window,但是返回的是undefined
- 原因:在进行编译的时候开启了严格模式,在严格模式中规定如果this指向的是window则抛空
- 所以在函数组件中一般不会使用this
- 函数式组件函数名就是标签名
2.5.2、类式组件(有状态组件)
- 回忆什么是类
- 在React中使用的是ES6中的class
- 声明方式:class 类名(Students,这里要注意类名开头大写) {}
- 构造方法constructor(接收实例传递的参数…){this.参数 = 参数},这个构造方法不是必要的
- 对象的实例化使用new关键字
let stu = new Students(传递的参数...)
- 这里的this指向的是实例对象
- 方法:类中的方法都是一般方法,每一个通过这个类实例出来的实例对象都可以调用这个实例方法,其中的this指向的是这个实例对象,这里的方法是通过原型链查找找到的并不在实例对象之中
- 类的继承
- extends关键字实现
- 子类可以直接使用父类的构造器方法、构造方法、属性
- 如果在创建的时候传递的参数,父类无法接受,那么这个时候就需要重写constructor,注意在重写constructor时一定先调用super()
- 如果在子类之中写一个同父类方法名一样的方法,那么在实例调用时调用的是子类的方法,而不是父类的方法,如果想要访问父类的方法就需要使用super.方法名来进行访问
- 回忆总结:
- 类中的构造器是可写可不写的,根据实际需求决定,构造器是对实例进行一些初始化的操作,比如添加指定属性
- 如果A类继承了B类,切A类中写了构造器,那么必须在A类中的构造器中调用super方法
- 类中所定义的方法,都是放在原型对象上的,共实例的使用,如果一个A类继承了B类,调用一个A类中没有的方法,它会按照原型链去查找这个方法,最先找到那个方法就使用那个,不会在进行查找下去
// 创建一个Fa类
class Fa {
// 类的构造器
constructor( name , age ){
this.name = name
this.age = age
}
// 类中的构造方法
say(){
console.log(`我叫${this.name},我今年${this.age}岁了`)
}
}
// 类的实例
let f = new Fa("tom",18)
class Son {
constructor(name,age,id){
// 重写构造器,并且添加一个id
super(name,age)
this.id = id
}
say(){
console.log(`我叫${this.name},我今年${this.age}岁了,我的学号是${this.id}`)
}
}
let s = new Son("jack",12,123456)
// 这里可以直接调用s.say(),它会去Fa的原型方法中调用say这个方法,当时如果在Son中也写了这个方法那么会调用Son原型上的say方法
- 类式组件(有状态组件)
- 创建类式组件:class Com extends React.Component { }
- 使用类式组件必须遵循几个规定:
- 必须继承react中的Component
- 必须写一个render方法,这里的render方法是放在原型对象上的,共实例使用,而在类式组件之中的实例在哪呢???
- render必须有返回值,返回的是JSX代码
- 在React中在执行
ReactDom.render(<Demo/>,document.getElementById("root"))
时都做了什么呢?- React解析组件标签,并找到对应的Demo组件
- 发现组件是一个类是组件,紧接着会去执行这个函数,随后会对这个类进行实例,并通过该实例代用原型上的render方法
- 将render返回值的虚拟DOM转换为真实的DOM,呈现在页面内之中
- 在类组件中需要关注的:1.props 2.state 3.refs 4.context,者四个属性都是继承React.Component得到的
class Com extends React.Component { //声明一个类组件,并且这个类要继承React.Component render(){ //写一个render方法(必须是这个名字,必须有这个方法) return (<div>这是一个类组件</div>) //返回jsx代码 } } ReactDom.render(<Com/>,document.getElementById("root")) //这里的render和上面类组件中的render方法是截然不同的,一定不要混淆,而这里的<Com/>称为组件实例对象
2.5.3 组件实例的三大属性:状态(state)
- 初始化state,在constructor中给它进行初始化,而在之后的使用中要给父类传递一个props,所以需要在super中传递一个props
//借助构造器初始化state状态,并取出其中的状态 //实例1:需求时点击进行页面内容的修改,这里使用的是类式组件 class Mood extends React.Component{ constructor(props){ {/*接收props*/} super(props) {/*将props传递给父类*/} this.state = { isGood: true } {/*初始化state值*/} } render(){ return <div>今天心情{this.state.isGood ?"很好" : "很糟糕"}</div> {/* 1.从state中取值:this.state.属性名 2.这个案例由于是进行内容的切换,所以可以使用三元运算符来进行操作 */} } }
- 类组件中绑定事件
- 在jsx代码中绑定函数使用的是(以点击事件为例)onClick = {}
- 在React中绑定事件都是将原生js中的事件重写定义,都是以小驼峰的像是呈现
- 而要触发事件时调用的是js中的函数所以要写在{}之中,这里要注意如果直接在{}中书写方法名()会在页面渲染是直接触发这个函数,并把这个返回值给到onClick,当我们在进行点击时这个方法是不会进行的,原因是这个方法的返回值是一个undefined,React会忽略掉这个
//案例2:在React中实现点击事件(这里只是能够触发函数,并不是之后React中函数的使用) function hint() { console.log("标题被点击了") } class Com extends React.Component{ render(){ return <div onClick = {hint}>点击标题触发事件</div> {/* 注意这里的事件以小驼峰绑定的 不要写方法名()直接调用 这里只是指定了当点击这个元素是要触发的函数是什么 */} } } {/*那么在这段代码中方法是无法调用类之中的任何属性的*/}
- 类组件中的方法
//案例3:在类组件中调用方法的注意点
class Com extends React.Component{
constructor(props){
super(props)
this.state = {
isShow : true
}
}
{/* 方法 */}
hint() {
console.log(this.state.isShow)
}
render(){
{/* 调用方法1 */}
return <div onClick = {hint}>点击标题触发事件</div>
{/*
可能会出现的错误及原因
1. 如果直接使用调用方法1进行触发函数会报错
原因:
(1) 首先要明确hint方法被放在哪了
: hint被放在了Com的实例对象上了
(2) 其次要知道如何在类中调用同一类中的方法
: 调用同一个类中的方法是this.hint
*/}
{/* 调用方法2 */}
return <div onClick = {this.hint}>点击标题触发事件</div>
{/*
可能会出现的错误及原因
1. 如果直接使用调用方法2进行触发函数会报错
原因:
(1) 首先要知道这里的this的指向
(2) 可能会有这样的困惑:为什么在render中的this指向的就是实例对象呢
解释:这里的this指向是根据怎么调用这个方法来决定的
render是有React进行实例化,并且是实例调用得render
(3) 通过实例对象调用方法时this指向的是当前的实例,如果是通过事件触发的方法,此时的this指向就会是一个undefined
(4) 这里是这样的,在类中的方法中开启里严格模式,所以在事件调用时,是直接调用的此时的this指向的是window,严格模式就会将其抛空
*/}
}
}
-
解决以上所有的错误&总结
(1) 写方法时要在函数组件内部书写
(2) 调用时需要使用this.函数名
(3) 这里要进行修改this的指向
a) 在点击事件绑定时使用bind(this)改变this的指向,这里还有一个好处就是可以传递参数
<div onClick = {this.hint.bind(this,参数)}>点击事件</div>
b) 在函数声明的时候使用箭头函数声明,这里的this是根据上下文来决定的let hint = () => { console.log(this) 这里的this是指向实例对象的 return xxx }
c) 在构造器之中使用bind方法改变this指向
constructor(props){ super(props) this.hint = this.hint.bind(this) }
-
setState的使用方法(修改state中的值,并重新调用render方法进行视图更新)
- 直接修改state中的值是可以修改的,但是不能直接修改一定要使用setState进行修改,在React开发中一定不能直接赋值
- setState直译过来就是设置状态,这个API在React.Component之中
- setState是进行的合并,而不是替换原来的state
class Stu extends React.Component { constructor(props){ super(props) this.state = { isShow : true text : "文本" } this.change = this.change.bind(this) } change(){ {/*this.isShow = !this.isShow*/ {/*这样确实可以改变state中的isShow的值,但是React不认可,也一定要注意不能这么用*/} this.setState({ isShow = !this.isShow }) {/*只能通过setState方法改变state之中的值,这是唯一途径*/} {/*那么这里设置的state是与原先的state进行一个比较,然后进行合并,在这里也就是将isShow取反其他数据不变*/} } render(){ return <div onClick = {this.change}>点击切换文字{this.state.isShow?"hello":"world"}</div> {/*这里使用的是bind改变this指向,进而使得在change函数之中的this指向实例对象*/} } }
- 调用setState时constructor构造器只会调用一次
- 调用setState时render会调用1+n次,这里的1是首次加载时调用的一次,这里的n是状态更新的次数(简单说就是调用setState的次数)
- 函数调用n次----只有再触发时才会调用这个函数,所以是n次
-
简写上面的代码
{/*这是之前的代码*/}
class Stu extends React.Component {
constructor(props){
super(props)
this.state = {
isShow: true
}
{/*初始换状态也可以进行一个简写*/}
this.hint = this.change.bind(this)
{/*在这里如果交互的方法(自定义方法)很多,那么我们就需要书写非常多的上一行代码*/}
}
hint(){
console.log("事件触发...")
}
render(){
return <div onClick = {this.hint}>点击事件</div>
{/*简写方式两种参考改变this指向的方案*/}
}
}
{/*以上是我们根据正常学习来写的代码,很正确但是很繁琐*/}
{/*这是简写的代码*/}
class Stu extends React.Component {
state = {
isShow : true
}
{/*这种赋值语句的形式,就是在类中追加一个state的一个属性,由于原先的实例中就有这个state属性所以在这里就直接取代了原本的state(官方的称呼:追加成员变量)*/}
hint() {
console.log("事件触发...")
}
hint = () =>{
console.log("事件触发...")
}
render(){
return <div onClick = {this.hint.bind(this)}>点击事件</div>
{/*除了这种写法还可以在声明是写成箭头函数的形式*/}
}
}
//这样下来我们的代码就简化了很多
- 总结state
- 是最重要的一个属性,它的值必须是一个对象
- state改变时就会调用render方法,进而实现页面的重现渲染
- 注意:
- render中this指向的是实例对象,因为render是实例对象调用的
- 自定义方法中的this指向的是undefined,解决
- 声明时使用箭头函数进行声明(赋值语句的箭头函数)
- 在绑定事件时使用bind给变this指向
- 在constructor中使用bind改变this指向(一般只是用前两种)
- 状态数据不能直接赋值修改其中的值,只能通过setState进行修改state中的数据
2.5.4 组件实例的三大属性:(props)
-
基本了解
- props是接收外界传值的一个集合
- 这里的接收外界传值就是将自定义组件上的属性和属性值以key:value的形式存储到props中
- 批量传递数据
{...obj}
,这里就是将obj中的所有属性方法全部传递到组件中- 展开运算
...
- 数组
let arr = [1,3,4,5];console.log(...arr)
这是将arr的每一项进行遍历 - 也可以合并两个数组
- 函数传参
- 在普通情况下展开运算符不能直接展开对象,一般使用{…obj}这样来拷贝一个对象,这种形式称之为字面量的形式复制一个对象,但是在react中通过插件就可以直接使用…obj,只用于标签属性的传递,其他地方不可以使用
- 使用{…obj}复制对象并且修改属性值,{…obj,name:“jack”}
- 数组
- 展开运算
-
限定props中的数据类型
- 引入一个新的包
props-type
,用来限制props的数据类型 - 使用方法入下
<script src="props-type的路径"></script> //这里就会多一个PropsType对象 <script> class Stu extends React.Component{ render(){ return <div>{this.props.name}{this.props.age}</div> } } Stu.propType = { name: PropType.string, //这是限制传递进来的props的数据类型,PropsType.数据类型 age: PropType.number.isRequired //继续在后面添加isRequired这是必填的限制 //不满足以上要求会弹出一个警告 } ReactDom.render(<Stu name="jack" age={18}/>,document.getElementById("root")) </script>
- 如果这里限制的是一个函数不能直接写成
PropType.function
,因为这里的function是关键字,应该写成PropType.func
这样就可以限制传入的是一个函数了
- 引入一个新的包
-
props中的默认值
- 对话者还是这个类组件
- 只用方式
class Stu extends React.Component{ render(){ return <div>{this.props.name}{this.props.age}</div> } } Stu.defaultProps = { sex : "男" //这里要注意传递的默认值要跟上面的限定要匹配 } //这里设置的是组件的sex属性在不传递的情况下,默认是男 ReactDom.render(<Stu name="jack" age={18}/>,document.getElementById("root"))
-
props重要特性只读,不能修改
-
props的简写方式
- 给props进行数据类型限制和默认值就是在类上添加两个属性
- 实现:在类中直接书写的话是添加给实例对象的,所以这里使用的是static(添加静态方法)
- 代码:使用静态方法就可以直接给类上添加这些属性
class Stu extends React.Component { static propType = { } static defaultProps = { } render(){ return <div></div> } }
-
类组件中的构造器和props(了解)
-
类中的构造器的作用
- 通过this.state初始化状态的
- 为事件处理绑定实例,通过bind(this)绑定实例对象
-
写了构造器但是不传递props和传递props有啥区别
- 没啥区别,不过在不传递props输出时会产生bug
constructor(){ super() console.log(this.props) {/*输出的是undefined,如果正常传递的话输出没有问题*/} }
-
类中的构造器完全可以省略,当写构造器的时候就要传递props
-
构造器传不传递props取决于:在构造器中是否使用this实例去访问props
-
-
函数式组件中使用props
- 函数组件中是没有this的,那么怎么来使用呢
- 函数是可以接收参数的,在函数中接收一个props传入函数内部,即可使用props
- 函数式组件只有props,没有state和refs
- 函数组件在不使用Hooks(16.8之后)的情况先是不可以使用状态管理和生命周期的
- 这里的限制和默认值就只能放在组件的外部了,使用组件名.propType/defaultProps
function Stu(props){
// 接收函数传递进来的数据
return (
<div>{props.text}</div>
// 使用数据
)
}
ReactDom.render(<Stu text="hello world"/>,document.getElementById("root"))
- props总结
- 可以单个传递,也可以批量传递
<Com name="tom" {...obj}></Com>
- props可以进行一个限制:使用的是一个插件
prop-type
,用法:组件名.propType = {属性名:PropType.限制}
- props可以设置一个默认值:使用的是组件名.defaultProps = {属性名:默认值},这里一定要注意如果之前限制了属性数据类型,一定要相匹配
- 函数式组件也可以使用props
- 函数式组件的props限制和默认值是写在函数外部的
- 类组件的props限制和默认值是可以内部外部都可以写的,但是推荐的是内部
- 组件标签的属性都会保存在props之中
- 每个组件都有props属性
- props是都外部获取的数据,只读不可修改
- 可以单个传递,也可以批量传递
2.5.4 组件实例的三大属性:(refs)和事件处理
- 字符串形式的ref如下:(这种写法可能会在之后的更新中废弃掉,不推荐使用)
- 之前想获取表单元素的value,是通过input的id,获取到这个元素节点,然后在获取到其中的value
- 而我们在React中获取一个元素只需要给该元素添加ref属性
- 这里的ref属性同原生JS中的id属性类似,在组件中用过this.refs就可以获取到这个元素节点
- string中的ref存在一个问题就是,如果在页面之中书写过多的string的ref效率会大大减低,不推荐使用
class Com extends React.Component {
showtText = () =>{
console.log(this.refs.input1.value)
// 这样就可以获取到输入框之中的值了
let val = document.getElementById("input").value
// 之前的原生写法首先通过选择器获取原生DOM对象,然后在使用其中的value
}
render(){
return (
<div>
<input type = "text" ref = "input1" id = "input"/>
<button onClick = {this.showtText}>点击获取输入框内容</button>
</div>
)
}
}
-
回调形式的ref
- 在组件内部书写一个ref这里我们给他赋值一个回调函数
- 在jsx中的{}直接写一个()=>{}箭头函数就是一个回调函数,这个回调函数是react调用的
- 这个回调函数的默认参数就是这个ref所在的DOM节点,并在这个回调函数之中一般只需要书写一行代码就可以了
class Com extends React.Component { hint=()=>{ console.log(this.input1.value) {/*这样就可以拿到该input中的值*/} } render(){ return ( <input type="text" ref={ currentNode=> this.input1 = currentNode}/> {/* 这里的this.input1是将他绑定给实例,然后把当前的这个input传递给这个this.input1 简单说就是把这个元素节点放到这个组件实例对象自身上 使用时直接this.input1即可操作该DOM对象 */} <button onClick={this.hint}>点击触发函数</button> ) } } ReactDom.render(<Com/>,document.getElement("root"))
- 回调形式的ref中的回调函数的调用次数
- 当回调函数写成内联的形式,他会触发两次,由于会重新代用render函数使用内联函数在执行一次render之后就会销毁内联函数
- 每次渲染时都会创建一个新的函数实例,所以React会对ref进行一个清空操作
- 第一次输出就会输出成销毁的内容为null
- 第二次输出才返回当前的元素对象
- 解决该情况的方案:把内联函数绑定在类组件之上就可以了
- 这个情况是无关紧要的,在开发过程中是完全可以写在内联函数中,官方说的
class Com extends React.Component { hint = () => { console.log(this.input1.value) {/*这样就可以拿到该input中的值*/} } saveInfo = (currentNode) =>{ this.input1 = currentNode {/*这样就可以解决render执行两次该函*/} } render(){ return ( {/*<input type="text" ref={ currentNode=> this.input1 = currentNode}/>*/} <input type="text" ref={this.saveInfo}/> <button onClick={this.hint}>点击触发函数</button> ) } } ReactDom.render(<Com/>,document.getElement("root"))
-
createRef API
- 使用这种形式来操作DOM元素的步骤
- 创建一个存储该DOM元素的一个容器
- 将一个属性追加到这个组件实例对象上,然后使用React中的createRef来创建储存改ref的容器
- 在使用时就直接绑定这个容器即可
- 在使用时就直接获取这个容器.current就可以拿到这个DOM了
- 注意的是这个容器是存储被ref标识的节点,这个容器只能存储一个节点,如果该容器被多次使用,将会以最后一次为准
- 这种方式是React最推荐的一种方式
class Com extends React.Component { myRef = React.createRef() // 这是一个函数返回的是一个存储ref的一个空间,只能存存储一个ref标识的节点 hint = () => { console.log(this.myRef.current.value) {/*这样就可以拿到该input中的值*/} } render(){ return ( <input type="text" ref={this.myRef}/> {/*如果需要存储多个DOM节点,那么就需要创建多个存储空间*/} <button onClick={this.hint}>点击触发函数</button> ) } } ReactDom.render(<Com/>,document.getElement("root"))
- 使用这种形式来操作DOM元素的步骤
-
总结Ref
- string形式的ref是最简单的一种,在条件允许的情况下尽量避免string形式的ref使用
- 优点:简单
- 缺点:使用多的情况下,效率低
- 能避免则避免,如果着急也可以使用的/xk
- 回调形式的ref,相对于string来说是比较麻烦的,不是自动收集元素的
- 开发中使用最多的还是使用内联的函数绑定
- 使用类绑定函数的情况跟内联的写法没有太多的差别,差别除了调用,其他的一样
- createRef是最麻烦的一种
- 需要创建容器然后在绑定ref标识的DOM
- 但也是React最推荐的一种形式
- string形式的ref是最简单的一种,在条件允许的情况下尽量避免string形式的ref使用
-
React中的事件处理
- 指定事件处理函数的时候一定要注意大小写
- 在React中的事件都是经过二次封装的合成函数,而不是使用的原生DOM事件,这是为了更好的兼容性
- 在React中的事件都是通过事件委托来处理的,委托给最外层的元素,这是为了更高效的执行函数
- 通过event.target可以得到发生事件的DOM元素
- 不要过度使用Ref,在某些请况下是可以避免的(事件绑定在触发事件的元素身上的情况下)
- 指定事件处理函数的时候一定要注意大小写
2.6、收集表单数据
2.6.1、受控组件
- 这里使用得是一个onChange事件,这个事件是随着值的改变而调用的事件
- 通过事件对象来获取表单元素中的值
event.target.value
- 在通过state状态绑定这个表单元素value,通过setState重新设置state状态,这样就会随着我们的输入表单元素中的值就会维护到state状态中
- 但是直接setState这样违背了它的原则,需要对state进行一个初始化
- 在取值的时候得在this.state中取出
- 总结一下:输入类的DOM元素,随着我们的输入就会维护到state状态中的,等需要用的时候在从中取出来,这就是受控组件这里就类似于Vue中的双向绑定
{/*
需求:当点击提交按钮时可以获取传入的用户名和密码,这里form标签中有一个onsubmit函数也是提交时触发的
*/}
class Com extends React.Component {
// state状态的创始化
state = {
username: "",
password: ""
}
// 表单提交的回调
handleSubmit = () =>{
const {username,password}=this.state
{/*
使用受控组件来进行获取元素value
代码如下:
*/}
e.preventDefault()
// 阻止表单提交页面跳转
console.log(`用户名是${username}密码是${password}`)
// 输出我们想要的结果
}
// 保存用户名到state状态之中
saveUsername = e =>{
// 绑定事件,默认参数就是这个事件对象
this.setState({
username: e.target.value
// 将输入的值维护到state装填之中
})
}
// 保存密码到state状态中
savePassword = e =>{
// 绑定事件,默认参数就是这个事件对象
this.setState({
password: e.target.value
// 将输入的值维护到state装填之中
})
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input type="text" name="username" onChange = {this.saveUsername}/>
{/*绑定一个onChange,随着我们的输入来维护state之中值,将这个组件变为受控组件*/}
<input type="password" name="password" onChange = {this.savePassword}/>
<button>提交</button>
{/*提交按钮,触发onSubmit事件*/}
</form>
)
}
}
2.6.2、非受控组件
- 页面中的表单元素的值,是现用现取(也就是说使用时间获取ref标识元素的value获取)就说明这个是一个非受控组件
{/*
需求:当点击提交按钮时可以获取传入的用户名和密码,这里form标签中有一个onsubmit函数也是提交时触发的
*/}
class Com extends React.Component {
// 表单提交的回调
handleSubmit = (e) =>{
{/*
那么这里怎么来实现输入框的内容获取呢?
首先我们想到的是使用ref来获取DOM元素然后获取其中的value
代码如下:
*/}
e.preventDefault()
// 阻止表单提交页面跳转
console.log(`用户名是${this.username.value}密码是${this.password.value}`)
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input type="text" name="username" ref={c => {this.username = c}}/>
<input type="password" name="password" ref={c => {this.password = c}}/>
<button>提交</button>
</form>
)
}
}
2.7、高阶函数 柯里化函数
2.7.1、高阶函数
- 概念:如果一个函数遵循下面两个规范之一,那么该函数就是做一个高阶函数
- 若函数A,接收的参数是一个函数,那么这个函数就是一个高阶函数
- 若函数A,调用的返回值依然是一个函数,那么该函数就是一个高阶函数
- 常见的高阶函数:Promise、setTimeout、arr.map()数组中的一些方法等等…
2.7.2、柯里化函数
- 概念:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理函数的编码形式
- 简单说就是函数返回的是一个函数继续接收后面括号的参数,并在最后返回所需结果的函数书写方式
- 例如:
- 函数声明:
let add = a => b => c => d => a + b + c + d
如果这个没有看懂没关系下面有综合案例分析 - 函数调用:
add(1)(2)(3)(4)
- 函数声明:
2.7.3、使用高阶函数优化表单案例
-
使用柯里化函数案例优化
class Com extends React.Component { // state状态的创始化 state = { username: "", password: "" } // 表单提交的回调 handleSubmit = () =>{ const { username, password }=this.state e.preventDefault() // 阻止表单提交页面跳转 console.log(`用户名是${username}密码是${password}`) // 输出我们想要的结果 } // 保存用户名到state状态之中 saveUsername = e =>{ // 绑定事件,默认参数就是这个事件对象 this.setState({ username: e.target.value // 将输入的值维护到state装填之中 }) } // 保存密码到state状态中 savePassword = e =>{ // 绑定事件,默认参数就是这个事件对象 this.setState({ password: e.target.value // 将输入的值维护到state装填之中 }) } saveDateType = (type) => { // 这个函数是一个高阶函数,并且也是一个柯里化函数 return event =>{ this.setState({ [type]:event.target.value // 这样的写法就完全可以代替上面的两个函数 }) } } {/* 分析一下这个案例 1. 我们观察可以看到saveUsername和savePassword是做的通一个事 2. 如果说我们在进行一个注册页面开发时,那就不仅有这两项了,还有手机号、邮箱、验证码等等 如果每一项都写一个函数来维护状态的话会很繁琐 3. 那么我们可以将我们想要修改的状态传入到函数当中 在onChange={this.saveDateType("type")} 这样我们又会面对一个问题,那就是我们这里虽然传值的但是这个函数也同样不能使用了 因为onChange接收的一定是一个函数,这里给到的是一个undefined 所以在之后的操作中不会在触发这个事件 4. 所有这里我们使用高阶函数的形式来解决这个问题 将onChange绑定的函数返回一个函数, 们的操作在返回的函数之中进行一个操作 5. 注意:我们在使用setState的时候是以键值对的形式传递进去的 其中的键(key)如果是直接写入的话那是一个字符串的形式 并不是我们想要的结果 如果想要一个变量放在key位置上需要对其包裹一个[] */} render(){ return ( <form onSubmit={this.handleSubmit}> {/*<input type="text" name="username" onChange = {this.saveUsername}/> <input type="password" name="password" onChange = {this.savePassword}/>*/} <input type="text" name="username" onChange = {this.saveDateType("username")} /> <input type="password" name="password" onChange = {this.saveDateType("password")} /> {/*既然上面做出了修改那么我们这里在调用函数的时候也需要进行一个修改*/} <button>提交</button> {/*提交按钮,触发onSubmit事件*/} </form> ) } }
-
不使用柯里化函数案例优化
class Com extends React.Component {
// state状态的创始化
state = {
username: "",
password: ""
}
saveDateType = (type,value) => {
this.setState({
[type]:value
})
}
这里来分析一波:
{/*
我们上面必须使用分柯里化函数吗?
不使用分柯里化函数怎么来实现
想明白上面的两个点我们就可以进行不使用柯里化函数实现要求了
这里吧原先的saveDateType函数做了一些修改,让我们同事接收到两个参数
接下来需要修改的就是在函数调用的地方了
这里我们的想法是这样的,既然onChang返回的是一个函数那么我们就给你一个函数好了
在函数内部我们在调用声明好的state状态维护函数
这里的回调函数我们可以直接传递进去一个事件对象
然后再将这个事件对象作为参数跟要修改的状态名一起传递个状态维护函数
*/}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="username"
onChange = { event => this.saveDateType("username",event.target.value) }
/>
<input
type="password"
name="password"
onChange = { event => this.saveDateType("username",event.target.value) }
/>
<button>提交</button>
{/*提交按钮,触发onSubmit事件*/}
</form>
)
}
}
2.8、React中的生命周期 非常重要
2.8.1、组件的挂载和卸载
- 挂载组件:React.render(组件标签,挂载点)
- 卸载组件:React.unmountComponentAtNode(挂载点)
- 挂载点就是使用document.getElementById("")获取到的元素
2.8.2、一个案例引出声明周期:
- 案例需求:
- 页面元素:一段话,一个按钮
- 在页面中一段话随时间变化透明度从1到零
- 当这段话完全透明之后再让这句话的透明度恢复到1
- 当点击下方按钮时使整个页面所有内容都消失
- 代码实现:
// 创建组件
class Life extends React.Component {
state = {opacity:1}
// 初始换state状态
death = () => {
ReactDom.unmountComponentAtNode(document.getElementById("root"))
// 点击卸载元素
}
// 挂载结束之后执行一次,实例对象调用的
componentDidMuont(){
this.timer = setInterval(()=>{
// 开启定时器,并且将这个定时器的返回值追加到组件实例对象上
let {opacity} = this.state
opactiy -= 0.1
this.setState({opacity})
},200)
}
//在卸载前执行的函数,实例对象调用的
componentWillUnmount(){
{/*
由于在挂载结束的生命周期函数中只是开启了定时器
并且在后续的操作中没有关闭
所以就会导致在触发这个生命周期后报出一个错误
导致错误的原因就在于定时器还在运行,但是这个组件已经销毁了,state中的状态也被销毁了
*/}
clearInterval(this.timer)
// 关闭定时器
}
// 第一次渲染页面,和更新状态时触发,实例对象调用的
render(){
// 编写render方法,使下方代码显示在网页当中
return (
<div>
<h3 style={{opacity:this.state.opacity}}>这是一段想对react说的话</h3>
{/*将这段话的透明度绑定到style属性中的透明度*/}
<button onClick={this.death}>学不会,不活了</button>
{/*绑定一个点击之后页面元素销毁的事件*/}
</div>
)
}
//下面代码是我们在定时器中要做的逻辑代码
{/*(setInterval(()=>{
let {opacity} = this.state
opactiy -= 0.1
this.setState({opacity})
},200))*/}
{/*
在分析一下:
我们怎么让这个opacity循环改变值呢------->setInterval(()=>{})
我们在类之中不能直接写这个setInterval,那么就要考虑把这个写在什么地方呢
假设1、我们把代码写在render会发生什么呢
效果可以实现文字透明度的改变,但是会越来越快
原因:因为我们在定时器中调用了setState API,它会调用render方法所以这里会越来越快,无限的回调
假设2、我们把它写在一个回调函数中,通过事件触发这个函数
效果可以实现,但是也不是我们想要的结果,我们是想让它在特定的时期自动调用,而不是我们自己去调用
未实现自行调用函数
综上:我们需要一个特定时期自行调用的,而且只会执行一次的函数
引出我们的:生命周期函数/生命周期钩子函数/生命周期钩子/生命周期回调函数
*/}
}
2.8.3、生命周期函数触发时机(旧版)
- 概念:
- 组件从创建到死亡它会经历一些特定的阶段
- React组件中包含一些列的钩子函数(生命周期回调函数),会在特定的时刻调用
- 我们在定义组件时,会在特定的生命周期回调函数中做特定的工作
- 看代码来解释生命周期过程
- 组件内挂载时的生命周期函数执行顺序:constructor----->componentWillMount----->render----->componentDidMount
// 验证上述顺序
class Count extends React.Component {
constructor(props){
console.log("Count---constryctor")
super(props)
this.state = {count:0}
}
//按钮点击加一事件回调函数
add = () =>{
let {count} = this.state
this.setState({
count: count + 1
})
}
// 挂载前调用的回调函数
componentWillMount(){
console.log("Count---componentWillMount")
}
//挂载后调用的回调函数
componentDidMount(){
console.log("Count---componentDidMount")
}
render(){
console.log("Count---render")
return (
<div>
<h3>count中的值:{this.state.count}</h3>
<button onClick={this.add}>+1</button>
</div>
)
}
}
//通过以上测试运行顺序为:constructor----->componentWillMount----->render----->componentDidMount
- 组件内更新时的生命周期函数执行顺序:
- 第一条线路:
- setState—>shouldComponentUpdate—>componentWillUpdate—>render—>componentDidUpdate
- 在每次更新状态的时候会触发这条线路
- 其中shouldComponentUpdate是控制是否允许继续进行的生命周期回调函数(简单的说他就一阀门)
- 在不写这个shouldComponentUpdate的情况下React会自动的生成一个这样的生命周期回调函数,并返回ture
- 这个生命周期回调函数是通过返回值来控制是否继续进行下面的生命周期的,如果返回的是false那么就会终止从而不会页面就不会进行重新渲染,返回true则正常进行
- componentWillUpdate这个生命周期回调函数是在更新前调用函数的
- componentDidlUpdate这个生命周期回调函数是render函数调用后调用的生命周期回调函数
- 代码验证:
class Count extends React.Component { constructor(props){ console.log("Count---constryctor") super(props) this.state = {count:0} } //按钮点击加一事件回调函数 add = () =>{ let {count} = this.state this.setState({ count: count + 1 }) } // 挂载前调用的回调函数 componentWillMount(){ console.log("Count---componentWillMount") } //挂载后调用的回调函数 componentDidMount(){ console.log("Count---componentDidMount") } //控制重新渲染的阀门 shouldComponentUpdate(){ console.log("Count---shouldComponentUpdate") return true //这里的返回值一定要写,这里如果写上shouldComponentUpdate就不会有默认返回值 //而且返回值只能是true和false之中的一个,不能是其他的 } //状态更新前触发的回调函数 componentWillUpdate(){ console.log("Count---componentWillUpdate") } //状态更新后触发的回调函数 componentDidUpdate(){ console.log("Count---componentDidUpdate") } render(){ console.log("Count---render") return ( <div> <h3>count中的值:{this.state.count}</h3> <button onClick={this.add}>+1</button> </div> ) } }
- 第二条线路:
- forceUpdate—>componentWillUpdate—>render—>componentDidUpdate
- forceUpdate:强制刷新的意思,强制更新是在不更改state状态下进行的更新,它比setState少走了一个环节,是不受阀门的影响的
- 使用场景不多,只有在不想改变状态并且还想触发页面局部刷新的时候使用
- 强制更新:组件实例.forceUpdate()
- 第三条线路
- 父组件render—>componentWillReceiveProps—>shouldComponentUpdate—>componentWillUpdate—>render—>componentDidUpdate
- componentWillReceiveProps
- 这个生命周期就是在组件将要接收props传值的时候调用
- 这个生命周期回调函数不会在页面一加载进来就触发(即便是一上来就传递了值),它只会在第二次之后的传值时才会进行一个触发
- 并且这个生命周期回调函数是可以接收参数的,这个参数就是传递进来的数据对象
- 代码验证:
class A extends React.Component { state = {text:"hello"} change = () =>{ setState({text:"world"}) } render(){ return ( A组件: <button onClick={this.change}>点击切换文字</button> <hr/> <B text={this.state.text}/> B组件是A组件的子组件 ) } } class B extends React.Component { componentWillReceiveProps(props){ console.log("B---componentWillReceiveProps",props) {/* 在这里第一次渲染传值的时候是不进行触发的 只有当他第二次传值时才会触发 并且还可以接收一个参数 这个参数就是父组件传递进来的数据 */} } componentWillUpdate(){ console.log("B---componentWillUpdate") } //状态更新后触发的回调函数 componentDidUpdate(){ console.log("B---componentDidUpdate") } render(){ console.log("B---render") return ( <div> B组件内容: 这是接收A组件的传值:{this.props.text} </div> ) } } ReactDom.render(<A/>,document.getElementById("root"))
- 第一条线路:
- 总结旧版本生命周期
- 生命周期一般分为三个时期
- 初始化阶段:由ReactDom.render()触发–初次渲染
- constructor
- componentWillMount
- render
- componentDidMount 常用开启定时器/延时器、发送网络请求、订阅消息
- 更新阶段:由组件内部this.setState()或者父组件重新render触发
- shouldComponentUpdate
- componentWillUpdate
- render 必用
- componentDidUpdate
- 卸载阶段:由ReactDom.unmountComponentAtNode()触发
- componentWillUnmount 常用一般在这里做一些收尾的事情,清除定时器、取消订阅
- 初始化阶段:由ReactDom.render()触发–初次渲染
- 生命周期一般分为三个时期
2.8.4、生命周期函数触发时机(新版:了解一下)
- 对比旧版本生命周期,新版本生命周期即将废弃三个生命,又提出了两个生命周期
- 即将废弃三个生命分别是:componentWillMount、componentWillUpdate、componentWillReceiveProps
- 如果在使用即将废弃的生命周期需要加前缀USESAFE_生命周期,这里的前缀不是字面不安全的意思,而是在之后使用过程中会产生bug
- 新增的两个生命周期回调函数使用的几率非常少,所以在记忆的时候只需要记住移出了三个生命周期函数的位置即可,了解新的生命周期函数的执行时机,至于怎么使用和起到的作用目前来说是不用知道的
- 在类组件之中使用getDerivedStateFromProps生命周期回调函数时,需要声明成静态方法,而且必须有返回值,返回值必须是一个状态对象或者是一个null
- 并且在getDerivedStateFromProps生命周期回调函数中可以接收两个参数props和state状态,这里返回的就会固定死state中的状态
- getSnapshotBeforeUpdate是在更新前获取快照(本次更新的数据),任何值都可以作为快照(本次更新的数据)返回,并且返回的快照(本次更新的数据)给到componentDidUpdate的第三个参数当中共其使用
- componentDidUpdate一共可以接收三个参数
- 上一次的props
- 上一次的state状态
- 通过getSnapshotBeforeUpdate返回的数据
- 案例:每隔1s返回一条新闻,当我在观看某条新闻时页面不会发生偏移
// 创建新闻组件
class List extends React.Component {
state = {newArr:[]}
// 创建一个存储新闻的数组
componentDidMount(){
// 当页面挂载完毕之后,开启一个定时器,每隔1s对数组进行添加一个新闻
const {newArr} = this.state
// 从state中结构出新闻数组
setInterval(()=>{
let news = "新闻" + (newArr.length + 1)
// 创建一条新闻
this.setState({
newArr:[news,...newArr]
})
},1000)
}
getSnapshotBeforeUpdate(){
// 这里我们需要返回的快照是上一次的scrollHeight,这个是内容的高度
return this.refs.list.scrollHeight
// 将这个快照传递给componentDidUpdate
}
componentDidUpdate(pervProps,pervState,height){
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
// 这里为什么是`+=`而不是赋值语句呢
// 如果只是赋值那么这个值永远是一条新闻的高度不会固定当前的位置
//`+=`就可以避免这个情况
}
render(){
return (
<div className = "list" ref = "list">
{this.state.newArr.map((item,index) => {
return <div className = "news" key = {index}>{item}</div>
})}
</div>
)
}
}
-
总结新版本生命周期
- 生命周期一般分为三个时期
- 初始化阶段:由ReactDom.render()触发–初次渲染
- constructor
- getDerivedStateFromProps 使用场景非常少(官网说的)
- render
- componentDidMount 常用开启定时器/延时器、发送网络请求、订阅消息
- 更新阶段:由组件内部this.setState()或者父组件重新render触发
- getDerivedStateFromProps
- shouldComponentUpdate
- render 必用
- getSnapshotBeforeUpdate 使用场景非常少(官网说的)
- componentDidUpdate
- 卸载阶段:由ReactDom.unmountComponentAtNode()触发
- componentWillUnmount 常用一般在这里做一些收尾的事情,清除定时器、取消订阅
- 初始化阶段:由ReactDom.render()触发–初次渲染
- 生命周期一般分为三个时期
-
重要的生命周期钩子函数
- render:初始化渲染或跟新调用
- componentDidMount:开启监听、发送网络请求
- componentWillUnmount:做一些收尾工作
-
即将废弃的生命周期钩子函数
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
三、DOM中的diff算法
3.1、验证diff算法
- 验证diffing算法存在
class Time extends React.Component {
state = {date:new Date()}
// 初始化状态
componentDidMount(){
// 挂载结束开启一个定时器,用来获取当前的时间并维护状态
setInterval(()=>{
this.setState({
date:new Date()
})
},1000)
}
render(){
return (
<div>
<h1>现在的时间</h1>
<input type="text"/>
{/*
如果说diffing算法不存在那么当我们在输入框输入内容的时候就会被清空
但是在我们真实操作的时候并没有被清空,由此证明了diffing算法的存在
两次虚拟DOM进行比较没有变换的不会去管,发生变化了的会进行一个重新转换为真实DOM
*/}
<span>现在是:{this.state.date.toTimeString()}</span>
{/*
对页面输出当前时间
当在span标签中在添加一个输入框,并对其进行输入这时也不会重新渲染这个元素
diffing算法的最小比较对象是DOM元素节点
*/}
</div>
)
}
}
3.2、key属性的作用
-
虚拟DOM中key的作用:
-
简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。
-
详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】, 随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1).若虚拟DOM中内容没变, 直接使用之前的真实DOM(也就不用管这个节点了)
(2).若虚拟DOM中内容变了, 则生成新的真实DOM, 随后替换掉页面中之前的真实DOMb. 旧虚拟DOM中未找到与新虚拟DOM相同的key, 根据数据创建新的真实DOM, 随后渲染到到页面
-
案例解释:
{/*创建组件*/} class Person extends React.Component { //状态的初始化 state = { list:[ {id:1,name:"Tom",age:18}, {id:2,name:"jack",age:19}, ] } {/* 分析:使用index作为key 初始数据: {id:1,name:"Tom",age:18}, {id:2,name:"Jack",age:19} 初始: <li key = 0>名字:Tom,年龄:18 <input type="text"/></li> value:123 <li key = 1>名字:Jack,年龄:19 <input type="text"/></li> value:456 更新数据: {id:3,name:"Lily",age:17}, {id:1,name:"Tom",age:18}, {id:2,name:"Jack",age:19} 更新数据后的虚拟DOM: <li key = 0>名字:Lily,年龄:17 <input type="text"/></li> value:123 <li key = 1>名字:Tom,年龄:18 <input type="text"/></li> value:456 <li key = 2>名字:Jack,年龄:19 <input type="text"/></li> value: ----------------------------------------------------------------- 这里我们可以看到当我在数据前面添加一条信息时 更新数据后的所有虚拟DOM的keydou发生了一个变换 而我们知道diff算法会先拿着key去跟之前的虚拟DOM做比较,发现其中内容变了 这时我们就需要将这个虚拟DOM元素转换为真实DOM元素然后替换掉之前的DOM元素 这样我就会看到明明之前有可以复用的元素节点,但还是做了一个新的DOM创建 这样就做了一些不必要的渲染,增加性能的消耗 虽然我们看到的效果是正常的,但是还是存在着一些问题 当我们在列表项中增加一个输入元素,并且在其中输入内容 更新时就会看到乱序的情况,上方value情况 */} render(){ return ( <div> <h3>使用index作为key唯一标识符</h3> <ul> {this.state.list.map((item,index) => { return ( <li key = {index}>{`名字:${item.name},年龄:${item.name}`}</li> ) })} </ul> </div> ) } {/*使用id作为key表示符*/} {/* 分析:使用index作为key 初始数据: {id:1,name:"Tom",age:18}, {id:2,name:"Jack",age:19} 初始: <li key = 1>名字:Tom,年龄:18 <input type="text"/></li> value:123 <li key = 2>名字:Jack,年龄:19 <input type="text"/></li> value:456 更新数据: {id:3,name:"Lily",age:17}, {id:1,name:"Tom",age:18}, {id:2,name:"Jack",age:19} 更新数据后的虚拟DOM: <li key = 3>名字:Lily,年龄:17 <input type="text"/></li> value: <li key = 1>名字:Tom,年龄:18 <input type="text"/></li> value:123 <li key = 2>名字:Jack,年龄:19 <input type="text"/></li> value:456 --------------------------------------------------- 在我们使用唯一标识符id作为key值时,是这样一个流程 首先拿着key值去对比之前的虚拟DOM元素,发现没有就只能将这个虚拟DOM转换成真实DOM 但是如果找到了,并且其中的内容并没有发生改变,这时就会复用原先的真实DOM,不会再去创建新的 这样大大提升了运行的效率 使用id作为标识,就不会出现对不上号的bug了 */} render(){ return ( <div> <h3>使用index作为key唯一标识符</h3> <ul> {this.state.list.map((item,index) => { return ( <li key = {item.id}>{`名字:${item.name},年龄:${item.name}`}</li> ) })} </ul> </div> ) } }
-
-
使用index作为key值可能会产生的问题
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
- 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
- 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
-
总结一下key的值要如何绑定
- 在普通遍历列表项的时候是可以使用index的
- 但是在一些会破坏列表顺序的操作之中就不能使用index,只能使用唯一的标识符
- 最好使用的是数组对象中的唯一标识
四、React使用脚手架搭建项目
4.1、使用creat-react-app创建react应用
4.1.1、react脚手架
- xxx脚手架:用来快速创建一个基于xxx库的模板项目
- 其中包含所有配置(语法检查、jsx编译、devServer…)
- 下载所有相关依赖
- 可以直接运行一个简单的效果
- react提供了一个用于创建react项目的脚手架:creat-react-app
- 项目的整体技术架为:react + webpack + es6 + eslint…
- 使用脚手架开发项目的特点:模块化、组件化、工程化
4.1.2 创建项目并介绍其中的命令
- 全局安装脚手架:npm i -g create-react-app
- 切换到创建项目的目录,创建项目:create-react-app 项目名称(不能是中文,不能包含特殊字符)
- 进入到项目文件夹之中
- 命令介绍:
- npm start:开启一个服务,也是我们之后使用最多的一个命令
- npm build:在项目完成时,进行这个命令,是为了打包、编译代码,将ES6转为ES5代码,将页面不识别的文件编译转换为网页识别的文件
- npm test:用于测试文件,使用很少
- npm eject:将隐藏的webpack.config.js等配置文件显示
- 开启项目:进入项目文件夹输入npm start命令,就会打开一个本地服务端口是3000的网页
4.1.3、项目目录文件
- node_modules:相关依赖文件夹
- public:静态资源文件夹,用于存放主页面、静态图片、icon网站图标、加壳配置文件、爬虫协议文件
- 其中最为重要的就是index.html文件
- 在public中只会出现一个.html这是应用的单页面(SPA)网页开发
- 而在.html文件中我们最为关注的就是body中的
<div id="root"></div>
,这是整个页面的一个挂载点
- src:原码文件
- App.css-------App组件的样式
- App.js -----App组件
- <React.StrictMode></React.StrictMode>是做一个严格要求子组件,如果子组件出现一些要废弃的功能会报出警告
- 这里React会帮我们把这个挂载到静态资源文件中的index.html中,所有这里不能随意改变文件的名字
- index.js----入口文件
- index.css-----样式
- reportWebVitals.js—用来记录页面性能分析的文件(需要web-vitals库的支持)
- setipTests.js-----组件单元测试的文件(需要jest-dom库的支持)
4.2、使用脚手架开发项目
组件文件名一般都会首字母大写,并且为了更好的支持组件化开发,每个组件都会放到一个专门的文件夹下,并且把所有相关的样式、行为、图片、字体都放在这个文件夹下
组件的后缀也要跟js脚本的后缀做出区别,组件使用的是.jsx,在每个组件中都要向外暴露组件,并且每个组讲在编写是都要引用react核心库中的Component
这里还要知道的是,引入组件是使用的ES6中的import引入,在引入时可以省略.js和.jsx后缀,暴露分为多种,默认暴露,分别暴露等
4.2.1、入口文件的配置
- 在我们使用脚手架进行项目开发时,需要了解index.js入口文件中每行代码的含义
- 这是我们自己在入口文件中的配置,一下代码是必写的
- 引入css样式:
import "./index.css"
import React from "react"
// 引入react核心库
import ReactDOM from "react-dom"
// 引入ReactDOM,用于渲染页面
import App from "./App"
// 引入App组件,将其渲染到页面之中
ReactDOM.render(<App/>,document.getElementById("root"))
// 将App组件挂载渲染到root上
4.2.2、外壳组件App的使用
- 在App.jsx组件中一般都是把使用的组件做一个整合,然后输出在页面上的
import React,{Component} from "react"
// 在这里的Component不是解构赋值的意思,这里的Component是分别暴露引入的方式
export default class Hello extends Component {
// 这里是拿类组件为例,上行代码是创建类组件并且向外暴露
// 其组件写法跟之前是一样的,就是多了一步向外暴露的操作
render(){
return <h2>hello react</h2>
}
}
- 了解之后的项目目录
- src
- index.js
- App.jsx
- index.css
- components
- component1
- index.jsx
- index.css
- …
- component2
- index.jsx
- index.css
- …
- component3
- index.jsx
- index.css
- …
- component1
- …
- src
4.2.3、样式的模块化
- 在直接写css样式的时候,如果要是多个组件都是用了一个相同的类名,那么会按照后引入组件的样式为准
- 所有为了避免这样的情况产生就要使用样式的模块化
- 解决方案:
- 在css样式命名时,后缀于文件名之间再添加.module的字段例如:
index.module.css
- 这样我们就能以
import xxx from 'index.module.css'
这样的形式引入文件 - 并且在使用时也需要发生一些更改
<div className={xxx.样式名}></div>
- 在css样式命名时,后缀于文件名之间再添加.module的字段例如:
- 但是我们这样使用的场景很少,因为之后再开发之中我们会用到less或者Scss来书写样式
4.2.4、VS Code中的react插件
- 代码片段
- rcc创建类组件
- rfc创建函数组件
- rcce创建类组件和暴露组件分离的形式
- rfce创建函数组件和暴露组件分离的形式
- 插件名:ES7 React/Redux/GraphQL/React-Native snippets
4.3、组件化编码流程
4.3.1、功能页面的组件化编码流程
- 拆分组件:根据功能结构把页面分为多个组件,拆分页面,抽取组件
- 实现静态组件:使用组件实现静态页面的效果
- 实现动态组件:
- 动态显示初始化数据
- 确定使用数据的数据类型
- 确定使用数据的数据名称
- 明确要保存的组件位置
- 交互(从绑定事件监听开始)
- 动态显示初始化数据
4.3.2、组件的组合使用案例:Todolist
- 将提供的静态网页进行拆分,提取组件
<div class="todo-container">
<div class="todo-wrap">
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
<ul class="todo-main">
<li>
<label>
<input type="checkbox"/>
<span>xxxxx</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
<li>
<label>
<input type="checkbox"/>
<span>yyyy</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
</ul>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成0</span> / 全部2
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</div>
</div>
- 将提供的静态样式进行拆分,将样式分在组件之中
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
- 拆分的组件
- 实现列表数据的动态渲染
- 首先以目前学的技术不好实现兄弟之间的传值
- 我们之前学过父组件向子组件进行数据的传递,是通过组件的属性传递,子组件通过this.props.属性名来读取数据
- 那么现在只要解决了子组件向父组件传值就可以实现这个Todolist的效果了
- 这里可以在子组件中特定的事件去触发父组件的事件,并传递一个默认值给到触发的父组件事件中
- 父组件接收数据,在触发的自定义事件中维护state中的list
- 状态在哪里更新状态的方法就会在那里
- 这里做一个优化,限制props传递的数据类型和是否必填,需要自己安转一个插件"prop-types"
- 安转 npm i prop-types --save
- 引入:import PropTypes from “prop-types”
- 使用:在类组件之中必须声明成静态的方法static propType = {属性名:PropTypes.数据类型.是否必填}
//App.jsx import Header from "./components/Header" import List from "./components/List" import Footer from "./components/Footer" // 引入拆解出来的组件 export default class App extends Component { state = { // 初始化state状态,注意我们这里不能只使用数组的形式 //因为里面不仅仅包含显示内容,还包含这状态所以这里使用的是数组对象的形式 date:[ {id:001,name:"吃饭",done:false}, {id:002,name:"睡觉",done:true} ] } // 添加一个列表项 addList = (todoObj) => { // 设置方法当共子组件来触发 const {date} = this.state let newDate = [todoObj,...date] this.setState({ date : newDate }) } {/* 更新列表项,这里涉及到两层传值 首先是App组件传递给List组件 然后List组件在传递给Item组件 这里触发state状态改变的函数一定是在state存在的组件中 */} updateList = (id,done) =>{ let {date} = this.state let newTodo = date.map(item=>{ // 进行循环遍历找到改变的列表项 if(item.id === id) return {...item,done} // 将这个列表项的done值改变之后放回 else return item }) this.setState({date:newTodo}) // 最后触发state状态的维护 } // 删除指定列表项 delItem = id =>{ let {date} = this.state // 把date解构出来 let newTodo = date.filter(item =>{ // 进行一个过滤,把确定删除项过滤掉 return item.id !== id }) this.setState({ // 状态的维护 date:newTodo }) } //列表全选 checkedAll = done =>{ const {date} = this.state //将date从state中解构出来 let newTodo = date.map(item=>{ //使用map处理列表,将所有的done更改为传递过来的值,并返回出去 return {...item,done} }) this.setState({ //更新状态 date:newTodo }) } //删除所有已完成列表项 clearAllItem = () =>{ const {date} = this.state let newTodo = date.filter(item=>{ //将已选中的过滤掉 return !item.done }) this.setState({ //更新状态 date:newTodo }) } render(){ return ( <div className="todo-container"> <div className="todo-wrap"> <Header addItem={this.addList}/> <List date={this.state.date} updateList={this.updateList} delItem={this.delItem}/> {/*将数据传递给子组件*/} <Footer checkedAll = {checkedAll} clearAllItem = {this.clearAllItem}/> </div> </div> ) } } // Header组件 import nanoid from "nanoid" import PropTypes from "prop-types" // 这个插件时生成一个唯一的字符串,安转 npm i nanoid -D //这是一个函数直接进行调用即可返回一个唯一字符串 export default class Header extends Component { handleEnter = (event) =>{ const {target,keyCode} = event if(keyCode !== 13) return if(target.value.tirm()===""){ alert("输入不能为空!") return } let dateObj = {id:nanoid(),name:target.value,done:false} // 这里我们使用到一个插件,用来生成唯一标识提供给key使用 this.props.addItem(dateObj) // 触发父组件的addItem自定义事件,并且传递一个默认值回到父组件 } // 限制输入数据props的限制 static propTypes = { addItem: PropTypes.func.isRequired // 限制addItem是一个函数类型的数据,是必填项 } render(){ return ( <div className="todo-header"> <input onKeyPress={this.handleEnter} type="text" placeholder="请输入你的任务名称,按回车键确认"/> </div> ) } } // List组件 import Item from "./components/Item" import PropTypes from "prop-types" export default class List extends Component { // 限制输入数据props的限制 static propTypes = { date: PropTypes.array.isRequired, // 限制date是一个数组类型的数据,是必填项 updateList:PropTypes.func.isRequired // 限制updateList是一个函数类型的数据,是必填项 delItem:PropTypes.func.isRequired // 限制delItem是一个函数类型的数据,是必填项 } render(){ let {date,updateList,delItem} = this.props return ( <ul> {date.map(item => { return <Item key = {item.id} {...item} updateList = {updateList} delItem={delItem}/> })} </ul> ) } } // 底部组件 export default class Footer extends Component { handleAll = (event) =>{ this.props.checkedAll(event.target.checked) } clearAll = () =>{ this.props.clearAllItem() } render(){ const {date} = this.props const doneCount = date.reduce((pre,item) =>pre + (date.done ? 1 : 0),0 ) //使用reduce计算已完成的总数 const dateSize = date.length //计算列表总长度 return ( <div className="todo-footer"> <label> <input type="checkbox" checked = {doneCount === dateSize&&dateSize !== 0} onChange={this.handleAll}/> </label> <span> <span>已完成{doneCount}</span> / 全部{dateSize} </span> <button onClick={this.clearAll} className="btn btn-danger">清除已完成任务</button> </div> ) } } // 列表项组件 export default class Item extends Component { state = { mouse:false } // 实现高亮效果 handleMouse = flag =>{ return ()=>{ this.setState({mouse:flag}) // 将鼠标移入移出状态绑定给state中的mosue } } // 改变复选框状态时触发的函数 changeCheck = (id) => { return (event)=>{ this.props.updateList(id,event.target.checked) // 将该变的复选框的id和状态放回给父组件的函数 } } // 删除一个列表项 handledel = id =>{ if(window.comfirm("确定要删除这一项吗?")) this.props.delItem(id) } render(){ let {id,name,done} = this.props let {mouse} = this.state return ( <li style={{backgroundColor:mouse?"#666":"#FFF"}} onMouseover={this.handleMouse(true)} onMouseout={this.handleMouse(false)}> {/*绑定鼠标移入移出事件,并通过绑定的mouse来决定是否高亮*/} <label> <input type="checkbox" defaultChecked={done} onChange={this.changeCheck(id,done)}/> {/*这里需要一个初始的值并且是按照传入的数据所决定的*/} <span>{name}</span> </label> <button onClick={this.handledel(id)} className="btn btn-danger" style={{display:mouse?"block":"none"}}>删除</button> {/*通过判断鼠标是否在这个组件上来决定按钮的显示与隐藏*/} </li> ) } }
4.3.3、todolist案例总结
-
拆分组件、实现静态组件,需要注意的是className(原生中的class)和style({{属性名:属性值}})的写法
-
动态初始化列表:如何确定将数据放在哪个组件之中的state中
- 当只有某个组件使用时,将放在自身的state即可
- 当某些组件使用时,将放在它们共同的父组件的state之中
-
关于父子之间的通信:
- 【父组件】给【子组件】传递数据:通过在子组件上的属性进行传值,子组件通过this.props获取数据
- 【子组件】给【父组件】传递数据:通过在子组件上传递一个函数,子组件来触发这个事件
-
注意defaultChecked和checked的区别,还有类似的defaultValue和value
- 有default前缀的都是只会使用一次属性值
- 没有default前缀的都需要绑定一个onChange的事件,否则就会报出警告
-
状态在哪里,操作状态的方法就在哪里
4.4、react中发送网络请求
4.4.1、了解
- React本身只关注于页面,并不包含发送ajax请求的代码
- 前端应用需要通过ajax请求与后端进行交互(json数据)
- 在React中发送网路请求需要引入第三方的ajax库或者是自己封装
- 一般我们使用的axios来进行网络请求
- axios:
- 封装XmlHttpRequest对象的ajax
- promise风格
- 可以使用在浏览器和node服务器端
- axios:
4.4.2、使用axios发送网路请求
- 安装:npm i axios --save
- 使用:在哪使用就在哪里引入
import axios from "axios"
//引入axios
class Com extends Component {
getDate = () =>{
axios.get("请求地址").then(
{/*
使用axios发送一个get请求
这里要注意的是如果使用的是proxy方式代理的话
这里的协议、域名、接口都要写本地的
否则还是做出跨域请求
*/}
response =>{
console.log("成功了...",response.date)
},
error =>{
console.log("失败了...",error)
}
)
}
}
4.4.3、 第一种代理方式
- 在package.json中最后添加`"proxy":"请求的服务器接口"`
- 同源策略:请求只能发送个协议、域名、端口号相同的接口,对于不同的是不能进行求情的,这种请求叫做跨域请求
- 这里的proxy就是一个转发请求的"中转站",由于html有ajax同源策略的限制,客户端可以发出请求,服务器也可以做出响应,但是受到同源策略的影响,客户端不能接受到返回的响应
- 而这里的proxy执行的过程:
- 首先在axios本地环境中查找是否存在对应的接口或是文件
- 在找不到的情况下,在去配置的服务器中找,找到做出响应
- 找不到会报404的错误
- proxy这个中转站是不受同源策略的影响的,所以可以接收转发来自不同的路径发来的请求和做出的响应
- 优点:简单
- 缺点:不能请求多个代理
4.4.4、 第二种代理方式
- 在src中创建代理配置文件
- 在src下创建配置文件:src/setupProxy.js
- 需要注意这里的setupProxy.js是不能改名的
- 配置setupProxy.js
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但我们一般将changeOrigin值设为true
*/
pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}),
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^/api2': ''}
})
)
}
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
4.5、github搜素案例
- 代码分析
// App组件
import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'
export default class App extends Component {
state = { //初始化状态
users:[], //users初始值为数组
isFirst:true, //是否为第一次打开页面
isLoading:false,//标识是否处于加载中
err:'',//存储请求相关的错误信息
}
//更新App的state
updateAppState = (stateObj)=>{
// 由于需要维护数据比较多,写多个方法就会重复很多代码
// 并且这里都是用来修改数据的使用集中写到一个方法之中
// 这里setState是进行数据合并
this.setState(stateObj)
// 进行state中的用户信息数组
}
render() {
return (
<div className="container">
<Search updateAppState={this.updateAppState}/>
<List {...this.state}/>
</div>
)
}
}
// Search组件
import React, { Component } from 'react'
export default class Search extends Component {
// 搜索事件,发送网络请求的事件处理函数
searchDate = () =>{
let {searchInp : {value : keyWorld}} = this
// 这里是以连续解构形式来获取input输入框的值,后面的是以什么变量名来接收这个值
// 所以这里的keyWorld就是input输入框的内容
this.props.updateAppState({
// 第一次加载时改变isFirst和isLoading的值
isFirst:false,
isLoading:true
})
axios.get(`https://api.github.com/search/users?q=${keyWorld}`).then(
// 这里需要注意的是,当我们频繁发送一个请求时,github官网可能会给我们做出一个拒绝访问
response =>{
// console.log("获取到信息了",response.date)
// 成功时触发的函数
// 由于这里是在Search组件中获取到的数据,但是我们想要在List组件中进行一个展示
//所以这里有用到了父子组件之间的传值
this.props.updateAppState({
// 当数据请求成功时将收到的数据和是否处于加载中的信息传递回去
users:response.date.item,
isLoading:false
})
},
error => {
console.log("获取信息失败了",error)
this.props.updateAppState({
// 当数据请求失败时将错误信息和是否处于加载中的信息传递回去
err: error,
isLoading:false
})
// 失败时调用的函数
}
)
{/*
这里出现了一个叫做连续的解构赋值
所谓连续性的解构赋值就是将对象中的对象中的属性取出来
使用的是{a:{b:{c}}}这样的形式就可以拿出c并且直接使用c
如果说我们对c这个变量名并不满意还可以给他改一个变量名
这里说的改变量名不是说把c直接改成一个变量名,这样实现{c:value}
*/}
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">搜索github用户</h3>
<div>
<input ref={c => this.searchInp = c} type="text" placeholder="输入关键词点击搜索"/>
{/*通过ref来获取当前元素,这里通过回调的形式来存储这个ref*/}
<button onClick = {this.searchDate}>搜索</button>
{/*绑定一个点击事件,用来发送网路请求获取数据*/}
</div>
</section>
)
}
}
// List组件
import React, { Component } from 'react'
import './index.css'
// 引入相关样式
// 这里要注意的是在React中使用样式时一定要注意方式,它跟原生使用是有区别的
export default class List extends Component {
render() {
let {users,isFirst,isLoading,err}
return (
<div className="row">
{
isFirst?<h2>欢迎使用github,请搜索头像</h2>:
{/*首先判断是否为第一次访问页面*/}
isLoading?<h2>Loading...</h2>:
{/*判断是否是在请求数据*/}
err?<h2>{err.messige}</h2>:
{/*判断是否出现错误*/}
users.map(item=>{
{/*最后都通过之后再将获取到的数据渲染出来*/}
<div className="card">
<a rel="noreferrer" href={item.html_url} target="_blank">
{/*这里target必须搭配rel进行使用否则会出现安全性问题*/}
<img alt="head_portrait" src={item.avatar_url} style={{width:'100px'}}/>
{/*在使用img时一定要加alt属性,否则就会报出警告*/}
</a>
<p className="card-text"></p>
</div>
})
}
</div>
)
}
}
4.6、消息订阅与发布(兄弟组件之间的通信)
4.6.1、pubsup-js的使用
- 安装:
npm i pubsup-js --save
- 在接收数据的组件中订阅消息,说好定义的是那个消息
- 直接使用数据的组件中设置state
- 订阅消息:PubSup.subscribe(“发布的消息名msg”,获取到消息所执行的回调函数(msg(一般不用这个只是作为一个占位符可以使用_来代替),date(订阅收到的数据))=>{})
- 发布消息:PubSup.public(“发布的消息名msg”,发布的数据)
- 消息订阅是在生命周期钩子函数componentMount中开启订阅
- 不仅仅实现了,兄弟之间的通信,应用于任意组件之间的通信
- 在使用pubsup-js时App外壳组件就不需要写的那么繁琐了
4.6.2、github案例优化
- 代码
// App组件
import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'
// 使用`pubsup-js`就可以避免在App组件中设置state状态和更新状态的方法了,使得在App组件中更简洁
export default class App extends Component {
render() {
return (
<div className="container">
<Search/>
<List/>
</div>
)
}
}
// Search组件
import React, { Component } from 'react'
import PubSup from "pubsup-js"
export default class Search extends Component {
// 搜索事件,发送网络请求的事件处理函数
searchDate = () =>{
let {searchInp : {value : keyWorld}} = this
// 这里是以连续解构形式来获取input输入框的值,后面的是以什么变量名来接收这个值
// 所以这里的keyWorld就是input输入框的内容
PubSup.public("gitPic",{
isFirst:false,
isLoading:true
})
// 使用消息发布来实现组件之间的传值
axios.get(`https://api.github.com/search/users?q=${keyWorld}`).then(
// 这里需要注意的是,当我们频繁发送一个请求时,github官网可能会给我们做出一个拒绝访问
response =>{
/* this.props.updateAppState({
// 当数据请求成功时将收到的数据和是否处于加载中的信息传递回去
users:response.date.item,
isLoading:false
}) */
PubSup.public("gitPic",{
users:response.date.item,
isLoading:false
})
},
error => {
/* console.log("获取信息失败了",error)
this.props.updateAppState({
// 当数据请求失败时将错误信息和是否处于加载中的信息传递回去
err: error,
isLoading:false
})
// 失败时调用的函数 */
PubSup.public("gitPic",{
err: error,
isLoading:false
})
}
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">搜索github用户</h3>
<div>
<input ref={c => this.searchInp = c} type="text" placeholder="输入关键词点击搜索"/>
{/*通过ref来获取当前元素,这里通过回调的形式来存储这个ref*/}
<button onClick = {this.searchDate}>搜索</button>
{/*绑定一个点击事件,用来发送网路请求获取数据*/}
</div>
</section>
)
}
}
// List组件
import React, { Component } from 'react'
import PubSup from "pubsup-js"
import './index.css'
// 引入相关样式
// 这里要注意的是在React中使用样式时一定要注意方式,它跟原生使用是有区别的
export default class List extends Component {
// 由于是在List中直接使用的这些数据所以将状态写到此处
state = { //初始化状态
users:[], //users初始值为数组
isFirst:true, //是否为第一次打开页面
isLoading:false,//标识是否处于加载中
err:'',//存储请求相关的错误信息
}
componentMount(){
this.token = Pubsup.sunscribe("getPic",(_,date)=>{
// _用来占位,date是订阅的接收到的消息数据
this.setState(date)
})
}
componentWillUnmount(){
PubSup.unsubscribe(this.token)
// 在即将卸载组件的时候需要取消订阅
}
render() {
let {users,isFirst,isLoading,err} = this.state
return (
<div className="row">
{
isFirst?<h2>欢迎使用github,请搜索头像</h2>:
{/*首先判断是否为第一次访问页面*/}
isLoading?<h2>Loading...</h2>:
{/*判断是否是在请求数据*/}
err?<h2>{err.messige}</h2>:
{/*判断是否出现错误*/}
users.map(item=>{
{/*最后都通过之后再将获取到的数据渲染出来*/}
<div className="card">
<a rel="noreferrer" href={item.html_url} target="_blank">
{/*这里target必须搭配rel进行使用否则会出现安全性问题*/}
<img alt="head_portrait" src={item.avatar_url} style={{width:'100px'}}/>
{/*在使用img时一定要加alt属性,否则就会报出警告*/}
</a>
<p className="card-text"></p>
</div>
})
}
</div>
)
}
}
4.6.3、fetch请求使用(了解)
- 他是内置的一个方法,不是对XHR的一个封装
- 无需安转引入即可使用,老版本可能不支持
- fetch是一个遵循关注分离的程序设计模式:就是将一个是分步拆分,达到某个阶段之后做出一些响应,是promise的风格,不会形成回调地狱
- 案例分析:拿github做演示
searchDate = () =>{ let {searchInp : {value : keyWorld}} = this // 这里是以连续解构形式来获取input输入框的值,后面的是以什么变量名来接收这个值 // 所以这里的keyWorld就是input输入框的内容 PubSup.public("gitPic",{ isFirst:false, isLoading:true }) /* PubSup.public("gitPic",{ users:response.date.item, isLoading:false }) PubSup.public("gitPic",{ err: error, isLoading:false */ fetch(`https://api.github.com/search/users?q=${keyWorld}`).then( response =>{ {/* 这里得到的response是一个对象,但是从中并没有得到我们想要的数据 这个无论是在成功还是说请求服务器地址写错的时候都会去执行 这是因为这一步只是做了一个请求服务器的功能 只要是发送出去数据就证明服务器连接成功 那我们想要得到数据在哪呢? 这里在原型对象上有一个方法json 如果直接使用.json()直接输出的话它是一个promise(pending...) 将response.json()作为返回值返回出去 由于返回出来的还是一个promise使用可以再次.then 进行promise的链式调用 如果返回值是一个非promise时它的状态就是成功,值就是这个非promise值 如果返回的值抛出了异常,这是它的值就是一个失败状态 如果是一个pending的promise就会继续进行下一步 此时得到的resoinse就是我们获取到的数据 */} return response.json() }, error =>{ // 这里只有在不可抗力所导致的错误时才会执行到这里 // 比如断网等 // 这里需要一个终止继续调用,返回一个新的promise对象 return new Promise(()={}) } ).then( response =>{ console.log(response) // 这里就是我们想要的数据 }, error =>{ // 请求错时会触发 } ) } //优化fetch请求 /* 这里我们发现两个error都是用来出错时执行的回调 所以这里我们可以使用.catch来进行一个错误的统一处理(兜底) */ fetch(`https://api.github.com/search/users?q=${keyWorld}`).then( response =>{ return response.json() } ).then( response =>{ console.log(response) } ).catch( error =>{ console.log("数据请求错误",error) } ) /* 而我们还可以进行一个优化的地方在哪呢? 我们观察可以看出由于数据请求都是在成功之后的链式结构 这里我们可以使用async await语法糖进行一个更好的优化 但是我们看到下方代码,发现对请求错误的响应好像没有处理 这里可以使用try{} c atch(error){}来处理错误响应 */ searchDate = async()=>{ // 这里要注意async await是要一起用的,不能单独使用 let response = await fetch(`https://api.github.com/search/users?q=${keyWorld}`) // 发送请求返回的是链接数据库是否成功 let result = await response.json() // 返回的是数据对象 console.log(result) // 这样就可以获取到我们想要的数据了 } //继续优化 searchDate = async()=>{ // 这里要注意async await是要一起用的,不能单独使用 try { let response = await fetch(`https://api.github.com/search/users?q=${keyWorld}`) // 发送请求返回的是链接数据库是否成功 let result = await response.json() // 返回的是数据对象 console.log(result) // 这样就可以获取到我们想要的数据了 } catch (error){ console.log("数据请求错误",error) } }
4.6.4、总结
- 设计状态时要考虑全面,例如带有网络请求的组件,要考虑请求失败怎么办。
- ES6小知识点:解构赋值+重命名
let obj = {a:{b:1}} const {a} = obj; //传统解构赋值 const {a:{b}} = obj; //连续解构赋值 const {a:{b:value}} = obj; //连续解构赋值+重命名
- 消息订阅与发布机制
- 先订阅,再发布(理解:有一种隔空对话的感觉)
- 适用于任意组件间通信
- 要在组件的componentWillUnmount中取消订阅
- 订阅:Pubsup.subscribe(订阅名,回调函数(订阅名,订阅数据)=>{})
- 发布:Pubsup.public(订阅名,发布数据)
- 取消订阅:Pubsup.unsubscribe(订阅的返回值)
- 订阅在挂载完毕声明周期回调函数进行,取消则是在销毁之前生命周期回调函数进行
- fetch发送请求(关注分离的设计思想)
try { const response= await fetch(`/api1/search/users2?q=${keyWord}`) const data = await response.json() console.log(data); } catch (error) { console.log('请求出错',error); }
五、React路由
5.1、SPA(单页面应用)
- 单页面Web应用(single page web application ,SPA)
- 整个应用只有一个完整的页面
- 点击页面中的链接不会刷新页面,只会做页面局部刷新
- 数据都需要ajax请求获取,并在前端异步展现
- 总结一句:页面不刷新,进行动态渲染页面,单页面多组件
5.2、路由
5.2.1、路由的了解
- 一个路由就是一个映射关系,简单说就是一个路由对应一个组件,只能一对一
- 这里的映射关系key对应的是地址栏中的path,而value对应的则是组件(前端路由)/函数(后端路由)
5.2.2、路由分类
- 后端路由:
- 理解: value是function, 用来处理客户端提交的请求。
- 注册路由: router.get(path, function(req, res))
- 工作过程:当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
- 前端路由:
- 浏览器端路由,value是component,用于展示页面内容。
- 注册路由:
<Route path="/test" component={Test}>
- 工作过程:当浏览器的path变为/test时, 当前路由组件就会变为Test组件
5.3、路由的基本使用
5.3.1、react-router-dom
- react的一个插件库
- 专门用来实现一个单页面应用
- 基于react的项目都会使用这个库
- 安装:npm i react-router-dom --save
- 通过案例来学习其中的插件及其注意事项
- 明确好界面中的导航区、展示区
- 导航区是用来进行路由切换的(组件切换)
- 展示区是将切换来的路由进行渲染展示的
- 导航区的a标签改为Link标签
<Link to="/xxxxx">Demo</Link>
- 这里的Link在解析之后还是一个超链接的形式
- 展示区写Route标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>
- 路由的两种模式
- BrowserRouter:遵循H5中的history模式
- HashRouter:哈希模式,/#/后面的称之为哈希值(锚点值),在其后面的是不会作为资源发送个服务器
- 的最外侧包裹了一个或
- 在react-router-dom中有一个组件,是用来Link做选中高亮的:NavLink
- 默认选中会添加一个active类名
- 如果添加的类型不是active就需要添加一个activeClassName属性,来添加一个选中效果
//App组件 import {Link,Route} from "react-router-dom" import Home from "./component/Home" import About from "./component/About" //引入插件 export default class App extends Component { render(){ return ( <div class="row"> <div class="col-xs-offset-2 col-xs-8"> <div class="page-header"><h2>React Router Demo</h2></div> </div> </div> <div class="row"> <div class="col-xs-2 col-xs-offset-2"> <div class="list-group"> {/* <a class="list-group-item active" href="./about.html">About</a> <a class="list-group-item" href="./home.html">Home</a> */} {/* 由于这里使用的是单页面应用,超链接会进行刷新页面进行一个页面的跳转 在React中常用路由链接实现组件之间的切换 */} <Link className="list-group-item" to="/home">Home</Link> {/* to属性是我们切换的组件,这里的to属性值会去同一个路由下的Route组件中进行一个匹配,匹配成功展示不同的组件*/} <Link className="list-group-item" to="/about">About</Link> {/* 这里的Link是从react-router-dom中的,只要使用其中的组件 就需要开启路由 路由模式分为两种:1.BrowserRouter 2.HashRouter 在哪里使用了路由就需要在外面包裹一层路由模式标签 而且这里的路由模式只能写一次,要不然两个路由之间是不能进行绑定的 一般在我们写项目的时候会将路由模式标签写在入口文件中,并且让它包裹App组件 */} </div> </div> <div class="col-xs-6"> <div class="panel"> <div class="panel-body"> {/* 这里是组件切换的位置,在这里注册路由 */} <Route path="/home" component={Home}/> {/* path:是路由链接 component:切换的组件 既然这里用到了其他组件,就必须将其引入 */} <Route path="/about" component={About}/> </div> </div> </div> </div> ) } } //Home组件 export default class Home extends Component { render(){ return <h3>这是Home组件</h3> } } //About组件 export default class About extends Component { render(){ return <h3>这是About组件</h3> } }
- 明确好界面中的导航区、展示区
5.3.2、路由组件和一般组件
- 路由组件
- 就是写在中的component属性之中的
- 路由组件放在pages文件夹中
- 路由组件会收到路由器传递来的三个重要属性
- 一般组件
- 就是程序员直接引入,并且使用标签的形式进行一个使用
- 一般组件放在component文件夹中
- 一般组件只能接收到属性传值中的数据
- 总结两种组件的区别
- 写法不同
- 一般组件:
- 路由组件:
- 存放位置不同
- 一般组件:存放在components文件夹中
- 路由组件:存放在pages文件夹中
- 接收到的props不同
- 一般组件:写组件标签时传递了什么,props就是什么
- 路由组件:接收到三个固定的属性
- history:
- go: ƒ go(n):前进或者后退n
- goBack: ƒ goBack():返回上一次路由
- goForward: ƒ goForward():前进一层路由
- push: ƒ push(path, state):跳转路由
- replace: ƒ replace(path, state):替换上一次路由
- 在history中也有一个location
- location:
- pathname: “/about”:路由名称
- search: “”
- state: undefined
- match:
- params: {}
- path: “/about”
- url: “/about”
- history:
- 写法不同
5.3.3、二次封装NavLink组件
- 由于我们书写NavLink需要书写多次样式和选中样式
- 所以这里需要我们需要进行二次封装,来简化代码
import {NavLink} from "react-router-dom"
// 由于是二次封装,不是自己写一个这样的标签,所以这里我们需要引入NavLink
export default class MyNavLink extends Component {
render(){
return <NavLink activeClassName="选中样式" className="普通样式" {...props}/>
// 这里解释一下为什么直接对其他值进行一个解构就可以
// 我们需要一个必传属性也就是to
// 如果我们的显示内容还是通过属性来进行传值显示就又会显得比较low
// 这里标签体内容也是可以被传递过来被props所接受的
// 使用this.props.children来获取标签体内容
// 存再children中,使用这里我们也不需要写成双标签然后在里面进行一个传值
}
}
// 使用我们二次封装的MyNavLink
import MyNavLink from "./component/MyNavLink"
// 引入我们的组件
<MyNavLink to="/route">路由跳转</MyNavLink>
// 使用我们封装的组件,这里需要注意开启路由模式
5.3.4、二次封装NavLink组件和NavLink组件
- NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
- 标签内容是一个特殊的标签属性
- 通过this.props.children可以获取标签体内容
5.3.5、Switch组件的使用
- 当我们注册的两个路由路径相同时将都会渲染两个组件,说明每个路由都匹配了一遍,这样效率极低
- 我们需要的是当我匹配到路由之后就不在进行先下比对了
- 这个组件的功能类似于原生js中的switch
- 它会从上往下匹配成功第一个路由时就不在进行向下比对了
- Switch组件是需要包裹在注册路由外层
- 我们一般情况下是不会让一个路径对应多个组件的,一个路径只会对应一个组件 是一一对应的关系
- 注册路由在一个以上才需要使用Switch包裹
- Switch可以提高路由匹配效率(进行单一匹配)
<Switch>
<Route path="/xxx" component={xxx}></Route>{/*在这里匹配成功之后就不会再向下匹配了*/}
<Route path="/xxx" component={xxx}></Route>
<Route path="/xxx" component={xxx}></Route>
</Switch>
5.3.6、样式丢失(样式使用的public中的静态资源)
- 我们使用内置服务器访问本地public跟路径下的一个文件时如果没有找到改文件,也会返回一些东西
- 返回的是index.html
- 在我们写多级路由的时候,当再次刷新页面时可能会丢失样式
- 原因:
- 样式在这里引入public中index.html的路径是相对的
- 浏览器会认为我们的/后的还是我们的文件路径
- 由于找不到文件所以会返回index.html,进而就会丢失样式
- 三种解决方法:
- 不使用./src/xxx.css样式,使用绝对路径/src/xxx.css
- 使用React中
%PUBLIC_URL%
代表的就是public文件的绝对路径 - 任性一定要使用./src/xxx.css样式时,可以通过改变路由模式来解决这个问题,这是因为在/#/后的浏览器会认为这是前端的资源不会带到服务器中请求
5.3.7、路由模糊匹配和严格匹配
- 模糊匹配
- 在,这种情况下是匹配不成功的,
- 但是在,这种情况下就可以匹配成功
- 上面代码就是我们说的模糊匹配
- 模糊匹配就是在Route标签中的必须严格要求存在的,换句话说就是Link中*只要开头是Route中的路径(path)*就可以匹配到
- 简单说就是人家要的一个不能少,顺序也不能乱
- 严格匹配
- 路由默认的是模糊匹配
- 严格匹配就是说不能多给路由,更不能少给
- 开启严格匹配:
- 在Route标签中添加一个属性exact = {true} /exact
- 添加后就必须时完全匹配才可以
- 不要轻易开启严格匹配,只要不耽误页面的呈现就不用开启,也就是不影响页面的时候就不需要开
- 总结:
- 默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
- 开启严格匹配:
- 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
5.3.8、路由的重定向Redirct组件的使用
- 当地址栏的路由没有成功匹配到就会走Redirct中的路由,使用Redirct指定的路由
<Switch>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Redirct to="/home"></Redirct>
{/*当路由匹配没有在Route中匹配到任何一个路由时,就会走Redirct*/}
</Switch>
5.3.9、多级路由(嵌套路由)
-
在项目开发是我们会经常使用路由的嵌套,那么React的路由匹配机制是什么呢?
- 先注册的会进行一个优先匹配,如果在注册路由之中没有匹配到那么会直接去到Redirect指定的路由
- 也就是说如果在嵌套路由之中使用了Redirect进行路由的重定向那么会出现,一个无论怎么点击触发那个二级路由(使用的是Link或者是NavLink)都不会正常显示,都会是Redirect指定的路由
- React的路由匹配机制是先注册的路由先进行匹配,也就是先匹配一级路由
-
那么如何解决这个问题呢?
- 我们只需要理由路由的模糊匹配机制,在子级路由前加上父级的路由路径
- 这样我们会首先在一级路由中匹配到路由,从而页面结构遵循着父级路由的样子,然后在二级路由再次匹配
-
注意:以下代码是通过Link或者是NavLink进行切换组件的
// App组件内容
import React from 'react'
import {Switch,Route,Link,Redirect} from 'react-router-dom'
import Home from "./components/Home"
import About from "./components/About"
export default function App() {
return (
<div>
<Link to="/home">Home</Link>
<Link to="/about">About</Link>
<Switch>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Redirect to="/home"></Redirect>
</Switch>
</div>
)
}
// Home组件
import React, { Component } from 'react'
export default class index extends Component {
render() {
return (
<div>
<h3>
这是Home页面内容
</h3>
</div>
)
}
}
// About组件内容
import React, { Component } from 'react'
import {Switch,Route,Link,Redirect} from 'react-router-dom'
import Self from "../Self"
import Classify from "../Classify"
export default class index extends Component {
render() {
return (
<div>
<h3>
这是About页面内容
</h3>
<Link to="/self">Self</Link>
<Link to="/classify">classify</Link>
<Switch>
<Route path="/self" component={Self}></Route>
<Route path="/classify" component={Classify}></Route>
{/*
如果是按照这个形式的路由进行一个切换的话,会产生永远只切换到Home组件
解决方案就是在路由前添加一个父级路由进行匹配
*/}
</Switch>
</div>
)
}
}
// 其他组件Classify和Self组件结构同Home组件,只是内容不同
- 总结:
- 注册子路由时要写上父路由的path值
- 路由的匹配是按照注册路由的顺序进行的
- 不要使用路由的严格匹配
5.4、路由之间的传值
5.4.1、第一种传递params参数
- 需要在Route中声明,在Link的to中传递参数,这里要传递的是动态数据的话,使用的是,模板字符串
- 声明的方式:key名
// 具体代码实操
export default class Message extends Component {
state = {
messageArr : [
// 声明的数组作为动态传递参数的依赖
{id:"01",title:"消息1"},
{id:"02",title:"消息2"},
{id:"03",title:"消息3"},
]
}
render(){
let {messageArr} = this.state
return (
<>
<ul>
{messageArr.map((msgObj)=>{
return (
<li key={msgObj.id}>
{/*向路由传递params参数*/}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
{/*这里是进行一个动态路由的传值*/}
</li>
)
})}
</ul>
{/*接收params传递来的数据*/}
<Route path="/home/message/detail/:id/:title"></Route>
{/*这里的:id和:title是声明接收跳转时传递的数据*/}
</>
{/*
接收参数是在子级路由对应的组件中props中获取
使用params传递数据是将数据放到match属性的params中
使用时直接this.props.match.params.声明的变量名(也就是:之后的名字)
*/}
)
}
}
- 总结:
- 路由链接(携带参数): 详情
- 注册路由(声明接收): ,这里如果不声明接收的话多余的就会被忽略
- 接收参数: let {name,age} = this.props.match.params
5.4.2、第二种传递search参数
- 直接在地址后面拼接上传递的参数,类似于ajax中的query传参
- search是无需声明接收参数的
export default class Message extends Component {
state = {
messageArr : [
// 声明的数组作为动态传递参数的依赖
{id:"01",title:"消息1"},
{id:"02",title:"消息2"},
{id:"03",title:"消息3"},
]
}
render(){
let {messageArr} = this.state
return (
<>
<ul>
{messageArr.map((msgObj)=>{
return (
<li key={msgObj.id}>
{/*向路由传递search参数*/}
<Link to={`/home/message/detail?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
</li>
)
})}
</ul>
{/*无需声明接收参数,正常注册就可以*/}
<Route path="/home/message/detail"></Route>
</>
)
}
}
{/*
使用search传参接收时,数据是在location的search之中
不过这里的形式是?id=xxx&title=xxx的形式,这种形式称之为urlencoded
这里我们可以使用querystring将urlencoded转换成对象形式的数据
qs.stringify(obj):将对象转成字符串
qs.parse(str):将字符串转成对象的形式
但是这里的第一个属性会被添加一个?
我们还需要进行一个截取操作
let res = this.props.location.search
let {id,title} = qs.parse(res.slice(1))
*/}
- 总结:
- 路由链接(携带参数):<Link to=’/demo/test?name=tom&age=18’}>详情
- 注册路由(无需声明,正常注册即可):
- 接收参数:this.props.location.search
- 注意:获取到的search是urlencoded编码字符串,需要借助querystring解析和去除?操作
5.4.3、第三种传递state参数
- 在Link中传递的是一个对象
- 不用声明接收
- 获取参数是在this.props.location.state中
export default class Message extends Component {
state = {
messageArr : [
// 声明的数组作为动态传递参数的依赖
{id:"01",title:"消息1"},
{id:"02",title:"消息2"},
{id:"03",title:"消息3"},
]
}
render(){
let {messageArr} = this.state
return (
<>
<ul>
{messageArr.map((msgObj)=>{
return (
<li key={msgObj.id}>
{/*向路由传递state参数*/}
<Link to= {{pathname:"/home/message/detail",state:{id:"01",title:"hello"}}}>详情</Link>
</li>
)
})}
</ul>
{/*无需声明接收参数,正常注册就可以*/}
<Route path="/home/message/detail"></Route>
</>
)
}
}
{/*
不会在地址栏出现数据
无需声明接收参数
刷新时不会丢失数据
传递的是一个state对象
这里的state跟状态对象不是一个东西
*/}
- 总结:
- 路由链接(携带参数):详情
- 注册路由(无需声明接收参数,正常使用注册即可):<Route path="/demo"component={Demo}>
- 接收参数:this.props.location.state
- 注意:其他两种方法是刷新页面不会丢失数据的(因为地址栏是不变的),而state接收参数也是刷新不会丢数据的,因为都存放在了history之中
5.4.4、总结三种路由传参
- params参数(使用的最多)
- 路由链接(携带参数):<Link to=’/demo/test/tom/18’}>详情
- 注册路由(声明接收):
- 接收参数:this.props.match.params
- search参数
- 路由链接(携带参数):<Link to=’/demo/test?name=tom&age=18’}>详情
- 注册路由(无需声明,正常注册即可):
- 接收参数:this.props.location.search
- 备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
- state参数
- 路由链接(携带参数):
<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
- 注册路由(无需声明,正常注册即可):
- 接收参数:this.props.location.state
- 备注:刷新也可以保留住参数
- 路由链接(携带参数):
5.4.5、push和repalce两种模式
- push模式是进行一个压栈操作,也就是会留下浏览记录
- 会有前进回退按钮,进行网页浏览记录的切换
- repalce模式是进行一个替换操作,也就是不会留下浏览记录(无痕模式)
- 使用方式:在Link标签中添加一个repalce属性,即可开启该模式
5.4.6、编程式路由导航
- 简单说就是不使用NavLink和Link进行一个组件切换,使用的是location中的push、repalce、goBack、goForward方法进行一个组件的切换
- 结合案例,把路由传值、编程式导航路由、嵌套路由结合到一起使用
{/*
首先是结构
src
pages------路由文组件文件夹
Home------组件文件夹
New------组件文件夹
Message------组件文件夹
Detail------组件文件夹
index.jsx
index.jsx
About------组件文件夹
index.jsx
components
MyNavLink
index.jsx
App.jsx------组件文件夹
index.js------入口文件
*/}
// 具体代码
// Home------组件文件夹index.jsx
import React, { Component } from 'react'
import MyNavLink from '../../components/MyNavLink'
// 二次封装分NavLink组件
import {Route,Switch,Redirect} from 'react-router-dom'
import News from './News'
import Message from './Message'
export default class Home extends Component {
render() {
return (
<div>
<h3>我是Home的内容</h3>
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message">Message</MyNavLink>
</li>
</ul>
{/* 注册路由 */}
<Switch>
<Route path="/home/news" component={News}/>
<Route path="/home/message" component={Message}/>
<Redirect to="/home/news"/>
</Switch>
</div>
</div>
)
}
}
// Message组件
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'
export default class Message extends Component {
state = {
messageArr:[
{id:'01',title:'消息1'},
{id:'02',title:'消息2'},
{id:'03',title:'消息3'},
]
}
replaceShow = (id,title)=>{
// 使用replace+params传值
this.props.history.replace(`/home/message/detail/${id}/${title}`)
// 使用replace+search传值
this.props.history.replace(`/home/message/detail/?id=${id}&title=${title}`)
// 使用replace+state传值
this.props.history.replace(`/home/message/detail`,{id,title})
}
pushShow = (id,title)=>{
// 使用push+params传值
this.props.history.push(`/home/message/detail/${id}/${title}`)
// 使用push+search传值
this.props.history.push(`/home/message/detail/?id=${id}&title=${title}`)
// 使用push+state传值
this.props.history.push(`/home/message/detail`,{id,title})
}
back = ()=>{
this.props.history.goBack()
// 回退一步
}
forward = ()=>{
this.props.history.goForward()
// 前进一步
}
go = n =>{
this.props.history.go(n)
// 前进或回退n步
}
render() {
const {messageArr} = this.state
return (
<div>
<ul>
{
messageArr.map((msgObj)=>{
return (
<li key={msgObj.id}>
{/* 向路由组件传递params参数 */}
{/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递search参数 */}
{/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递state参数 */}
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
<button onClick={()=> this.pushShow(msgObj.id,msgObj.title)}>push查看</button>
<button onClick={()=> this.replaceShow(msgObj.id,msgObj.title)}>replace查看</button>
</li>
)
})
}
</ul>
<hr/>
{/* 声明接收params参数 */}
{/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}
{/* search参数无需声明接收,正常注册路由即可 */}
{/* <Route path="/home/message/detail" component={Detail}/> */}
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={()=> this.go(2) }>go</button>
</div>
)
}
}
// Detail组件
import React, { Component } from 'react'
// import qs from 'querystring'
const DetailData = [
{id:'01',content:'你好,中国'},
{id:'02',content:'你好,世界'},
{id:'03',content:'你好,地球'}
]
export default class Detail extends Component {
render() {
console.log(this.props);
// 接收params参数
// const {id,title} = this.props.match.params
// 接收search参数
// const {search} = this.props.location
// const {id,title} = qs.parse(search.slice(1))
// 接收state参数
const {id,title} = this.props.location.state || {}
const findResult = DetailData.find((detailObj)=>{
return detailObj.id === id
}) || {}
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
5.4.7、总结路由之间传值
-
在编程式路由之中配对:
- push + params:
- 携带数据:this.props.history.push(“路由路径/参数1/参数2…”)
- 接收参数:
- 使用数据:this.props.match.params
- push + search:
- 携带数据:this.props.history.push(“路由路径/&xxx=参数1&xxx=参数2…”)
- 接收参数:,无需接收正常注册即可
- 使用数据:
- this.location.search得到的是urlencoded编码字符串
- 使用querystring解析
- let res = qs.pares(this.location.search.slice(1))得到对象
- push + state:
- 携带数据:this.props.history.push(“路由路径”,state(携带的数据对象))
- 接收参数:,无需接收正常注册即可
- 使用数据:this.props.location.state
- push + params:
-
使用Link或者NavLink进行组件的切换
- params携带数据:
- 携带数据:
<Link to={
路由路径/参数1/参数2…}}></Link>
- 接收参数:
- 使用数据:this.props.match.params
- 携带数据:
- search携带数据:
- 携带数据:
<Link to={
路由路径/?xxx=参数1&xxx=参数2…}></Link>
- 接收参数:,无需接收正常注册即可
- 使用数据:
- this.props.location得到的是urlencoded编码字符串
- qs.parse(search.slice(1))
- 携带数据:
- state携带数据:
- 携带数据:
<Link to={{pathname:'路由路径',state:{携带数据对象}}></Link>
- 接收参数:,无需接收正常注册即可
- 使用数据:this.props.location.state
- 携带数据:
- params携带数据:
-
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
- this.prosp.history.push()
- this.prosp.history.replace()
- this.prosp.history.goBack()
- this.prosp.history.goForward()
- this.prosp.history.go()
5.4.8、withRouter
- 用于解决在一般组件无法使用路由组件中props特有的属性
- withRoute不是一个组件
- 它是用于加工一般组件的方法
import {withRouter} from "react-router-dom"
// 声明一个一般组件
class Header extends Component {
back = () =>{
this.prosp.history.goBack()
// 实现回退功能
}
render(){
return (
<button onClick={this.back}></button>
)
}
}
export default withRouter(Header)
// 使用withRouter加工Header组件,使其拥有路由组件中的三大属性
5.4.9、BrowserRouter和HashRouter的区别
-
底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值(兼容性更好) -
url中path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test -
刷新后对路由state参数的影响
(1).BrowserRouter没有任何影响,因为state保存在history对象中。
(2).HashRouter刷新后会导致路由state参数的丢失!!! -
备注:HashRouter可以用于解决一些路径错误相关的问题。
title} = this.props.match.params
// 接收search参数
// const {search} = this.props.location
// const {id,title} = qs.parse(search.slice(1))
// 接收state参数
const {id,title} = this.props.location.state || {}
const findResult = DetailData.find((detailObj)=>{
return detailObj.id === id
}) || {}
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
#### 5.4.7、总结路由之间传值
- 在编程式路由之中配对:
- push + params:
- 携带数据:this.props.history.push("路由路径/参数1/参数2...")
- 接收参数:<Route path="路由路径/:参数1/:参数2..."></Route>
- 使用数据:this.props.match.params
- push + search:
- 携带数据:this.props.history.push("路由路径/&xxx=参数1&xxx=参数2...")
- 接收参数:<Route path="路由路径"></Route>,无需接收正常注册即可
- 使用数据:
- this.location.search得到的是urlencoded编码字符串
- 使用querystring解析
- let res = qs.pares(this.location.search.slice(1))得到对象
- push + state:
- 携带数据:this.props.history.push("路由路径",state(携带的数据对象))
- 接收参数:<Route path="路由路径"></Route>,无需接收正常注册即可
- 使用数据:this.props.location.state
- 使用Link或者NavLink进行组件的切换
- params携带数据:
- 携带数据:`<Link to={`路由路径/参数1/参数2...}`}></Link>`
- 接收参数:<Route path="路由路径/:参数1/:参数2..."></Route>
- 使用数据:this.props.match.params
- search携带数据:
- 携带数据:`<Link to={`路由路径/?xxx=参数1&xxx=参数2...`}></Link>`
- 接收参数:<Route path="路由路径"></Route>,无需接收正常注册即可
- 使用数据:
- this.props.location得到的是urlencoded编码字符串
- qs.parse(search.slice(1))
- state携带数据:
- 携带数据:`<Link to={{pathname:'路由路径',state:{携带数据对象}}></Link>`
- 接收参数:<Route path="路由路径"></Route>,无需接收正常注册即可
- 使用数据:this.props.location.state
- 借助this.prosp.history对象上的API对操作路由跳转、前进、后退
- this.prosp.history.push()
- this.prosp.history.replace()
- this.prosp.history.goBack()
- this.prosp.history.goForward()
- this.prosp.history.go()
#### 5.4.8、withRouter
- 用于解决在一般组件无法使用路由组件中props特有的属性
- withRoute不是一个组件
- 它是用于加工一般组件的方法
```jsx
import {withRouter} from "react-router-dom"
// 声明一个一般组件
class Header extends Component {
back = () =>{
this.prosp.history.goBack()
// 实现回退功能
}
render(){
return (
<button onClick={this.back}></button>
)
}
}
export default withRouter(Header)
// 使用withRouter加工Header组件,使其拥有路由组件中的三大属性
5.4.9、BrowserRouter和HashRouter的区别
-
底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值(兼容性更好) -
url中path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test -
刷新后对路由state参数的影响
(1).BrowserRouter没有任何影响,因为state保存在history对象中。
(2).HashRouter刷新后会导致路由state参数的丢失!!! -
备注:HashRouter可以用于解决一些路径错误相关的问题。
最后
以上就是聪慧眼睛为你收集整理的React学习笔记(上)的全部内容,希望文章能够帮你解决React学习笔记(上)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复