我们介绍了状态机是什么,以及类似“状态机 2.0”的状态图如何帮助您构建更强大的应用程序。
它使用 Xstate(statecharts)和 reactJS 来构建聊天机器人流程🔥
可用脚本
在项目目录中,您可以运行:
npm start
    以开发模式运行应用程序。
 打开http://localhost:3000在浏览器中查看。
如果您进行编辑,页面将重新加载。
 您还将在控制台中看到任何 Lint 错误。
我们将介绍状态机是什么,以及类似“状态机 2.0”的状态图如何帮助您构建更强大的应用程序。
我们将使用xstate,它是一个statechart库和 ReactJS。但你实际上可以reactJS用任何其他框架来替换它。
总体目标是通过让 UI 成为状态函数来减少开发 UI 时的认知负荷。
| 当前状态 | 用户界面 | 
|---|---|
| 列表 | 显示列表 | 
| 列表加载 | 显示特定列表加载图像 | 
| 无结果 | 显示无结果消息 | 
这篇文章的代码可以在以下位置找到:
我们介绍了状态机是什么,以及类似“状态机 2.0”的状态图如何帮助您构建更强大的应用程序。
它使用 Xstate(statecharts)和 reactJS 来构建聊天机器人流程🔥
在项目目录中,您可以运行:
npm start以开发模式运行应用程序。
 打开http://localhost:3000在浏览器中查看。
如果您进行编辑,页面将重新加载。
 您还将在控制台中看到任何 Lint 错误。
我一直觉得状态机这个词有点奇怪。
 一开始把它理解成:
该函数仅根据给定的输入执行与应用程序当前状态相关的操作。
const currentState = "isLoading";
function machine(input) {
  if (currentState === "isLoading") {
    // *only* do things related to `isLoading` state with `input`
  }
  if (currentState === "isError") {
    // *only* do things related to `isError` state with `input`
  }
}
这是一个熟悉的状态机:
// currentState is `idle`
fetch() // currentState is `fetching`
.then(
  (successResults) => {
    //  currentState is 'success'
    // stateful data is 'successResults'
  }
  (errorMsg) => {
    // currentState is 'error'
    // stateful data is 'errorMsg'
  }
);
因为一次currentState只能做一件事,所以你不会遇到这些检查:
 // NOPE, NOPE, NOPE
if (isLoading && !isError) // ...
if (!isLoading && isError) // ...
if (isLoading && isError) // ...
一个有效的复杂系统总是从一个简单的有效系统发展而来。——约翰·加尔
有两种类型的状态:
这里的答案将决定使用哪个组件:
if (currentState === 'error') {
  return <Error />;
}
context在 中被称为xState。它们可以回答以下问题:这里的答案将决定组件具有哪些道具:
if (currentState === 'error') {
  return <Error msg={context.errorMsg}>
}
UI 应该是一个状态函数。
 这与 UI 是我们当前拥有的数据函数不同。
👍 状态函数:
if (currentState === list.noResults) {
  return "No Results found";
}
if (currentState === list.isError) {
  return "Oops!";
}
👎 我们目前拥有的数据:
if (list.length === 0) {
  // the list is empty, so we probably don't have any results"
  return "No Results found";
}
if (list.errMsg) {
  // list.err is not empty, show an error message #yolo
  return "Oops";
}
这里的对话从以下转变:
如果结果为零该怎么办?
记住:
结果为零可能是因为错误,也可能是因为我们还未获取任何数据,或者是因为确实没有任何结果。每种情况都代表着不同的状态。
到:
“当我们处于
error、initial或状态时,UI 是什么样的noResults?”
您现在正在构建 UI 来解释每个状态。
标题会变吗?
图标会变吗?
某些功能会失效吗?
是否应该设置一个“重试”按钮?
状态图是一种可以包含其他状态机的状态机...甚至更多!
所有这些的基础是状态图的配置。
您声明:
loading, error, noResults, listing, details, etc..actions/events州内可能发生的情况:只有当我们处于该州时才会发生action/TRY_AGAINlisting.errorconditionals/guards在进入其他状态之前需要传递的,例如:只有当我们收到成功响应时,我们才会进入该状态noResults,并且total === 0配置一个状态机是非常酷的,从中你可以理解绝大多数的 UI 逻辑。
在查看解释之前,请先尝试理解下面的配置:
// guards.js - conditional functions used to determine what the next step in the flow is
const guards = {
  shouldCreateNewTicket: (ctx, { data }) => data.value === "new_ticket",
  shouldFindTicket: (ctx, { data }) => data.value === "find_ticket"
};
// actions.js - functions that perform an action like updating the stateful data in the app
const actions = {
  askIntroQuestion: ctx => {
    return {
      ...ctx,
      chat: ["How may I help you?"]
    };
  }
};
// constants/state.js constants to represent the current state of the app
const intro = "@state/INTRO";
const question = "@state/QUESTION";
const newTicket = "@state/NEW_TICKET";
const findTicket = "@state/FIND_TICKET";
// constants/actions.js: constants to represent actions to be taken
const ANSWER = "@state/ANSWER";
const config = Machine({
  initial: intro,
  states: {
    [intro]: {
      initial: question,
      on: {
        [ANSWER]: [
          {
            cond: "shouldCreateNewTicket",
            actions: "updateCtxWithAnswer",
            target: newTicket
          },
          {
            cond: "shouldFindTicket",
            actions: "updateCtxWithAnswer",
            target: findTicket
          }
        ]
      },
      states: {
        [question]: { onEntry: "askIntroQuestion" }
      }
    },
    [newTicket]: {},
    [findTicket]: {}
  }
}).withConfig({
  actions,
  guards
});
 
