我们介绍了状态机是什么,以及类似“状态机 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_AGAIN
listing.error
conditionals/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
采取行动askIntroQuestion
ANSWER
活动:
shouldCreateNewTicket
updateCtxWithAnswer
newTicket
州shouldFindTicket
updateCtxWithAnswer
findTicket
州哟!这个可视化效果是根据实际代码构建的!
我❤️这个!
这些不是代码注释,也不是spec-32.pdf
8 个月未更新的共享硬盘。
想象一下,这在多大程度上有助于推动有关产品流程的对话,以及它如何使利益相关者就应用程序的每个状态达成一致。
可以清楚地知道是否存在一个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
的状态promise
getTicket
error
了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
只能做一件事,所以你不需要在这里进行布尔检查。isLoading
error
<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