react入门学习实现一个TodoList

一 : 脚手架

react官方提供的脚手架工具:Create-react-app

优点:

  • 官方提供,比较健壮;
  • 使用简单,可定制性强;
  • 调试代码方便;

如果没有深厚webpack配置功底,能够确保驾驭更复杂的脚手架工具的话,使用这个脚手架是一个最佳选择

脚手架的代码不能直接运行,需要脚手架工具编译才可以在浏览器运行,grunt/webpack等工具来帮助写脚手架

Create React App:

npm install -g create-react-app
create-react-app my-app

cd my-app
npm start

生成文件

my-app
  - README.md  项目说明文件,支持markdown语法
  - package.json 任何一个脚手架工具,都会有,代表node包文件
  - .gitinore  使用git管理代码的时候,一些文件不想传到git仓库上,路径写下来
  - node_modules 这个项目一些第三方依赖的包,脚手架使用时需要的一些第三方依赖
  - public
      - favicon.ico 网页标题icon
      - index.html  
      - manifest.json 把网页当成APP使用,保存快捷图标时的配置
  - src 项目源代码
       - index.js  入口
       - App.test.js 自动化测试文件
package.json
{
  "name": "my-app",  //项目名称
  "version": "0.1.0",  //版本号
  "private": true,   //私有项目
  "dependencies": {   //安装的第三方依赖
    "react": "^16.8.4",
    "react-dom": "^16.8.4",
    "react-scripts": "2.1.8"
  },
  "scripts": {  //提供指令调用   npm run start启动
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

serviceWorker的作用:

PWA: progressive web application
通过写网页的形式,来写手机app应用,引入serviceWorker,借助网页来写手机APP应用的功能
效果:写了一个网页,并上传到支持https协议的服务器上,这个网页会具备这样的特性(当用户第一次访问这个网页,需要联网才能看到,下次断网的情况下,依然可以看到上次访问过的页面)serviceWorker会把之前浏览过的页面,存储在浏览器缓存中。

src/index.js

import * as serviceWorker from './serviceWorker';  引用

serviceWorker.unregister();  调用

把网页当成APP显示时的配置
手机/电脑 通过快捷方式,直接进入网址

{
  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [  快捷方式图标
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",  跳转网址
  "display": "standalone",  
  "theme_color": "#000000",  主题颜色
  "background_color": "#ffffff"
}

二:组件

src/index.js


import App from './App';
加载一个App的文件,这个App 就是一个小的组件

App来自App.js文件(src/App.js)

---------------------
src/App.js
import React, { Component } from 'react';

定义一个类App,继承了React.Component的类
当一个类继承了React.Component的类时候,它就是一个组件了
class App extends Component {  
  render() {  
    return (
      <div>
        hello world!
      </div>
    );
  }
  render函数返回什么,这个组件就展示什么内容
}

export default App;  导出

 { Component } 等价于 React.Component

import { Component } from 'react';
等价于
import React from 'react';
const Component = React.Component

--------------------
src/index.js

import App from './App';   引入组件


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

ReactDOM.render() 在做什么:

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

ReactDOM  第三方模块
ReactDOM有一个方法render,可以把一个组件挂载到一个dom节点上

<App /> 是jsx语法,如果使用了jsx语法,
就一定要在项目中引入import React from 'react'语法

组件名首字母必须大写

import app from './App';
ReactDOM.render(<app />, document.getElementById('root'));
这种写法会报错,jsx不支持
在JSX语法中,如果要使用自己创建的组件,组件开头必须用大写字母开头

三:最基础的JSX语法

src/App.js
import React, { Component } from 'react';

class App extends Component {
  render() {
    // JSX
    return (
      <div>
        hello, 阿鲁提尔
      </div>
    );
  }
}

export default App;

在react中,在js写html标签,就是JSX语法
<App /> 自定义组件
<div> 小写开头,h5原始标签

四:使用React编写Todolist功能

src/TodoList.js


import React, { Component,Fragment } from 'react';

class Todolist extends Component {

  //在react中定义数据
  // 在js中,一个类就一定会有一个constructor构造函数
  // 当创建一个Todolist实例,constructor这个函数是优于其他任何函数
  // 最先被执行的函数
  // 会接受一个props参数
  constructor(props){
    super(props)  
    //Todolist 继承了React.Component组件,所以在创建Todolist时,super是调用了Component的构造函数
    //调用一次

    //定义数据
    //数据定义在状态里面,this.state就是Todolist的状态
    //state负责存储组件里面的数据
    this.state = {
      inputValue:'',
      list:['学习英文','学习React']
    }
  }

  render() {
    return (
      <Fragment>
        <div>
          <input 
            value={this.state.inputValue} 
            onChange={this.handleInputChange.bind(this)}
          />
          <button onClick={this.handleBtnClick.bind(this)}>提交</button>
        </div>
        <ul>
          {
            this.state.list.map((item,index)=>{
              return (
                <li 
                onClick={this.handleItemDelete.bind(this,index)} 
                key={index}
                >
                  {item}
                </li>
              )
            })
          }
        </ul>  
      </Fragment>
    )
  }

  handleInputChange(e){
    //改变数据,向里面传入对象
    this.setState({
      inputValue: e.target.value
    })
    // this.state.inputValue = e.target.value

  }

  handleBtnClick(){
    
    this.setState({
      list:[...this.state.list,this.state.inputValue],
      inputValue:''
    })
  }

  handleItemDelete(index){
    const list = [...this.state.list]
    list.splice(index,1)

    this.setState({
      list,
    })

    错误写法,删除出bug,留下做个标记,一会慢慢研究
    // let list = this.state.list.splice(index,1)
    // this.setState({
    //   list,
    // })

    强烈不推荐错误写法,违反书写规定
    // this.state.list.splice(index,1)
    // this.setState({
    //   list:this.state.list
    // })
    //immutable
    //state 不允许我们做任何的改变 所以赋值出来再修改
    //一旦修改state里面的内容,后面会影响React性能优化
  }
}

export default Todolist;


//React规定,最外层必须包裹一个元素
// 如果想最外层包裹一个元素,这个元素又不被显示出来
//在react16版本里面,提供了一个Fragment占位符

五:JSX语法细节补充

0. JSX中的注释:

 {/* do something*/}

1. <input /> 中value和defaultValue的区别:

单独使用 <input value={this.state.inputValue} /> 时,虽然页面可以正常显示,但是控制台会出现如上报错。

  • 如果input 表单只是显示默认值,不会手动去修改该值,则值需要使用defaultValue
  • 如果需要获取用户输入的值,则一定要用value属性,但是一定要配合onchange事件一起使用,否则值是不会变的

2. 样式className代替class(与类冲突)

<input className="input"
   value={this.state.InputValue} 
   onChange={this.handleInputValue.bind(this)}
/>

3. dangerouslySetInnerHTML的作用:
在上面的TodoList中,input输入<h1>学习node.js</h1>,<h1>也会被当成文本显示,dangerouslySetInnerHTML的作用就是,不显示<h1>标签,当做html添加到页面

dangerouslySetInnerHTML 危险的设置元素innerHTML属性
<li 
  key={index} 
  onClick={this.handleRemoveInputValue.bind(this,index)}
  dangerouslySetInnerHTML = {{__html:item}}
  >
    {/* {item} 使用了dangerouslySetInnerHTML = {{__html:item}},这里item就不需要写了 */}
</li>

//dangerouslySetInnerHTML = {{__html:item}}  

dangerouslySetInnerHTML实现的功能

4. for和htmlFor的区别

<label for="insertArea">输入内容</label>
<input 
  id="insertArea" 
  className="input" 
  value={this.state.InputValue} 
  onChange={this.handleInputValue.bind(this)} 
  onKeyDown={this.handleEnterInput.bind(this)} 
/>

增加输入框选择区域的功能,虽然可以正常显示,但是会出现如下报错


这是因为这个for会和循环的for产生歧义。要换成htmlFor

六:组件的拆分与组件之间的传值

父组件向子组件传值

src/TodoList.js

import TodoItem from './TodoItem';
<ul>
  {
    this.state.list.map((item,index)=>{
      return (
        <div>
          <TodoItem content={item} />  //使用组件,通过属性传值
          {/*<li    //注释内容
            key={index} 
            onClick={this.handleRemoveInputValue.bind(this,index)}
            dangerouslySetInnerHTML = {{__html:item}}
            >
          </li>*/}
        </div>
        )
    })
  }
</ul>



src/TodoItem.js

import React,{Component} from 'react';
class TodoItem extends Component{
    render(){
        return <div>{this.props.content}</div>  
        {/*子组件通过this.props.content 获取值*/}
    }
}
export default TodoItem;

子组件向父组件传值

src/TodoList.js
import React,{Component,Fragment} from 'react';
import './style.css'
import TodoItem from './TodoItem';


class TodoList extends Component{
  constructor(props){
    super(props)

    this.state={
      InputValue:'',
      list:['学习英语','学习React']
    }
  }

  render(){
    return (
      <Fragment>
        <div>
          <label htmlFor="insertArea">输入内容</label>
          <input 
            id="insertArea" 
            className="input" 
            value={this.state.InputValue} 
            onChange={this.handleInputValue.bind(this)} 
            onKeyDown={this.handleEnterInput.bind(this)} 
          />
          <button onClick={this.handleUpInputValue.bind(this)}>提交</button>
        </div>
        <ul>
          {
            this.state.list.map((item,index)=>{
              return (
                <div>
                  <TodoItem 
                    index={index} 
                    content={item}
                    deleteItem = {this.handleRemoveInputValue.bind(this)} 
                    {/*把函数传给子组件,并把this指向TodoList
                    把index和item通过属性传递给TodoItem 
                    在TodoItem组件中,使用
                        this.props.content
                        this.props.index
                        this.props.deleteItem
                    即可使用*/}
                   />
                </div>
                )
            })
          }
        </ul>
      </Fragment>
    )
  }
  handleInputValue(e){
    this.setState({
      InputValue: e.target.value
    })

  }
  handleUpInputValue(){
    this.setState({
      list:[...this.state.list,this.state.InputValue],
      InputValue:''
    })
  }
  handleEnterInput(e){
    if(e.keyCode=="13"&&!this.state.InputValue==''){
      this.setState({
        list:[...this.state.list,this.state.InputValue],
        InputValue:''
      })
    }else if(e.keyCode=="13"&&this.state.InputValue==''){
      console.log("不能为空")
    }
  }
  handleRemoveInputValue(index){
    const list = [...this.state.list]
    list.splice(index,1)
    this.setState({
      list,
    })
  }
}

export default TodoList;

----------------------------------------------------
src/TodoItem.js
import React,{Component} from 'react';

class TodoItem extends Component{
    constructor(props){
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    render(){
        return (
          <div 
            onClick={this.handleClick}>
                {this.props.content}
            </div>)
    }
    handleClick(){
        this.props.deleteItem(this.props.index)
    }
}

export default TodoItem;
总结:  父组件中(src/TodoList.js)
<TodoItem 
  index={index} 
  content={item}
  deleteItem = {this.handleRemoveInputValue.bind(this)} 
/>

  把函数传给子组件,并把this指向TodoList
  把index和item通过属性传递给TodoItem 
  在TodoItem组件中,使用
    this.props.content
    this.props.index
    this.props.deleteItem
  即可使用


子组件中(src/TodoItem.js)
render(){
  return (
    <div 
      onClick={this.handleClick}>  
      {this.props.content}  绑定item
    </div>)
}
handleClick(){
  this.props.deleteItem(this.props.index)  
  调用父级函数,并把父级item传递进去
}

七:TodoList代码优化

src/TodoItem.js

import React, { Component } from 'react';

class TodoItem extends Component {
  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }

  render() {
    const {content} = this.props
    return (
      <li onClick={this.handleClick}>
        {content}
      </li>
    )
  }

  handleClick(){
    const { deleteItem,index } = this.props;
    deleteItem(index)
  }
}

export default TodoItem;
src/TodoList.js

import React, { Component,Fragment } from 'react';
import TodoItem from './TodoItem';
import './style.css';

class Todolist extends Component {
  constructor(props){
    super(props)
    this.state = {
      inputValue:'',
      list:['学习英文','学习React']
    }
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
  }

  render() {

    return (
      <Fragment>
        <div>
          <label htmlFor="insertArea">输入内容</label>
          <input 
            id="insertArea"
            className="input"
            value={this.state.inputValue} 
            onChange={this.handleInputChange}
          />
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul>
        {
          this.getTodoItem()
        }
        </ul>  
      </Fragment>
    )
  }
  getTodoItem(){
    return this.state.list.map((item,index)=>{
      return (
          <TodoItem
            key={index}
            content={item} 
            index={index} 
            deleteItem={this.handleItemDelete} 
          />
      )
    })
  }
  handleInputChange(e){
    // 新版支持一个函数
    // this.setState(()=>{
    //   return {
    //     inputValue: e.target.value
    //   }
    // })
    // ----------------

    // ES6里面也可以去掉return,直接返回一个对象
    // 有一个括号,表示返回里面的对象
    // this.setState(()=>({
    //     inputValue: e.target.value
    //   })
    // )
    //这样写报错的原因,setState如果传一个函数,是一个异步的,为了性能上的提升
    //异步函数 e.target.value会有一些问题

    // ----------------
    // 解决方法
    const value = e.target.value  //在外层保存一下
    this.setState(()=>({
        inputValue: value
    }));

    // ----------------
    // 最初版
    // this.setState({
    //   inputValue: e.target.value
    // })

  }

  handleBtnClick(){
    //prevState 指的是修改数据之前是什么样的
    //等价于this.state
    this.setState((prevState)=>({
      list:[...prevState.list,prevState.inputValue],
      inputValue:''
    }));
  }

  handleItemDelete(index){
    this.setState((prevState)=>{
      const list = [...prevState.list];
      list.splice(index,1)
      return {list}
    });
  }
}

export default Todolist;

八:React高级内容

React developer tools 安装及使用插件:
google浏览器扩展程序 安装React Developer Tools

不是React开发的网站

React开发环境

线上React开发的网站

安装完成之后,控制台会增加一个React菜单,可以查看页面结构和数据
PropTypes 与 DefaultProps 的应用:
每个组件都有自己的prop参数,这个参数是从父组件接收的一些属性。

  • PropTypes的作用是在接受参数的时候,对参数类型做限制。
  • DefaultProps的作用是定义参数的默认值。
src/TodoItem.js

import React, { Component } from 'react';
----------------
引入propTypes
import PropTypes from 'prop-types';
------------------
class TodoItem extends Component {
  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }

  render() {
    const {test,content} = this.props
    return (
      <li onClick={this.handleClick}>
        {test} - {content}
      </li>
    )
  }

  handleClick(){
    const { deleteItem,index } = this.props;
    deleteItem(index)
  }
}

--------------------------
使用
 //组件名字TodoItem,对它的属性做强校验
TodoItem.propTypes = {
    content: PropTypes.string,  //content的类型必须是string     
    //PropTypes是上面引入的PropTypes
    deleteItem: PropTypes.func,
    index: PropTypes.number,
    test: PropTypes.number  test是没传的值,不会做检验

    如果要求必须传test这个属性可以这样写:
    test: PropTypes.string.isRequired

    也可以写判断
    content: PropTypes.oneOfType([PropTypes.number,PropTypes.string])
    数字或者字符串
}  


有的时候必须要求传递test,但父组件确实没办法传递,可以给test定义一个默认值,来解决报错问题
TodoItem.defaultProps = {
  test: 'hello world'
}


--------------------------

export default TodoItem;

从父级传递来的属性有:
content,deleteItem,index


PropTypes 不会阻止程序的正常运行,但是会在控制台弹出一个报错。


isRequired:表示必须要在父组件里传递,不传递会报警告

PropTypes的类型:

  optionalArray: PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,
 
  // Anything that can be rendered: numbers, strings, elements or an array
  // (or fragment) containing these types.
  optionalNode: PropTypes.node,
 
  // A React element (ie. <MyComponent />).
  optionalElement: PropTypes.element,
  optionalMessage: PropTypes.instanceOf(Message),
  ...

Typechecking With PropTypes - 文档

props,state与render函数的关系:

  • 当组件的state或者props发生改变的时候,render函数就会重新执行
  • 当父组件的render函数被运行时,它的子组件的render都会被将被重新运行一次(可以从两个方面理解,1.自身的props变化,重新运行;2.父组件重新渲染,顺带把包含的子组件一起重新运行。)
    React中的 虚拟DOM:
    深入了解虚拟DOM:
    虚拟DOM中的Diff算法:
    React中ref的使用:
    在React中使用ref来操作DOM
src/TodoList.js

  render() {

    return (
      <Fragment>
        <div>
          <label htmlFor="insertArea">输入内容</label>
          <input 
            id="insertArea"
            className="input"
            value={this.state.inputValue} 
            onChange={this.handleInputChange}
            ref={(input)=>{this.input = input}}
            {/*构建一个ref引用,这个引用叫this.input指向对应的input dom节点*/}
          />
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul>
        {
          this.getTodoItem()
        }
        </ul>  
      </Fragment>
    )
  }

  handleInputChange(e){
    console.log(e.target)  
    //其实就是dom节点 <input id="insertArea" class="input" value="">
    // const value = e.target.value
    const value = this.input.value  //e.target使用this.input来替换
    this.setState(()=>({
        inputValue: value
    }));
  }

不推荐使用ref,原因:React中建议数据驱动的方式来编写我们的代码,尽量不要直接操作DOM

  render() {

    return (
      <Fragment>
        <div>
          <label htmlFor="insertArea">输入内容</label>
          <input 
            id="insertArea"
            className="input"
            value={this.state.inputValue} 
            onChange={this.handleInputChange}
            ref={(input)=>{this.input = input}}
          />
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul ref={(ul)=>this.ul=ul}>  {/*绑定ul*/}
        {
          this.getTodoItem()
        }
        </ul>  
      </Fragment>
    )
  }

  handleBtnClick(){  //点击按钮
    //prevState 指的是修改数据之前是什么样的
    //等价于this.state
    this.setState((prevState)=>({
      list:[...prevState.list,prevState.inputValue],
      inputValue:''
    }));
    console.log(this.ul.querySelectorAll("li").length); 
    //当点击添加时,获取的页面数据长度总少一个,因为this.setState是异步的

    正确办法,使用this.setState第二个参数,也是一个函数,当第一个函数被执行成功的回调
    this.setState((prevState)=>({
      list:[...prevState.list,prevState.inputValue],
      inputValue:''
    }),()=>{
      console.log(this.ul.querySelectorAll("li").length);
    });
  }

九:React中的生命周期函数


生命周期函数指在某一个时刻组件会自动调用执行的函数。

Initialization 是组件初始化
constructor(props) {
    super(props);
    this.state = {
      inputValue:''
    }
  }
在这里会定义state,接收props
constructor是es6语法中自带的函数,所以不算React的生命周期函数。
但是和React生命周期函数没有太大的区别
Mounting 挂载(组件第一次挂载到页面上)
  • componentWillMount(){}
    在组件即将被挂载到页面的时刻自动执行

  • render(){}
    组件渲染

  • componentDidMount(){}
    在组件被挂载到页面之后,自动被执行

componentWillMount和componentDidMount只会在组件第一次被放到页面上的时候执行(第一次挂载的时候)

Updation 组件更新的时候
  • shouldComponentUpdate(){}
    组件被更新之前,他会自动被执行(当组件即将被变更的时候)
    shouldComponentUpdate会要求返回一个布尔类型的结果
shouldComponentUpdate(){
  return true
}

shouldComponentUpdate根据单次直译:你的组件需要被更新吗?

  • componentWillUpdate(){}
    组件被更新之前,它会自动执行,但是他在shouldComponentUpdate之后执行
    如果shouldComponentUpdate返回true它才执行
    如果返回false,这个函数就不会被执行了

  • render(){}
    渲染组件

  • componentDidUpdate(){}
    组件更新完成之后,他会被执行

  • componentWillReceiveProps(){}
    一个组件如果是顶层组件,没有props传递进来,是不会执行的
    当一个组件从父组件接受了参数
    如果这个组件第一次存在于父组件中,不会执行
    如果这个组件之前已经存在于父组件中,才会执行
    (只要父组件的render函数被重新执行了,子组件的这个生命周期函数就会被执行)

Unmounting 组件从页面去除
  • componentWillUnmount(){}
    当这个组件即将被从页面中剔除的时候,会被执行

React developer tools插件使用:

打开F12,勾选中Highlight Updates,每次组件被更新,会有闪框提示

每次输入内容,子组件都会更新

对于一个组件,render函数被执行有两种情况:

  • 在props和state发生变化的时候子组件会被渲染;
  • 父组件render函数执行

input输入内容时,子组件被重复渲染在于第二种情况:这个逻辑没问题,但会带来性能上的损耗
父组件input框内容发生变化,子组件没有必要进行重新渲染,现在的机制会造成子组件进行很多无谓的渲染。
性能优化:

src/TodoItem.js  子组件使用shouldComponentUpdate
shouldComponentUpdate(){
  return false;
}
{/*shouldComponentUpdate 我的子组件被渲染一次之后,如果子组件需要被更新,那么强制要求不更新*/}
子组件只会被render一次,之后不会被重新渲染

上面写法不是最优写法

src/TodoItem.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class TodoItem extends Component {
  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
    //React提升代码性能一:把this作用域的修改放到constructor里面来做,可以保证整个程序里面作用域的绑定只会执行一次,而且可以避免组件一些无谓的渲染
  }

  // React提升代码性能二:React底层setState内置了性能机制,是异步的函数,可以把多次数据的改变结合成一次来做,降低虚拟DOM的使用频率

  //React提升代码性能三:React底层用了虚拟DOM这个概念,还有同层比对,配值概念,来提升虚拟DOM比对速度,来提升React性能
  shouldComponentUpdate(nextProps,nextState){
    //React提升代码性能四:当我的组件要被更新的时候,props会被更新成什么
    // nextProps 指接下来props会被更新成什么样
    // nextState 指接下来state会被更新成什么样
    if(nextProps.content !== this.props.content){
      //如果接下来变化的content不等于当前props.content,说明组件接收的content值发生了变化
      //需要让这个组件重新渲染
      return true;
    }else{
      //没有变化 返回false,组件没有被重新渲染的必要
      return false
    }
  }

  render() {
    console.log("child render")
    const {content} = this.props
    return (
      <li onClick={this.handleClick}>
        {content}
      </li>
    )
  }

  handleClick(){
    const { deleteItem,index } = this.props;
    deleteItem(index)
  }
}

TodoItem.propTypes = {
  content: PropTypes.string, 
  deleteItem: PropTypes.func,
  index: PropTypes.number,
}  

export default TodoItem;

通过shouldComponentUpdate生命周期提升了组件性能,避免一个组件做无谓的render
render函数从新执行就意味着React底层需要对组件生成一份虚拟DOM,和之前虚拟DOM做比对,虽然虚拟DOM的比对比真实DOM比对性能要快得多,但是能省略比对过程当然
可以解决更多的性能,shouldComponentUpdate就是做这件事用的

在React中想发送一个ajax请求:
不能再render函数中发送ajax请求,会造成死循环,render函数会被反复执行,只要在input框内输入内容,render函数就会重新执行,就重新发送一次ajax请求,这样不合理。ajax获取一次数据就行了,render函数会获取很多次ajax

在React中,哪一个生命周期函数只会被执行一次?
componentDidMount(){} 在组建被挂在到页面上的时候,会被执行一次,之后就不会再被重新执行了,把ajax请求放到这里比较合适

思考:componentWillMount(){}也只执行一次,把ajax请求放到这里可不可以?
也是没有任何问题的,但是当我们去写react native,或者用React做服务器端同构也就是更深一点的技术,可能会和以后一些更高端技术产生冲突。为了避免冲突,ajax就放到componentDidMount中,永远都不会有任何问题。

思考:把ajax请求放到constructor中可不可以?
也可以,因为constructor也是只执行一次的函数,推荐把ajax写在componentWillMount中

在React项目中如何发送一个ajax请求?
React没有内置ajax,需要安装一些第三方模块来帮助发送ajax请求。

npm install -g yarn

进入目录
yarn add axios

src/TodoList.js
import React, { Component,Fragment } from 'react';
import TodoItem from './TodoItem';
import axios from 'axios';  //引入axios
import './style.css';

class Todolist extends Component {
  constructor(props){
    super(props)
    this.state = {
      inputValue:'',
      list:[]
    }
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
  }

  render() {
    return (
      <Fragment>
        <div>
          <label htmlFor="insertArea">输入内容</label>
          <input 
            id="insertArea"
            className="input"
            value={this.state.inputValue} 
            onChange={this.handleInputChange}
          />
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul>
        {
          this.getTodoItem()
        }
        </ul>  
      </Fragment>
    )
  }

  //使用axios
  componentDidMount(){  
    axios.get('/api/todolist')
      .then(()=>{alert('succ')})
      .catch(()=>{alert('error')})
  }

  //构建一个ref引用,这个引用叫this.input指向对应的input dom节点
  getTodoItem(){
    return this.state.list.map((item,index)=>{
      return (
          <TodoItem
            key={index}
            content={item} 
            index={index} 
            deleteItem={this.handleItemDelete} 
          />
      )
    })
  }
  handleInputChange(e){
    // console.log(e.target)  //其实就是dom节点 <input id="insertArea" class="input" value="">
    // const value = e.target.value  //在外层保存一下
    const value = e.target.value  //e.target使用this.input来替换
    this.setState(()=>({
        inputValue: value
    }));

  }

  handleBtnClick(){
    this.setState((prevState)=>({
      list:[...prevState.list,prevState.inputValue],
      inputValue:''
    }));
  }

  handleItemDelete(index){
    this.setState((prevState)=>{
      const list = [...prevState.list];
      list.splice(index,1)
      return {list}
    });
  }
}

export default Todolist;

使用Charles实现本地数据mock
前后端分离,需要前端本地进行接口数据的模拟
下载安装charles工具
桌面创建todolist.json,希望发送todolist接口的时候,能够把桌面上的todolist.json返回回来

todolist.json

["学习React","学习英语","学习Vue"]


需要借助charles工具来解决这个问题
charles工具=>Tools=>Map Local
add 增加一条配置


请求成功

查看数据

charles可以抓到浏览器向外发送的一些请求,然后可以对一些请求做一些处理,比如看到请求地址http://localhost:3000/api/todolist,只要请求这个地址,就把桌面文件返回,charles就是一个中间的代理服务器

  componentDidMount(){
    axios.get('/api/todolist')
      .then((res)=>{
        console.log(res.data)
        // this.setState(()=>{
        //   return {
        //     list: res.data
        //   }
        // })
        this.setState(()=>({
          list:[...res.data]  
          /构建一个新的数组传递,避免不小心把res.data做了修改/
        }))
      })
      .catch(()=>{alert('error')})
  }

十:React中实现CSS过渡动画

十一:Redux入门

想做大型应用,需要在React基础上配套一个数据层框架结合使用,全球范围内比较好的搭配数据层框架——Redux。


Redux基础设计理念:把组件之中的数据放到公用存储区域存储,组件改变数据不需要传递,改变Store里的数据之后,其他组件会感知到Store数据发生改变,再去获取数据,这样不管组件层次有多深,走的流程相同。

Redux = Reducer + Flux

Redux的起源:React在2013年开源时,Facebook团队除了放出React框架,还放出了一个框架Flux,Flux框架是官方推出的最原始辅助React使用的数据层框架。业界使用Flux发现一些缺点,比如公共存储区域Store可以有很多store组成,数据存储的时候可能存在数据依赖问题,总之不是特别好用,有人把Flux做了一个升级,升级成了目前使用的Redux。
在Redux里面除了借鉴Flux以前很多设计理念之外,又引入了Reducer概念。
Redux的工作流程:

Redux的工作流程图例

  • Store存放了所有数据(存储数据的公共区域)
  • React Components(组件)从Store里拿数据,每个组件也要去改Store里数据
    流程简述一:

    流程简述二:
    首先由一个组件,组件要去获取Store里一些数据,和Store说"我要获取数据"这句话就是Action Creators,Action Creators创建了一句话之后告诉Store,Store接收到"我要获取数据",Store并不知道需要什么样的数据,他去查一下,Reducers知道应该给你什么样的数据,Reducers告诉Store应该给组件什么样的数据,Store知道了之后把数据给到组件。

使用Antd美化TodoList页面:
Antd是React的UI框架。

yarn add antd

引入样式:
import 'antd/dist/antd.css'; 
src/TodoList.js

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input,Button,List } from 'antd';

