第1章 React入门
1.1 React的基本认识
1.1.1 官网
1) 英文官网: https://reactjs.org/
2) 中文官网: https://doc.react-china.org/
1.1.2 介绍描述
1) 用于构建用户界面的 JavaScript 库(只关注于View)
- JS库:
- jQuery——函数库(方法、函数包装DOM操作)
- React 基本上不操作DOM——JS框架
- 构建用户界面:把数据展现出来
2) 由Facebook开源
1.1.3 React的特点
1) Declarative(声明式编码)
(申请一块内存,只需要声明一个变量即可)不需要亲自操作DOM,只需要告诉它,我要更新,就会帮你更新,只需要更新数据,界面不需要手动更新(以前需要更新DOM)
2) Component-Based(组件化编码)
简化特别复杂的功能,可以拆分为多个简单的部分(一个小的界面功能就是一个组件),维护也方便
3) Learn Once, Write Anywhere(支持客户端与服务器渲染)
一次学习,随处编写:不仅能写web应用,还能写React Native打包为Android、IOS应用
4) 高效
5) 单向数据流
1.1.4 React高效的原因(区域、次数——更新界面效率提高)
1) 虚拟(virtual)DOM,,不总是直接操作DOM
- 虚拟DOM:对象——与组件对应,修改映射到真实的DOM上(批量修改、界面重绘次数少)
2) DOM Diff算法,,最小化页面重绘
- 界面中组件是否更新(更新区域小)
1.2 React的基本使用
注意: 此时只是测试语法使用, 并不是真实项目开发使用
1.2.1 效果
将h1标签利用react放到test中
1.2.3 相关js库
可以到bootcdn引用地址,访问链接Ctrl+S保存到本地
1) react.js
: React的核心库
- development.js:开发版,开发编写的时候使用
- production.min.js:生产版,上线的时候使用,压缩过的
2) react-dom.js
: 提供操作DOM的react扩展库
3) babel.min.js
: 解析JSX语法代码转为纯JS语法代码的库,这里不是ES6转ES5(jsx是js扩展语法)
1.2.4 在页面中导入js
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
1.2.5 编码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>01_HelloWorld</title>
</head>
<body>
<div id="test"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">/*告诉babel.js解析里面的jsx的代码*/
// 1. 创建虚拟DOM元素对象
var vDom = <h1>Hello React!</h1> // jsx,不是字符串,不能加引号
// 2. 将虚拟DOM渲染到页面真实DOM容器中
ReactDOM.render(vDom, document.getElementById('test')) // react-dom.js提供的 render——渲染 将vDom加入到#test中
</script>
</body>
</html>
1.2.6 使用React开发者工具调试
React Developer Tool.crx
1.3 React JSX
1.3.1 效果
两个#test分别加入相应内容
代码:
<div id="test1"></div>
<div id="test2"></div>
<script src="../js/react.development.js"></script>
<script src="../js/react-dom.development.js"></script>
<script src="../js/babel.min.js"></script>
<script> // 还没用到jsx语法,不需要babel
const msg = 'I Like You!'
const myId = 'Atguigu'
// 1.创建虚拟DOM
// var element = React.createElement('h1', {id:'myTitle'},'hello')
const vDom1 = React.createElement('h2', {id:myId.toLowerCase()},msg.toUpperCase())
// 2.渲染虚拟DOM
ReactDOM.render(vDom1, document.getElementById('test1'))
</script>
<script type="text/babel">
// 1.创建虚拟DOM
const vDom2 = <h3 id={myId.toUpperCase()}>{msg.toLowerCase()}</h3> // 变量用{}括起来
// 2.渲染虚拟DOM
ReactDOM.render(vDom2, document.getElementById('test2'))
</script>
1.3.2 虚拟DOM
1) React提供了一些API来创建一种 特别 的一般js对象
a. var element = React.createElement('h1', {id:'myTitle'},'hello')
b. 上面创建的就是一个简单的虚拟DOM对象,babel将会把jsx语法转为上述的形式
2) 虚拟DOM对象最终都会被React转换为真实的DOM(虚拟DOM中的对应真实DOM中的标签元素)
3) 我们编码时基本只需要操作react的虚拟DOM相关数据, react会转换为真实DOM变化而更新界面
补充知识:debugger
可以在某条js代码处添加断点
虚拟DOM——轻对象,更新虚拟DOM页面不重绘
真实DOM——重对象,更新真实DOM页面会发生变化(页面重绘)
1.3.3 JSX
1) 全称: JavaScript XML
2) react定义的一种类似于XML的JS扩展语法: XML+JS
3) 作用: 用来创建react虚拟DOM(元素)对象
var ele = <h1>Hello JSX!</h1>
注意1: 它不是字符串, 也不是HTML/XML标签
注意2: 它最终产生的就是一个JS对象
4) 标签名任意: HTML标签或其它标签
5) 标签属性任意: HTML标签属性或其它
6) 基本语法规则
遇到 <开头的代码, 以标签的语法解析: html同名标签转换为html同名元素, 其它标签需要特别解析
遇到以 { 开头的代码,以JS语法解析: 标签中的js代码必须用{ }包含
7) babel.js的作用
浏览器不能直接解析JSX代码, 需要babel转译为纯JS的代码才能运行
只要用了JSX,都要加上
type="text/babel"
, 声明需要babel来处理
1.3.4 渲染虚拟DOM(元素)
1) 语法: ReactDOM.render(virtualDOM, containerDOM)
2) 作用: 将虚拟DOM元素渲染到页面中的真实容器DOM中显示
3) 参数说明
参数一: 纯js或jsx创建的虚拟dom对象
参数二: 用来包含虚拟DOM元素的真实dom元素对象(一般是一个div)
1.3.5 建虚拟DOM的2种方式
1) 纯JS(一般不用)
React.createElement('h1', {id:'myTitle'}, title)
2) JSX:
<h1 id='myTitle'>{title}</h1>
1.3.6 JSX练习
需求: 动态展示列表数据
代码:
<h2>前端JS框架列表</h2>
<div id="example1"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
功能: 动态展示列表数据
- 如何将一个数据的数组,转换为一个标签的数组
使用数组的map()方法
*/
// 数据:名称、类型,数组存放
const names = ['jQuery', 'zepto', 'angular', 'react', 'vue']
// 1.创建虚拟DOM,有嵌套结构,最好用小括号括起来
const ul = (
<ul>
{
names.map((name, index) => <li key={index}>{name}</li>)
}
</ul>
)
// 2.渲染虚拟DOM
ReactDOM.render(ul, document.getElementById('example1'))
</script>
1.4 模块与组件和模块化与组件化的理解
1.4.1 模块
1) 理解: 向外提供特定功能的js程序, 一般就是一个js文件
2) 为什么: js代码更多更复杂
3) 作用: 复用js, 简化js的编写, 提高js运行效率
有特定功能的js文件,内部有数据及对数据的操作
数据:变量
操作:函数
私有的函数向外暴露
- 暴露一个函数:暴露函数本身
- 暴露多个函数:以对象形式暴露
1.4.2 组件
1) 理解: 用来实现特定(局部)功能效果的代码集合(html/css/js)
2) 为什么: 一个界面的功能更复杂
3) 作用: 复用编码, 简化项目编码, 提高运行效率
1.4.3 模块化
当应用的js都以模块来编写的, 这个应用就是一个模块化的应用
形容项目或编码
1.4.4 组件化
当应用是以多组件的方式实现, 这个应用就是一个组件化的应用
第2章 React面向组件编程
面向对象——面向模块——面向组件
2.1 基本理解和使用
2.1.1 效果
组件标签:可以随便取的标签,首字母大写,与HTML标签区分开
2.1.2 自定义组件(Component) :
1) 定义组件(2种方式)
方式1: 工厂函数组件(简单组件:没有状态的组件)——效率高,不需要创建对象
function MyComponent(){
return <h2>工厂函数组件(简单组件)</h2>
}
方式2: ES6类组件(复杂组件)——需要创建对象,有了状态只能使用这种方式
class MyComponent2 extends React.Component{
render(){
console.log(this) // 组件类对象 MyComponent2{...}
return <h2>ES6类组件(复杂组件)</h2>
}
}
2) 渲染组件标签
ReactDOM.render(<MyComponent />, document.getElementById('example1'))
ReactDOM.render(<MyComponent2 />, document.getElementById('example2'))
2.1.3 注意
1) 组件名必须首字母大写
2) 虚拟DOM元素只能有一个根元素
3) 虚拟DOM元素必须有结束标签
2.1.4 render()渲染组件标签的基本流程
1) React内部会创建组件实例对象
2) 得到包含的虚拟DOM并解析为真实DOM
3) 插入到指定的页面元素内部
2.2 组件三大属性1: state
2.2.1 效果
2.2.2 理解
1) state是组件对象最重要的属性, 值是对象(可以包含多个数据)
2) 组件被称为”状态机”, 通过更新组件的state来更新对应的页面显示(重新渲染组件)
2.2.3 编码操作
1) 初始化状态:
constructor (props) {
super(props)
this.state = {
stateProp1 : value1,
stateProp2 : value2
}
}
2) 读取某个状态值
this.state.statePropertyName
3) 更新状态—->组件界面更新
this.setState({
stateProp1 : value1,
stateProp2 : value2
})
2.2.4 代码
<div id="example"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
需求: 自定义组件, 功能说明如下
1. 显示h2标题, 初始文本为: 你喜欢我
2. 点击标题更新为: 我喜欢你
*/
// 1.定义组件
class Like extends React.Component {
constructor(props){
super(props)
// 初始化状态
this.state = {
isLikeMe: false
}
// 将新增方法中的this强制绑定为组件对象
this.handleClick = this.handleClick.bind(this) // 也可以不在这里绑定
}
handleClick(){
// console.log(this) // handleClick是新添加方法,内部this默认不是组件对象,而是undefined render是重写组件类的方法 到上面或下面绑定this
// 得到状态,并取反
const isLikeMe = !this.state.isLikeMe
// 更新状态
this.setState({isLikeMe}) // isLikeMe: isLikeMe
}
render() {
// 读取状态
// const isLikeMe = this.state.isLikeMe
const {isLikeMe} = this.state // 解构赋值
return <h2 onClick={this.handleClick}>{isLikeMe?'你喜欢我':'我喜欢你'}</h2> // this——组件对象 // this.handleClick.bind(this) —— 在这里绑定也可以
}
}
// 2.渲染组件标签
ReactDOM.render(<Like />, document.getElementById('example'))
</script>
2.3 组件三大属性2: props
2.3.1 效果
需求: 自定义用来显示一个人员信息的组件
1). 姓名必须指定
2). 如果性别没有指定, 默认为男
3). 如果年龄没有指定, 默认为18
2.3.2 理解
1) 每个组件对象都会有props(properties的简写)属性
2) 组件标签的所有属性都保存在props中
2.3.3 作用
1) 通过标签属性从组件外向组件内传递变化的数据
2) 注意: 组件内部不要修改props数据
2.3.4 编码操作
1) 内部读取某个属性值
this.props.propertyName
2) 对props中的属性值进行类型限制和必要性限制
注意:
自 React v15.5 起,
React.PropTypes
已移入另一个包中。请使用prop-types
库 代替。需要先引入该库。我们提供了一个 codemod 脚本来做自动转换。
Person.propTypes = {
name: React.PropTypes.string.isRequired,
age: React.PropTypes.number.isRequired
}
3) 扩展属性: 将对象的所有属性通过props传递
<Person {...person}/>
4) 默认属性值
Person.defaultProps = {
name: 'Mary'
}
5) 组件类的构造函数
constructor (props) {
super(props)
console.log(props) // 查看所有属性
}
2.3.5 代码
<div id="example1"></div>
<div id="example2"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/prop-types.js"></script> <!-- 验证类型和必要性 -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
需求: 自定义用来显示一个人员信息的组件, 效果如页面. 说明
1). 如果性别没有指定, 默认为男
2). 如果年龄没有指定, 默认为18
*/
// 1、定义组件
/*function Person(props){
return (
<ul>
<li>姓名:{props.name}</li>
<li>性别:{props.sex}</li>
<li>年龄:{props.age}</li>
</ul>
)
}*/
class Person extends React.Component{
render(){
return (
<ul>
<li>姓名:{this.props.name}</li> // this——组件对象
<li>性别:{this.props.sex}</li>
<li>年龄:{this.props.age}</li>
</ul>
)
}
}
// 指定属性默认值
Person.defaultProps = {
sex: '男',
age: 18
}
// 指定属性值的类型和必要性
Person.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
// 2、渲染组件标签
const p1 = {
name: 'Tom',
sex: '女',
age: 18
}
// ReactDOM.render(<Person name={p1.name} sex={p1.sex} age={p1.age}/>, document.getElementById('example1'))
ReactDOM.render(<Person {...p1}/>, document.getElementById('example1')) // ...作用:1.打包:function fn(...as){} fun(1,2,3) 2.解包:const arr1=[1,2,3] const arr2=[6,...arr1,9] 这里也是在解包
const p2 = {
name: 'JACK',
age: 17
}
ReactDOM.render(<Person name={p2.name} age={p2.age}/>, document.getElementById('example2'))
</script>
2.3.6 面试题
问题: 请区别一下组件的props和state属性
1) state: 组件自身内部可变化的数据
2) props: 从组件外部向组件内部传递数据,组件内部只读不修改
2.4 组件三大属性3: refs与事件处理
2.4.1 效果
需求: 自定义组件, 功能说明如下:
- 点击按钮, 提示第一个输入框中的值
- 当第2个输入框失去焦点时, 提示这个输入框中的值
2.4.2 组件的3大属性之二: refs属性
1) 组件内的标签都可以定义ref属性来标识自己
a. <input type=”text” ref={input => this.msgInput = input}/>
b. 回调函数在组件初始化渲染完或卸载时自动调用
2) 在组件中可以通过this.msgInput来得到对应的真实DOM元素
3) 作用: 通过ref获取组件内容特定标签对象, 进行读取其相关数据
2.4.3 事件处理
1) 通过onXxx属性指定组件的事件处理函数(注意大小写)
a. React使用的是自定义(合成)事件, 而不是使用的原生DOM事件
b. React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
2) 通过event.target得到发生事件的DOM元素对象
handleFocus(event) {
event.target // 返回input对象
}
2.4.4 强烈注意
1) 组件内置的方法中的this为组件对象
2) 在组件类中自定义的方法中this为null
a. 强制绑定this: 通过函数对象的bind()
b. 箭头函数(ES6模块化编码时才能使用)
2.4.5 代码
<div id="example"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
需求: 自定义组件, 功能说明如下:
1. 界面如果页面所示
2. 点击按钮, 提示第一个输入框中的值
3. 当第2个输入框失去焦点时, 提示这个输入框中的值
*/
// 1、定义组件 // React中必须要结束标签
class MyComponent extends React.Component{
constructor(props){
super(props)
this.showInput = this.showInput.bind(this)
this.handleBlur = this.handleBlur.bind(this)
}
showInput(){
const input = this.refs.content
// alert(input.value)
alert(this.input.value)
}
handleBlur(event){
alert(event.target.value)
}
render(){
return(
<div>
<input type="text" ref="content"/>
<input type="text" ref={input => this.input = input}/>
<button onClick={this.showInput}>提示输入</button>
<input type="text" placeholder="失去焦点提示内容" onBlur={this.handleBlur}/>
</div>
)
}
}
// 2、渲染组件标签
ReactDOM.render(<MyComponent/>, document.getElementById('example'))
</script>
2.4.6 区别
- state
- props
- refs
2.5 组件的组合
2.5.1 效果
功能: 组件化实现此功能
1. 显示所有todo列表
2. 输入文本, 点击按钮显示到列表的首位, 并清除输入的文本
2.5.2 功能界面的组件化编码流程(无比重要)
1) 拆分组件: 拆分界面,抽取组件
2) 实现静态组件: 使用组件实现静态页面效果(只有静态界面,没有动态数据和交互)
3) 实现动态组件
实现初始化数据动态显示
实现交互功能(从绑定事件监听开始)
2.5.3 代码
<div id="example"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
// 静态组件——>动态组件
/*
* -名称、类型:todos、数组,Add需要、List需要
* -数据保存在哪个组件内?
* 放到App
* 看数据是某个组件需要(给这个),还是某些组件需要(给共同的父组件)
* -需要在子组件中改变父组件的状态
* 子组件中不能直接改变父组件的状态
* 状态在哪个组件,更新状态的行为就应该定义在哪个组件,由子组件来调用(通过组件属性传递)
* 父组件定义函数,传递给子组件,子组件调用
*/
class App extends React.Component{
constructor(props){
super(props)
// 初始化状态
this.state = {
todos: ['吃饭', '睡觉', '敲代码', '打游戏']
}
this.addTodo = this.addTodo.bind(this) // 没定义加上bind
}
addTodo(todo){
// this.state.todos.unshift(todo) // 不能这样做
const {todos} = this.state
todos.unshift(todo)
// 更新状态
this.setState({todos})
}
render(){
const {todos} = this.state
return(
<div>
<h1>Simple TODO List</h1>
<Add count={todos.length} addTodo={this.addTodo}/>
<List todos={todos}/>
</div>
)
// <List todos={this.state.todos}/> 前面赋值了
}
}
class Add extends React.Component{
constructor(props){
super(props)
this.add = this.add.bind(this)
}
add(){
// 1、读取输入的数据
const todo = this.todoInput.value.trim()
// 2、检查合法性
if(!todo){
return
}
// 3、添加
this.props.addTodo(todo)
// 4、清除输入
this.todoInput.value = ''
}
render(){
return(
<div>
<input type="text" ref={input => this.todoInput=input}/>
<button onClick={this.add}>add #{this.props.count+1}</button>
</div>
)
}
}
Add.propTypes = {
count: PropTypes.number.isRequired,
addTodo: PropTypes.func.isRequired
}
class List extends React.Component{
render(){
return(
<ul>
{
this.props.todos.map((todo, index) => <li key={index}>{todo}</li>)
}
</ul>
)
/*this.props.todos.map((todo, index) => {return <li key={index}>{todo}</li>})*/
/* 加了大括号需要加return */
}
}
List.protoTypes = {
todos: PropTypes.array.isRequired
}
ReactDOM.render(<App />,document.getElementById('example'))
</script>
2.6 收集表单数据
2.6.1 效果
需求: 自定义包含表单的组件
1. 输入用户名密码后, 点击登陆提示输入信息
3. 不提交表单
2.6.2 理解
1) 问题: 在react应用中, 如何收集表单输入数据
2) 包含表单的组件分类
a. 受控组件: 表单项输入数据能自动收集成状态
b. 非受控组件: 需要时才手动读取表单输入框中的数据
2.6.3 代码
<div id="example"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
需求: 自定义包含表单的组件
1. 界面如下所示
2. 输入用户名密码后, 点击登陆提示输入信息
3. 不提交表单
*/
class LoginForm extends React.Component{
constructor(props){
super(props)
// 初始化状态
this.state = {
pwd: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
}
handleSubmit(event){
const name = this.nameInput.value // 非受控组件
const {pwd} = this.state // 受控组件
alert(`准备提交的用户名为${name},密码为${pwd}`)
// 阻止事件的默认行为
event.preventDefault()
}
// 当事件和标签是同一个时,使用event更方便
handleChange(event){
// 读取输入的值
const pwd = event.target.value
// 更新pwd的状态
this.setState({pwd})
}
render(){
return(
<form action="/test" onSubmit={this.handleSubmit}>
用户名:<input type="text" ref={input => this.nameInput = input}/>
密码:<input type="password" value={this.state.pwd} onChange={this.handleChange}/>
<input type="submit" value="登录"/>
</form>
)
}
// 原生jsonChange事件是在失去焦点时触发,react中是输入则触发
}
ReactDOM.render(<LoginForm />, document.getElementById('example'))
</script>
2.7 组件生命周期
2.7.1 效果
需求: 自定义组件
1. 让指定的文本做显示/隐藏的渐变动画
2. 切换持续时间为2S
3. 点击按钮从界面中移除组件界面
2.7.2 理解
1) 组件对象从创建到死亡它会经历特定的生命周期阶段
2) React组件对象包含一系列的勾子函数(生命周期回调函数), 在生命周期特定时刻回调
3) 我们在定义组件时, 可以重写特定的生命周期回调函数, 做特定的工作
2.7.3 生命周期流程图
Mount:挂载,将虚拟标签放到容器(页面)中
render()渲染
左边初始化过程,这些方法称为声明周期回调函数,或称为生命周期的勾子,这些方法在特定的时刻调用
(回调函数:你定义的,你没有调用,但是最终执行了;声明式编程,流程设定好,、命令式编程jQuery,每一步自己操作)
will将、did完成
2.7.4 生命周期详述
1) 组件的三个生命周期状态:
Mount:插入真实 DOM
Update:被重新渲染
Unmount:被移出真实 DOM
2) React 为每个状态都提供了勾子(hook)函数,可重写
componentWillMount()
componentDidMount()
componentWillUpdate()
componentDidUpdate()
componentWillUnmount()
3) 生命周期流程:
a. 第一次初始化渲染显示: ReactDOM.render()
constructor(): 创建对象初始化state
componentWillMount() : 将要插入回调
render() : 用于插入虚拟DOM回调
componentDidMount() : 已经插入回调
b. 每次更新state: this.setSate()
componentWillUpdate() : 将要更新回调
render() : 更新(重新渲染)
componentDidUpdate() : 已经更新回调
c. 移除组件: ReactDOM.unmountComponentAtNode(containerDom)
- componentWillUnmount() : 组件将要被移除回调
三个阶段,可以都打印一下,看下方法执行的过程(与写的顺序无关)
2.7.5 重要的勾子
1) render(): 初始化渲染或更新渲染调用
2) componentDidMount(): 开启监听, 发送ajax请求
3) componentWillUnmount(): 做一些收尾工作, 如: 清理定时器
4) componentWillReceiveProps(): 后面需要时讲
2.7.6 代码
<div id="example"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
需求: 自定义组件
1. 让指定的文本做显示/隐藏的动画
2. 切换时间为2S
3. 点击按钮从界面中移除组件界面
*/
class Life extends React.Component{
constructor(props){
super(props)
// 初始化状态
this.state = {
opacity: 1
}
this.distroyComponent = this.distroyComponent.bind(this)
}
distroyComponent(){
ReactDOM.unmountComponentAtNode(document.getElementById('example'))
}
// 重写方法
componentDidMount(){
// 启动循环定时器
this.intervalId = setInterval(function () { // 两个函数需要同一个变量,放到上一层共同组件上
console.log('定时器执行……')
let {opacity} = this.state
opacity -= 0.1
if(opacity<=0){
opacity = 1
}
// 更新状态
this.setState({opacity})
}.bind(this), 200) // componentDidMount的this
}
componentWillUnmount(){
// 清理定时器
clearInterval(this.intervalId)
}
render(){ // 一旦改变,就会重新调用;永远写在其他下方,构造器在最上方
const {opacity} = this.state
return(
<div>
<h2 style={{opacity: opacity}}>{this.props.msg}</h2>
<button onClick={this.distroyComponent}>不活了</button>
</div>
)
}// style中两个大括号,外面的代表写的是js代码,里面的是对象(样式名:值,也可以写ES6)
}
ReactDOM.render(<Life msg="react太难了"/>, document.getElementById('example'))
</script>
2.8 虚拟DOM与DOM Diff算法
虚拟DOM:减少操作真实DOM的次数,更新界面次数变少
DOM Diff算法:计算哪里需要更新,哪里不需要更新,减少更新界面的区域
共同提高更新界面的效率
2.8.1 效果
<div id="example"></div>
<br>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
/*
验证:
虚拟DOM+DOM Diff算法: 最小化页面重绘
*/
class HelloWorld extends React.Component {
constructor(props) {
super(props)
this.state = {
date: new Date()
}
}
componentDidMount () {
setInterval(() => {
this.setState({
date: new Date()
})
}, 1000)
}
render () {
console.log('render()')
return (
<p>
Hello, <input type="text" placeholder="Your name here"/>!
<span>It is {this.state.date.toTimeString()}</span>
</p>
)
}
}
ReactDOM.render(
<HelloWorld/>,
document.getElementById('example')
)
</script>
只有时间更新,其他不更新
2.8.2 基本原理图
初始化:虚拟DOM树(div>p>span……),更新虚拟DOM界面不会变——>更新真实DOM界面才会变化(更新状态)
更新(关键):调用setState()更新状态(会进行对比)——>根据差异更新真实DOM、重绘页面变化的区域
第3章 react应用(基于react脚手架)
3.1 使用create-react-app创建react应用
3.1.1 react脚手架
1) xxx脚手架: 用来帮助程序员快速创建一个基于xxx库的模板项目
包含了所有需要的配置
指定好了所有的依赖
可以直接安装/编译/运行一个简单效果
2) react提供了一个用于创建react项目的脚手架库: create-react-app
3) 项目的整体技术架构为: react + webpack + es6 + eslint
4) 使用脚手架开发的项目的特点: 模块化、组件化、工程化
3.1.2 创建项目并启动
npm install -g create-react-app
全局下载
create-react-app hello-react
cd hello-react
npm start
注:
npm root -g
查看全局下载目录
C:\Users\Shinelon\AppData\Roaming\npm\node_modules
3.1.3 react脚手架项目结构
ReactNews
|--node_modules---第三方依赖模块文件夹
|--public
|-- index.html-----------------主页面
|--scripts
|-- build.js-------------------build打包引用配置
|-- start.js-------------------start运行引用配置
|--src------------源码文件夹
|--components-----------------react组件
|-- app.jsx
|--index.css
|--index.js-------------------应用入口js(main.js)
|--.gitignore------git版本管制忽略的配置
|--package.json----应用包配置文件
|--README.md-------应用描述说明的readme文件
package.json
"dependencies"
:运行时依赖"devDependencies"
:开发时依赖,编译打包时需要,开发时不需要,编译打包时工具包
public/index.html
主界面
- 只有一个
<div id="root"></div>
,依靠组件
src/index.js
应用入口
- 引入包(
import * from "*"
)、CSS(import "*.css"
) - 渲染组件
README.md
对项目的说明文件
SPA(Single Page Application):单应用
- index.html
<div id="root"></div>
src/components/app.jsx
import React, {Component} from 'react'
import logo from '../logo.svg'
export default class App extends Component {
render() {
return(
<div>
<img className='logo' src={logo} alt="logo"/>
<p className="title">react组件</p>
</div>
)
}
}
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/app'
import './index.css'
ReactDOM.render(<App />, document.getElementById('root'))
src/index.css
.logo{
width: 200px;
height: 200px;
}
.title{
color: red;
font-size: 25px;
}
cd react_app
npm start
或npm run start
3.2 demo: 评论管理
3.2.1 效果
3.2.2 拆分组件
应用组件: App
- state: comments/array
添加评论组件: CommentAdd
state: username/string, content/string
props: add/func
评论列表组件: CommentList
- props: comment/object, delete/func, index/number
评论项组件: CommentItem
- props: comments/array, delete/func
3.2.3 实现静态组件
render(){return}中的内容
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/app/app'
ReactDOM.render(<App/>, document.getElementById('root'))
src/components/app/app.jsx
import React from 'react'
import CommentAdd from '../comment-add/comment-add'
import CommentList from '../comment-list/comment-list'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
comments: []
}
this.delete = this.delete.bind(this)
}
componentDidMount () {
//模拟异步获取数据
setTimeout(() => {
const comments = [
{
username: "Tom",
content: "ReactJS好难啊!",
id: Date.now()
},
{
username: "JACK",
content: "ReactJS还不错!",
id: Date.now() + 1
}
]
this.setState({
comments
})
}, 1000)
}
add = (comment) => {
let comments = this.state.comments
comments.unshift(comment)
this.setState({ comments })
}
delete (index) {
let comments = this.state.comments
comments.splice(index, 1)
this.setState({ comments })
}
render () {
return (
<div>
<header className="site-header jumbotron">
<div className="container">
<div className="row">
<div className="col-xs-12">
<h1>请发表对React的评论</h1>
</div>
</div>
</div>
</header>
<div className="container">
<CommentAdd add={this.add}/>
<CommentList comments={this.state.comments} delete={this.delete}/>
</div>
</div>
)
}
}
// export default App 可以写到上面
src/components/comment-add/comment-add.jsx
import React from 'react'
import PropTypes from 'prop-types'
class CommentAdd extends React.Component {
constructor (props) {
super(props)
this.state = {
username: '',
content: ''
}
this.addComment = this.addComment.bind(this)
this.changeUsername = this.changeUsername.bind(this)
this.changeContent = this.changeContent.bind(this)
}
addComment () {
// 根据输入的数据创建评论对象
let { username, content } = this.state
let comment = { username, content }
// 添加到comments中, 更新state
this.props.add(comment)
// 清除输入的数据
this.setState({
username: '',
content: ''
})
}
changeUsername (event) {
this.setState({
username: event.target.value
})
}
changeContent (event) {
this.setState({
content: event.target.value
})
}
render () {
return (
<div className="col-md-4">
<form className="form-horizontal">
<div className="form-group">
<label>用户名</label>
<input type="text" className="form-control" placeholder="用户名"
value={this.state.username} onChange={this.changeUsername}/>
</div>
<div className="form-group">
<label>评论内容</label>
<textarea className="form-control" rows="6" placeholder="评论内容"
value={this.state.content} onChange={this.changeContent}></textarea>
</div>
<div className="form-group">
<div className="col-sm-offset-2 col-sm-10">
<button type="button" className="btn btn-default pull-right" onClick={this.addComment}>提交</button>
</div>
</div>
</form>
</div>
)
}
}
CommentAdd.propTypes = {
add: PropTypes.func.isRequired
}
export default CommentAdd
src/components/comment-list/comment-list.css
.reply {
margin-top: 0px;
}
src/components/comment-list/comment-list.jsx
import React from 'react'
import PropTypes from 'prop-types'
import CommentItem from '../comment-item/comment-item'
import './commentList.css'
class CommentList extends React.Component {
constructor (props) {
super(props)
}
render () {
let comments = this.props.comments
let display = comments.length > 0 ? 'none' : 'block'
下面h2中由于用了双大括号,一直报错,因此请直接到github看源码
return (
<div className="col-md-8">
<h3 className="reply">评论回复:</h3>
<h2 style=双大括号 display: display 双大括号>暂无评论,点击左侧添加评论</h2>
<ul className="list-group">
{
comments.map((comment, index) => {
console.log(comment)
return <CommentItem comment={comment} key={index} index={index} delete={this.props.delete}/>
})
}
</ul>
</div>
)
}
}
CommentList.propTypes = {
comments: PropTypes.array.isRequired,
delete: PropTypes.func.isRequired
}
export default CommentList
src/components/comment-item/comment-item.css
li {
transition: .5s;
overflow: hidden;
}
.handle {
width: 40px;
border: 1px solid #ccc;
background: #fff;
position: absolute;
right: 10px;
top: 1px;
text-align: center;
}
.handle a {
display: block;
text-decoration: none;
}
.list-group-item .centence {
padding: 0px 50px;
}
.user {
font-size: 22px;
}
src/components/comment-item/comment-item.jsx
import React from 'react'
import PropTypes from 'prop-types'
import './commentItem.css'
class CommentItem extends React.Component {
constructor (props) {
super(props)
this.deleteComment = this.deleteComment.bind(this)
}
deleteComment () {
let username = this.props.comment.username
if (window.confirm(`确定删除${username}的评论吗?`)) {
this.props.delete(this.props.index)
}
}
render () {
let comment = this.props.comment
return (
<li className="list-group-item">
<div className="handle">
<a href="javascript:" onClick={this.deleteComment}>删除</a>
</div>
<p className="user"><span >{comment.username}</span><span>说:</span></p>
<p className="centence">{comment.content}</p>
</li>
)
}
}
CommentItem.propTypes = {
comment: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
delete: PropTypes.func.isRequired
}
export default CommentItem
3.2.4 实现动态组件
动态展示初始化数据
初始化状态数据
传递属性数据
响应用户操作, 更新组件界面
绑定事件监听, 并处理
更新state
第4章 react ajax
4.1 理解
4.1.1 前置说明
1) React本身只关注于界面, 并不包含发送ajax请求的代码
2) 前端应用需要通过ajax请求与后台进行交互(json数据)
3) react应用中需要集成第三方ajax库(或自己封装)
4.1.2 常用的ajax请求库
1) jQuery: 比较重, 如果需要另外引入不建议使用
2) axios: 轻量级, 建议使用
a. 封装XmlHttpRequest对象的ajax
b. promise风格
c. 可以用在浏览器端和node服务器端
3) fetch: 原生函数, 但老版本浏览器不支持
a. 不再使用XmlHttpRequest对象提交ajax请求
b. 为了兼容低版本的浏览器, 可以引入兼容库fetch.js
4.1.3 效果
需求:
1. 界面效果如下
2. 根据指定的关键字在github上搜索匹配的最受关注的库
3. 显示库名, 点击链接查看库
4. 测试接口: https://api.github.com/search/repositories?q=r&sort=stars
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>11_ajax</title>
<style>
h2{
text-align: center;
margin-top: 200px;
}
</style>
</head>
<body>
<div id="example"></div>
<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.js"></script>
<script type="text/babel">
/*
需求:
1. 界面效果如下
2. 根据指定的关键字在github上搜索匹配的最受关注的库
3. 显示库名, 点击链接查看库
4. 测试接口: https://api.github.com/search/repositories?q=r&sort=stars
*/
class MostStarRepo extends React.Component{
state = {
repoName: '',
repoUrl: ''
}
componentDidMount(){
// 使用axios发送异步的ajax请求
const url = `https://api.github.com/search/repositories?q=re&sort=stars`
axios.get(url)
.then(response => {
const result = response.data
// console.log(response)
// 得到数据
const {name, html_url} = result.items[0]
// 更新状态
this.setState({repoName:name, repoUrl: html_url})
})
.catch((error) => {
alert(error.message)
})
// 使用fetch发送异步的ajax请求
/*fetch(url)
.then(response => {
return response.json()
})
.then(data => {
// 得到数据
const {name, html_url} = data.items[0]
// 更新状态
this.setState({repoName:name, repoUrl: html_url})
})
*/
}
render(){
const {repoName, repoUrl} = this.state
if(!repoName){
return(
<h2>LOADING...</h2>
)
}else{
return(
<h2>Most star repo is <a href={repoUrl}>{repoName}</a></h2>
)
}
}
}
ReactDOM.render(<MostStarRepo />, document.getElementById('example'))
</script>
</body>
</html>
4.2 axios
4.2.1 文档
https://github.com/axios/axios
4.2.2 相关API
1) GET请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
2) POST请求
axios.post('/user', {
firstName: 'Fred',
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
4.3 Fetch
4.3.1 文档
1) https://github.github.io/fetch/
2) https://segmentfault.com/a/1190000003810652
4.3.2 相关API
1) GET请求
fetch(url).then(function(response) {
return response.json()
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
});
2) POST请求
fetch(url, {
method: "POST",
body: JSON.stringify(data),
}).then(function(data) {
console.log(data)
}).catch(function(e) {
console.log(e)
})
4.4 demo: github users
4.4.1 效果
4.4.2 拆分组件
App
* state: searchName/string
Search
* props: setSearchName/func
List
* props: searchName/string
* state: firstView/bool, loading/bool, users/array, errMsg/string
4.4.3 编写静态组件
4.4.4 编写动态组件
componentWillReceiveProps(nextProps): 监视接收到新的props, 发送ajax
使用axios库发送ajax请求
public/index.html
<div id="root"></div>
src/index.js
import React from 'react'
import { render } from 'react-dom'
import App from './components/app'
import './index.css'
render(<App />, document.getElementById('root'))
src/index.css
.album {
min-height: 50rem; /* Can be removed; just added for demo purposes */
padding-top: 3rem;
padding-bottom: 3rem;
background-color: #f7f7f7;
}
.card {
float: left;
width: 33.333%;
padding: .75rem;
margin-bottom: 2rem;
border: 1px solid #efefef;
text-align: center;
}
.card > img {
margin-bottom: .75rem;
border-radius: 100px;
}
.card-text {
font-size: 85%;
}
src/componets/app.jsx
import React from 'react'
import Search from './search'
import UserList from './user-list'
export default class App extends React.Component {
state = {
searchName: ''
}
refreshName = (searchName) => this.setState({searchName})
render() {
return (
<div className="container">
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<Search refreshName={this.refreshName}/>
</section>
<UserList searchName={this.state.searchName}/>
</div>
)
}
}
src/componets/search.jsx
/**
* 上部的搜索模块
*/
import React, {Component} from 'react'
import PropTypes from 'prop-types'
class Search extends Component {
static propTypes = {
refreshName: PropTypes.func.isRequired
}
search = () => {
var name = this.nameInput.value
this.props.refreshName(name)
}
render() {
return (
<div>
<input type="text" placeholder="enter the name you search"
ref={(input => this.nameInput = input)}/>
<button onClick={this.search}>Search</button>
</div>
)
}
}
export default Search
src/componets/user-list.jsx
/**
* 下部的用户列表模块
*/
import React from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
// npm install axios --save
class UserList extends React.Component {
static propTypes = {
searchName: PropTypes.string.isRequired
}
state = {
firstView: true,
loading: false,
users: null,
error: null
}
async componentWillReceiveProps(nextProps) {
let searchName = nextProps.searchName
console.log('发送ajax请求', searchName)
const url = `https://api.github.com/search/users?q=${searchName}`
this.setState({ firstView: false, loading: true })
// 使用axios库
axios.get(url)
.then((response) => {
console.log(response)
this.setState({ loading: false, users: response.data.items })
})
.catch((error)=>{
// debugger
console.log('error', error.response.data.message, error.message)
this.setState({ loading: false, error: error.message })
})
try {
const result = await axios.get(url)
this.setState({ loading: false, users: result.data.items })
} catch(err) {
// debugger
console.log('----', err.message)
}
}
render () {
if (this.state.firstView) {
return <h2>Enter name to search</h2>
} else if (this.state.loading) {
return <h2>Loading result...</h2>
} else if (this.state.error) {
return <h2>{this.state.error}</h2>
} else {
return (
<div className="row">
{
this.state.users.map((user) => (
<div className="card" key={user.html_url}>
<a href={user.html_url} target="_blank">
<img src={user.avatar_url} style={{width: '100px'}} alt='user'/>
</a>
<p className="card-text">{user.login}</p>
</div>
))
}
</div>
)
}
}
}
export default UserList
第5章 几个重要技术总结
5.1 组件间通信
5.1.1 方式一: 通过props传递
1) 共同的数据放在父组件上, 特有的数据放在自己组件内部(state)
2) 通过props可以传递一般数据和函数数据, 只能一层一层传递
3) 一般数据–>父组件传递数据给子组件–>子组件读取数据
4) 函数数据–>子组件传递数据给父组件–>子组件调用函数
父组件传到孙组件、兄弟组件之间不能直接通信,经过子组件、服务器传递
5.1.2 方式二: 使用消息订阅(subscribe)-发布(publish)机制
1) 工具库: PubSubJS
2) 下载: npm install pubsub-js --save
3) 使用:
import PubSub from 'pubsub-js' //引入
PubSub.publish('delete', data) //发布消息
// 消息名,消息
PubSub.subscribe('delete', function(msg, data){ }); //订阅
// 消息名,回调函数
以上一个demo为例(用户搜索的),search和userlist(main)之间需要通信,它们是兄弟组件
在这里是通过父组件,利用props进行通信
这是以前的app.ejs
现在不通过父组件来通信
src/components/app.ejs
import React from 'react'
import Search from './search'
import UserList from './user-list'
export default class App extends React.Component {
render() {
return (
<div className="container">
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<Search/>
</section>
<UserList/>
</div>
)
}
}
src/components/search.ejs
/**
* 上部的搜索模块
*/
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import PubSub from 'pubsub-js' //引入
class Search extends Component {
search = () => {
var searchName = this.nameInput.value
if(searchName){
// 搜索
// 发布消息 search
PubSub.publish('search', searchName)
}
}
render() {
return (
<div>
<input type="text" placeholder="enter the name you search"
ref={(input => this.nameInput = input)}/>
<input type="submit" value="Search" onClick={this.search} />
</div>
)
}
}
export default Search
src/components/user-list.ejs
/**
* 下部的用户列表模块
*/
import React from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import PubSub from 'pubsub-js' //引入
class UserList extends React.Component {
static propTypes = {
searchName: PropTypes.string.isRequired
}
state = {
firstView: true,
loading: false,
users: null,
error: null
}
componentDidMount(){
// 订阅消息 search
PubSub.subscribe('search', (msg, searchName) => { // 指定了新的name,需要请求
this.setState({ firstView: false, loading: true })
// 使用axios库
const url = `https://api.github.com/search/users?q=${searchName}`
axios.get(url)
.then((response) => {
console.log(response)
this.setState({ loading: false, users: response.data.items })
})
.catch((error)=>{
// debugger console.log('error', error.response.data.message, error.message)
this.setState({ loading: false, error: error.message })
})
try {
const result = axios.get(url)
this.setState({ loading: false, users: result.data.items })
} catch(err) {
// debugger
console.log('----', err.message)
}
})
}
render () {
if (this.state.firstView) {
return <h2>Enter name to search</h2>
} else if (this.state.loading) {
return <h2>Loading result...</h2>
} else if (this.state.error) {
return <h2>{this.state.error}</h2>
} else {
return (
<div className="row">
{ this.state.users.map((user) => (
<div className="card" key={user.html_url}>
<a href={user.html_url} target="_blank">
<img src={user.avatar_url} style={{width: '100px'}} alt='user'/>
</a>
<p className="card-text">{user.login}</p>
</div>
))
}
</div>
)
}
}
}
export default UserList
再以之前的评论的为例
App.ejs中有个删除评论的函数,传给了List组件(并没有用到),接着传给Item
delete={this.delete}
5.1.3 方式三: redux
后面专门讲解
5.2 事件监听理解
5.2.1 原生DOM事件
1) 绑定事件监听
a. 事件名(类型): 只有有限的几个, 不能随便写
b. 回调函数
2) 触发事件
a. 用户操作界面
b. 事件名(类型)
c. 数据
5.2.2 自定义事件(消息机制)
1) 绑定事件监听
a. 事件名(类型): 任意
b. 回调函数: 通过形参接收数据, 在函数体处理事件
2) 触发事件(编码)
a. 事件名(类型): 与绑定的事件监听的事件名一致
b. 数据: 会自动传递给回调函数
5.3 ES6常用新语法
1) 定义常量/变量: const/let
2) 解构赋值:let {a, b} = this.props
import {aa} from 'xxx'
3) 对象的简洁表达: {a, b}
4) 箭头函数:
a. 常用场景
组件的自定义方法:
xxx = () => {}
参数匿名函数
b. 优点:
* 简洁
* 没有自己的this,使用引用this查找的是外部this
5) 扩展(三点)运算符: 拆解对象(const MyProps = {}, <Xxx {...MyProps}>)
6) 类: class/extends/constructor/super
7) ES6模块化: export default | import
第6章 react-router4
6.1 相关理解
6.1.1 react-router的理解
1) react的一个插件库(依赖/基于React)
2) 专门用来实现一个SPA应用
3) 基于react的项目基本都会用到此库
6.1.2 SPA的理解
1) 单页Web应用(single page web application,SPA)
2) 整个应用只有一个完整的页面
3) 点击页面中的链接不会刷新页面, 本身也不会向服务器发请求
4) 当点击路由链接时, 只会做页面的局部更新
5) 数据都需要通过ajax请求获取, 并在前端异步展现
6.1.3 路由的理解
1) 什么是路由?
a. 一个路由就是一个映射关系(key:value)
b. key为路由路径(path), value可能是function(后台路由)/component(前台路由)
2) 路由分类
a. 后台路由: node服务器端路由, value是function, 用来处理客户端提交的请求并返回一个响应数据
b. 前台路由: 浏览器端路由, value是component, 当请求的是路由path时, 浏览器端前没有发送http请求, 但界面会更新显示对应的组件
3) 后台路由
a. 注册路由: router.get(path, function(req, res))
b. 当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
4) 前端路由
a. 注册路由: <Route path="/about" component={About}>
b. 当浏览器的hash变为#about
时, 当前路由组件就会变为About组件
6.1.4 前端路由的实现
(底层实现)
1) history库
a. 网址: https://github.com/ReactTraining/history
b. 管理浏览器会话历史(history)的工具库
c. 包装的是原生BOM中window.history
和window.location.hash
2) history API
a. History.createBrowserHistory()
: 得到封装window.history的管理对象
b. History.createHashHistory()
: 得到封装window.location.hash的管理对象
c. history.push()
: 添加一个新的历史记录
d. history.replace()
: 用一个新的历史记录替换当前的记录
e. history.goBack()
: 回退到上一个历史记录
f. history.goForword()
: 前进到下一个历史记录
g. history.listen(function(location){})
: 监视历史记录的变化
3) 测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>history test</title>
</head>
<body>
<p><input type="text"></p>
<a href="/test1" onclick="return push('/test1')">test1</a><br><br>
<button onClick="push('/test2')">push test2</button><br><br>
<button onClick="back()">回退</button><br><br>
<button onClick="forword()">前进</button><br><br>
<button onClick="replace('/test3')">replace test3</button><br><br>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
let history = History.createBrowserHistory() // 方式一
history = History.createHashHistory() // 方式二
// console.log(history)
function push (to) {
history.push(to)
return false
} // 可以返回
function back() {
history.goBack()
}
function forword() {
history.goForward()
}
function replace (to) {
history.replace(to)
} // 不能返回
history.listen((location) => {
console.log('请求路由路径变化了', location)
})
</script>
</body>
</html>
6.2 react-router相关API
6.2.1 组件
1) <BrowserRouter>
:BrowserRouter是react路由的容器
2) <HashRouter>
:这个是用来兼容老浏览器的
3) <Route>
:Route的作用就是用来渲染路由匹配的组件。路由渲染有三种方式,每一种方式都可以传递match,location,history对象
4) <Redirect>
:路由重定向
5) <Link>
:Link的作用和a标签类似
6) <NavLink>
:NavLink和Link一样最终都是渲染成a标签,NavLink可以给这个a标签添加额外的属性
7) <Switch>
:Switch组件内部可以是Route或者Redirect,只会渲染第一个匹配的元素
6.2.2 其它
1) history
对象:这里的history对象是使用history插件生成的,http://www.cnblogs.com/ye-hcj/p/7741742.html已经详细讲过了
记住一点,再使用location做对比的使用,通过history访问的location是动态变化的,最好通过Route访问location
2) match
对象:match对象表示当前的路由地址是怎么跳转过来的
3) withRouter
函数:当一个非路由组件也想访问到当前路由的match,location,history对象,那么withRouter将是一个非常好的选择
6.3 基本路由使用
6.3.1 效果
![react-router demo1 (3)](https://cdn.jsdelivr.net/gh/wallleap/cdn/img/pic/illustration/react-router demo1 (3).gif)
并没有刷新页面
6.3.2 准备
1) 下载react-router: npm install --save react-router@4
我们只需要web版本:npm install --save react-router-dom
2) 由于使用到了BootStrap,因此在index.html中引入bootstrap.css: <link rel="stylesheet" href="/css/bootstrap.css">
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React-router</title>
<link rel="stylesheet" href="/css/bootstrap.css">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
一般会将路由组件和非路由组件分开写
pages/views存放路由组件
components存放其他组件
6.3.3 路由组件: views/about.jsx
import React,{Component} from 'react'
export default class About extends Component{
render(){
return(
<div>About组件内容</div>
)
}
}
6.3.4 路由组件: views/home.jsx
import React, {Component} from 'react'
export default class Home extends Component{
render(){
return (
<div>Home组件内容</div>
)
}
}
由于每个NavLink都需要自定义active样式(加入属性activeClassName),因此提出来
import React, {Component} from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component {
render(){
// 利用this.props三点运算符接受所有的属性
return(
<NavLink {...this.props} activeClassName='activeClass'/>
)
}
}
6.3.6 应用组件: components/app.jsx
import React from 'react'
import {NavLink, Route, Switch, Redirect} from 'react-router-dom'
import MyNavLink from './my-nav-link'
import About from '../views/about'
import Home from '../views/home'
export default class App extends React.Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header">
<h2>React Router Demo</h2>
</div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/*导航路由链接,不能使用a标签,to指向的path*/}
<MyNavLink className="list-group-item" to='/about'>About</MyNavLink>
<MyNavLink className="list-group-item" to='/home'>Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/*可切换的路由组件,使用switch只有匹配才显示,route的path对应上方的to、component对应路由组件,Redirect自动重定向到about、默认到about组件*/}
<Switch>
<Route path='/about' component={About}/>
<Route path='/home' component={Home}/>
<Redirect to='/about'/>
</Switch>
</div>
</div>
</div>
</div>
</div>
)
}
}
6.3.7 自定义样式: index.css
activeClass{
color: red !important;
}
6.3.8 入口JS: index.js
import React from 'react'
//import ReactDOM from 'react-dom'
import {render} from 'react-dom'
import {BrowserRouter, HashRouter} from 'react-router-dom'
import App from './components/app'
import './index.css'
// ReactDOM.render(
render(
(
<BrowserRouter>
<App/>
</BrowserRouter>
/* 组件需要用路由器组件包含起来,两者任选一个 */
/*<HashRouter>
<App />
</HashRouter>*/
),
document.getElementById('root')
)
总结:如何编写路由效果?
- 编写路由组件
- 在父路由组件中指定
- 路由连接:
<NavLink></NavLink>
- 路由:
<Route></Route>
- 路由连接:
6.4 嵌套路由使用
嵌套路由——路由组件中的路由
6.4.1 效果
6.4.2 二级路由组件: views/news.jsx
import React from 'react'
export default class News extends React.Component {
state = {
newsArr: ['news001', 'news002', 'news003']
}
render () {
return (
<div>
<ul>
{
this.state.newsArr.map((news, index) => <li key={index}>{news}</li>)
}
</ul>
</div>
)
}
}
6.4.3 二级路由组件: views/message.jsx
import React from 'react'
import {Link, Route} from 'react-router-dom'
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount () {
// 模拟发送ajax请求
setTimeout(() => {
const data = [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
this.setState({
messages: data
})
}, 1000)
}
render () {
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<a href='???'>{m.title}</a>
</li>
)
})
}
</ul>
</div>
)
}
}
6.4.4 一级路由组件: views/home.jsx
import React, {Component} from 'react'
import {Switch, Route, Redirect} from 'react-router-dom'
import MyNavLink from '../components/my-nav-link'
import News from './news'
import Message from './message'
export default class Home extends Component{
render(){
return (
<div>
<h2>Home组件内容</h2>
<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>
)
}
}
6.5 向路由组件传递参数数据
传递的是id值
6.5.1 效果
6.5.2 三级路由组件: views/message-detail.jsx
import React from 'react'
const messageDetails = [
{id: 1, title: 'Message001', content: '中国,你是最棒的'},
{id: 3, title: 'Message003', content: '对的,没错,非常赞成楼上'},
{id: 6, title: 'Message006', content: '我也赞成'},
]
// 函数的组件
export default function MessageDetail(props) {
const id = props.match.params.id
const md = messageDetails.find(md => md.id===id*1)
return (
<ul>
<li>ID: {md.id}</li>
<li>TITLE: {md.title}</li>
<li>CONTENT: {md.content}</li>
</ul>
)
}
6.5.3 二级路由组件: views/message.jsx
import React from 'react'
import {Link, Route} from 'react-router-dom'
import MessageDetail from "./message-detail"
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount () {
// 模拟发送ajax请求
setTimeout(() => {
const data = [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
this.setState({
messages: data
})
}, 1000)
}
render () {
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to={`${path}/${m.id}`}>{m.title}</Link>
</li>
)
})
}
</ul>
<Route path={`${path}/:id`} component={MessageDetail}></Route>
</div>
)
}
}
路由链接与非路由链接:是否发了请求(路由连接不发)
<NavLink to=''></NavLink>`` <Link to=''></Link>
<a href=""></a>
6.6 多种路由跳转方式
前面讲的路由切换都是通过点击链接的方式切换的,不是链接也能够
6.6.1 效果
6.6.2 二级路由: views/message.jsx
import React from 'react'
import {Link, Route} from 'react-router-dom'
import MessageDetail from "./message-detail"
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount () {
// 模拟发送ajax请求
setTimeout(() => {
const data = [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
this.setState({
messages: data
})
}, 1000)
}
// props中有history属性,它由push等方法
ShowDetail = (id) => {
this.props.history.push(`/home/message/${id}`)
}
ShowDetail2 = (id) => {
this.props.history.replace(`/home/message/${id}`)
}
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
render () {
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to={`${path}/${m.id}`}>{m.title}</Link>
<button onClick={() => this.ShowDetail(m.id)}>查看详情(push)</button>
<button onClick={() => this.ShowDetail2(m.id)}>查看详情(replace)</button>
</li>
)
})
}
</ul>
<p>
<button onClick={this.back}>返回</button>
<button onClick={this.forward}>前进</button>
</p>
<hr/>
<Route path={`${path}/:id`} component={MessageDetail}></Route>
{/*<Route path={`home/message/meassagedetail/:id`} component={MessageDetail}></Route>*/}
</div>
)
}
}
总结:
- 路由器标签
<BrowserRouter>
:BrowserRouter是react路由的容器<HashRouter>
:多了一个#号
- 路由
<Route>
:Route的作用就是用来渲染路由匹配的组件。路由渲染有三种方式,每一种方式都可以传递match,location,history对象
<Redirect>
:路由重定向- 链接
<Link>
:Link的作用和a标签类似<NavLink>
:可以添加其他属性,例如activeClassName
<Switch>
:Switch组件内部可以是Route或者Redirect,只会渲染第一个匹配的元素
- this.props.
- match
- params
- history
- push()
- replace()
- goback()
- goforward()
- match
第7章 react-ui
7.1 最流行的开源React UI组件库
7.1.1 material-ui(国外)
1) 官网: http://www.material-ui.com/#/
2) github: https://github.com/callemall/material-ui
7.1.2 ant-design(国内蚂蚁金服)
1) PC官网: https://ant.design/index-cn
2) 移动官网: https://mobile.ant.design/index-cn
3) Github: https://github.com/ant-design/ant-design/
4) Github: https://github.com/ant-design/ant-design-mobile/
7.2 ant-design-mobile使用入门
7.2.1 效果
7.2.2 使用create-react-app创建react应用
npm install create-react-app -g
create-react-app antm-demo
cd antm-demo
npm start
7.2.3 搭建antd-mobile的基本开发环境
1) 下载
npm install antd-mobile --save
2) src/components/App.jsx
import React, {Component} from 'react'
import {Button, Toast} from 'antd-mobile'
export default class App extends Component {
handleClick = () => {
Toast.info('提交成功', 2)
}
render() {
return (
<div>
<Button type="primary" onClick={this.handleClick}>提交</Button>
</div>
)
}
/* type值不同样式不同 */
}
3) src/index.js
import React from 'react';
import {render} from 'react-dom'
import App from "./components/App"
// 引入整体css
import 'antd-mobile/dist/antd-mobile.css'
render(<App />, document.getElementById('root'))
4) index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React ant design mobile</title>
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
if(!window.Promise) {
document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');
}
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
7.2.4 实现按需打包(组件js/css)
1) 下载依赖包
yarn add react-app-rewired --dev
yarn add babel-plugin-import --dev
2) 修改默认配置:
l package.json
{
"name": "react_ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"antd-mobile": "^2.1.3",
"react": "^16.2.0",
"react-dom": "^16.2.0"
},
"devDependencies": {
"babel-plugin-import": "^1.6.3",
"react-app-rewired": "^1.4.0",
"react-scripts": "1.0.17"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom"
}
}
l config-overrides.js
const {injectBabelPlugin} = require('react-app-rewired');
module.exports = function override(config, env) {
config = injectBabelPlugin(['import', {libraryName: 'antd-mobile', style: 'css'}], config);
return config;
};
3) 编码
// import 'antd-mobile/dist/antd-mobile.css'
// import Button from 'antd-mobile/lib/button'
// import Toast from 'antd-mobile/lib/toast'
import {Button, Toast} from 'antd-mobile'
第8章 redux
8.1 redux理解
8.1.1 学习文档
1) 英文文档: https://redux.js.org/
2) 中文文档: http://www.redux.org.cn/
3) Github: https://github.com/reactjs/redux
8.1.2 redux是什么?
1) redux是一个独立专门用于做状态管理的JS库(不是react插件库)
2) 它可以用在react, angular, vue等项目中, 但基本与react配合使用
3) 作用: 集中式管理react应用中多个组件共享的状态
8.1.3 redux工作流程
8.1.4 什么情况下需要使用redux
1) 总体原则: 能不用就不用, 如果不用比较吃力才考虑使用
2) 某个组件的状态,需要共享
3) 某个状态需要在任何地方都可以拿到
4) 一个组件需要改变全局状态
5) 一个组件需要改变另一个组件的状态
8.2 redux的核心API
8.2.1 createStore()
1) 作用: 创建包含指定reducer的store对象
2) 编码:
import {createStore} from 'redux'
import counter from './reducers/counter'
const store = createStore(counter)
8.2.2 store
对象
1) 作用:
redux库最核心的管理对象
2) 它内部维护着:
state
reducer
3) 核心方法:
getState()
dispatch(action)
subscribe(listener)
4) 编码:
store.getState()
store.dispatch({type:'INCREMENT', number})
store.subscribe(render)
8.2.3 applyMiddleware()
1) 作用:
应用上基于redux的中间件(插件库)
2) 编码:
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk' // redux异步中间件
const store = createStore(
counter,
applyMiddleware(thunk) // 应用上异步中间件
)
8.2.4 combineReducers()
1) 作用:
合并多个reducer函数
2) 编码:
export default combineReducers({
user,
chatUser,
chat
})
8.3 redux的三个核心概念
8.3.1 action
1) 标识要执行行为的对象
2) 包含2个方面的属性
a. type: 标识属性, 值为字符串, 唯一, 必要属性
b. xxx: 数据属性, 值类型任意, 可选属性
3) 例子:
const action = {
type: 'INCREMENT',
data: 2
}
4) Action Creator(创建Action的工厂函数)
const increment = (number) => ({type: 'INCREMENT', data: number})
8.3.2 reducer
1) 根据老的state和action, 产生新的state的纯函数
2) 样例
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.data
case 'DECREMENT':
return state - action.data
default:
return state
}
}
3) 注意
a. 返回一个新的状态
b. 不要修改原来的状态
8.3.3 store
1) 将state,action与reducer联系在一起的对象
2) 如何得到此对象?
import {createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
3) 此对象的功能?
getState(): 得到state
dispatch(action): 分发action, 触发reducer调用, 产生新的state
subscribe(listener): 注册监听, 当产生了新的state时, 自动调用
8.4 使用redux编写应用
8.4.1 效果
使用react实现
8.4.2 下载依赖包
npm install --save redux
8.4.3 src/redux/action-types.js
/*
Action对象的type常量名称模块
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
8.4.4 src/redux/actions.js
/*
action creator模块
*/
import {INCREMENT, DECREMENT} from './action-types'
export const increment = number => ({type: INCREMENT, number})
export const decrement = number => ({type: DECREMENT, number})
8.4.5 src/redux/reducers.js
/*
包含n个reducer函数的模块
根据老的state和指定action, 处理返回一个新的state
*/
import {INCREMENT, DECREMENT} from './action-types'
export function counter(state = 0, action) {
console.log('counter', state, action)
switch (action.type) {
case INCREMENT:
return state + action.number
case DECREMENT:
return state - action.number
default:
return state
}
}
8.4.6 src/components/app.jsx
/*
应用组件
*/
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import * as actions from '../redux/actions'
import { INCREMENT } from '../redux/action-types'
export default class App extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
}
increment = () => {
// 1.得到选择增加数量
const number = this.refs.numSelect.value * 1
// 2.调用store的方法更新状态
this.props.store.dispatch(actions.increment(number))
}
decrement = () => {
// 1.得到选择减小数量
const number = this.refs.numSelect.value * 1
// 2.调用store的方法更新状态
this.props.store.dispatch(actions.decrement(number))
}
incrementIfOdd = () => {
// 1.得到选择增加数量
const number = this.refs.numSelect.value * 1
// 2.得到原本的count状态
let count = this.props.store.getState()
// 判断,满足条件猜更新状态
if (count % 2 === 1) {
// 调用store的方法更新状态
this.props.store.dispatch(actions.increment(number))
}
}
incrementAsync = () => {
// 1.得到选择增加数量
const number = this.refs.numSelect.value * 1
// 2.启动延时定时器,模拟异步
setTimeout(() => {
// 3.调用store的方法更新状态
this.props.store.dispatch(actions.increment(number))
}, 1000)
}
render() {
const count = this.props.store.getState()
return (
<div>
<p>
click {count} times {' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{' '}
<button onClick={this.increment}>+</button>
{' '}
<button onClick={this.decrement}>-</button>
{' '}
<button onClick={this.incrementIfOdd}>increment if odd</button>
{' '}
<button onClick={this.incrementAsync}>increment async</button>
</div>
)
}
}
8.4.7 src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {createStore} from 'redux'
import App from './components/app'
import {counter} from './redux/reducers'
// 根据counter函数创建store对象
const store = createStore(counter) // 内部会第一次调用reduer函数,得到初始state
// 定义渲染根组件标签的函数
const render = () => {
ReactDOM.render(
<App store={store}/>,
document.getElementById('root')
)
}
// 初始化渲染
render()
// 注册(订阅)监听, 一旦状态发生改变, 自动重新渲染
store.subscribe(render)
也可以把index.js中store提取出来
src/redux/store.js
import {createStore} from 'redux'
import {counter} from './reducers'
// 根据counter函数创建store对象
const store = createStore(counter) // // 内部会第一次调用reduer函数,得到初始state
export default store
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/app'
import store from './redux/store'
// 定义渲染根组件标签的函数
const render = () => {
ReactDOM.render(
<App store={store}/>,
document.getElementById('root')
)
}
// 初始化渲染
render()
// 注册(订阅)监听, 一旦状态发生改变, 自动重新渲染
store.subscribe(render)
8.4.8 问题
1) redux与react组件的代码耦合度太高(大多数地方用到store)
2) 编码不够简洁
8.5 react-redux
8.5.1 理解
1) 一个react插件库
2) 专门用来简化react应用中使用redux
8.5.2 React-Redux将所有组件分成两大类
1) UI组件
a. 只负责 UI 的呈现,不带有任何业务逻辑
b. 通过props接收数据(一般数据和函数)
c. 不使用任何 Redux 的 API
d. 一般保存在components
文件夹下
2) 容器组件
a. 负责管理数据和业务逻辑,不负责UI的呈现
b. 使用 Redux 的 API
c. 一般保存在containers
文件夹下
8.5.3 相关API
1) Provider
让所有组件都可以得到state数据
<Provider store={store}>
<App />
</Provider>
2) connect()
用于包装 UI 组件生成容器组件
import { connect } from 'react-redux'
connect(
mapStateToprops,
mapDispatchToProps
)(Counter)
3) mapStateToprops()
将外部的数据(即state对象)转换为UI组件的标签属性
const mapStateToprops = function (state) {
return {
value: state
}
}
4) mapDispatchToProps()
将分发action的函数转换为UI组件的标签属性
简洁语法可以直接指定为actions对象或包含多个action方法的对象
8.5.4 使用react-redux
1) 下载依赖包
npm install --save react-redux
2) redux/action-types.js
不变
3) redux/actions.js
不变
4) redux/reducers.js
不变
5) components/counter.jsx
/*
UI组件: 不包含任何redux API
*/
import React from 'react'
import PropTypes from 'prop-types'
export default class Counter extends React.Component {
static propTypes = {
count: PropTypes.number.isRequired,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired
}
increment = () => {
const number = this.refs.numSelect.value * 1
this.props.increment(number)
}
decrement = () => {
const number = this.refs.numSelect.value * 1
this.props.decrement(number)
}
incrementIfOdd = () => {
const number = this.refs.numSelect.value * 1
let count = this.props.count
if (count % 2 === 1) {
this.props.increment(number)
}
}
incrementAsync = () => {
const number = this.refs.numSelect.value * 1
setTimeout(() => {
this.props.increment(number)
}, 1000)
}
render() {
return (
<div>
<p>
click {this.props.count} times {' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{' '}
<button onClick={this.increment}>+</button>
{' '}
<button onClick={this.decrement}>-</button>
{' '}
<button onClick={this.incrementIfOdd}>increment if odd</button>
{' '}
<button onClick={this.incrementAsync}>increment async</button>
</div>
)
}
}
6) containters/app.jsx
/*
包含Counter组件的容器组件
*/
import React from 'react'
// 引入连接函数
import {connect} from 'react-redux'
// 引入action函数
import {increment, decrement} from '../redux/actions'
import Counter from '../components/counter'
// 向外暴露连接App组件的包装组件
export default connect(
state => ({count: state}),
{increment, decrement}
)(Counter)
7) store.js
不变
8) index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import App from './containers/app'
import store from './redux/store'
// 定义渲染根组件标签的函数
ReactDOM.render(
(
<Provider store={store}>
<App/>
</Provider>
),
document.getElementById('root')
)
8.5.5 问题
1) redux默认是不能进行异步处理的, (前面的都是react实现的)
2) 应用中又需要在redux中执行异步任务(ajax, 定时器)
8.6 redux异步编程
8.6.1 下载redux插件(异步中间件)
npm install --save redux-thunk
8.6.2 redux/store.js
import React from 'react'
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import {composeWithDevTools} from 'redux-devtools-extension'
import reducers from './reducers'
// 根据counter函数创建store对象
export default createStore(
reducers,
applyMiddleware(thunk) // 应用上异步中间件
)
8.6.3 index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import App from './containers/app'
import store from './redux/store'
// 定义渲染根组件标签的函数
ReactDOM.render(
(
<Provider store={store}>
<App/>
</Provider>
),
document.getElementById('root')
)
8.6.4 redux/actions.js
/*
action creator模块
*/
import {
INCREMENT,
DECREMENT
} from './action-types'
export const increment = number => ({type: INCREMENT, number})
export const decrement = number => ({type: DECREMENT, number})
// 异步action creator(返回一个函数)
export const incrementAsync = number => {
return dispatch => { // 要在这返回一个函数,得在store中应用上异步中间件
// 异步的代码
setTimeout(() => {
// 1s之后才分发一个增加的action
dispatch(increment(number))
}, 1000)
}
}
同步的action都返回一个对象
异步的action返回的是一个函数
8.6.5 components/counter.jsx
/*
包含Counter组件的容器组件
*/
import React from 'react'
import PropTypes from 'prop-types'
export default class Counter extends React.Component {
static propTypes = {
count: PropTypes.number.isRequired,
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired
}
increment = () => {
const number = this.refs.numSelect.value*1
this.props.increment(number)
}
decrement = () => {
const number = this.refs.numSelect.value*1
this.props.decrement(number)
}
incrementIfOdd = () => {
const number = this.refs.numSelect.value*1
let count = this.props.count
if(count%2===1) {
this.props.increment(number)
}
}
// Async
incrementAsync = () => {
const number = this.refs.numSelect.value*1
this.props.incrementAsync(number)
}
render () {
return (
<div>
<p>
click {this.props.count} times {' '}
</p>
<select ref="numSelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>{' '}
<button onClick={this.increment}>+</button>{' '}
<button onClick={this.decrement}>-</button>{' '}
<button onClick={this.incrementIfOdd}>increment if odd</button>{' '}
<button onClick={this.incrementAsync}>increment async</button>
</div>
)
}
}
8.6.6 containers/app.jsx
/*
包含Counter组件的容器组件
*/
import React from 'react'
// 引入连接函数
import {connect} from 'react-redux'
// 引入action函数
import {increment, decrement, incrementAsync} from '../redux/actions'
import Counter from '../components/counter'
// 向外暴露连接App组件的包装组件
export default connect(
state => ({count: state.counter}),
{increment, decrement, incrementAsync}
)(Counter)
redux/action-types.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
8.7 使用上redux调试工具
8.7.1 安装chrome浏览器插件
8.7.2 下载工具依赖包
npm install --save-dev redux-devtools-extension
8.7.3 编码
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore( counter, composeWithDevTools(applyMiddleware(thunk))
)
8.8 Redux版评论
8.8.1 需求
使用react_redux和中间件实现异步评论功能
8.8.2 安装
……
npm install --save react-redux
npm install --save redux-thunk
npm install --save-dev redux-devtools-extension
8.8.3 目录结构
- REACT_REDUX
- public
- css
- bootstrap.css
- index.html
- src
- components
- app
- app.jsx
- comment-add
- comment-add.jsx
- comment-item
- comment-item.jsx
- comment-item.css
- comment-list
- comment-list.jsx
- comment-list.css
- redux
- action-types.js
- actions.js
- reducers.js
- store.js
- index.js
8.8.4 文件内容
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React App</title>
<link rel="stylesheet" href="/css/bootstrap.css">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import App from './components/app/app'
import store from './redux/store'
// 定义渲染根组件标签的函数
ReactDOM.render(
(
<Provider store={store}>
<App/>
</Provider>
),
document.getElementById('root')
)
src/componets/app/app.jsx
import React from 'react'
import {connect} from 'react-redux'
import CommentAdd from '../comment-add/comment-add'
import CommentList from '../comment-list/comment-list'
import {getComments} from '../../redux/actions'
class App extends React.Component {
componentDidMount() {
//模拟异步获取数据
this.props.getComments()
}
render() {
return (
<div>
<header className="site-header jumbotron">
<div className="container">
<div className="row">
<div className="col-xs-12">
<h1>请发表对React的评论</h1>
</div>
</div>
</div>
</header>
<div className="container">
<CommentAdd/>
<CommentList/>
</div>
</div>
)
}
}
export default connect(
null,
{getComments}
)(App)
src/componets/comment-add/comment-add.jsx
import React from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' import {addComment} from '../../redux/actions' class CommentAdd extends React.Component { constructor (props) { super(props) this.state = { username: '', content: '' } this.addComment = this.addComment.bind(this) this.changeUsername = this.changeUsername.bind(this) this.changeContent = this.changeContent.bind(this) } addComment () { // 根据输入的数据创建评论对象 let { username, content } = this.state let comment = { username, content } // 添加到comments中, 更新state this.props.addComment(comment) // 清除输入的数据 this.setState({ username: '', content: '' }) } changeUsername (event) { this.setState({ username: event.target.value }) } changeContent (event) { this.setState({ content: event.target.value }) } render () { return ( <div className="col-md-4"> <form className="form-horizontal"> <div className="form-group"> <label>用户名</label> <input type="text" className="form-control" placeholder="用户名" value={this.state.username} onChange={this.changeUsername}/> </div> <div className="form-group"> <label>评论内容</label> <textarea className="form-control" rows="6" placeholder="评论内容" value={this.state.content} onChange={this.changeContent}></textarea> </div> <div className="form-group"> <div className="col-sm-offset-2 col-sm-10"> <button type="button" className="btn btn-default pull-right" onClick={this.addComment}>提交</button> </div> </div> </form> </div> ) } } CommentAdd.propTypes = { addComment: PropTypes.func.isRequired } export default connect( null, {addComment} )(CommentAdd)
src/componets/comment-item/comment-item.jsx
import React from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' import './commentItem.css' import {deleteComment} from '../../redux/actions' class CommentItem extends React.Component { constructor(props) { super(props) } deleteComment = () => { let username = this.props.comment.username if (window.confirm(`确定删除${username}的评论吗?`)) { this.props.deleteComment(this.props.index) } } render() { let comment = this.props.comment return ( <li className="list-group-item"> <div className="handle"> <a href="javascript:" onClick={this.deleteComment}>删除</a> </div> <p className="user"><span>{comment.username}</span><span>说:</span></p> <p className="centence">{comment.content}</p> </li> ) } } CommentItem.propTypes = { comment: PropTypes.object.isRequired, index: PropTypes.number.isRequired, deleteComment: PropTypes.func.isRequired } export default connect( null, {deleteComment} )(CommentItem)
- `src/componets/comment-item/comment-item.css`
```css
li {
transition: .5s;
overflow: hidden;
}
.handle {
width: 40px;
border: 1px solid #ccc;
background: #fff;
position: absolute;
right: 10px;
top: 1px;
text-align: center;
}
.handle a {
display: block;
text-decoration: none;
}
.list-group-item .centence {
padding: 0px 50px;
}
.user {
font-size: 22px;
}
src/componets/comment-list/comment-list.jsx
import React from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' import CommentItem from '../comment-item/comment-item' import './commentList.css' class CommentList extends React.Component { render () { let comments = this.props.comments let display = comments.length > 0 ? 'none' : 'block' return ( <div className="col-md-8"> <h3 className="reply">评论回复:</h3> <h2 style=双大括号 display: display 双大括号>暂无评论,点击左侧添加评论!!!</h2> <ul className="list-group"> { comments.map((comment, index) => { console.log(comment) return <CommentItem comment={comment} key={index} index={index}/> }) } </ul> </div> ) } } CommentList.propTypes = { comments: PropTypes.array.isRequired, } export default connect( state => ({comments: state.comments}) )(CommentList)
src/componets/comment-list/comment-list.css
.reply { margin-top: 0px; }
redux/action-types.js
export const ADD_COMMENT = 'ADD_COMMENT' export const DELETE_COMMENT = 'DELETE_COMMENT' export const RECEIVE_COMMENTS = 'RECEIVE_COMMENTS'
redux/actions.js
import { ADD_COMMENT, DELETE_COMMENT, RECEIVE_COMMENTS } from './action-types' export const addComment = (comment) => ({type: ADD_COMMENT, data: comment}) export const deleteComment = (index) => ({type: DELETE_COMMENT, data: index}) const receiveComments = (comments) => ({type: RECEIVE_COMMENTS, data: comments}) export const getComments = () => { return dispatch => { setTimeout(() => { const comments = [ { username: "Tom", content: "ReactJS好难啊!", id: Date.now() }, { username: "JACK", content: "ReactJS还不错!", id: Date.now() + 1 } ] dispatch(receiveComments(comments)) }, 1000) } }
redux/reducers.js
import {combineReducers} from 'redux'
import {
ADD_COMMENT,
DELETE_COMMENT,
RECEIVE_COMMENTS
} from './action-types'
const initComments = []
function comments(state = initComments, action) {
switch (action.type) {
case ADD_COMMENT:
return [...state, action.data]
case DELETE_COMMENT:
return state.filter((c, index) => index !== action.data)
case RECEIVE_COMMENTS:
return action.data
default:
return state
}
}
export default combineReducers({
comments
})
redux/store.js
import React from 'react'
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import {composeWithDevTools} from 'redux-devtools-extension'
import reducers from './reducers'
// 根据counter函数创建store对象
export default createStore(
reducers,
composeWithDevTools(applyMiddleware(thunk)) // 应用上异步中间件
)
8.9 相关重要知识: 纯函数和高阶函数
8.9.1 纯函数
1) 一类特别的函数: 只要是同样的输入,必定得到同样的输出
2) 必须遵守以下一些约束
a. 不得改写参数
b. 不能调用系统 I/O 的API
c. 能调用Date.now()或者Math.random()等不纯的方法
3) reducer函数必须是一个纯函数
8.9.2 高阶函数
4) 理解: 一类特别的函数
a. 情况1: 参数是函数
b. 情况2: 返回是函数
5) 常见的高阶函数:
a. 定时器设置函数
b. 数组的map()/filter()/reduce()/find()/bind()
c. react-redux中的connect函数
6) 作用:
能实现更加动态, 更加可扩展的功能