React学习笔记(三)—— 组件高级

一、列表和keys

1.1、Lists and Keys (列表和键)

首先,我们回顾一下在javascript中怎么去变换列表。

下面了的代码,我们用到了数组函数的map方法来实现数组的每一个值变成它的2倍,同时返回一个新数组,最后打印出了这个数组:

const numbers = [1,2,3,4,5];
const doubled = numbers.map(number=>number * 2);
console.log(doubled);

最终在控制台的打印结果是:[2,4,6,8,10]。

在React中,转换一个数组到列表,几乎是相同的。

下面,我们依次通过调用数组的map方法,并返回一个用li标签包含数组值当元素,最后分配它们到listItems数组里面:

const numbers = [1,2,3,4,5];
const listItems = numbers.map(number => <li>{number}</li>);

现在我们把完整的listItems用一个ul标签包起来,同时render it to the DOM

ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);

这样就会在页面中显示一个带列表符号的ul列表,项目编号是从1到5。

1.2、Basic List Component(一个基础的列表组件)

我们经常会在一个组件里面输出一个列表elements。

好,我们来写一个组件实现前面同样的功能,这个组件接受一个数字数组,最后返回一个无序列表。

    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map(number => <li>{number}</li>);
        return (
            <ul>
                {listItems}
            </ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers}/>,
        document.getElementById('root')
    );

如果你运行上面的代码,你将会得到一个警告:你需要为每一个li元素提供一个key属性,这个“Key”字符串属性当你创建一个列表元素的时候必须添加。我们将在接下来讨论一下它为什么这么重要。

让我们在numbers.map()分配一个key属性到列表里面的li标签里来解决上面给出的警告提示问题,代码如下:

    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map(number => <li key={number.toString()}>{number}</li>);
        return (
            <ul>{listItems}</ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers} />,
        document.getElementById('root')
    );

1.3、Keys(如何设置的key属性值)

那我们为什么非要需要这个key属性呢?其实这个key属性可以帮助React确定一下那个列表选项改变了、是新增加的、或者被删除了,反正这个key属性就是用来让react跟踪列表在过去的时间发生了什么变化。key属性值应该在数组里面指定,这样就能保证列表元素就能有一个稳定的身份验证值。

    const numbers = [1,2,3,4,5];
    const listItems = numbers.map(number => <li key={number.toString()}>{number}</li>);

最好的方法设置key值就是指定一个独一无二的字符串值来把当前列表元素同它的兄弟列表元素分离开来。但是通常情况下,你的后台给你的接口数据中都应该有一个当前数据为一个的”id”值,那么你就可以用这个id值来设置key属性值。代码大概像这样子:

const todoItems = todos.map(todo => <li key={todo.id}>{todo.text}</li>);

如果你的接口没有这样的一个id值来确定列表元素的key属性,那么最后的办法就是把当前列表的元素的索引值设置为key属性值了。如:

    const todoItems = todos.map((todo,index) => (
        //只有在todo数据里面没有独一无二的id值的情况才这么做
        <li key={index}>
            {todo.text}
        </li>
    ));

我们不推荐你使用索引值来作为key属性值,因为你的列表重新排序的时候,这样会严重使程序变得很慢。如果你愿意的话,可以在这里(in-depth explanation about why keys are necessary)得到更多有关的信息!

1.4、Extracting Components with Keys(当我们提取一个组件到另一个组件的时候,需要注意怎么管理key)

key属性只有在数组数据环境中才有意义,其它地方是没有意义的。

例如,当我们实现一个ListItem组件的时候,这个组件封装了一个li元素,那么我们不应该在li元素上直接设置key属性,因为没有意义,key是用来跟踪数组才有意义,于是我们在NumberList组件使用到ListItem组件的时候,在数组方法里面设置key属性才有意义。好,我们先来看一个错误设置key属性的版本:

    function ListItem(props){
        const value = props.value;
        return (
            //这里是错误的,因为这里不需要指定key属性
            <li key={value.toString()}>
                {value}
            </li>
        );
    };
    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map(number => (
            //这里也是错误的,因为这里才是真的需要指定key属性值的地方
            //记住一个要点就是:key属性只会在用到有关js处理数组有关的环境中用到
            <ListItem value={number} />
        ));
        return (
            <ul>
                {listItems}
            </ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers} />,
        document.getElementById('root')
    );

正确地设置key属性版本的例子在下面:

    function ListItem(props){
        //正确的,这儿不需要设置key属性
        return (
            <li>{props.value}</li>
        );
    };
    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map((number) => (
            //正确,这儿才是真的需要设置key属性的地方
            <ListItem key={number.toString()} value={number}/>
        ));
        return (
            <ul>
                {listItems}
            </ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers} />,
        document.getElementById('root')
    );

为了帮你这里理解这一层,我自己的理解就是:并不是渲染到页面中的li标签需要key属性,(同时li标签也是没有关系的,我们在这里之所有用到li标签,只是更形象的说明问题,其实你也可以用div等等其它标签)之所要设置key属性,是React内部用来方便管理一个数组数据,跟踪数组里面的每一个数据!所以一个最好的法则就是,凡是需要调用map方法的时候你使用key属性就对了!

1.5、Keys Must Only Be Unique Among Siblings(key属性值相对于兄弟数据来说是独一无二的,记住只是相对于兄弟数据,其它数据没有关系)

key属性值只会跟当前(同一个)数组数据之间是独一无二的,而不用是全局独一无二的,例如,有两个数组,那么它们的key就可以是一样的。如:

    function Blog(props){
        const sidebar = (
            <ul>
                {props.posts.map((post) => (
                    <li key={post.id}>
                        {post.title}
                    </li>
                ))}
            </ul>
        );
        const content = props.posts.map((post) => (
            <div key={post.id}>
                <h3>{post.title}</h3>
                <p>{post.content}</p>
            </div>
        ));
        return (
            <div>
                {sidebar}
                <hr />
                {constent}
            </div>
        );
    };
    const posts = [
      {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
      {id: 2, title: 'Installation', content: 'You can install React from npm.'}
    ];
    ReactDOM.render(
      <Blog posts={posts} />,
      document.getElementById('root')
    );

key属性只是给react一个用于跟踪数据的线索而已,并不是传递给组件的,如果你需要个组件设置一样一个属性,那么可以用不同的属性名代替:

    const content = posts.map((post) => (
        <Post key={post.id} id={post.id} title={post.title} />
    ));

在这个例子中,Post组件可以读id属性,但是不能读key属性。

二、受控组件与非受控组件

2.1、受控组件

如果一个表单元素的值是由React 来管理的,那么它就是一个受控组件。React 组件渲染表单元素,并在用户和表单元素发生交互时控制表单元素的行为,从而保证组件的 state 成为界面上所有元素状态的唯一来源对于不同的表单元素, React 的控制方式略有不同,下面我们就来看一下三类常用表单元素的控制方式。

2.1.1、文本框

文本框包含类型为text 的input 无素和 textarea元素。它们受控的主要原理是,通过表单元素的 value属性设置表单元素的值,通过表单元素的onChange 事件监听值的变化,并将变化同步到React 组件的 state中。

LoginForm.js

import React, { Component } from "react";

export default class LoginForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: "",
    };
  }

  changeHandle = (e) => {
    let target = e.target;
    this.setState({ [target.name]: target.value });
  };

  handleSubmit = (e) => {
    console.log(
      "username:" + this.state.username + " password:" + this.state.password
    );
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <div>
          <h2>用户登录</h2>
          <form onSubmit={this.handleSubmit}>
            <fieldset>
              <legend>用户信息</legend>
              <p>
                <label>帐号:</label>
                <input
                  type="text"
                  name="username"
                  value={this.state.username}
                  onChange={this.changeHandle}
                />
              </p>
              <p>
                <label>密码:</label>
                <input
                  type="password"
                  name="password"
                  onChange={this.changeHandle}
                  value={this.state.password}
                />
              </p>
              <p>
                <button>登录</button>
              </p>
            </fieldset>
          </form>
        </div>
        <div>
          uid:{this.state.username} - pwd:{this.state.password}
        </div>
      </div>
    );
  }
}