const data = [
  'Racing car sprays burning fuel into crowd.',
  'Japanese princess to wed commoner.',
  'Australian walks 100km after outback crash.',
  'Man charged over missing wedding girl.',
  'Los Angeles battles huge wildfires.',
];

class TodoList extends Component {

  render() {
    return (
      <div style={{marginTop: '10px',marginLeft: '10px'}}>
        <div>
          <Input placeholder="todo info" style={{width: '300px',marginRight:'10px'}} />
          <Button type="primary">提交</Button>
        </div>
        <List
          style={{marginTop: '10px',width:'300px'}}
          bordered
          dataSource={data}
          renderItem={item => (<List.Item>{item}</List.Item>)}
        />
      </div>
    )
  }
}

export default TodoList;

Antd工具在开发一些后台管理系统的时候用的非常多。

创建Redux中的Store:
安装Redux

yarn add redux
新建文件 src/store/index.js  /仓库(图书管理员)

import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);  // 把笔记本传递给store

export default store;
--------------------------------------------------------

新建文件 src/store/reducer.js   /记录本
const defaultState = {
    inputValue:'111',
    list:[1,2]
} 

export default (state=defaultState,action)=>{  //函数接受两个参数
    return state;
}

/ reducer 是笔记本,笔记本存放很多关于图书馆数据操作,数据情况。
/ state 存放整个图书馆里所有书籍信息
/ state=defaultState 默认什么信息都不存储
-----------------------------------------------------------------

