Part2-传值、路由与请求

预习视频

【最新】零基础快速入门React 17.x

视频P21-P37

今日学习目标

  • 组件传值

  • 生命周期

  • 受控组件与不受控组件

  • 路由搭建

一、组件状态this.state的基本使用【重要】

组件状态this.state的基本使用总结:

// 定义状态数据:
constructor(props){
  super(props)

  this.state = {
    num: 20
  }
}

// 使用状态数据:
return (
  <div>
    <p>{this.state.num}</p>
  </div>
)


// 修改状态数据:
1、通过事件或者定时器触发:
  <button onClick={this.handleClick.bind(this)}>点击增加1</button>

2、在事件函数中:
this.setState({
  num: this.state.num+1
})

二、组件属性this.props【重要】

App5.js中:

//定义一个头部组件(子组件)
import React, { Component } from 'react'

export default class Sub extends Component {
    // 定义默认值,在class内部,通过static修饰的属性,就是静态属性
    static defaultProps = {
        num: 888
    }
    render() {
        return (
            <div>
                <h2>{this.props.num}</h2>
            </div>
        )
    }
}

// 父组件
import React, { Component } from 'react'
import Sub from './Sub'

export default class App extends Component {
    render() {
        return (
            <div>
                <Sub num={123} />
                <Sub />
            </div>
        )
    }
}

三、子组件中限制传进来的props属性的数据类型 【了解】

子组件中可以限制传进来的props属性值的数据类型

实现步骤:

1、先导入import PropTypes from 'prop-types' (注意:无需安装,直接引入即可)

2、在子组件中定义静态属性propTypes:

static propTypes = {
	//props属性名:PropTypes.类型
	title: PropTypes.类型
}

实际运用:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export default class Sub extends Component {
    // 在class内部,通过static修饰的属性,就是静态属性
    static defaultProps = {
        num: 888
    }
    static propTypes = {
        // num: PropTypes.number.isRequired   //设置为isRequired表示必须传值
        num: PropTypes.number
    }
    render() {
        return (
            <div>
                <h2>{this.props.num}</h2>
            </div>
        )
    }
}

四、子传父【重要】

子级通过 this.props.子级事件名(参数) 将事件传递给父级,父级在子组件标签上写:

<tag 子级事件名={this.父组件事件名.bind(this)}></tag>

通过这种方式来接收子级的事件。

例如:

// 子组件
import React, { Component } from 'react'
export default class Sub extends Component {
    // 在class内部,通过static修饰的属性,就是静态属性
    static defaultProps = {
        num: 888
    }
    render() {
        return (
            <div>
                <h2>{this.props.num}</h2>
                <button onClick={this.handleClick.bind(this)}>修改num</button>
            </div>
        )
    }
    handleClick(){
        // 给props对象挂在一个changeNum方法,携带参数999
        this.props.changeNum(999)
    }
}

// 父组件
import React, { Component } from 'react'
import Sub from './Sub'

export default class App extends Component {
    state = {
        num: 123
    }
    render() {
        return (
            <div>
                {/* <Tag 子组件传递的事件名={this.事件名.bind(this)} /> */}
                <Sub num={this.state.num} changeNum={this.changeNum.bind(this)} />
            </div>
        )
    }
    changeNum(arg){
        // console.log(arg);    // 直接获取传递过来的参数
        this.setState({
            num: arg
        })
    }
}

五、使用context进行props属性值的多级传递 【了解】

React 组件之间的通信是基于 props 的数据传递,数据需要一级一级从上到下传递。如果组件级别过多传递就会过于麻烦。React中Context配置可以解决组件跨级值传递。

实现步骤:
     1. 在父组件中,声明数据类型和值
            1.1 声明上下文数据类型
                static childContextTypes = {
                    数据名: PropTypes.数据类型
                }
            1.2 向上下文控件中存值
                getChildContext(){
                    return {
                    	数据: 值
                    }
                }

    2. 在“无论哪一级别”子组件中,只需声明需要的全局上下文数据,就可自动注入子组件的context属性中
        static contextTypes = {
             数据名: 数据类型
        }
        或是:
        static contextTypes = {
             数据名: PropTypes.数据类型
        }

        在子组件中使用:
            {this.context.con}
