今天看啥  ›  专栏  ›  feng-fu

【译】如何使用statecharts来模拟redux应用的行为

feng-fu  · 掘金  · 前端  · 2018-02-08 02:53
原文链接:How to model the behavior of Redux apps using StateCharts,翻译只作为学习之用,版权归原作者所有。

不管喜欢与否,我们的应用程序总会在特定的时间里处于特定的状态。在构建用户界面时(UI),我们会使用数据来表示其中的一些状态(比如Redux中的store),但是我们从来不会刻意的给每个状态去命名。

更重要的是,有些事件在特定的状态下不应该被触发。

事实证明,这种描述状态和从一个状态转变为另一个状态的事件的想法是一个深入研究的概念。例如,StateCharts提供了描述被动应用程序行为的可视化形式,例如用户界面。

在本文中,我将讨论如何将Redux应用程序的行为从组件,容器或中间件(我们通常保留这种逻辑的地方)中解耦出来,并且可以使用状态图完全包含和描述。这使我们的应用程序的行为更容易重构和可视化。

Redux和StateCharts

Redux非常简单。比如我们有一个触发事件的UI组件。那么当这个事件发生的时候使用dispatch来触发,然后reducer通过其中的action来更新store。最后,我们的组件刻意直接获取 到更新后得store

// 组件
function Counter({ currentCount, onPlusClick }) {
  return <div>
    <button onClick={onPlusClick}>plus</button>
    {currentCount}
  </div>
}
// 将组件和redux连接
connect(
  state => ({ currentCount: state.currentCount }),
  dispatch => ({ 
    onPlusClick: () => dispatch({ type: INCREMENT })
  })
)(Counter)
// 创建reducer来处理INCREMENT这个action
function currentCountReducer(state = 0, action) {
  switch(action.type) {
    case INCREMENT:
      return state + 1;
    default:
      return state;
  }
}

这几乎就是Redux的一切了。

为了介绍StateCharts,我们不是直接将事件映射到更新操作,而是将其映射到一个不更新任何数据的通用操作(没有reducer处理它):

connect(
  state => ({ currentCount: state.currentCount }),
  dispatch => ({ 
    onPlusClick: () => dispatch({ type: CLICKED_PLUS })
  })
)(Counter)

没有reducer会去处理CLICKED_PLUS这个类型,所以我们让状态图处理它:

const StateCharts = {
  initial: 'Init',
  states: {
    Init: {
      on: { CLICKED_PLUS: 'Increment' }
    },
    Increment: {
      onEntry: INCREMENT, // <- update when we enter this state
      on: { CLICKED_PLUS: 'Increment' }
    }
  }
}

StateCharts将处理它接收的事件,类似于reducer的方式,但只有当它处于允许接受这样状态的时候。否则事件是不会更新store的。

在上述例子中,我们开始处于Init状态。当CLICKED_PLUS事件发生时,我们过渡到Increment它有一个状态onEntry字段。这使得状态图派发一个INCREMENT动作 - 这次由一个reducer来处理,这个reducer会更新store

您可能会问,为什么我们将容器从更新的知道中分离出来?我们这样做是为了让更新需要发生的所有行为都包含在StateCharts的JSON结构中。这意味着它也可以被可视化:

这可以通过简单地改变StateCharts的JSON描述来改变我们的应用程序的行为。让我们通过CLICKED_PLUS使用分层状态的概念将两个转换组合成一个来改进我们的设计:

要做到这一点,我们只需要改变我们的状态图定义。我们的UI组件和reducer保持不变。

{
  initial: 'Init',
  states: {
    Init: {
      on: { CLICKED_PLUS: 'Init.Increment' },
      states: {
        Increment: {
          onEntry: INCREMENT
        }
      }
    }
  }
}

异步的副作用

让我们想象一下,当点击a时,我们想要启动一个HTTP请求。下面是我们目前在Redux中没有StateCharts的情况:

connect(
  null,
  dispatch => ({ 
    onFetchDataClick: () => dispatch({ type: FETCH_DATA_CLICKED })
  })
)(FetchDataButton)

那么我们可能会用到一个epic来处理这个action。下面我们使用了redux-observable,当然也可以使用redux-saga或者redux-thunk

function handleFetchDataClicked(action$, store) {
  return action$.ofType('FETCH_DATA_CLICKED')
    .mergeMap(action =>
      ajax('http://foo.bar')
        .mapTo({ type: 'FETCH_DATA_SUCCESS' })
        .takeUntil(action$.ofType('FETCH_DATA_CANCEL'))
    )
}