运行结果:

用户名和密码两个表单元素的值是从组件的 state中获取的,当用户更改表单元素的值时,onChange事件会被触发,对应的 handleChange处理函数会把变化同步到组件的 state,新的 state又会触发表单元素重新渲染,从而实现对表单元素状态的控制。

这个例子还包含一个处理多个表单元素的技巧:通过为两个 input元素分别指定name属性,使用同一个函数 handleChange处理元素值的变化,在处理函数中根据元素的name属性区分事件的来源。这样的写法显然比为每一个 input元素指定一个处理函数简洁得多。textarea的使用方式和input几乎一致,这里不再赘述。

2.1.2、高阶函数

高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。

1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。

常见的高阶函数有:Promise、setTimeout、arr.map()等等

2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。

常见的高阶函数有:Function.bind

自定义高阶函数

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      /**
       * 
       *高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
       1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
       2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
       常见的高阶函数有:Promise、setTimeout、arr.map()等等
       */

      /**
       * cal 是一个函数
       * n1数字,参数2
       * n2数字,参数3
       */
      function superCal(cal, n1, n2) {
        if (cal && n1 > 0 && n2 > 0) {
          return () => {
            return cal(n1 * 2, n2 * 2);
          };
        }
      }

      let result = superCal((a, b) => a + b, 1, 2)();
      console.log(result);
    </script>
  </body>
</html>

示例1:

import React, { Component } from "react";

export default class LoginForm extends Component {
  state = {
    uid: "abc",
    pwd: "123",
    code: "5678",
  };

  handleSubmit = (e) => {
    console.log(`uid:${this.state.uid} - pwd:${this.state.pwd}`);
    //阻止默认事件
    e.preventDefault();
  };

  //高阶函数+闭包
  formChange = (propKey) => {

    return (e) => {
      this.setState({ [propKey]: e.target.value });
    };

  };

  render() {
    return (
      <div>
        <h2>用户登录</h2>
        <form onSubmit={this.handleSubmit}>
          <fieldset>
            <legend>用户信息</legend>
            <p>
              <label htmlFor="uid">帐号:</label>
              <input
                type="text"
                name="uid"
                id="uid"
                value={this.state.uid}
                onChange={this.formChange("uid")}
              ></input>
            </p>
            <p>
              <label htmlFor="pwd">密码:</label>
              <input
                type="password"
                name="pwd"
                id="pwd"
                value={this.state.pwd}
                onChange={this.formChange("pwd")}
              ></input>
            </p>
            <p>
              <label htmlFor="pwd">验证:</label>
              <input
                type="text"
                name="code"
                id="code"
                value={this.state.code}
                onChange={this.formChange("code")}
              ></input>
            </p>
            <p>
              <button>登录</button>
            </p>
          </fieldset>
          {`uid:${this.state.uid} - pwd:${this.state.pwd} - code:${this.state.code}`}
        </form>
      </div>
    );
  }
}

运行效果:

2.1.3、柯里化

函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      /**
       * 
       *函数的柯里化:
       通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
       */

      /**
       * cal 是一个函数
       */
      function superCal(cal) {
        if (cal) {
          return (n1) => {
            return (n2) => {
              return cal(n1 * 2, n2 * 2);
            };
          };
        }
      }

      let result = superCal((a, b) => a + b)(1)(2);
      console.log(result);
    </script>
  </body>
</html>

结果:

2.1.4、列表

列表select元素是最复杂的表单元素,它可以用来创建一个下拉列表:

<select>
<option value="react">React</option>
<option value="redux">Redux</option>
<option selected value="mobx">MobX</ option>
</select>

通过指定selected属性可以定义哪一个选项(option)处于选中状态,所以上面的例子中,Mobx这一选项是列表的初始值,处于选中状态。在React中,对select的处理方式有所不同,它通过在select上定义 value属性来决定哪一个option元素处于选中状态。这样,对select的控制只需要在select 这一个元素上修改即可,而不需要关注 option元素。下面是一个例子:

import React, { Component } from "react";

export default class SelectForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: "mobx",
    };
  }

  changeHandle = (e) => {
    let target = e.target;
    this.setState({ value: target.value });
  };

  handleSubmit = (e) => {
    console.log("value:" + this.state.value);
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <div>
          <h2>使用技术</h2>
          <form onSubmit={this.handleSubmit}>
            <fieldset>
              <legend>技术信息</legend>
              <p>
                <label>技术列表:</label>
                <select value={this.state.value} onChange={this.changeHandle}>
                  <option value="react">React</option>
                  <option value="redux">Redux</option>
                  <option value="mobx">MobX</option>
                </select>
              </p>
              <p>
                <button>提交</button>
              </p>
            </fieldset>
          </form>
        </div>
        <div>value:{this.state.value}</div>
      </div>
    );
  }
}

运行

 

 2.1.5、复选框与单选框

复选框是类型为checkbox的input元素,单选框是类型为 radio的input元素,它们的受控方式不同于类型为text 的 input元素。通常,复选框和单选框的值是不变的,需要改变的是它们的checked 状态,因此React 控制的属性不再是value属性,而是checked属性。例如:

import React, { Component } from "react";

export default class SelectForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      react: false,
      redux: false,
      mobx: false,
    };
  }

  handleChange = (e) => {
    let target = e.target;
    this.setState({ [e.target.name]: target.checked });
  };

  handleSubmit = (e) => {
    console.log(JSON.stringify(this.state));
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <div>
          <h2>使用技术</h2>
          <form onSubmit={this.handleSubmit}>
            <fieldset>
              <legend>技术信息</legend>
              <p>
                <label>技术列表:</label>
                <input
                  type="checkbox"
                  name="react"
                  value="react"
                  checked={this.state.react}
                  onChange={this.handleChange}
                />
                React
                <input
                  type="checkbox"
                  name="redux"
                  value="redux"
                  checked={this.state.redux}
                  onChange={this.handleChange}
                />
                Redux
                <input
                  type="checkbox"
                  name="mobx"
                  value="mobx"
                  checked={this.state.mobx}
                  onChange={this.handleChange}
                />
                Mobx
              </p>
              <p>
                <button>提交</button>
              </p>
            </fieldset>
          </form>
        </div>
        <div>{JSON.stringify(this.state)}</div>
      </div>
    );
  }
}

运行结果:

2.1.6、升级后的BBS

让每个帖子支持编辑功能:

PostItem.js

import React, { Component } from "react";
import like from "./like.png";