src/TodoList.js
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input,Button,List } from 'antd';
import store from './store';  /简化写法 import store from './store/index.js'; 

class TodoList extends Component {

  constructor(props){
    super(props);
    this.state = store.getState();
    console.log(this.state)  /通过store.getState() 获取store中的数据
  }
  render() {
    return (
      <div style={{marginTop: '10px',marginLeft: '10px'}}>
        <div>
          <Input value={this.state.inputValue} placeholder="todo info" style={{width: '300px',marginRight:'10px'}} />
          <Button type="primary">提交</Button>
        </div>
        <List
          style={{marginTop: '10px',width:'300px'}}
          bordered
          dataSource={this.state.list}
          renderItem={item => (<List.Item>{item}</List.Item>)}
        />
      </div>
    )
  }
}

export default TodoList;
Action和Reducer的编写:

安装工具 拓展程序 redux devtools



安装完成之后,控制台会多出redux选项


如何使用 redux devtools

src/store/index.js文件
// 在代码中添加 window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()

import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(
    reducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);  // 把笔记本传递给store

/ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
/ 当做第二个参数传递进去,意思是如果有这个变量就执行这个变量对应的方法

/ __REDUX_DEVTOOLS_EXTENSION__这个变量是Redux DevTools的浏览器拓展
/ 意思是如果下面安装了Redux DevTools,那么就在页面上使用这个工具