intro来自states.intro
  intro是问题onEntry我们将intro.question采取行动askIntroQuestionANSWER活动: 
    shouldCreateNewTicketupdateCtxWithAnswernewTicket州shouldFindTicketupdateCtxWithAnswerfindTicket州哟!这个可视化效果是根据实际代码构建的!
我❤️这个!
这些不是代码注释,也不是spec-32.pdf8 个月未更新的共享硬盘。
想象一下,这在多大程度上有助于推动有关产品流程的对话,以及它如何使利益相关者就应用程序的每个状态达成一致。
可以清楚地知道是否存在一个error国家,
 或者是否应该有一个noResults与一个error国家
 
这是规格和流程...我知道很无聊...但请继续听我说。
作为用户,我希望能够:
loading状态和error状态Create new ticket
Find ticket
如果发现:
如果没有找到:
以下是一些机器配置:
const flowMachine = Machine({
  initial: intro,
  states: {
    [intro]: {
      initial: question,
      on: {
        [ANSWER]: [
          {
            target: newTicket,
            cond: "shouldCreateNewTicket",
            actions: "updateCtxWithAnswer"
          },
          {
            target: findTicket,
            cond: "shouldFindTicket",
            actions: "updateCtxWithAnswer"
          }
        ]
      },
      states: {
        [question]: { onEntry: "askIntroQuestion" }
      }
    },
    [findTicket]: {
      initial: question,
      on: {
        [ANSWER]: { target: `.${pending}`, actions: 'updateCtxWithAnswer' }
      },
      states: {
        [question]: { onEntry: 'askFindTicket' },
        [error]: {},
        [noResults]: {},
        [pending]: {
          invoke: {
            src: 'getTicket',
            onDone: [
              {
                target: done,
                actions: 'updateCtxWithResults',
                cond: 'foundTicket'
              },
              { target: noResults }
            ],
            onError: error
          }
        },
        [done]: { type: 'final' }
      },
      onDone: pingTicket
  }
});
findTicket:pending的状态promisegetTicketerror了foundTicket是真的,我们就转到done状态foundTicket为假,我们转到noResults状态根据当前状态渲染组件非常棒。
 以下是根据currentState应用情况选择渲染组件或传递不同 props 的众多方法之一。
 再次强调:currentState此处指的是应用状态,“isLoading、error 等”currentState.context指的是当前状态数据。
/**
 * Array of
 * [].<StateName, function>
 *
 * NOTE: specificity matters here so a more specific state
 * should be first in the list. e.g:
 * 'findTicket.noResults'
 * 'findTicket'
 *
 * On state 'findTicket.foo', 'findTicket' will be matched
 */
const stateRenderers = [
  [newTicket, ({ onSelect, currentState }) =>
    <Choices
      options={currentState.context.options}
      onSelect={onSelect} />
  ],
  [`${findTicket}.${noResults}`, () =>
    <Msg>Sorry, we can't find your ticket</Msg>],
  [`${findTicket}.${error}`, () => <Msg>Oops, we ran into an error!</Msg>],
  [findTicket, ({ onSelect }) => <FindTicketForm onSelect={onSelect} />]
];
// components/Choices.jsx
const Choices = ({ currentState, ...props}) => (
  // based on current state, get a function from `stateRenders`
  // and render it with the props we have
  const [stateName, renderState] =
      stateRenderers.find(([key]) => currentState.matches(key));
  return renderState(props);
)
 
这是根据当前应用程序状态显示组件的不同设置
 。
这里需要注意的是, 一次currentState只能做一件事,所以你不需要在这里进行布尔检查。isLoadingerror
<ChatBody data-testid="ChatBody">
  // display any chat info that exists in context
  {currentState.context.chat.map(({ question, answer }) => (
    <React.Fragment key={`${question}.${answer}`}>
      <ChatMsgQuestion>{question}</ChatMsgQuestion>
      {answer && <ChatMsgAnswer>{answer}</ChatMsgAnswer>}
    </React.Fragment>
  ))}
  // display message based on the current state that we're in
  // NOTE: only one of this is possible at a time
  {currentState.matches(pending) && <ChatMsgLoading />}
  {currentState.matches(error) && <ChatMsgError />}
  {currentState.matches(noResults) && (
    <ChatMsgWarning>{getNoResultsMsg(currentState)}</ChatMsgWarning>
  )}
  {currentState.matches(itemOrdered) && (
    <ChatMsgSuccess>{getSuccessMsg(currentState)}</ChatMsgSuccess>
  )}
</ChatBody>
好吧……希望你已经读到这里了。
 查看代码了解更多。
我认为这很好地建立在已经发挥作用的模式之上,redux例如消息传递、一个流向、数据管理与组件的分离。
我发现使用这种模式可以非常轻松地适应需求变化。
事情是这样的:
好的,我们只需要进入这个新状态。一旦我们进入这个状态,我们只需要让 UI 反映出这个特定状态应该是什么样子。
https://xstate.js.org 
https://statecharts.github.io