import React, { Component } from 'react'
import PropTypes from 'prop-types'

// 儿子组件
import React, { Component } from 'react'
import PropTypes from 'prop-types'

export default class Erzi extends Component {
    // 定义上下文类型
    static contextTypes = {
        num: PropTypes.number
    }
    render() {
        return (
            <div>
            		{/* 使用context接收 */}
                <h1>{this.context.num}</h1>
            </div>
        )
    }
}

// 父组件
import React, { Component } from 'react'
import Erzi from './Erzi'

export default class Father extends Component {
    render() {
        return (
            <div>
                <Erzi />
            </div>
        )
    }
}

// 爷爷组件
import React, { Component } from 'react'
import Father from './Father'
import PropTypes from 'prop-types'

export default class Yeye extends Component {
    // 定义子组件上下文类型
    static childContextTypes = {
        num: PropTypes.number
    }
    // 获取子组件上下文
    getChildContext(){
        return {
            num: this.state.num
        }
    }
    state = {
        num: 888
    }
    render() {
        return (
            <div>
                <Father />
            </div>
        )
    }
}

这里敲黑板:

childContextTypes 和 contextTypes 是不能省略的,跨级传值必须指定数据类型

六、React生命周期【重要】

生命周期:就是指一个对象的生老病死。 React的生命周期指从组件被创建到销毁的过程。掌握了组件的生命周期,就可以在适当的时候去做一些事情。

React生命周期可以分成三个阶段:

1、实例化(挂载阶段):对象创建到完全渲染

2、存在期(更新期):组件状态的改变

3、销毁/清除期:组件使用完毕后,或者不需要存在与页面中,那么将组件移除,执行销毁。

1、实例化/挂载阶段

constructor()

componentWillMount()

render()

componentDidMount()

export default class App3 extends Component {
    // 生命周期第一个阶段: 挂载/初始化阶段
    constructor(props){
        console.log("1.1 constructor: 构造初始化")
        // 调用父类构造方法
        super(props)

        // 初始化状态数据
        this.state = {

        }
        // 事件函数this的绑定
        this.handleClick = this.handleClick.bind(this)

    }

    UNSAFE_componentWillMount(){
        console.log("1.2 componentWillMount")
        //做一些准备性的工作,比如提示正在加载
    }

    componentDidMount() {
        console.log("1.4 componentDidMount")
        //异步加载数据
    }
    

    handleClick(){
        alert("点击了p标签")
    }

    render() {
        console.log("1.3 render")
        return (
            <div>
                <p onClick={this.handleClick}>这是一个展示生命周期的组件</p>
                <p onClick={this.handleClick}>这是一个展示生命周期的组件</p>
                <p onClick={this.handleClick}>这是一个展示生命周期的组件</p>
            </div>
        )
    }
}

2、存在期/更新期

存在期:且件已经渲染好并且用户可以与它进行交互。通常是通过一次鼠标点击、手指点按者键盘事件来触发一个事件处理器。随着用户改变了组件或者整个应用的state,便会有新的state流入组件树,并且我们将会获得操控它的机会。

componentWillReceiveProps()

shouldComponentUpdate()

componentWillUpdate()

render()

componentDidUpdate()

在上面的组建中继续书写生命周期函数:

	render() {
        console.log("1.3/2.4 render")
        return (
            <div>
                <p onClick={this.handleClick}>这是一个展示生命周期的组件</p>
                <p onClick={this.handleClick}>这是一个展示生命周期的组件</p>
                <p onClick={this.handleClick}>这是一个展示生命周期的组件{this.state.num}</p>
            </div>
        )
    }

	handleClick(){
        this.setState({
            num:22
        })
    }
	
    // 生命周期第二个阶段  存在期/更新期
    componentWillReceiveProps(){
        console.log("2.1 componentWillReceiveProps")
    }
    shouldComponentUpdate(nextProps, nextState) {
        console.log("2.2 shouldComponentUpdate 可以判断修改前后的值是不是一样,不一样才去执行render。减少不必要的render,优化更新性能")
        console.log("旧的值:", this.state.num)
        console.log("新的值:", nextState.num)

        // return true   则执行render
        // return false   则不执行render
        //这里返回值是布尔值,但不应该写死,
        //而是判断新旧两个值是不是不相等,不相等就要执行render(就要返回true)
        return this.state.num !== nextState.num

    }
    componentWillUpdate(nextProps, nextState) {
        console.log("2.3 componentWillUpdate 更新前的生命周期回调")
    }
    componentDidUpdate(prevProps, prevState) {
        console.log("2.5 componentDidUpdate 更新后的生命周期回调")
    }

