hushicai / hushicai.github.io

Blog
https://hushicai.github.io
27 stars 1 forks source link

【译】使用有限状态机实现鲁棒性强的React UI #17

Open hushicai opened 5 years ago

hushicai commented 5 years ago

原文链接:Robust React User Interfaces with Finite State Machines

用户界面可以表达为以下两个方面:

从信用卡支付设备、气泵屏幕到你们公司创建的软件,用户界面都会对用户和其他来源的动作做出反应,并相应地更改其状态。

这个概念不仅限于技术,它是一切事物运作方式的基本组成部分:

对于每一个动作,都有一个相同和相反的反应。 — Isaac Newton

这是一个可以帮助我们开发更好的用户界面的概念,但在我们开始前,我希望你尝试一些东西。

例如一个照片相册库,它的用户交互流程如下:

  1. 显示一个搜索输入框和一个搜索按钮 ,允许用户搜索照片。
  2. 单击搜索按钮时,将使用搜索词从Flickr上获取照片。
  3. 在小尺寸的照片网格中显示搜索结果。
  4. 单击/轻触照片时,显示完整尺寸的照片。
  5. 再次点击/轻触完整尺寸的照片时,返回相册视图。

现在想想如何开发它。

也许可以尝试用React来编写它。

我会等! 我只是一篇文章。

我不会去任何地方。

完了吗?真棒! 那不是太难,对吧?

现在考虑以下您可能忘记的场景:

这些只是我们在规划、开发或测试过程中可能出现的一些潜在问题。

在软件开发中,几乎没有什么比你认为已经涵盖所有可能的用例更糟糕的事情了,当你发现(或接收)新的边缘情况时,一旦您考虑它们,将进一步使代码复杂化。

我们通常难以介入一些已经存在的项目,它们所有的这些用例都没有文档化,反而是隐藏在杂乱的代码中,留给你解读。

显而易见

如果我们可以确定所有可能的UI状态,这些状态来自作用在每个状态上的所有可能动作,该怎么办?

如果我们能够可视化这些状态、动作和状态之间的转换,怎么办?

设计师直观地做到了这一点,即所谓的“用户流程”(或“UX流程”),根据用户交互描述UI的下一个状态应该是什么。

image

在计算机科学术语中,有一个计算模型,称为有限自动机或“有限状态机”(FSM),它可以表达相同类型的信息。

也就是说,它们描述了对当前状态执行动作时的下一个状态。

就像用户流程一样,这些有限状态机可以清晰明确地可视化。

例如,以下是描述交通信号灯FSM的状态转换图:

image

什么是有限状态机

状态机是对应用程序中的行为进行建模的有用方法:对于每个动作,都会以状态更改的形式进行反应。

经典有限状态机有5个部分:

  1. 一个状态集合(例如idleloadingsuccesserror等)
  2. 一个动作集合(例如SEARCHCANCELSELECT_PHONE等)
  3. 一个初始状态(例如idle
  4. 一个转换函数(例如transition('idle', 'SEARCH') === 'loading'
  5. 最终状态(本文不适用)

确定性的有限状态机(我们将要处理的)也有一些约束:

描述有限状态机

一个有限状态机可以表示为从状态到其“转换”的映射,其中每个转换是一个动作和随动作而产生的下一个状态,这个映射只是一个普通的JavaScript对象。

让我们来看一个美国红绿灯的例子,这是最简单的FSM示例之一。

假设我们从green开始,然后在一些TIMER后,转换为yellow,然后在另一个TIMER后,转换为red,然后在另一个TIMER之后转回green

const machine = {
  green: { TIMER: 'yellow' },
  yellow: { TIMER: 'red' },
  red: { TIMER: 'green' }
};
const initialState = 'green';

一个 转换函数 回答了以下问题:

给定当前状态和一个动作,下一个状态会是什么?

在以上的代码示例中,基于一个动作(这里是TIMER),转换到下一个状态,只是在machine对象中查找currentStateaction,因为:

// ...
function transition(currentState, action) {
  return machine[currentState][action];
}

transition('green', 'TIMER');
// => 'yellow'

而不是使用 if / elseswitch 语句来确定下一个状态,例如,if (currentState ===='green') return 'yellow'

我们将所有逻辑移动到一个可以序列化为JSON的普通JavaScript对象中。

这是一种可以在测试、可视化、重用、分析、灵活性以及可配置性等方面获得巨大回报的策略。

React中的有限状态机

请看一个更复杂的例子,让我们看看我们是如何使用有限状态机来表示我们的图库应用程序。

该应用程序可以处于以下几种状态之一:

然后可以由用户或应用程序本身执行多个动作:

最初,可视化地将这些状态和动作结合在一起的最佳方法就是使用两种非常强大的工具:铅笔和纸;在状态之间绘制箭头,并将导致状态转移的动作标记在箭头上。

我们现在可以在对象中表示这些转换,就像在交通灯示例中一样:

const galleryMachine = {
  start: {
    SEARCH: 'loading'
  },
  loading: {
    SEARCH_SUCCESS: 'gallery',
    SEARCH_FAILURE: 'error',
    CANCEL_SEARCH: 'gallery'
  },
  error: {
    SEARCH: 'loading'
  },
  gallery: {
    SEARCH: 'loading',
    SELECT_PHOTO: 'photo'
  },
  photo: {
    EXIT_PHOTO: 'gallery'
  }
};

const initialState = 'start';

现在让我们看看如何将这种有限状态机配置和转换功能应用到我们的图库应用程序中吧。

在App的组件状态中,将有一个属性指示当前的有限状态,gallery

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      gallery: 'start', // initial finite state
      query: '',
      items: []
    };
  }
  // ...