尽管我们将container从副作用中解耦(container只是简单地告诉epic“嘿,取数据按钮被点击了”),但是我们仍然存在这样的问题:无论我们处于何种状态,都会触发HTTP请求。

如果我们处于FETCH_DATA_CLICKED不应触发HTTP请求的状态呢?

这种情况可以很容易地由状态图来处理。当FETCH_DATA_CLICKED情况发生,我们过渡到FetchingData状态。只有在进入这个状态(onEntry)的时候才会FETCH_DATA_REQUEST调用action:

{
  initial: 'Init',
  states: {
    Init: {
      on: {
        FETCH_DATA_CLICKED: 'FetchingData',
      },
      initial: 'NoData',
      states: {
        ShowData: {},
        Error: {},
        NoData: {}
      }
    },
    FetchingData: {
      on: {
        FETCH_DATA_SUCCESS: 'Init.ShowData',
        FETCH_DATA_FAILURE: 'Init.Error',
        CLICKED_CANCEL: 'Init.NoData',
      },
      onEntry: 'FETCH_DATA_REQUEST',
      onExit: 'FETCH_DATA_CANCEL',
    },
  }
}

然后,我们改变我们的epic基于新添加的FETCH_DATA_REQUEST行为,而不是:

function handleFetchDataRequest(action, store) {
  // handling FETCH_DATA_REQUEST rather than FETCH_DATA_CLICKED
  return action$.ofType('FETCH_DATA_REQUEST')
    .mergeMap(action =>
      ajax('http://foo.bar')
        .mapTo({ type: 'FETCH_DATA_SUCCESS' })
        .takeUntil(action$.ofType('FETCH_DATA_CANCEL'))
    )
}

这样,只有当我们处于FetchingData状态时才会触发请求。

再次,通过这样做,我们将所有的行为都放到了JSON StateCharts中,使得重构变得容易,让我们可以直观地看到原本隐藏在代码中的东西:

这个特定设计的一个有趣的特性是,当我们退出FetchingData状态时,FETCH_DATA_CANCEL动作被调度。我们不仅可以在进入状态时发送操作,还可以在退出时发送操作。正如我们在epic中的定义,这将导致HTTP请求中止。

需要注意的是,只有在查看得到的状态图可视化之后,我才添加这个特定的HTTP中止行为。通过简单地看一下这个图,显然HTTP请求在退出时应该已经被清理了FetchingData。没有这样的视觉表现,这可能不是那么明显。

现在,我们可以收集状态图控制我们store更新的直觉。根据我们所处的当前状态,我们知道哪些副作用需要发生,什么时候需要发生。

这里的主要观点是,我们的reducerepic将总是基于StateCharts的输出动作而不是我们的UI来作出反应。

事实上,StateCharts也可以作为一个有状态的事件发射器来实现:告诉它发生了什么(触发一个事件),并且通过记住你所处的最后一个状态,告诉你该做什么(动作)。

StateCharts解决的问题

作为前端开发人员,我们的工作是将静态图像带入生活。这个过程有几个问题:

当我们将静态图像转换为代码时,我们失去了对应用程序的高度理解  -  随着应用程序的不断变大,理解哪个代码段负责哪部分的界面变得越来越困难。 不是所有的问题都可以用单纯用界面来回答  - 当用户重复点击按钮时会发生什么?如果用户想要在运行中取消请求,该怎么办? 事件分散在我们的代码中,并具有不可预知的影响 ** - 当用户点击一个按钮时,究竟发生了什么?我们需要更好的抽象,帮助我们理解射击事件的影响。 大量的isFetching,isShowing,isDisabled变量  -我们需要跟踪改变我们的UI一切。 StateCharts通过提供严格的应用程序行为视觉形式**来帮助解决这些问题。绘制状态图让我们可以对我们的应用程序有一个高度的理解,让我们用视觉线索回答问题。

在这个过程中,应用程序的所有状态都被探测到,并且事件被明确标记,使我们能够预测任何给定事件之后将发生的事情。

而且,StateCharts可以直接从设计人员的模型中构建出来,使得非工程师也可以了解发生的事情,而不必深入实际的代码。

Learn more

作为一个具体的例子,我已经构建了redux-StateCharts,这是一个Redux中间件,可以像前面的例子中使用的那样使用。它使用xstate库 - 用于转换状态图的纯函数。

如果您想了解更多关于状态图的信息,请点击这里:StateCharts.github.io/




原文地址:访问原文地址
快照地址: 访问文章快照