【翻译】如何在React中使用async/await (componentDidMount Async)

15,206 阅读9分钟

最近更新:瓦伦提诺.加格利亚提 2018年4月30号 原文链接

如何在React中使用async/await

pic1

你想像在NodeJS中使用async/await 那样在React中同样使用它们?

create-react-app 构建的项目支持开箱即用。

但是如果你想在自己搭建的webpack配置的项目中使用,你可能会遇到 regeneratorRuntime is not defined 的异常错误。

如果你遇到了这个错误还想在React中使用async/await,白日做梦。

在此,我们将按部就班的实现:

  • 修复 regeneratorRuntime is not defined 异常报错
  • 结合Fetch在React中使用async/await
  • 结合Fetch使用async/await处理异常

准备好了吗?

(以下均是原文链接,紧接着会有翻译)

  1. 什么是async/await
  2. 一个Promise的例子
  3. 使用async/await语法规则
  4. 处理异常
  5. 封装
  6. FAQ

什么是async/await

首先我们先简单介绍一下async/await.

async/await 只是JS中Promise的语法糖而已。

啥?JS里的Promise还不够你折腾?

Promise固然很好,但是某些情况下你会以长长的then/catch链告终。

我想说的是,如果你发觉自己在这样的处境内也很好,起码你简化了代码。

但是,async/await 可以以一种方式帮助你像编写同步代码那样的编写异步代码。

这会使得你的代码更加整洁和可读,且你还可以使用try/catch去合理的处理异常。

async/await 既方便又整洁:在某个种层面上你会想要把它用到你的React组件内。

让我们看看如何实现。

一个Promise的例子

从克隆我的webpack仓库开始(这是一个基于webpack V4 的包含了开箱即用的React的快速开始项目):

git clone git@github.com:valentinogagliardi/webpack-4-quickstart.git

进入到文件夹内,安装依赖:

cd webpack-4-quickstart/ && npm i

用你最喜欢的编辑器打开工程目录,然后清空App.js里的内容。

接下来让我们享受Promise吧。

假设你想通过fetch从API层获取数据。

这是很标准的的React流程。

你把API调用放到组件的componentDidMount方法里,代码如下:

// FILE: App.js
import React, { Component } from "react";
import ReactDOM from "react-dom";

class App extends Component {
  constructor() {
    super();
    this.state = { data: [] };
  }

  componentDidMount() {
    fetch(`https://api.coinmarketcap.com/v1/ticker/?limit=10`)
      .then(res => res.json())
      .then(json => this.setState({ data: json }));
  }