以上执行的是组件内部state数据更新前后的生命周期函数,

其实,对于组件的props属性值发生改变的时候,同样需要更新视图,执行render

componentWillReceiveProps() 这个方法是将要接收新的props值的时候执行,而props属性值从父组件而来,所以需要定义父组件:

class App3 extends Component {
    ...
    
	//生命周期第二个阶段  存在期/更新期
    UNSAFE_componentWillReceiveProps(nextProps){
        console.log("2.1 componentWillReceiveProps 这个方法props属性值更新的时候才会执行,更新state数据则不会执行这个方法")
        
        console.log(nextProps)
    }
	...
    shouldComponentUpdate(nextProps, nextState) {
        console.log("2.2 shouldComponentUpdate 可以判断修改前后的值是不是一样,不一样才去执行render。减少不必要的render,优化更新性能")

        // return true   则执行render
        // return false   则不执行render
        //这里返回值是布尔值,但不应该写死,
        //而是判断新旧两个值是不是不相等,不相等就要执行render(就要返回true)
        return (this.state.num !== nextState.num || this.props.fatherNum !== nextProps.fatherNum)  //不仅仅是state数据跟新时候需要执行render,props属性的值更新的时候也要执行render,所以要多加一个判断条件

    }
}

export default class Father extends Component{
    constructor(props){
        // 调用父类构造方法
        super(props)

        // 初始化状态数据
        this.state = {
            fatherNum:0
        }

    }

    componentDidMount() {
        setTimeout(() => {
            this.setState({
                fatherNum:10
            })
        }, 2000);
    }
    

    render(){

        return(
            <App3 fatherNum={this.state.fatherNum}/>
        )
    }
}

3、销毁期

componentWillUnmount() 销毁组件前做一些回收的操作

	componentDidMount() {
        console.log("1.4 componentDidMount")
        console.log("------------------------------------------")
        
        document.addEventListener("click", this.closeMenu);
    }
    closeMenu(){
        console.log("click事件 closeMenu")
    }

	// 生命周期第三个阶段  卸载期/销毁期
    componentWillUnmount() {
        console.log("3.1 componentWillUnmount 做一些回收的操作")
        document.removeEventListener("click", this.closeMenu);
    }

index.js中3秒后重新渲染页面:

setTimeout(() => {
    ReactDOM.render(
        <div>hello world</div>
        , document.getElementById('root'));
}, 3000);

4、生命周期小结

React组件的生命周期 3大阶段10个方法 1、初始化期(执行1次) 2、存在期 (执行N次) 3、销毁期 (执行1次)

小结: componentDidMount : 发送ajax异步请求

​ shouldComponentUpdate : 判断props或者state是否改变,目的:优化更新性能

七、不受控组件和受控组件【重要】

1、不受控组件

​ 组件中表单元素没有写value值,与state数据无关,不受组件管理。(不推荐)

//不受控组件
import React, { Component } from 'react'

export default class App4 extends Component {

        
    handleClick(){
        console.log(this.refs.username.value)
        console.log(this.refs.password.value)
    }
    render() {
        return (
            <div>
                {/* 这种组件和组件本身state数据没有关系,所以不受组件管理,称为不受控组件(无约束组件)*/}
                {/* 这种写法的每个表单元素的验证在登录按钮的点击事件完成,用户体验很差 */}
                用户名:<input type="text" ref="username"/> <br/> <br/>
                密&emsp;码:<input type="text" ref="password"/><br/> <br/>
                <button onClick={this.handleClick.bind(this)}>登录</button>
            </div>
        )
    }
}

2、受控组件