转换函数将会是App类的一个方法,以便我们可以检索当前的有限状态:

 // ...
  transition(action) {
    const currentGalleryState = this.state.gallery;
    const nextGalleryState =
      galleryMachine[currentGalleryState][action.type];

    if (nextGalleryState) {
      const nextState = this.command(nextGalleryState, action);

      this.setState({
        gallery: nextGalleryState,
        ...nextState // extended state
      });
    }
  }
  // ...

这看起来和之前描述的transition(currentState, action)函数类似,但有一些区别:

执行命令

当状态发生变化时,可能会执行“副作用”(或“命令”,因为我们将引用它们)。

例如,当用户单击“搜索”按钮并发出“搜索”动作时,状态将转换为loading,然后会向Flickr发起一个异步的搜索请求(否则,loading将是谎言,开发者永远都不应该撒谎)。

我们可以在command(nextState,action)方法中处理这些副作用,它对于给定的下一个有限状态和动作的有效负载,可以确定要执行什么,以及扩展状态应该是什么:

  // ...
  command(nextState, action) {
    switch (nextState) {
      case 'loading':
        // execute the search command
        this.search(action.query);
        break;
      case 'gallery':
        if (action.items) {
          // update the state with the found items
          return { items: action.items };
        }
        break;
      case 'photo':
        if (action.item) {
          // update the state with the selected photo item
          return { photo: action.item };
        }
        break;
      default:
        break;
    }
  }
  // ...

动作可以拥有除了动作类型之外的有效负载,它可能需要被用来更新应用程序状态, 例如,当SEARCH操作成功时,可以在负载中带上搜索结果中的items,发出一个SEARCH_SUCCESS操作:

    // ...
    fetchJsonp(
      `https://api.flickr.com/services/feeds/photos_public.gne?lang=en-us&format=json&tags=${encodedQuery}`,
      { jsonpCallback: 'jsoncallback' })
      .then(res => res.json())
      .then(data => {
        this.transition({ type: 'SEARCH_SUCCESS', items: data.items });
      })
      .catch(error => {
        this.transition({ type: 'SEARCH_FAILURE' });
      });
    // ...

上面的command方法将立即返回任何扩展状态(即有限状态以外的状态),this.state将随着有限状态的变化,被setState(...)所更新。

最终的状态机应用

由于我们已经为应用程序声明性地配置了有限状态机,因此我们可以基于当前有限状态的有条件渲染,以更清晰的方式呈现正确的UI:

  // ...
  render() {
    const galleryState = this.state.gallery;

    return (
      <div className="ui-app" data-state={galleryState}>
        {this.renderForm(galleryState)}
        {this.renderGallery(galleryState)}
        {this.renderPhoto(galleryState)}
      </div>
    );
  }
  // ...

CSS中的有限状态

您可能已经注意到上面代码中的data-state = {galleryState}

通过设置该data-attribute,我们可以使用属性选择器有条件地设置应用程序的任何部分:

.ui-app {
  // ...

  &[data-state="start"] {
    justify-content: center;
  }

  &[data-state="loading"] {
    .ui-item {
      opacity: .5;
    }
  }
}

这比使用className更好,因为您可以强制执行约束,即一次只能为数据状态设置一个值,同时还可以取得和className一样的效果。

大多数流行的CSS-in-JS解决方案也支持属性选择器。

优势

使用有限状态机来描述复杂应用程序的行为并不是什么新鲜事。

传统上,这是通过switchgoto语句完成的,但通过将有限状态机描述为状态、操作和下一状态之间的声明性映射,您可以使用该数据来可视化状态转换过程:

image

此外,使用声明性有限状态机允许您:

结论和题外话

有限状态机是用于对应用程序中可以表示为有限状态的部分进行建模的抽象,几乎所有应用程序都具有这些部分。

本文中介绍的FSM编码模式:

从现在开始,当遇到“布尔标志”的变量(如isLoadedisSuccess)时,我建议您停下来思考如何将应用程序状态建模为有限状态机。

这样,您可以重构您的应用程序,以状态表示 state === 'loaded' 或state === 'success'`,使用枚举状态代替布尔标志。

资源