export default class PostItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      editing: false,
      post: props.post,
    };
  }

  static getDerivedStateFromProps(props, state) {
    if (props.post.vote !== state.post.vote) {
      return { post: props.post };
    }
    return null;
  }

  handleVote = () => {
    this.props.onVote(this.props.post.id);
  };

  handleTitleChange = (e) => {
    const newPost = { ...this.state.post, title: e.target.value };
    this.setState({ post: newPost });
  };

  handleEditPost = (e) => {
    if (this.state.editing) {
      this.props.onSave({
        ...this.state.post,
        date: new Date().toLocaleString(),
      });
    }
    this.setState({
      editing: !this.state.editing,
    });
  };

  render() {
    const { post } = this.state;
    return (
      <li className="item">
        <div className="title">
          {this.state.editing ? (
            <form>
              <textarea
                value={post.title}
                onChange={this.handleTitleChange}
                cols="30"
                rows="3"
              />
            </form>
          ) : (
            post.title
          )}
        </div>
        <div>
          创建人:<span>{post.author}</span>
        </div>
        <div>
          创建时间:<span>{post.date}</span>
        </div>
        <div>
          <img src={like} alt="点赞" onClick={this.handleVote}></img>
          {post.vote}
        </div>
        <div>
          <button onClick={this.handleEditPost}>
            {this.state.editing ? "保存" : "编辑"}
          </button>
        </div>
      </li>
    );
  }
}

PostList.js

import React, { Component } from "react";

import PostItem from "./PostItem";

/**
 * 有状态组件定义
 */
export class PostList extends Component {
  constructor(props) {
    super(props);
    //状态初始化
    this.state = {
      posts: [], //所有帖子
    };
    this.timer = null; //时钟,模拟后台加载数据
    //将voteHandle函数中的this指向组件对象
    this.voteHandle = this.voteHandle.bind(this);
  }

  data = [
    {
      id: 1001,
      title: "百度萝卜快跑超百台无人车落地武汉,订单量突破200",
      author: "小明",
      date: "2023-03-01 12:12:18",
      vote: 0,
    },
    {
      id: 1002,
      title: "我国自主研制空间站双光子显微镜首获航天员皮肤三维图",
      author: "小军",
      date: "2022-12-15 23:15:26",
      vote: 0,
    },
    {
      id: 1003,
      title: "清华大学一教授团队为村民“打印”一栋住宅!",
      author: "小华",
      date: "2022-11-26 18:17:44",
      vote: 0,
    },
  ];

  //当组件挂载完成后
  componentDidMount() {
    //模拟后台AJAX加载
    this.timer = setTimeout(() => {
      this.setState({
        posts: this.data,
      });
    }, 1000);
  }

  //当组件将被卸载时
  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  //根据id点赞
  voteHandle(id) {
    //如果当前帖子的id是我们要投票的帖子,则生成一个新对象,并更新投票数
    //如果不是要找的帖子,则直接返回
    let newPosts = this.state.posts.map((item) =>
      item.id === id ? { ...item, vote: item.vote + 1 } : item
    );
    //更新状态,刷新UI
    this.setState({ posts: newPosts });
  }

  saveHandle = (newPost) => {
    const newPosts = this.state.posts.map((item) =>
      item.id === newPost.id ? newPost : item
    );
    this.setState({ posts: newPosts });
  };

  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        {this.state.posts.map((item) => (
          <PostItem
            key={item.id}
            post={item}
            posts={this.state.posts}
            onVote={this.voteHandle}
            onSave={this.saveHandle}
          />
        ))}
      </div>
    );
  }
}

export default PostList;

运行结果:

参考代码二:

PostItem.js

import React, { Component } from "react";

import like from "./images/dz.png";

/**
 * 受控组件
 */
export default class PostItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      editing: false, //编辑中
      post: props.post, //当前帖子
    };
  }

  //当props变化时更新本组件的状态
  static getDerivedStateFromProps(props, state) {
    console.log(props, state);
    if (props.post.vote !== state.post.vote) {
      return { post: props.post };
    }
    return null;
  }

  //投票
  onVote = (id) => {
    this.props.onVote(id);
  };

  //保存事件
  saveHandle = (e) => {
    if (this.state.editing) {
      let newPost = {
        ...this.state.post,
        date: new Date().toLocaleString(),
      };
      this.props.onSave(newPost);
      this.setState({ editing: !this.state.editing, post: newPost });
    }

    this.setState({
      editing: !this.state.editing,
    });
  };

  //标题修改事件
  titleChange = (e) => {
    this.setState({
      post: { ...this.state.post, title: e.target.value },
    });
  };

  render() {
    let post = this.state.post;

    return (
      <li>
        <div>
          {this.state.editing ? (
            <form>
              <textarea
                value={post.title}
                cols="40"
                rows="3"
                onChange={this.titleChange}
              ></textarea>
            </form>
          ) : (
            post.title
          )}
        </div>
        <div>
          创建人:<span>{post.author}</span>
        </div>
        <div>
          创建时间:<span>{post.date}</span>
        </div>
        <div>
          <img
            src={like}
            alt="点赞"
            onClick={() => {
              this.onVote(post.id);
            }}
          />
          {post.vote}
        </div>
        <p>
          <button onClick={this.saveHandle}>
            {this.state.editing ? "保存" : "编辑"}
          </button>
        </p>
      </li>
    );
  }
}

PostList.js

import React, { Component } from "react";

import PostItem from "./PostItem";

/**
 * 有状态组件定义
 */
export class PostList extends Component {
  constructor(props) {
    super(props);
    //状态初始化
    this.state = {
      posts: [], //所有帖子
    };
    this.timer = null; //时钟,模拟后台加载数据
    //将voteHandle函数中的this指向组件对象
    this.voteHandle = this.voteHandle.bind(this);
  }

  data = [
    {
      id: 1001,
      title: "百度萝卜快跑超百台无人车落地武汉,订单量突破200",
      author: "小明",
      date: "2023-03-01 12:12:18",
      vote: 0,
    },
    {
      id: 1002,
      title: "我国自主研制空间站双光子显微镜首获航天员皮肤三维图",
      author: "小军",
      date: "2022-12-15 23:15:26",
      vote: 0,
    },
    {
      id: 1003,
      title: "清华大学一教授团队为村民“打印”一栋住宅!",
      author: "小华",
      date: "2022-11-26 18:17:44",
      vote: 0,
    },
  ];

  //当组件挂载完成后
  componentDidMount() {
    //模拟后台AJAX加载
    this.timer = setTimeout(() => {
      this.setState({
        posts: this.data,
      });
    }, 1000);
  }

  //当组件将被卸载时
  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  //根据id点赞
  voteHandle(id) {
    //如果当前帖子的id是我们要投票的帖子,则生成一个新对象,并更新投票数
    //如果不是要找的帖子,则直接返回
    let newPosts = this.state.posts.map((item) =>
      item.id === id ? { ...item, vote: item.vote + 1 } : item
    );
    console.log(newPosts);
    //更新状态,刷新UI
    this.setState({ posts: newPosts });
  }

  //保存事件
  saveHandle = (newPost) => {
    const newPosts = this.state.posts.map((item) =>
      item.id === newPost.id ? newPost : item
    );
    this.setState({ posts: newPosts });
    console.log(newPosts);
  };

  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        {this.state.posts.map((item) => (
          <PostItem
            key={item.id}
            post={item}
            onVote={this.voteHandle}
            onSave={this.saveHandle}
          />
        ))}
      </div>
    );
  }
}

export default PostList;

效果

2.2、非受控组件

2.2.1、使用非受控组件

在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。

例如,下面的代码使用非受控组件接受一个表单的值:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

 

import React, { Component } from "react";

export default class SearchForm extends Component {
  constructor(props) {
    super(props);
    //创建一个表单元素的引用
    this.keyword = React.createRef();
  }

  submitHandle = (e) => {
    console.log(this.keyword.current.value);
    this.keyword.current.style.backgroundColor = "lightgreen";
    e.preventDefault();
    //return false;
  };

  render() {
    return (
      <div>
        <form onSubmit={this.submitHandle}>
          <p>
            <label htmlFor="keyword">关键字:</label>
            <input type="text" id="keyword" name="keyword" ref={this.keyword} />
            <button>搜索</button>
          </p>
        </form>
      </div>
    );
  }
}

 

 