​ 组件中表单元素的value值受组件的state数据控制,并且该表单元素有onChange事件,我们可以在事件中对该表单做实时验证,验证值是否合法然后做相应的操作(推荐)

//受控组件
import React, { Component } from 'react'

export default class App4 extends Component {

    constructor(props){
        // 调用父类构造方法
        super(props)

        // 初始化状态数据
        this.state = {
            value:"admin"
        }
        // 事件函数this的绑定
        this.handleChange = this.handleChange.bind(this)

    }

    handleChange(e){
        console.log(e.target.value)
        this.setState({
            value:e.target.value
        })
        
        //可以在这个方法内部做一些实时验证,验证值是否合法做相应的操作
    }
    render() {
        return (
            <div>
               
                用户名:<input type="text" value={this.state.value} onChange={this.handleChange}/> <br/> <br/>
                密&emsp;码:<input type="text" /><br/> <br/>
                <button>登录</button>
            </div>
        )
    }
}

3、小结

React中的表单组件,分为2类: 1. 不受控组件(和状态无关) 1.1 在input标签组件中,使用ref属性,来代表组件标识 1.2 组件渲染后,会自动把有ref属性的dom对象,挂到this.refs上 this.refs = { ref名1 : dom对象 ref名2 : dom对象 } 1.3 在需要的场景(一般指回调),使用this.refs来获取需要的对象,然后再做dom操作

​ 2、 受控组件(和状态紧密相关) ​ 2.1 初始化状态 ​ 2.2 在input标签的value属性上,绑定状态(输入框的默认值和状态有关) ​ 2.3 在input标签上维护onChange属性,触发时即时修改状态 ​ 2.4 自动:当状态修改,立即触发声明周期的render方法,显示最先的状态值

使用: 如果对值的控制度不高,直接不受控组件 如果要严格把控值的操作,受控可以做表单验证等严格的逻辑(推荐)

八、路由原理【了解】

SPA: Single Page Application

例如:https://music.163.com/#/

现实生活中的路由:用来管理网络和计算机之间的关系

程序中的路由:用来管理url地址视图之间的关系

路由原理:

​ 1、准备视图(html)

​ 2、准备路由的路线(可以是一个对象,键名是路线名称和值是视图地址)

​ 3、通过hash地址的路线,获取“视图地址”

​ 4、在指定标签中,加载需要的视图页面

代码演示:

router.html

	<!-- 1、定义视图,(点击)改变哈希地址 -->
    <ul>
        <li><a href="#/index">首页</a></li>  
        <li><a href="#/list">列表页</a></li>
        <!-- 注意:href填的是hash地址,这里的 #/ 不要去掉 ,为的是不改变域名(地址栏中#前面的为域名)-->
    </ul>
    <div id="routerView"></div>

    <script>
        // 2、定义路由
        let louter = {
            //路由名称     视图页面
            "#/index": "./index.html",
            "#/list": "./list.html"
        }
        // 通过 louter[键名] 来获取值
        console.log(louter["#/index"])
    
        //3、检测哈希地址的改变(事件监听)
        window.addEventListener('hashchange',()=>{
            console.log("location.hash为: ", location.hash)   //location.hash可以获取哈希地址
            // 可以看出location.hash就是我们的键,我们可以通过 louter[键名] 来获取值,即获取视图页面

            let url = louter[location.hash]
            // console.log(url)
            // 加载对应视图
            // 在<div id="routerView"></div>中load加载需要的视图页面
            $("#routerView").load(url)

        })
        

    </script>

记得在head中添加jq

<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>

九、路由【重要】

react-router是跨平台的,内置通用组件和通用Hooks。react-router-dom是在react-router基础上提供了Link和NavLink,而且依赖history库提供了两个浏览器端适用的BrowserRouter和HashRouter。

Router4.x以前的版本,一般都有react-router。

Router4.x以后的版本,一般都指react-router-dom,一些常用的组件都封装好了。现在的项目基本都是使用react-router-dom。

1、安装

npm install react-router-dom@5.2.0 --save-dev

# 或
yarn add react-router-dom@5.2.0

2、创建页面