export default store;
src/TodoList.js

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input,Button,List } from 'antd';
import store from './store';

class TodoList extends Component {

  constructor(props){
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this)
    this.handleBtnClick = this.handleBtnClick.bind(this)

    this.handleStoreChange = this.handleStoreChange.bind(this)
    store.subscribe(this.handleStoreChange)  
    //这个组件去订阅Store,只要Store数据发生改变
    //subscribe()里面写一个函数,这个函数就被自动执行

    console.log(this.state)  //通过store.getState() 获取store中的数据

    
  }
  render() {
    return (
      <div style={{marginTop: '10px',marginLeft: '10px'}}>
        <div>
          <Input 
              value={this.state.inputValue} 
              placeholder="todo info" 
              style={{width: '300px',marginRight:'10px'}} 
              onChange={this.handleInputChange}
            />
          <Button 
            type="primary"
            onClick={this.handleBtnClick}
            >
            提交
          </Button>
        </div>
        <List
          style={{marginTop: '10px',width:'300px'}}
          bordered
          dataSource={this.state.list}
          renderItem={(item,index) => (<List.Item onClick={this.handleItemDelete.bind(this,index)}>{item}</List.Item>)}
        />
      </div>
    )
  }

  handleInputChange(e){
    const action = {
      type:'change_input_value',
      value: e.target.value
    }
    store.dispatch(action)  
    // 把action 传递给Store  
    // Store需要去查小手册(Reducers),把当前数据和action一起传递给小手册(Reducers)
    // Store会把当前Store存的数据,和接收到的action一起转发给reducers
    // reducers来告诉Store来做什么
  }

  handleStoreChange(){
    this.setState(store.getState())
    //当感知到Store数据发生改变,就调用store.getState(),从store里从新取一次数据
    //再调用setState 替换当前组件数据
  }

  handleBtnClick(){
    const action = {
      type:'add_todo_item'
    }
    store.dispatch(action) 
  }
  
  handleItemDelete(index){
    const action = {
      type:'delete_todo_item',
      index
    }
    store.dispatch(action) 
  }
}