2.2.2、默认值

在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值。在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。在一个组件已经挂载之后去更新 defaultValue 属性的值,不会造成 DOM 上值的任何更新。

如果直接给value赋值则会提示错误:

 

使用defaultValue

render() {
  return (
    <form onSubmit={this.handleSubmit}>
      <label>
        Name:
        <input
          defaultValue="Bob"
          type="text"
          ref={this.input} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

同样,<input type="checkbox"> 和 <input type="radio"> 支持 defaultChecked<select> 和 <textarea> 支持 defaultValue

 

import React, { Component } from "react";

export default class SearchForm extends Component {
  constructor(props) {
    super(props);
    //创建一个表单元素的引用
    this.keyword = React.createRef();
  }

  submitHandle = (e) => {
    console.log(this.keyword.current.value);
    this.keyword.current.style.backgroundColor = "lightgreen";
    e.preventDefault();
    //return false;
  };

  render() {
    return (
      <div>
        <form onSubmit={this.submitHandle}>
          <p>
            <label htmlFor="keyword">关键字:</label>
            <input
              defaultValue="请输入搜索关键字"
              type="text"
              id="keyword"
              name="keyword"
              ref={this.keyword}
            />
            <button>搜索</button>
          </p>
          <p>
            同样,
            <input type="checkbox" defaultChecked={true} /> 和{" "}
            <input type="radio" defaultChecked={false} /> 支持 defaultChecked,
            <select defaultValue="usa">
              <option value="zh-cn">中国</option>
              <option value="usa">美国</option>
            </select>{" "}
            和 <textarea defaultValue="请输入多行文本" cols="40" rows="3" />{" "}
            支持 defaultValue。
          </p>
        </form>
      </div>
    );
  }
}

 

结果:

 

2.2.3、文件输入

在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。

<input type="file" />

在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 ref 从而在提交表单时获取文件的信息。

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<FileInput />);

三、React新特性

3.1、render新的返回类型

React16之前render方法必须返回单个元素,现在render可以返回多种不同的元素:

render() 方法是 class 组件中唯一必须实现的方法。

当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

  • React 元素。通常通过 JSX 创建。例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件,无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
  • 数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
  • Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档。
  • 字符串或数值类型。它们在 DOM 中会被渲染为文本节点。
  • 布尔类型或 null。什么都不渲染。(主要用于支持返回 test && <Child /> 的模式,其中 test 为布尔类型。)

render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

如需与浏览器进行交互,请在 componentDidMount() 或其他生命周期方法中执行你的操作。保持 render() 为纯函数,可以使组件更容易思考。

注意

如果 shouldComponentUpdate() 返回 false,则不会调用 render()

3.1.1、返回数组

export default class Hi extends Component {
  render() {
    return [<span>Hello</span>, <span>React</span>, <span>render()!</span>];
  }
}
import React, { Component } from "react";

export default class Render1 extends Component {
  render() {
    return [
      <h2 key={1}>Hello </h2>,
      <h2 key={2}> React</h2>,
      <h2 key={3}> Render!</h2>,
    ];
  }
}

需要增加key,不然报错。

3.1.2、fragments

  • 可以将子列表分组,而无需向DOM添加额外节点
  • 简单理解:空标签
  • <React.Fragment></React.Fragment> 或 <></>
  • render() {
      return (
        <React.Fragment>
          <ChildA />
          <ChildB />
          <ChildC />
        </React.Fragment>
      )
    }
    • 以下面的代码为例,如果Columns组件返回多个td元素才能实现效果,但是如果我们在Columns组件中使用了div父元素,则会使td元素失效。Fragment则可以解决这个问题。
    • //table.js
      const Table = () => {
        render() {
          return (
            <table>
              <tr>
                <Columns />
              </tr>
            </table>
          )
        }
      }
      //columns.js
      const Columns = () => {
       render() {
          return (
            <div>
              <td>Hello</td>
              <td>World</td>
            </div>
          )
        }
      }
      //以上代码输出:
      <table>
        <tr>
          <div>
            <td>Hello</td>
            <td>World</td>
          </div>
        </tr>
      </table>
      //此时 td 是失效的,可以使用Fragemengt解决这个问题
      //用法:
      //columns.js
      const Columns = () => {
       render() {
          return (
            <React.Fragment>
              <td>Hello</td>
              <td>World</td>
            </React.Fragment>
          )
        }
      }
      //通过上面的方法我们就可以正确的输出table啦:
      <table>
        <tr>
          <td>Hello</td>
          <td>World</td>
        </tr>
      </table>

      短语法

    • 可以使用一种新的,且更简短的类似空标签的语法来声明 Fragments
    • <> </>
    • 不支持 key 或属性
    • const Cloumns = () => {
      render() {
          return (
            <>
              <td>Hello</td>
              <td>World</td>
            </>
          )
        }
      }

      带key 的Fragments

    • 使用显式 <React.Fragment> 语法声明的片段可能具有 key
    • key 是唯一可以传递给 Fragment 的属性
    • function Glossary(props) {
        return (
          <dl>
            {props.items.map(item => (
              // 没有`key`,React 会发出一个关键警告
              <React.Fragment key={item.id}>
                <dt>{item.term}</dt>
                <dd>{item.description}</dd>
              </React.Fragment>
            ))}
          </dl>
        )
      } 

3.1.3、Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

render() {
  // React 挂载了一个新的 div,并且把子元素渲染其中
  return (
    <div>      {this.props.children}
    </div>  );
}

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

render() {
  // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  return ReactDOM.createPortal(
    this.props.children,
    domNode  );
}

一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

注意:

当在使用 portal 时, 记住管理键盘焦点就变得尤为重要。

对于模态对话框,通过遵循 WAI-ARIA 模态开发实践,来确保每个人都能够运用它。

示例:

import React, { Component } from "react";
import ReactDOM from "react-dom";

export default class Hi extends Component {
  constructor(props) {
    super(props);
    this.container = document.createElement("div");
    document.body.appendChild(this.container);
  }

  render() {
    return ReactDOM.createPortal(<h2>Portal</h2>, this.container);
  }
}

运行结果:

卸载时需要移除

  componentWillUnmount() {
    document.body.removeChild(this.container);
  }

3.2、错误边界

部分 UI 的异常不应该破坏了整个应用。为了解决 React 用户的这一问题,React 16 引入了一种称为 “错误边界” 的新概念。 错误边界是用于捕获其子组件树 JavaScript 异常,记录错误并展示一个回退的 UI 的 React 组件,而不是整个组件树的异常。错误组件在渲染期间,生命周期方法内,以及整个组件树构造函数内捕获错误。
componentDidCatch(error, info)

此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

  1. error —— 抛出的错误。
  2. info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息

componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显示降级 UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {    // "组件堆栈" 例子:    //   in ComponentThatThrows (created by App)    //   in ErrorBoundary (created by App)    //   in div (created by App)    //   in App    logComponentStackToMyService(info.componentStack);  }
  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的降级 UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

React 的开发和生产构建版本在 componentDidCatch() 的方式上有轻微差别。

在开发模式下,错误会冒泡至 window,这意味着任何 window.onerror 或 window.addEventListener('error', callback) 会中断这些已经被 componentDidCatch() 捕获的错误。

相反,在生产模式下,错误不会冒泡,这意味着任何根错误处理器只会接受那些没有显式地被 componentDidCatch() 捕获的错误。

注意

如果发生错误,你可以通过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在未来的版本中将不推荐这样做。 可以使用静态 getDerivedStateFromError() 来处理降级渲染。

特别注意:

  • 事件处理 (比如调用了一个不存在的方法this.abc(),并不会执行componentDidCatch)
  • 异步代码 (例如 setTimeoutrequestAnimationFrame 回调函数)
  • 服务端渲染
  • 错误边界自身抛出来的错误 (而不是其子组件)

当render()函数出现问题时,componentDidCatch会捕获异常并处理

此时,render()函数里面发生错误,则 componentDidCatch 会进行调用,在里面进行相应的处理
render() {
  let a = [1,2,3]
  let value = a[3].toString()   对 undefined 进行操作
  return (......)
}

防止 页面 级别的崩溃~

示例:

ErrorCom.js

import React, { Component } from "react";

export default class ErrorCom extends Component {
  state = { n: 0 };

  clickHandle = () => {
    for (let i = 0; i < 100; i++) {
      this.setState(
        {
          n: this.state.n + 1,
        },
        () => {
          if (this.state.n === 3) {
            throw new Error("发生了错误");
          }
        }
      );
    }
  };

  render() {
    return (
      <div>
        <h2>组件</h2>
        点击3次就异常了
        <button onClick={this.clickHandle}>{this.state.n}</button>
      </div>
    );
  }
}

index.js

import ErrorCom from "./ErrorCom";

const vnode = (
  <div>
    <ErrorCom />
    <Hi />
  </div>
);

默认情况:

 发生错误时整个应用崩溃

 改进,定义ErrorBoundary组件

import React, { Component } from "react";

export default class ErrorBoundary extends Component {
  state = { error: null };

  componentDidCatch(error, info) {
    this.setState({ error });
  }

  render() {
    if (this.state.error) {
      return (
        <div>
          <h2>发生了错误</h2>
          <div>{this.state.error && this.state.error.toString()}</div>
        </div>
      );
    }
    return this.props.children;
  }
}

修改index.js文件

const vnode = (
  <div>
    <ErrorBoundary>
      <ErrorCom />
    </ErrorBoundary>
    <Hi />
  </div>
);

发生错误时,仅当前控制失效了:

 3.3、自定义DOM属性

React 16 之前会忽略不是把的HTML和SVG属性,现在React会把不识别的属性传递给DOM。

React16之前:

  <div cust-attr="someting"></div>
会被渲染成:

  <div></div>

React 16渲染出来的节点:

  <div cust-attr="someting"></div>

 3.4、组件的state

3.4.1、组件state

1,设计合适的state

state必须能代表一个组件UI呈现的完整状态集,代表一个组件UI呈现的最小状态集。

state必须能代表一个组件UI呈现的完整状态集又可以分成两类数据:用作渲染组件时使用到的数据的来源,用作组件UI展现形式的判断依据:

class Hello extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: 'react', //用作渲染组件时使用到的数据的来源
            display: true //用作组件UI展现形式的判断依据
        }
    }
    render() {
        return (
            <div>
                {
                    this.state.display ? <h1>{this.state.user}</h1> : <></>
                }
            </div>
        )
    }
}
export default Hello;

普通属性:

在es6中,可以使用this.属性名定义一个class的属性,也可以说属性是直接挂载在this下的一个变量。因此,state和props实际上也是组件的属性,只不过是react在Component class中预定义好的属性。除了state和props以外的其他组件属性称为组件的普通属性。

class Hello extends Component {
    constructor(props) {
        super(props);
        this.timer = null; //普通属性
        this.state = {
            date: new Date()
        }
        this.updateDate = this.updateDate.bind(this);
    }
    componentDidMount(){
        this.timer = setInterval(this.updateDate, 1000);
    }
    componentWillUnmount(){
        clearInterval(this.timer);
    }
    updateDate(){
        this.setState({
            date: new Date()
        })
    }
    render() {
        return (
            <div>
                <h1>{this.state.date.toString()}</h1>
            </div>
        )
    }
}
export default Hello;

组件中用到的一个变量是否应该作为state可以通过下面4条依据判断:

  1. 这个变量是否通过props从父组件中获取?如果是,那么它不是一个状态
  2. 这个变量是否在生命周期中都保持不变?如果是,那么它不是一个状态
  3. 这个变量是否可以通过其他状态(state)或者属性(props)计算得到?如果是,那么它不是一个状态
  4. 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态,这种情况更适合定义为组件的一个普通属性

3.4.2、正确修改state

①不能直接修改state,需要使用setState()

②state的更新是异步的

React会将多次setState的状态合并成一次状态修改,不能依赖当前的state计算下一个state(props也是异步的)。

例如:连续两次点击加入购物车,实际数量只会加1,在React合并多次修改为1次的情况下,相当于执行了:

Object.assign(
    previousState,
    {quantity: this.state.quantity + 1},
    {quantity: this.state.quantity + 1}
)

示例:

import React, { Component } from "react";

export default class ErrorCom extends Component {
  state = { n: 0 };

  clickHandle = () => {
    for (let i = 0; i < 100; i++) {
      this.setState(
        {
          n: this.state.n + 1,
        },
        () => {
          if (this.state.n === 300) {
            throw new Error("发生了错误");
          }
        }
      );
    }
  };

  render() {
    return (
      <div>
        <h2>组件</h2>
        点击3次就异常了
        <button onClick={this.clickHandle}>{this.state.n}</button>
      </div>
    );
  }
}

上面的代码虽然循环了100次,实际每次只增加了1。

这种情况下,可以使用另一个接收一个函数作为参数的setState,这个函数有两个参数,第一个是当前修改后的最新状态的前一个状态preState,第二个参数是当前最新的属性props:

this.setState((preState,props) => ({
    quantity: preState.quantity + 1;
}))

3.4.3、state的更新是一个合并的过程

后设置的state会覆盖前面的状态,如果不存在则添加。

3.4.4、state与不可变对象

直接修改state,组件不会render;state包含的所有状态都应该是不可变对象,当state中某个状态发生变化时,应该重新创建这个状态对象,而不是直接修改原来的状态。创建新的状态有以下三种方法:

状态的类型是不可变类型(数字、字符串、布尔值、null、undefined):因为状态是不可变类型,所以直接赋一个新值即可
状态的类型是数组:可以使用数组的concat或者es6的扩展语法,slice方法、filter方法。不能使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法会在原数组基础上修改。

this.setState((preState) => ({
    arr: [...preState.arr,'react'];
}))
this.setState((preState) => ({
    arr: preState.arr.concat(['react'])
}))

状态的类型是普通对象(不包含字符串、数组):使用ES6的Object.assgin方法或者对象扩展语法

Object.assign({},preState.owner,{name:"tom"});

或者

{...preState.owner,name:"tom"}

对象更新示例:

import React, { Component } from 'react'

export default class StuList extends Component {
    constructor(props){
        super(props);
        this.state={
            stu:{id:1001,name:'tom',age:19}
        }
    }

    //普通属性
    a=100;
    //普通属性
    users = [
        { id: 1001, name: "John" },
        { id: 1002, name: "Jack" },
        { id: 1003, name: "Rose" },
        { id: 1004, name: "Lili" },
        { id: 1005, name: "Jery" },
      ];

      handleAddProp=(e)=>{
        //方法一
        //Object.assign是重新定义对象,使用后面的对象覆盖前面的对象,有就替换,没有就添加
        let newStudent=Object.assign({},this.state.stu,{height:198});
        this.setState({stu:newStudent});
        //方法二
        this.setState({stu:{...this.state.stu,weight:70,age:88}});
      }