然后我们新建两个页面,分别命名为“home”和“detail”。在页面中编写如下代码:

// Home.js
import React, { Component } from 'react'

export default class Home extends Component {
    render() {
        return (
            <div>
                <h1>首页</h1>
                <button>去详情页</button>
            </div>
        )
    }
}

// Detail.js
import React, { Component } from 'react'

export default class Detail extends Component {
    render() {
        return (
            <div>
                <h1>详情页</h1>
                <button>回首页</button>
            </div>
        )
    }
}

3、创建router组件

然后再新建一个路由组件,命名为 router.js,并编写如下代码:

import React from 'react'
/* 
    路由有两种模式:
    一种是HashRouter,带有#,
    一种是BrowserRouter,不带#,但需要后端支持修改根目录地址
    开发阶段可以先使用BrowserRouter
*/
import {BrowserRouter as Router, Route, Switch, useHistory} from 'react-router-dom'
import Home from './Home'
import Detail from './Detail'

// 定义一个纯路由组件
const BasicRoute = () => {
    return (
        <Router history={useHistory}>
            {/* 套用Switch作路由匹配 */}
            <Switch>
                <Route exact path="/home" component={Home}></Route>
                <Route exact path="/detail" component={Detail}></Route>
            </Switch>
        </Router>
    )
}

export default BasicRoute;

字段解释:

以上代码中,标签上有个exact属性,这个属性是用来正确匹配子路由的。route的匹配规则有点类似于正则的贪婪匹配,为了避免这种情况,我们需要使用准确的匹配规则,这个时候就需要使用到route的两个属性(exact与strict)。

exact属性为true时路径中的hash值必须和path完全一致才渲染对应的组件,如果为false则'/'也可以匹配'/xxx';(如果strict属性为false,则末尾是否包含反斜杠结尾不影响匹配结果)

strict属性主要就是匹配反斜杠,规定是否匹配末尾包含反斜杠的路径,如果strict为true,则如果path中不包含反斜杠结尾,则他也不能匹配包含反斜杠结尾的路径,这个需要和exact结合使用

总结:

如果没有子路由的情况,建议大家配都加一个exact;如果有子路由,建议在子路由中加exact,父路由不加; 而strict是针对是否有反斜杠的,一般可以忽略不配置。

4、根路径与404页面

项目需要根路径与404页面,因此我们可以创建两个文件:MyIndex.jsError.js,分别写入:

// MyIndex.js
import React, { Component } from 'react'

export default class MyIndex extends Component {
    render() {
        return (
            <div>
            		<ul>
                    <li><a href="/home">首页</a></li>
                    <li><a href="/detail">详情页</a></li>
                </ul>
            		{/* 用于显示下方的路由插槽内容 */}
                <div className="box">{this.props.children}</div>
            </div>
        )
    }
}

// Error.js
import React, { Component } from 'react'

export default class Error extends Component {
    render() {
        return (
            <div>
                404
            </div>
        )
    }
}

然后在 router.js 中引入并进行重定向:

import React from 'react'
// Redirect用作重定向
import { BrowserRouter as Router, Route, Switch, Redirect, useHistory } from 'react-router-dom'
import Home from './Home'
import Detail from './Detail'
import Error from './Error'
import MyIndex from './MyIndex'

const BasicRoute = () => {
    return (
        <Router history={useHistory}>
            {/* 套用Switch作路由匹配 */}
            <Switch>
                <Route path="/" component={() =>
                    <MyIndex>
                      <Switch>
                        <Route path="/home" component={Home}></Route>
                        <Route path="/detail" component={Detail}></Route>
                        <Route component={Error}></Route>
                      </Switch>
                    </MyIndex>
                }>
                </Route>
            </Switch>
        </Router>
    )
}

export default BasicRoute;

如果你还希望 Home.js 进入时直接重定向到 /home/sub1,可以在 Home.js 中使用:

import {withRouter} from 'react-router-dom'

class xxx extends Component {
    UNSAFE_componentWillMount(){
      if(this.props.history.location.pathname==='/'){
            // replace重定向
        		this.props.history.replace('/home');
        }
    }
}

export default withRouter(xxx)

5、路由引入