export default TodoList;
src/store/reducer.js

const defaultState = {
    inputValue:'',
    list:[1,2]
} 

//为什么需要深拷贝?
//Redux的限制,reducer可以接受state,但绝不能修改state
export default (state=defaultState,action)=>{  //函数接受两个参数
    console.log(state,action)
    if(action.type === "change_input_value"){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.inputValue = action.value;
        return newState  //返回给Store,替换Store的老数据
    }
    if(action.type === "add_todo_item"){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.list.push(newState.inputValue);
        newState.inputValue="";
        return newState  //返回给Store,替换Store的老数据
    }
    if(action.type === "delete_todo_item"){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.list.splice(action.index,1);
        return newState  //返回给Store,替换Store的老数据
    }
    return state;
}

// reducer 是笔记本,笔记本存放很多关于图书馆数据操作,数据情况。
// state 存放整个图书馆里所有书籍信息
// state=defaultState 默认什么信息都不存储

// state指的是上一次存储数据
// action指的是用户传过来的那句话
ActionTypes的拆分:
新建src/store/actionTypes.js 文件存放 action Type

export const CHANGE_INPUT_VALUE = 'change_input_value';  //export 导出
export const ADD_TODO_ITEM = 'add_todo_item'; 
export const DELETE_TODO_ITEM = 'delete_todo_item'; 

