转载

异步编程真的好吗?

More than React系列文章:

《 More than React(一)为什么ReactJS不适合复杂的前端项目? 》

《 More than React(二)React.Component损害了复用性? 》

《 More than React(三)虚拟DOM已死? 》

《 More than React(四)HTML也可以静态编译? 》

《 More than React(五)异步编程真的好吗? 》

本文首发于InfoQ:http://www.infoq.com/cn/articles/more-than-react-part05

《More than React》系列的上一篇文章 HTML也可以编译? 介绍了 Binding.scala 如何在渲染 HTML 时静态检查语法错误和语义错误,从而避免 bug ,写出更健壮的代码。本篇文章将讨论Binding.scala和其他前端框架如何向服务器发送请求并在页面显示。

在过去的前端开发中,向服务器请求数据需要使用异步编程技术。异步编程的概念很简单,指在进行 I/O 操作时,不阻塞当前执行流,而通过回调函数处理 I/O 的结果。不幸的是,这个概念虽然简单,但用起来很麻烦,如果错用会导致 bug 丛生,就算小心翼翼的处理各种异步事件,也会导致程序变得复杂、更难维护。

Binding.scala 可以用 I/O 状态的绑定代替异步编程,从而让程序又简单又好读,对业务人员也更友好。

我将以一个从 Github 加载头像的 DEMO 页面为例,说明为什么异步编程会导致代码变复杂,以及 Binding.scala 如何解决这个问题。

DEMO 功能需求

作为 DEMO 使用者,打开页面后会看到一个文本框。

在文本框中输入任意 Github 用户名,在文本框下方就会显示用户名对应的头像。

异步编程真的好吗?

要想实现这个需求,可以用 Github API 发送 获取用户信息 的 HTTPS 请求。

发送请求并渲染头像的完整流程的验收标准如下:

  • 如果用户名为空,显示“请输入用户名”的提示文字;
  • 如果用户名非空,发起 Github API,并根据 API 结果显示不同的内容:

    *   如果尚未加载完,显示“正在加载”的提示信息;

    </pre>

    *   如果成功加载,把回应解析成 JSON,从中提取头像 URL 并显示;
    `
    *   如果加载时出错,显示错误信息。

    异步编程和 MVVM

    过去,我们在前端开发中,会用异步编程来发送请求、获取数据。比如 ECMAScript 2015 的 Promise 和 HTML 5 的 fetch API。 而要想把这些数据渲染到网页上,我们过去的做法是用 MVVM 框架。在获取数据的过程中持续修改 View Model ,然后编写 View 把 View Model 渲染到页面上。这样一来,页面上就可以反映出加载过程的动态信息了。比如,ReactJS 的 state 就是 View Model,而 render 则是 View ,负责把 View Model 渲染到页面上。 用 ReactJS 和 Promise 的实现如下:

    class Page extends React.Component {
      state = {
        githubUserName: null,
        isLoading: false,
        error: null,
        avatarUrl: null,
      };
      currentPromise = null;
      sendRequest(githubUserName) {
        const currentPromise = fetch(`https://api.github.com/users/${githubUserName}`);
        this.currentPromise = currentPromise;
        currentPromise.then(response => {
          if (this.currentPromise != currentPromise) {
            return;
          }
          if (response.status >= 200 && response.status < 300) {
            return response.json();
          } else {
            this.currentPromise = null;
            this.setState({
              isLoading: false,
              error: response.statusText
            });
          }
        }).then(json => {
          if (this.currentPromise != currentPromise) {
            return;
          }
          this.currentPromise = null;
          this.setState({
            isLoading: false,
            avatarUrl: json.avatar_url,
            error: null
          });
        }).catch(error => {
          if (this.currentPromise != currentPromise) {
            return;
          }
          this.currentPromise = null;
          this.setState({
            isLoading: false,
            error: error,
            avatarUrl: null
          });
        });
        this.setState({
          githubUserName: githubUserName,
          isLoading: true,
          error: null,
          avatarUrl: null
        });
      }
      changeHandler = event => {
        const githubUserName = event.currentTarget.value;
        if (githubUserName) {
          this.sendRequest(githubUserName);
        } else {
          this.setState({
            githubUserName: githubUserName,
            isLoading: false,
            error: null,
            avatarUrl: null
          });
        }
      };
      render() {
        return (
          <div>
            <input type="text" onChange={this.changeHandler}/>
            <hr/>
            <div>
              {
                (() => {
                  if (this.state.githubUserName) {
                    if (this.state.isLoading) {
                      return <div>{`Loading the avatar for ${this.state.githubUserName}`}</div>
                    } else {
                      const error = this.state.error;
                      if (error) {
                        return <div>{error.toString()}</div>;
                      } else {
                        return <img src={this.state.avatarUrl}/>;
                      }
                    }
                  } else {
                    return <div>Please input your Github user name</div>;
                  }
                })()
              }
            </div>
          </div>
        );
      }
    }
    `</pre>
    一共用了 100 行代码。
    由于整套流程由若干个闭包构成,设置、访问状态的代码五零四散,所以调试起来很麻烦,我花了两个晚上才调通这 100 行代码。
    ## Binding.scala
    现在我们有了 Binding.scala ,由于 Binding.scala 支持自动远程数据绑定,可以这样写:
    <pre>`@dom def render = {
      val githubUserName = Var("")
      def inputHandler = { event: Event => githubUserName := event.currentTarget.asInstanceOf[Input].value }
      <div>
        <input type="text" oninput={ inputHandler }/>
        <hr/>
        {
          val name = githubUserName.bind
          if (name == "") {
            <div>Please input your Github user name</div>
          } else {
            val githubResult = FutureBinding(Ajax.get(s"https://api.github.com/users/${name}"))
            githubResult.bind match {
              case None =>
                <div>Loading the avatar for { name }</div>
              case Some(Success(response)) =>
                val json = JSON.parse(response.responseText)
                <img src={ json.avatar_url.toString }/>
              case Some(Failure(exception)) =>
                <div>{ exception.toString }</div>
            }
          }
        }
      </div>
    }
    `</pre>
    一共 25 行代码。
    完整的 DEMO 请访问 [ScalaFiddle](https://scalafiddle.io/sf/JGxViqE/1)。
    之所以这么简单,是因为 Binding.scala 可以用 [FutureBinding](https://javadoc.io/page/com.thoughtworks.binding/unidoc_2.11/latest/com/thoughtworks/binding/FutureBinding.html) 把 API 请求当成普通的绑定表达式使用,表示 API 请求的当前状态。
    每个 `FutureBinding` 的状态有三种可能,`None`表示操作正在进行,`Some(Success(...))`表示操作成功,`Some(Failure(...))`表示操作失败。
    还记得绑定表达式的 `.bind` 吗?它表示“each time it changes”。 由于 `FutureBinding` 也是 [Binding](https://javadoc.io/page/com.thoughtworks.binding/unidoc_2.11/latest/com/thoughtworks/binding/Binding.html) 的子类型,所以我们就可以利用 `.bind` ,表达出“每当远端数据的状态改变”的语义。
    结果就是,用 Binding.scala 时,我们编写的每一行代码都可以对应验收标准中的一句话,描述着业务规格,而非“异步流程”这样的技术细节。
    让我们回顾一下验收标准,看看和源代码是怎么一一对应的:
    `
  • 如果用户名为空,显示“请输入用户名”的提示文字;
    if (name == "") {
      &amp;lt;div&amp;gt;Please input your Github user name&amp;lt;/div&amp;gt;
  • 如果用户名非空,发起 Github API,并根据 API 结果显示不同的内容:
    } else {
      val githubResult = FutureBinding(Ajax.get(s"https://api.github.com/users/${name}"))
      githubResult.bind match {
    *   如果尚未加载完,显示“正在加载”的提示信息;&lt;pre&gt;case None =>
    </pre>
    &lt;div&gt;Loading the avatar for { name }&lt;/div&gt;
    *   如果成功加载,把回应解析成 JSON,从中提取头像 URL 并显示;<pre>

val json = JSON.parse(response.responseText) &lt;img src={ json.avatar_url.toString }/&gt;`

*   如果加载时出错,显示错误信息。<pre>`case Some(Failure(exception)) =&gt; // 如果加载时出错,

&lt;div&gt;{ exception.toString }&lt;/div&gt; // 显示错误信息。

  •   }
    }