  render() {
    return (
      <div>
        
            <h2>学生信息:{JSON.stringify(this.state)}</h2>
            <p>
                <button onClick={this.handleAddProp}>增加属性height,weight</button>
            </p>
      </div>
    )
  }
}

 

 

 数组更新示例:

import React, { Component } from 'react'

export default class StuList extends Component {
    constructor(props){
        super(props);
        this.state={
            stus:[1,2,3,4,5,6,7,8,9,10]
        }
    }
      handleEditArray=(e)=>{
        //追加11到数组结尾
        //this.setState({stus:[...this.state.stus,11]});
        //this.setState({stus:this.state.stus.concat(11)});
        //过滤
        //this.setState({stus:this.state.stus.filter(n=>n%2===1)});
        //映射
        //this.setState({stus:this.state.stus.map(n=>n%2===1?n:-1)});

        //返回的是的移除的数组
        // let newStus=[];
        // let index=1;
        // for(var i=0; i<this.state.stus.length;i++){
        //     if(i!==index){
        //         newStus.push(this.state.stus[i]);
        //     }
        // }
        let newStus=JSON.parse(JSON.stringify(this.state.stus));
        newStus.splice(1,1);
        this.setState({stus:newStus});
      }

  render() {
    return (
      <div>
        
            <h2>数组:{JSON.stringify(this.state.stus)}</h2>
            <p>
                <button onClick={this.handleEditArray}>修改数组</button>
            </p>
      </div>
    )
  }
}

13.5、Axios

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

源代码与英文帮助:https://github.com/axios/axios

3.5.1、特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

3.5.2、浏览器支持

3.5.3、安装

使用 npm:

$ npm install axios

使用 bower:

$ bower install axios

使用 cdn:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

3.5.4、案例

执行 GET 请求

// 为给定 ID 的 user 创建请求
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);
});

执行 POST 请求

axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

执行多个并发请求

function getUserAccount() {
return axios.get('/user/12345');
}

function getUserPermissions() {
return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));

3.5.5、axios API

可以通过向 axios 传递相关配置来创建请求

axios(config)
// 发送 POST 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
// 获取远端图片
axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
})
.then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
axios(url[, config])
// 发送 GET 请求(默认的方法)
axios('/user/12345');

3.5.6、请求方法的别名

为方便起见,为所有支持的请求方法提供了别名

axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
注意

在使用别名方法时, urlmethoddata 这些属性都不必在配置中指定。

3.5.7、并发

处理并发请求的助手函数

axios.all(iterable)
axios.spread(callback)

3.5.8、创建实例

可以使用自定义配置新建一个 axios 实例

axios.create([config])
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});

3.5.9、实例方法

以下是可用的实例方法。指定的配置将与实例的配置合并。

axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])

3.5.10、请求配置

这些是创建请求时可以用的配置选项。只有 url 是必需的。如果没有指定 method,请求将默认使用 get 方法。

{
// `url` 是用于请求的服务器 URL
url: '/user',

// `method` 是创建请求时使用的方法
method: 'get', // default

// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',

// `transformRequest` 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data, headers) {
// 对 data 进行任意转换处理
return data;
}],

// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对 data 进行任意转换处理
return data;
}],

// `headers` 是即将被发送的自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},

// `params` 是即将与请求一起发送的 URL 参数
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: {
ID: 12345
},

// `paramsSerializer` 是一个负责 `params` 序列化的函数
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},

// `data` 是作为请求主体被发送的数据
// 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
// 在没有设置 `transformRequest` 时,必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属:FormData, File, Blob
// - Node 专属: Stream
data: {
firstName: 'Fred'
},

// `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
// 如果请求话费了超过 `timeout` 的时间,请求将被中断
timeout: 1000,

// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default

// `adapter` 允许自定义处理请求,以使测试更轻松
// 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
adapter: function (config) {
/* ... */
},

// `auth` 表示应该使用 HTTP 基础验证,并提供凭据
// 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
auth: {
username: 'janedoe',
password: 's00pers3cret'
},

// `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json', // default

// `responseEncoding` indicates encoding to use for decoding responses
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // default

// `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
xsrfCookieName: 'XSRF-TOKEN', // default

// `xsrfHeaderName` is the name of the http header that carries the xsrf token value
xsrfHeaderName: 'X-XSRF-TOKEN', // default

// `onUploadProgress` 允许为上传处理进度事件
onUploadProgress: function (progressEvent) {
// Do whatever you want with the native progress event
},

// `onDownloadProgress` 允许为下载处理进度事件
onDownloadProgress: function (progressEvent) {
// 对原生进度事件的处理
},

// `maxContentLength` 定义允许的响应内容的最大尺寸
maxContentLength: 2000,

// `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
validateStatus: function (status) {
return status >= 200 && status < 300; // default
},

// `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
// 如果设置为0,将不会 follow 任何重定向
maxRedirects: 5, // default

// `socketPath` defines a UNIX Socket to be used in node.js.
// e.g. '/var/run/docker.sock' to send requests to the docker daemon.
// Only either `socketPath` or `proxy` can be specified.
// If both are specified, `socketPath` is used.
socketPath: null, // default

// `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
// `keepAlive` 默认没有启用
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),

// 'proxy' 定义代理服务器的主机名称和端口
// `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
// 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
proxy: {
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
},

// `cancelToken` 指定用于取消请求的 cancel token
// (查看后面的 Cancellation 这节了解更多)
cancelToken: new CancelToken(function (cancel) {
})
}

3.5.11、响应结构

某个请求的响应包含以下信息

{
// `data` 由服务器提供的响应
data: {},

// `status` 来自服务器响应的 HTTP 状态码
status: 200,

// `statusText` 来自服务器响应的 HTTP 状态信息
statusText: 'OK',

// `headers` 服务器响应的头
headers: {},

// `config` 是为请求提供的配置信息
config: {},
// 'request'
// `request` is the request that generated this response
// It is the last ClientRequest instance in node.js (in redirects)
// and an XMLHttpRequest instance the browser
request: {}
}

使用 then 时,你将接收下面这样的响应 :

axios.get('/user/12345')
.then(function(response) {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
});

在使用 catch 时,或传递 rejection callback 作为 then 的第二个参数时,响应可以通过 error 对象可被使用,正如在错误处理这一节所讲。

3.5.12、配置默认值

你可以指定将被用在各个请求的配置默认值

全局的 axios 默认值

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

自定义实例默认值

// Set config defaults when creating the instance
const instance = axios.create({
baseURL: 'https://api.example.com'
});

// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

配置的优先顺序

配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者。这里是一个例子:

// 使用由库提供的配置的默认值来创建实例
// 此时超时配置的默认值是 `0`
var instance = axios.create();

// 覆写库的超时默认值
// 现在,在超时前,所有请求都会等待 2.5 秒
instance.defaults.timeout = 2500;

// 为已知需要花费很长时间的请求覆写超时设置
instance.get('/longRequest', {
timeout: 5000
});

3.5.13、拦截器

在请求或响应被 then 或 catch 处理前拦截它们。

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

如果你想在稍后移除拦截器,可以这样:

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

可以为自定义 axios 实例添加拦截器

const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});

3.5.14、错误处理

axios.get('/user/12345')
.catch(function (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});

Y可以使用 validateStatus 配置选项定义一个自定义 HTTP 状态码的错误范围。

axios.get('/user/12345', {
validateStatus: function (status) {
return status < 500; // Reject only if the status code is greater than or equal to 500
}
})

3.5.15、取消

使用 cancel token 取消请求

Axios 的 cancel token API 基于cancelable promises proposal,它还处于第一阶段。

可以使用 CancelToken.source 工厂方法创建 cancel token,像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// cancel the request
cancel();