使用:
src/TodoList.js
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from './store/actionTypes';

CHANGE_INPUT_VALUE 替换 'change_input_value'
ADD_TODO_ITEM 替换 'add_todo_item'
DELETE_TODO_ITEM 替换 'delete_todo_item'


src/store/reducer.js
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from './actionTypes';

CHANGE_INPUT_VALUE 替换 'change_input_value'
ADD_TODO_ITEM 替换 'add_todo_item'
DELETE_TODO_ITEM 替换 'delete_todo_item'

使用actionCreator 统一创建 action
新建 src/store/actionCreators.js

import { CHANGE_INPUT_VALUE,ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes';

export const getInputChangeAction = (value)=> ({
    type: CHANGE_INPUT_VALUE,
    value
})

export const getAddItemAction = ()=> ({
    type: ADD_TODO_ITEM
})

export const getDeleteItemAction = (index)=> ({
    type: DELETE_TODO_ITEM,
    index
})

使用:
src/TodoList.js
import { getInputChangeAction,getAddItemAction,getDeleteItemAction } from './store/actionCreators'

  handleInputChange(e){
    // const action = {
    //   type: CHANGE_INPUT_VALUE,
    //   value: e.target.value
    // }
    const action = getInputChangeAction(e.target.value)
    store.dispatch(action)  
  }

  handleBtnClick(){
    // const action = {
    //   type: ADD_TODO_ITEM
    // }
    const action = getAddItemAction()
    store.dispatch(action) 
  }
  
  handleItemDelete(index){
    // const action = {
    //   type: DELETE_TODO_ITEM,
    //   index
    // }
    const action = getDeleteItemAction(index)
    store.dispatch(action) 
  }

好处:

  • 提高代码可维护性
  • 方便前端自动测试化工具
Redux 知识点复习补充

Redux 设计和使用的三项原则:

  • Store是唯一的
整个项目只有一个store 在src/store/index.js

(整个应用中只有一个store公共存储空间)
  • 只有store能够改变自己的内容(在reducer中改变了store的数据是不被允许的,在reducer中是复制一个新的对象进行操作)
  • Reducer必须是纯函数
    纯函数指的是,给定固定的输入,就一定会有固定的输出,而且不会有任何副作用
    state和action都确定的时候,那么return出来的结果永远都是固定的,不纯的函数,比如newState.inputValue = new Date(),因为return newState的值不是固定的
    副作用指 state.inputValue = action.value,这段代码就是有副作用的代码,对接受的参数做了修改
Redux 核心API
  • createStore 创建store
  • store.dispatch 派发action(action传递给Store)
  • store.getState 获取store数据内容
  • store.subscribe 订阅store的改变

十二:Redux进阶

UI组件和容器组件

UI组件(傻瓜组件)- 只负责页面的一些显示
容器组件(聪明组件)- 负责页面逻辑处理

划分组件的原因:把组件的逻辑和渲染放到一个组件中管理维护起来比较困难。或内容比较多。需要对组件进行一个拆分。

  • UI组件负责页面渲染
  • 容器组件负责页面逻辑
创建UI组件
src/TodoListUI.js

import React, { Component } from 'React';
import { Input,Button,List } from 'antd';

class TodoListUI extends Component {
  render() {
    return (
      <div style={{ marginTop: '10px', marginLeft: '10px' }}>
        <div>
          <Input
            value={this.props.inputValue}
            placeholder="todo info"
            style={{ width: '300px', marginRight: '10px' }}
            onChange={this.props.handleInputChange}
          />
          <Button
            type="primary"
            onClick={this.props.handleBtnClick}
          >
            提交
          </Button>
        </div>
        <List
          style={{ marginTop: '10px', width: '300px' }}
          bordered
          dataSource={this.props.list}
          renderItem={(item, index) => (<List.Item onClick={()=>{this.props.handleItemDelete(index)}}>{item}</List.Item>)}
        />
      </div>
    )
  }
}

export default TodoListUI;

创建一个容器组件
src/TodoList.js

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store';
import { getInputChangeAction, getAddItemAction, getDeleteItemAction } from './store/actionCreators'
import TodoListUI from './TodoListUI';

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this)
    this.handleBtnClick = this.handleBtnClick.bind(this)
    this.handleStoreChange = this.handleStoreChange.bind(this)
    this.handleItemDelete = this.handleItemDelete.bind(this)
    store.subscribe(this.handleStoreChange)
  }
  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}
        handleInputChange={this.handleInputChange}
        handleBtnClick={this.handleBtnClick}
        handleItemDelete={this.handleItemDelete}
      />
    )
  }

  handleInputChange(e) {
    const action = getInputChangeAction(e.target.value)
    store.dispatch(action)
  }

  handleStoreChange() {
    this.setState(store.getState())
  }

  handleBtnClick() {
    const action = getAddItemAction()
    store.dispatch(action)
  }

  handleItemDelete(index) {
    const action = getDeleteItemAction(index)
    store.dispatch(action)
  }
}

export default TodoList;
无状态组件

上面的UI组件(傻瓜组件)只有一个render函数,就可以用无状态组件来定义这个组件。

无状态组件如何定义?
无状态组件其实就是一个函数。

src/TodoListUI.js 改写 UI组件改成无状态组件
import React, { Component } from 'react';
import { Input,Button,List } from 'antd';

const TodoListUI = (props)=>{ 
  // {/* this.props.inputValue 改成props.inputValue */}
  // {/*不需要this 从传参获取*/} 
  return (
    <div style={{ marginTop: '10px', marginLeft: '10px' }}>
        <div>
          <Input
            value={props.inputValue} 

            placeholder="todo info"
            style={{ width: '300px', marginRight: '10px' }}
            onChange={props.handleInputChange}
          />
          <Button
            type="primary"
            onClick={props.handleBtnClick}
          >
            提交
          </Button>
        </div>
        <List
          style={{ marginTop: '10px', width: '300px' }}
          bordered
          dataSource={props.list}
          renderItem={(item, index) => (<List.Item onClick={()=>{props.handleItemDelete(index)}}>{item}</List.Item>)}
        />
      </div>
  )
}
//接收一个props 同时要求返回一个JSX

