预习视频
【最新】零基础快速入门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/>
密 码:<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/>
密 码:<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.js
与 Error.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,然后:
从Home使用state携带参数跳到List,并打印this.props.history.location.state
在Detail执行this.props.history.goBack()
即可看到state丢失
十、数据请求【重要】
安装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。