注意: 可以使用同一个 cancel token 取消多个请求

3.5.16、使用 application/x-www-form-urlencoded format

默认情况下,axios将JavaScript对象序列化为JSON。 要以application / x-www-form-urlencoded格式发送数据,您可以使用以下选项之一。

浏览器

在浏览器中,您可以使用URLSearchParams API,如下所示:

const params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);

请注意,所有浏览器都不支持URLSearchParams(请参阅caniuse.com),但可以使用polyfill(确保填充全局环境)。

或者,您可以使用qs库编码数据:

const qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));

或者以另一种方式(ES6),

import qs from 'qs';
const data = { 'bar': 123 };
const options = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(data),
url,
};
axios(options);

Node.js

在node.js中,您可以使用querystring模块,如下所示:

const querystring = require('querystring');
axios.post('http://something.com/', querystring.stringify({ foo: 'bar' }));

您也可以使用qs库。

Semver

在axios达到1.0版本之前,破坏性更改将以新的次要版本发布。 例如0.5.1和0.5.4将具有相同的API,但0.6.0将具有重大变化。

Promises

axios 依赖原生的 ES6 Promise 实现而被支持. 如果你的环境不支持 ES6 Promise,你可以使用 polyfill.

3.5.17、TypeScript

axios包括TypeScript定义。

import axios from 'axios';
axios.get('/user?ID=12345');

 3.5.18、简单应用

import React, { Component } from "react";
import axios from "axios";

export default class StudentListAxios extends Component {
  state = {
    students: [],
  };

  componentDidMount() {
    axios
      .get("http://127.0.0.1:8081/api/students")
      .then((res) => {
        console.log(res);
        this.setState({ students: res.data });
      })
      .catch((error) => alert(error));
  }