  render() {
    return (
      <div>
        <ul>
          {this.state.data.map(el => (
            <li>
              {el.name}: {el.price_usd}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default App;

ReactDOM.render(<App />, document.getElementById("app"));

(上面这个组件只是一个示例:没有进行异常捕获处理,让我们假设,我们的fetch调用一帆风顺,没有出现任何错误。)

如果你通过webpack-dev-server运行需要执行:

npm start

你会看到代码效果如期而至(虽然很丑,但是起码运行正常):

pic2

毫无想象力可言对吧?

我们可以做的更好吗?在componentDidMount方法上使用async/await真的会更好吗?

让我们试试看!

使用async/await语法规则

从7.6.0版本开始Node就广泛支持async/await语法了。

在前端则是另一番景象。async/await并不能全面覆盖所有的浏览器。

pic3

(我知道,谁他娘的在乎IE?)

总而言之,在React中使用async/await 一点也不神奇。

我们应该在React的组件的哪里使用async/await呢?

像获取网络数据或者初始化事务放在React的componentDidMount方法里一样,把async/await放到这里是一个不错选择。

以下是几个在React里使用async/await的步骤:

  1. 把async关键字放到你的函数前
  2. 在函数体内使用await关键字
  3. catch捕获异常

还有一件事就是:async/await 并不是支持所有的浏览器,这个细节你必须注意。

create-react-app支持开箱即用的async/await。

但是如果你想在自己的配置的webpack模板工程内使用,你会遇到一个错误(我们马上就会提到)。

现在让我们在React组件内运用async/await吧。

打开App.js文件然后修改componentDidMount函数:

// FILE: App.js
import React, { Component } from "react";
import ReactDOM from "react-dom";

class App extends Component {
  constructor() {
    super();
    this.state = { data: [] };
  }

  async componentDidMount() {
    const response = await fetch(`https://api.coinmarketcap.com/v1/ticker/?limit=10`);
    const json = await response.json();
    this.setState({ data: json });
  }

  render() {
    return (
      <div>
        <ul>
          {this.state.data.map(el => (
            <li>
              {el.name}: {el.price_usd}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default App;

ReactDOM.render(<App />, document.getElementById("app"));

没有捕获到异常,再一次的让我们假设我们的fetch调用平安无事。

在浏览器的console界面内瞅一眼。

工作正常吗?

pic4

regeneratorRuntime is not defined? 这是啥?

为了使用async/await,你该如何解决这个报错?

简单!

解决这个报错的关键就是babel-preset-env

在我的webpack 快速入门项目里包含着这个配置,如果你用的是自己已有的webpack配置模板,请确保你做了如下安装:

npm i babel-preset-env --save-dev

打开.babelrc文件,做如下内容更新:

{
    "presets": [
      ["env", {
        "targets": {
          "browsers": [
            ">0.25%",
            "not ie 11",
            "not op_mini all"
          ]
        }
      }], "react"
    ]
}

可以查看Jamie Kyle’last 2 wersions harmful学习更多。

保存文件然后瞅一眼浏览器。

搞定了!

pic5

整洁而优雅,但是我们革命尚未完成!

异常该怎么处理呢?

如果用户们掉线了或者API宕机了怎么办?

下个章节我们就会讨论如何使用fetch和async/await处理异常

处理异常

我们看过很多不进行异常处理的示例:

async componentDidMount() {
  const response = await fetch(`https://api.coinmarketcap.com/v1/ticker/?limit=10`);
  const json = await response.json();
  this.setState({ data: json });
}

我承认,在真正的app内,你会把fetch请求从视图层解耦出来。

然而,fetch APIs 在处理异常方面有一些说明。

TJ Van Toll有一篇不错的的文章对此作说明介绍:Handling Failed HTTP Responses With fetch()

所以,我们该如何让我们的代码更可靠呢?

让我们在组件里做个实验吧。

通过移除URL里的coinmarketcap引入一个异常:

async componentDidMount() {
  const response = await fetch(`https://api.com/v1/ticker/?limit=10`);
  const json = await response.json();
  this.setState({ data: json });
}

运行代码,瞅一眼console,你会发现:

  • TypeError: NetworkError when attempting to fetch resource, (火狐浏览器)
  • Uncaught (in promise) TypeError: Failed to fetch, (谷歌浏览器)

这里有一个没能捕获的异常。

让我们抓住它。

添加一个try/catch块:

async componentDidMount() {
  try {
    const response = await fetch(`https://api.com/v1/ticker/?limit=10`);
    const json = await response.json();
    this.setState({ data: json });
  } catch (error) {
    console.log(error);
  }
}

然后再次运行代码。

你会看到日志打印如下。

首先就是:wrap fetch inside a try/catch block to handle network errors.

现在让我们在尝试一下其他方式。

如果度过TJ Van Toll的那篇文章,对于下面这个示例你就不会感到震惊。

请看代码:

async componentDidMount() {
  try {
    const response = await fetch(`http://httpstat.us/500`);
  } catch (error) {
    console.log(error);
  }
}

你在日志面板看到什么了吗?毛都没有,没有异常,如同寸草不生的不毛之地。

为啥?

坏消息就是:当出现网络异常情况的时候,Fetch只会返回一个Promise对象。 譬如:用户掉线,DNS域名解析异常。

对于404 或者500这样的返回,你不会看到任何异常。

也就意味着你必须自己检查处理response返回体

好消息就是:Fetch的正常返回体都携带一个属性字段(叫做“ok”)是布尔类型的,true或false取决于HTTP的返回结果。

在下面的案例中,你可以用下面的方式处理异常:

async componentDidMount() {
  try {
    const response = await fetch(`http://httpstat.us/500`);
    if (!response.ok) {
      throw Error(response.statusText);
    }
  } catch (error) {
    console.log(error);
  }
}

如我们设想的那样,没有任何异常信息打印。

这时候,你可以向用户展示一些错误信息或者其他的有意义的内容。

所以第二点就是:对于HTTP的异常情况,Fetch并不会返回一个Promise对象。 自行检查返回体的ok字段。

回到我们的示例,完整代码如下:

async componentDidMount() {
  try {
    const response = await fetch(`https://api.coinmarketcap.com/v1/ticker/?limit=10`);
    if (!response.ok) {
      throw Error(response.statusText);
    }
    const json = await response.json();
    this.setState({ data: json });
  } catch (error) {
    console.log(error);
  }
}

这个版本的处理异常的方式提供一个可靠的起始点。

再次重申,在真正的开发场景中你势必会吧fetch请求从视图层解耦出来,但那又是另外一回事了。

想要学习更多关于Promise rejection in Async Functions请点击How to Throw Errors From Async Functions in Javascript

封装

从7.6.0版本往后,Node就开始全面支持async/await语法了。

async/await 能让你的代码更整洁和可读。

语法很方便,你会想要在你的React组件内使用的。

create-react-app 支持开箱即用的async/await.

但是如果你是用自己配制的webpack模板,你会遇到regeneratorRuntime is not defined异常。

解决这个异常的的关键是?babel-preset-env 和一些简单的配置。

从此你就只可以在React世界中放飞async/await了。

用在哪呢?

用的最多的地方就是用来获取网络数据和初始化一些数据的生命周期函数componentDidMount里,这里是一个绝佳的位置使用async/await.

遵循以下步骤,你便可以在React中使用async/await:

  • 配置babel,指定目标浏览器
  • 在componentDidMount方法钱使用async关键字
  • 在componentDidMount函数体内使用await关键字
  • 确定你有做异常处理

如果你在自己的代码里使用 Fetch API ,在处理异常的时候要当心一些警告。

然后你就准备好了!

FAQ

componentDidMount是使用async/await最为合理的地方。

毕竟当组件瓜子啊完毕之后你想尽快的获取数据。

我的一位学生指出,你不能在componentWillMount方法内使用async/await

那是正确的:你不能在componentWillMount方法内使用Promise。

不管怎么说,我会尽量少去componentWillMount这个方法内做一些操作的:那是一个在React生命周期中逐渐消逝得到方法

其他一些常见问题就是当在React中使用了async/await之后,包体的大小。

在不远的过去,如果不使用babel polyfill,你是无法在浏览器端使用async/await的。

使用它的结果就是,包体体积庞大。

如今有了babel-preset-env和目标浏览器的配置,情况不可同日而语。

包体大小仍在可接受的范围之内。

谢阅!

在老司机开车之前,抓紧时间上车!