结论

本文对比了 ECMAScript 2015 的异步编程和 Binding.scala 的 FutureBinding 两种通信技术。Binding.scala 概念更少,功能更强,对业务更为友好。

技术栈 ReactJS + Promise + fetch Binding.scala
编程范式 MVVM + 异步编程 远程数据绑定
如何管理数据加载流程 程序员手动编写异步编程代码 自动处理
能不能用代码直接描述验收标准 不能
从RESTful API加载数据并显示所需代码行数 100行 25行

这五篇文章介绍了用 ReactJS 实现复杂交互的前端项目的几个难点,以及 Binding.scala 如何解决这些难点,包括:

  • 复用性
  • 性能和精确性
  • HTML模板
  • 异步编程

除了上述四个方面以外,ReactJS 的状态管理也是老大难问题,如果引入 Redux 或者 react-router 这样的第三方库来处理状态,会导致架构变复杂,分层变多,代码绕来绕去。而Binding.scala 可以用和页面渲染一样的数据绑定机制描述复杂的状态,不需要任何第三方库,就能提供服务器通信、状态管理和网址分发的功能。

如果你正参与复杂的前端项目,使用ReactJS或其他开发框架时,感到痛苦不堪,你可以用Binding.scala一举解决这些问题。 Binding.scala快速上手指南 中包含了从零开始创建Binding.scala项目的每一步骤。

后记

Everybody’s Got to Learn How to Code ——奥巴马

编程语言是人和电脑对话的语言。对掌握编程语言的人来说,电脑就是他们大脑的延伸,也是他们身体的一部分。所以,不会编程的人就像是失去翅膀的天使。

电脑程序是很神奇的存在,它可以运行,会看、会听、会说话,就像生命一样。会编程的人就像在创造生命一样,干的是上帝的工作。

我有一个梦想,梦想编程可以像说话、写字一样的基础技能,被每个人都掌握。

如果网页设计师掌握Binding.scala,他们不再需要找工程师实现他们的设计,而只需要在自己的设计稿原型上增加魔法符号 .bind ,就能创造出会动的网页。

如果QA、BA或产品经理掌握Binding.scala,他们写下验收标准后,不再需要检查程序员干的活对不对,而可以把验收标准自动变成可以运转的功能。

我努力在Binding.scala的设计中消除不必要的技术细节,让人使用Binding.scala时,只需要关注他想传递给电脑的信息。

Binding.scala是我朝着梦想迈进的小小产物。我希望它不光是前端工程师手中的利器,也能成为普通人迈入编程殿堂的踏脚石。

原文  http://insights.thoughtworkers.org/more-than-react-part05/
正文到此结束
Loading...