  handleDelete = (e) => {
    const that = this;
    let id = e.target.id;
    axios
      .delete(`http://127.0.0.1:8081/api/students/${id}`)
      .then((res) => {
        if (res.data === 1) {
          let newStudent = this.state.students.filter(
            (item) => item.studentId !== id
          );
          that.setState({
            students: newStudent,
          });
        }
      })
      .catch((error) => alert(error));
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <h2>学生列表</h2>
        <table border="1" width="100%">
          <tbody>
            <tr>
              <th>学号</th>
              <th>姓名</th>
              <th>性别</th>
              <th>操作</th>
            </tr>
            {this.state.students.map((stu) => (
              <tr key={stu.studentId}>
                <td>{stu.studentId}</td>
                <td>{stu.studentName}</td>
                <td>{stu.classId}</td>
                <td>
                  <a href="# " id={stu.studentId} onClick={this.handleDelete}>
                    删除
                  </a>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
}

结果:

3.1.19、统一URL

react axios设置统一请求地址 / react axios设置开发 & 生产环境不同接口地址 /react 封装axios

  • 思路:
    • axios设置公共请求头:通过axios配置,设置统一的baseURL;

    • 开发环境 & 生产环境接口地址: 通过脚手架提供的环境变量来解决:


  • 1.在项目根目录创建 .env.development文件,配置REACT_APP_URL = http://localhost:8080
    在开发期间生效,npm start的时候自动读取.env.development文件中的环境变量来设置接口;
    在项目根目录创建 .env.production文件,配置REACT_APP_URL = 线上地址,
    npm run build的时候自动读取.env.development文件中的环境变量来设置接口

  • 2.在该文件中添加环境变量REACT_APP_URL(注意: 环境变量约定以REACT_APP_ 开头)

  • 3.设置REACT_APP_URL的值默认为 http://localhost:8080
    并重启脚手架

  • 4.创建utils/url.js文件, 在url.js中创建BASE_URL变量,设置其值为process.env.REACT_APP_URL

  • 5.在页面中导入url文件,并修改请求路径,图片路径等等;

如果URL为undefined则有可能是以下原因:

检查是否安装依赖process模块,可以使用npm install process命令\

检查创建的.env.文件名是否在根目录下

如果是Vue项目:
检查创建的配置文件里属性名是否以VUE_APP_开头,例如VUE_APP_API

如果是React项目:
检查创建的配置文件里属性名是否以REACT_APP_开头,例如REACT_APP_URL

注意配置文件里的属性值是string类型

检查使用属性是否写为process.env.属性名,例如process.env.VUE_APP_API

如果以上都没有问题,需要执行npm run serve命令才能使配置文件生效

可以找个页面做控制台打印console.log(process.env);看看当前配置里是否有VUE_APP_API当前你定义的属性

axios官方文档:http://www.axios-js.com/docs/

到第6步骤,axios请求接口成功,但实际上每个页面的axios.get(都添加了BASE_URL);
接下来封装一个简易的axios,只需在一个js文件中设置统一请求路径

6 创建api.js,封装axios
在页面中导入封装好的axios即可
扩展: 封装react axios拦截器统一添加token
import axios from 'axios'
import { BASE_URL } from './url'

// import {HashRouter} from 'react-router-dom'    //如果使用的是hash路由类型,使用这个
// const router = new HashRouter()

import {BrowserRouter} from 'react-router-dom'    
const router = new BrowserRouter()

const API = axios.create({
    baseURL: BASE_URL
})

// 添加请求拦截器
API.interceptors.request.use(config=>{
    const { url } = config;

    //当url不是/login 或 不是/register请求路由时(这两个路由不需要token), 请求头添加token    
    if(!url.startsWith('/login') ||  !url.startsWith('/register')){ 
        //当url以/user开头的时候,
        config.headers.Authorization = localStorage.getItem('token');
    }
    return config
})

API.interceptors.response.use(response => {

    const { status }  = response.data; //这里的response数据结构不一样,直接打印出来看,参照后端返回的结果

    if(status === 400 || status === 401 || status === 402 || 403){ 
        localStorage.removeItem('token');
        //当token超时or失效 403账号无权限的时候直接跳转到/login页重新登录
        router.history.push('/login')
    }
    return response
})

export { API } 

 studentApi.js

import axios from "axios";
import { BASE_URL } from "../utils/url";

axios.defaults.baseURL = BASE_URL;

export default function testUrl() {
  console.log(axios.defaults.baseURL);
}

studentListAxiosPro.js

import React, { Component } from "react";
import axios from "axios";
import testUrl from "./api/studentApi";

testUrl();

 

运行结果:

3.1.20、封装请求

一、下载
npm i axios
yarn add axios
二、创建 api 文件夹
在项目 src 目录中创建一个 api 目录,用来存放所有的请求接口。
三、创建请求模块文件
我们以获取学生数据为例。
在 api 目录中创建一个 studentApi.js 文件,该文件中用来设置所有关于学生的请求:
import axios from "axios";

export const getStudentAsync = (params) =>
  axios.get("/api/students", {
    params,
  });

export const deleteStudentAsync = (studentId, config) =>
  axios.delete(`/students/${studentId}`, config);
四、封装 axios
在项目 src 目录中创建一个 utils 目录,用来存放工具类的文件,我们将 axios 的封装文件 axios.js 也可以放在里面。
import axios from "axios";
import { BASE_URL } from "./url";

axios.defaults.baseURL = BASE_URL;

// 响应拦截器
axios.interceptors.response.use(
  (res) => res.data, // 拦截到响应对象,将响应对象的 data 属性返回给调用的地方
  (err) => Promise.reject(err)
);
最后在 index.js 中引入该文件:
import './utils/axios.js';
五、组件中调用 api 接口
例如在学生列表的组件中,我们要调用封装好的接口:
import React, { Component } from "react";
import { getStudentAsync, deleteStudentAsync } from "./api/studentApi";

export default class StudentListAxiosPro extends Component {
  state = {
    students: [],
  };

  componentDidMount() {
    this.getStudents();
  }

  getStudents = async () => {
    let res = await getStudentAsync();
    this.setState({
      students: res,
    });
  };

  handleDelete = async (e) => {
    const that = this;
    let id = e.target.id;

    let res = await deleteStudentAsync(id);
    if (res === 1) {
      let newStudent = this.state.students.filter(
        (item) => item.studentId !== id
      );
      that.setState({
        students: newStudent,
      });
    }
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <h2>学生列表</h2>
        <table border="1" width="100%">
          <tbody>
            <tr>
              <th>学号</th>
              <th>姓名</th>
              <th>性别</th>
              <th>操作</th>
            </tr>
            {this.state.students.map((stu) => (
              <tr key={stu.studentId}>
                <td>{stu.studentId}</td>
                <td>{stu.studentName}</td>
                <td>{stu.classId}</td>
                <td>
                  <a href="# " id={stu.studentId} onClick={this.handleDelete}>
                    删除
                  </a>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
}

 结果:

统一的错误与数据处理

import axios from "axios";

axios.defaults.baseURL = process.env.REACT_APP_URL;

// 响应拦截器
axios.interceptors.response.use(
  (res) => res.data, // 拦截到响应对象,将响应对象的 data 属性返回给调用的地方
  (err) => {
    console.log(err);
    alert("发生了错误:" + err.message + "\n" + err.stack);
    return Promise.reject(err);
  }
);

3.6、Fetch

3.6.1、基本使用

  1. fetch:原生函数,React内置,不再使用 XMLHttpRequest 对象提交 ajax 请求
  2. 老版本浏览器可能不支持
fetch(url).then(res => {
            return res.json() //res不是需要的请求数据
        }).then(data => {
            console.log(data) //data是请求数据
        }).catch(e =>{
           console.log(e) //e是异常信息
        })

3.6.2、参数说明

(1)fetch的返回值是一个promise对象

(2)options表示对象格式的参数

method:HTTP请求方式,默认是GET

body:请求的参数

若发送的是json格式参数,则需要使用JSON.stringify({id:'123'})

fetch(`${HOST}:${PORT}/bookapi/books`,{
            method:'get',
            headers:{
                'Content-Type':'application/json'
            }
        }).then(res=>{
            console.log(res)
            return res.json()  //将服务器发送的响应头信息返回给浏览器
        }).then(data=>{ //服务器返回给客户端的数据
            _this.setState({
                data: data
            })
        }).catch(err=>{
            console.log(err)
        })
    }

headers:HTTP请求头,可以设置响应头的信息

因为一般使用JSON数据格式,所以设置ContentType为application/json

credentials:默认为omit,忽略的意思,也就是不带cookie还有两个参数,same-origin,意思就是同源请求带cookie;include,表示无论跨域还是同源请求都会带cookie

3.6.3、总结

(1)fetch更底层,只处理请求失败的错误,对于400、500等异常它任务是请求成功的

(2)fetch是XMLHttpRequest对象的代理(替代方案),运行与    XMLHttpRequest对象没有关系

(3)axios是XMLHttpRequest对象的封装,底层的实现依然使用的是XMLHttpRequest对象

3.6.4、案例

import React, { Component } from "react";

export default class StudentList extends Component {
  state = {
    students: [],
  };

  componentDidMount() {
    fetch("http://127.0.0.1:8081/api/students", {})
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        this.setState({ students: data });
      })
      .catch((error) => alert(error));
  }

  handleDelete = (e) => {
    const that = this;
    let id = e.target.id;
    fetch(`http://127.0.0.1:8081/api/students/${id}`, {
      method: "delete",
    })
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        debugger;
        let newStudent = this.state.students.filter(
          (item) => item.studentId !== id
        );
        debugger;
        that.setState({
          students: newStudent,
        });
      })
      .catch((error) => alert(error));
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <h2>学生列表</h2>
        <table border="1" width="100%">
          <tbody>
            <tr>
              <th>学号</th>
              <th>姓名</th>
              <th>性别</th>
              <th>操作</th>
            </tr>
            {this.state.students.map((stu) => (
              <tr key={stu.studentId}>
                <td>{stu.studentId}</td>
                <td>{stu.studentName}</td>
                <td>{stu.classId}</td>
                <td>
                  <a href="# " id={stu.studentId} onClick={this.handleDelete}>
                    删除
                  </a>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
}

结果:

href="#"的错误

1
2
Line 10:33:  The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a
link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md jsx-a11y/anchor-is-valid

解决

在 eslint 的配置加上的 rules 加上

1
"jsx-a11y/anchor-is-valid": "off"

例如我的 create-react-app 项目的.eslintrc.json 的配置是:

1
2
3
4
5
6
{
"extends": "react-app",
"rules": {
"jsx-a11y/anchor-is-valid": "off"
}
}

a 标签改成

1
<a href="# ">{text}</a>

注意:#后面有一个空格

重启

3.6、组件与服务器通信

3.6.1、组件挂载阶段通信

componentDidMount是调用服务器API最安全的地方,也是React官方推荐的进行服务器通信的地方。除了在componentDidMount,在componentWillMount中进行服务器通信也是比较常见的一种方式。

componentWillMount会在组件被挂载前调用,因此从时间上来讲,componentWillMount中执行服务器通信要早于componentDidMount。但两者执行的时间差微乎其微,完全可以忽略不计。

componentDidMount是执行组件与服务器通信的最佳地方,原因:

在componentDidMount执行服务器通信可以保证获取到数据时,组件已经处于挂载状态,此时可以操作DOM

当组件在服务器端渲染时,componentWillMount会执行两次,一个在服务器端,一次在浏览器端,而componentDidMount能保证在任何情况下只会被调用一次,从而不会发送多余的数据请求。

服务器端使用Spring Boot:

接口提供JSON数据:

http://localhost:8081/api/students

 前后端分离,前端使用React+Axios从服务器获取数据,展示学生信息:

在前端项目中依赖axios

创建StudentList组件

 

3.6.2、组件更新阶段通信

例如,组件需要以props中某个属性作为与服务器通信的请求采纳数,当这个属性值发生更新时,组件自然需要重新余服务器通信。

componentWillReceiveProps(nextProps){
if(nextProps.category !== this.props.category) {
// fetch
}
}

componentWillReceiveProps的执行并不能说明props一定发生了修改

 

 

四、视频

 https://www.bilibili.com/video/BV1Vb411D7qa/?share_source=copy_web&vd_source=475a31f3c5d6353a782007cd4c638a8a

五、作业

1、定义一个对话框组件,要求显示在body尾部标签内,使用portal技术,卸载时要求删除容器

2、使用Ant Design Mobile完成如下作业

1、使用React脚手架创建项目或使用Vite创建React项目、项目中安装AntDesignMobile,使用AntDesignMobile完成一个类似论坛页面,效果如下图:

2、要求使用React的列表数据渲染方法渲染组件。Array.map()方法 3、(扩展)根据帖子中的图片数据动态改变图片的显示方式。

(1) 帖子只有一张图时:

(2) 有两张或四张图时:

(3) 三张或超过六张时:

3、完成radio、file的受控组件示例。

4、完成一个用户注册示例,尽量多的使用到各种表单元素。

5、使用微服务完成一个论坛的后台,2张表,帖子管理,用户管理。

6、使用fetch调用微服务暴露的接口。

7、使用axios调用微服务暴露的接口,要求封装代码。

posted @ 2023-03-09 16:34  张果  阅读(752)  评论(0编辑  收藏  举报
AmazingCounters.com