然后在 index.js 中引入:

import ReactDOM from 'react-dom'
// import App from './App1'
import Router from './router'

ReactDOM.render(
    <Router />,
    document.getElementById('root')
)

此时,到浏览器输入 http://localhost:3000/#/http://localhost:3000/#/detail 就可以看到对应的页面。

备注:

其实我们也可以创建一个 routerConfig.js 的文件,专门用来做路由配置:

import Home from './Home'
import List from './List'
import Error from './Error'


const routerConfig = [
    {pathname: "/home", component: Home, exact: true},
    {pathname: "/list", component: List, exact: true},
    {pathname: "*", component: Error, exact: false}
]

export default routerConfig

然后引入到 router.js 中,如:

import { BrowserRouter as Router, Switch, Route, useHistory, Redirect } from 'react-router-dom'
import App from './App'
import routerConfig from './routerConfig'

const BasicRoute = () => {
  return (
    <Router history={useHistory}>
      <Switch>
        <Route path="/" component={() => {
            return (
              <App>
                <Switch>
                  {
                    routerConfig.map((item, index) => {
                      return <Route key={index} exact={item.exact} path={item.pathname} component={item.component}></Route>
                    })
                  }
                </Switch>
              </App>
            )
          }}></Route>
      </Switch>
    </Router>
  )
}

export default BasicRoute;

6、通过事件跳转页面

给按钮加上onClick事件,通过this.props.history.push这个函数跳转到detail页面。在路由组件中加入的代码就是将history这个对象注册到组件的props中去,然后就可以在子组件中通过props调用history的push方法跳转页面。

Home.js 中:

import React, { Component } from 'react'

export default class Home extends Component {
    render() {
        return (
            <div>
                <h1>首页</h1>
                <button onClick={()=>{this.props.history.push('detail')}}>去详情页</button>
            </div>
        )
    }
}

如果想返回上一页,可以在 Detail.js 中:

import React, { Component } from 'react'

export default class Detail extends Component {
    render() {
        return (
            <div>
                <h1>详情页</h1>
                <button onClick={this.goback.bind(this)}>回首页</button>
            </div>
        )
    }
    goback(){
      	// go(-1)可以回到上一页,goBack()也可以返回
        // this.props.history.go(-1);
      	this.props.history.goBack();
    }
}

7、传参

很多场景下,我们还需要在页面跳转的同时传递参数,在react-router-dom中,同样提供了两种方式进行传参。

* url传参

{/* Home.js */}
<button onClick={()=>{this.props.history.push('/detail/88')}}>去详情页</button>

{/* router.js   react-router-dom就是通过“/:”去匹配url传递的参数 */}
<Route exact path="/detail/:id" component={Detail}></Route>

{/* Detail.js中获取参数 */}
componentDidMount(){
  console.log(this.props.match.params);
}

* 隐式传参

{/* Home.js */}
<button onClick={()=>{this.props.history.push({
    pathname: '/detail',
    state: {
      id: 777
    }
  })}}>去详情页</button>

{/* router.js */}
<Route exact path="/detail" component={Detail}></Route>

{/* Detail.js中获取参数 */}
componentDidMount(){
  console.log(this.props.history.location.state);
}

8、【验证】HashRouter模式下,goBack()方法会造成state丢失

为了验证这个bug,我们首先将 BrowserRouter 切换为 HashRouter,然后:

  1. 从Home使用state携带参数跳到List,并打印this.props.history.location.state

  2. 从List使用url携带参数跳到Detail

  3. 在Detail执行this.props.history.goBack()

即可看到state丢失

十、数据请求【重要】

安装axios:

yarn add axios

在组件中引入:

import axios from 'axios';

const baseUrl = 'http://47.93.114.103:3001'

export default class App4 extends Component {
    ...
    componentDidMount(){
        axios.get(baseUrl+'/nav')
            .then(res=>{
                console.log(res)
            })
    }
}

十一、作业

作业1(初级)

请完成以下demo的跳转与布局(手写css)。

项目地址:http://codesohigh.com/manage/

作业2(中级)

使用react+bootstrap进行布局。

作业3(高级)

预习Redux与ReactRedux

最后更新于