// class TodoListUI extends Component {
//   render() {
//     return (
//       <div style={{ marginTop: '10px', marginLeft: '10px' }}>
//         <div>
//           <Input
//             value={this.props.inputValue}
//             placeholder="todo info"
//             style={{ width: '300px', marginRight: '10px' }}
//             onChange={this.props.handleInputChange}
//           />
//           <Button
//             type="primary"
//             onClick={this.props.handleBtnClick}
//           >
//             提交
//           </Button>
//         </div>
//         <List
//           style={{ marginTop: '10px', width: '300px' }}
//           bordered
//           dataSource={this.props.list}
//           renderItem={(item, index) => (<List.Item onClick={()=>{this.props.handleItemDelete(index)}}>{item}</List.Item>)}
//         />
//       </div>
//     )
//   }
// }

export default TodoListUI;

当一个普通组件只有render函数,可以使用无状态组件替换掉普通组件
优点:无状态组件性能比较高。
无状态组件就是一个函数,而class类生成对象还会有一些生命周期函数,执行起来需要执行render和生命周期函数,执行的东西远比函数执行的多。
注:虽然理论上UI组件只做页面渲染,有时候简单做一些逻辑也是可以的。

Redux中发送异步请求获取数据

componentDidMount 中获取数据


实现从/todolist.json借口获取数据并绑定
todolist.json

["学习React","学习英语","学习Vue"]
src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));
src/TodoList.js

import React, { Component } from 'react';
import 'antd/dist/antd.css';
import store from './store';
import { getInputChangeAction, getAddItemAction, getDeleteItemAction,initListAction } from './store/actionCreators'
import TodoListUI from './TodoListUI';
import axios from 'axios';  //引入第三方请求组件

class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this)
    this.handleBtnClick = this.handleBtnClick.bind(this)

    this.handleStoreChange = this.handleStoreChange.bind(this)
    this.handleItemDelete = this.handleItemDelete.bind(this)
    store.subscribe(this.handleStoreChange)
    //这个组件去订阅Store,只要Store数据发生改变
    //subscribe()里面写一个函数,这个函数就被自动执行

    console.log(this.state)  //通过store.getState() 获取store中的数据


  }
  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}
        handleInputChange={this.handleInputChange}
        handleBtnClick={this.handleBtnClick}
        handleItemDelete={this.handleItemDelete}
      />
    )
  }

  componentDidMount(){
    axios.get('/todolist.json').then((res)=>{  //成功函数
      const data = res.data;  //接口获取到数据,改变store中的数据
      //store有个方法 dispatch 先要创建一个action
      const action = initListAction(data)  
      store.dispatch(action)  
      //把action传递给store ,store拿到之前的数据连同action一同传递给reducer
      console.log(data)
    }).catch((fail)=>{  //失败函数

    })
  }

  handleInputChange(e) {
    // const action = {
    //   type: CHANGE_INPUT_VALUE,
    //   value: e.target.value
    // }
    const action = getInputChangeAction(e.target.value)
    store.dispatch(action)
    // 把action 传递给Store  
    // Store需要去查小手册(Reducers),把当前数据和action一起传递给小手册(Reducers)
    // Store会把当前Store存的数据,和接收到的action一起转发给reducers
    // reducers来告诉Store来做什么
  }

  handleStoreChange() {
    this.setState(store.getState())
    //当感知到Store数据发生改变,就调用store.getState(),从store里从新取一次数据
    //再调用setState 替换当前组件数据
  }

  handleBtnClick() {
    // const action = {
    //   type: ADD_TODO_ITEM
    // }
    const action = getAddItemAction()
    store.dispatch(action)
  }

  handleItemDelete(index) {
    // const action = {
    //   type: DELETE_TODO_ITEM,
    //   index
    // }
    const action = getDeleteItemAction(index)
    store.dispatch(action)
  }
}

export default TodoList;
src/TodoListUI.js

import React, { Component } from 'react';
import { Input,Button,List } from 'antd';

const TodoListUI = (props)=>{ 
  // {/* this.props.inputValue 改成props.inputValue */}
  // {/*不需要this 从传参获取*/} 
  return (
    <div style={{ marginTop: '10px', marginLeft: '10px' }}>
        <div>
          <Input
            value={props.inputValue} 

            placeholder="todo info"
            style={{ width: '300px', marginRight: '10px' }}
            onChange={props.handleInputChange}
          />
          <Button
            type="primary"
            onClick={props.handleBtnClick}
          >
            提交
          </Button>
        </div>
        <List
          style={{ marginTop: '10px', width: '300px' }}
          bordered
          dataSource={props.list}
          renderItem={(item, index) => (<List.Item onClick={()=>{props.handleItemDelete(index)}}>{item}</List.Item>)}
        />
      </div>
  )
}
//接收一个props 同时要求返回一个JSX

// class TodoListUI extends Component {
//   render() {
//     return (
//       <div style={{ marginTop: '10px', marginLeft: '10px' }}>
//         <div>
//           <Input
//             value={this.props.inputValue}
//             placeholder="todo info"
//             style={{ width: '300px', marginRight: '10px' }}
//             onChange={this.props.handleInputChange}
//           />
//           <Button
//             type="primary"
//             onClick={this.props.handleBtnClick}
//           >
//             提交
//           </Button>
//         </div>
//         <List
//           style={{ marginTop: '10px', width: '300px' }}
//           bordered
//           dataSource={this.props.list}
//           renderItem={(item, index) => (<List.Item onClick={()=>{this.props.handleItemDelete(index)}}>{item}</List.Item>)}
//         />
//       </div>
//     )
//   }
// }

export default TodoListUI;
src/store/actionCreators.js

import { CHANGE_INPUT_VALUE,ADD_TODO_ITEM, DELETE_TODO_ITEM,INIT_LIST_ACTION } from './actionTypes';

export const getInputChangeAction = (value)=> ({
    type: CHANGE_INPUT_VALUE,
    value
})

export const getAddItemAction = ()=> ({
    type: ADD_TODO_ITEM
})

export const getDeleteItemAction = (index)=> ({
    type: DELETE_TODO_ITEM,
    index
})

export const initListAction = (data) => ({  //接受list.json接口数据
    type: INIT_LIST_ACTION,
    data
})
src/store/actionTypes.js

export const CHANGE_INPUT_VALUE = 'change_input_value';  //export 导出
export const ADD_TODO_ITEM = 'add_todo_item'; 
export const DELETE_TODO_ITEM = 'delete_todo_item'; 
export const INIT_LIST_ACTION = 'init_list_action';   //接受list.json 定义常量
src/store/index.js

import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(
    reducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);  // 把笔记本传递给store

// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
// 当做第二个参数传递进去,意思是如果有这个变量就执行这个变量对应的方法

// __REDUX_DEVTOOLS_EXTENSION__这个变量是Redux DevTools的浏览器拓展
// 意思是如果下面安装了Redux DevTools,那么就在页面上使用这个工具

export default store;
src/store/reducer.js

import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,INIT_LIST_ACTION} from './actionTypes';

const defaultState = {
    inputValue:'',
    list:[]
} 

//为什么需要深拷贝?
//Redux的限制,reducer可以接受state,但绝不能修改state
export default (state=defaultState,action)=>{  //函数接受两个参数
    console.log(state,action)
    if(action.type === CHANGE_INPUT_VALUE){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.inputValue = action.value;
        return newState  //返回给Store,替换Store的老数据
    }
    if(action.type === ADD_TODO_ITEM){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.list.push(newState.inputValue);
        newState.inputValue="";
        return newState  //返回给Store,替换Store的老数据
    }
    if(action.type === DELETE_TODO_ITEM){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.list.splice(action.index,1);
        return newState  //返回给Store,替换Store的老数据
    }
    if(action.type === INIT_LIST_ACTION){
        const newState = JSON.parse(JSON.stringify(state))  //深拷贝
        newState.list = action.data
        return newState  //返回给Store,替换Store的老数据
    }
    return state;
}

// reducer 是笔记本,笔记本存放很多关于图书馆数据操作,数据情况。
// state 存放整个图书馆里所有书籍信息
// state=defaultState 默认什么信息都不存储

// state指的是上一次存储数据
// action指的是用户传过来的那句话
使用Redux-thunk中间件进行ajax请求发送

ajax异步或者复杂逻辑放在组件里实现时,组件会显得过于臃肿,希望移除到其他地方统一管理。
Redux-thunk中间件可以把异步请求或复杂逻辑放到action中处理
Redux-thunk是Redux的一个中间件。
在React中Redux-thunk这个中间件用的非常多。
Redux-thunk:github地址

yarn add redux-thunk   //安装

使用  在store中
import { createStore, applyMiddleware } from 'redux';   要引入 applyMiddleware 
import thunk from 'redux-thunk';  //引入redux-thunk模块
import rootReducer from './reducers/index';  

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)  //第二个参数引入
);

意思是 创建Store时,使用reducer构建初始数据,然后创建store时,store会使用一个中间件thunk

我们说的中间件,是redux的中间件,不是react的

引入多个中间件
引入thunk 以及 redux-devtools
githup 搜索 redux-devtools 查到文档

const composeEnhancers =
  typeof window === 'object' &&
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
      // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
    }) : compose;

const enhancer = composeEnhancers(
  applyMiddleware(...middleware),
  // other store enhancers if any
);
使用

通过这种编码,既支持window下的devtools,同时也引入了Redux-thunk

src/store/index.js

import { createStore,applyMiddleware ,compose } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
  // 如果window的__REDUX_DEVTOOLS_EXTENSION_COMPOSE__存在,就去调用一下这个方法
  // 否则就等于一个compose函数,compose函数需要从redux中引入进来

const enhancer = composeEnhancers(  //让上面存储的composeEnhancers,执行一下
    // 顺便把thunk通过applyMiddleware执行一下,传递进去
    applyMiddleware(thunk),  
);

const store = createStore(reducer, enhancer);

// 实际上 __REDUX_DEVTOOLS_EXTENSION__ 也是redux的中间件
// 我们说的中间件,是redux的中间件
export default store;

上面代码只做了一件事,安装了thunk,然后在store创建的时候,使用了thunk,代码从GitHub对应的指南里面拷贝过来的
配置好了之后,围绕redux-thunk来编写代码

src/TodoList.js
把异步操作的代码从组件中移除,移除到action里面
import { getInputChangeAction, getAddItemAction, getDeleteItemAction,getTodoList } from './store/actionCreators'
// 引入getTodoList

  componentDidMount(){
    const action = getTodoList();
    store.dispatch(action)  
    /dispatch了一个函数,实际上store只能接受一个对象
    /store发现是一个函数就会干一件事,帮助自动执行一下对应的函数
    /action对应的函数是actionCreators.js 中getTodoList 的 return 函数
    // axios.get('/todolist.json').then((res)=>{
    //   const data = res.data; 
    //   const action = initListAction(data)  
    //   store.dispatch(action)  
    // }).catch((fail)=>{})
    // 把异步操作的代码从组件中移除,移除到action里面
  }

当使用了Readux-thunk之后 action可以是一个函数了(只有用了thunk之后,才能是函数)

getTodoList 的 rerun 函数再去取json的数据,获取数据,去改变store中的数据
只要改变store中的数据,又要走redux的流程
去调用之前写的initListAction,去创建action
想去调用store.dispatch方法

store.dispatch方法如何获取到?
返回的函数自动就会接收dispatch方法
只要调用dispatch 把 action派发出去就行
这个action 实际是一个对象
store会判断action是一个对象,直接就接收这个对象,改变原始状态

src/store/actionCreators.js

import axios from 'axios';  //引入第三方请求组件

/* ------------未变化------------- */
import { CHANGE_INPUT_VALUE,ADD_TODO_ITEM, DELETE_TODO_ITEM,INIT_LIST_ACTION } from './actionTypes';

export const getInputChangeAction = (value)=> ({
    type: CHANGE_INPUT_VALUE,
    value
})

export const getAddItemAction = ()=> ({
    type: ADD_TODO_ITEM
})

export const getDeleteItemAction = (index)=> ({
    type: DELETE_TODO_ITEM,
    index
})

export const initListAction = (data) => ({  //接受list.json接口数据
    type: INIT_LIST_ACTION,
    data
})
/* ------------未变化------------- */

// 当使用了Readux-thunk之后 action可以是一个函数了

export const getTodoList = () => {  //调用getTodoList生成内容是函数的action时
    return (dispatch) => {  // 这个函数能够接收到dispatch方法
        axios.get('/todolist.json').then((res) => {
          const data = res.data; 
          const action = initListAction(data)
          dispatch(action) //直接调用dispatch方法即可
        }).catch((fail)=>{})
    }
    // 正常来说,return应该是一个对象 但使用了Readux-thunk之后,return结果可以是一个函数
    // 在这个函数里面 可以做异步操作
}

疑问:把ajax放到componentDidMount中不是挺好的,为什么要这么麻烦
如果把异步函数放到组件的生命周期函数中来做,这个生命周期函数有可能变得越来越复杂,越来越多,这个组件会变得越来越大,所以建议把复杂的业务逻辑或异步函数拆分到一个地方去管理,现在借助Redux-thunk就可以放到actionCreators中去管理了
放到这里管理又带来一个额外好处,做自动化测试的时候,去测试getTodoList方法会非常的简单,比测试组件的生命周期函数要简单的多。
反复写5遍中间件的使用流程,捋清流程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容