TypeScript 中的 XState 简介
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

当系统和程序规模较小时,状态管理通常比较简单,应用程序的状态及其随时间变化的各种方式也很容易理解。但随着规模的扩大和应用程序复杂性的增加,挑战便随之而来。随着系统规模的增长,不仅需要制定状态管理计划,更需要对整个系统的运行方式有一个清晰的愿景。状态机正是在此发挥作用,它能够帮助我们对应用程序状态进行建模,从而提供全面的状态管理解决方案。
状态机使我们能够构建结构化且健壮的用户界面,同时迫使我们开发者仔细思考应用程序可能处于的每一种状态。这种额外的洞察力不仅可以加强开发者之间的沟通,还可以促进开发者、设计师和产品经理之间的沟通。
什么是状态图和状态机?
有限状态机是一种数学系统,它只能处于有限个已定义状态中的一种。交通信号灯就是一个简单的例子。交通信号灯只有四种状态:红灯、黄灯和绿灯各亮一种状态,另外两种状态则表示交通信号灯发生故障。
状态图用于描绘有限系统的各种状态,类似于基本用户流程图。一旦确定了有限数量的状态,就可以定义状态转换——即在不同状态之间转换的事件集合。状态和状态转换的基本组合构成了状态机。随着应用程序的增长,可以轻松添加新的状态和状态转换。配置状态机的过程迫使我们仔细思考应用程序的每一个可能状态,从而明确应用程序的设计。
XState是由 David Khourshid 开发的一个库,它使我们能够在 JavaScript/TypeScript 中创建和运行状态机,并提供了一套全面且易于查阅的文档。它还提供了 XState 可视化工具,使技术人员和非技术人员都能看到如何在给定系统的有限状态集中进行操作,从而为设计人员和开发人员提供了一种“通用语言”。
使用 TypeScript — 上下文、模式和转换
我们还可以使用 TypeScript 为 XState 机器定义类型。XState 与 TypeScript 配合良好,因为 XState 会让我们预先考虑各种应用程序状态,从而使我们能够清晰地定义类型。
XStateMachine实例接受两个对象参数: `states`configuration和options`transitions`。`states` 对象configuration定义了状态和转换的整体结构。`transitions`options对象允许我们进一步自定义机器,下文将对此进行详细解释。
const xStateMachine = Machine<Context, Schema, Transitions>(
xStateConfig,
xStateOptions
);
我们用来构建机器的三个类型参数是`state` schema、 `state`transitions和 `data` context。它们帮助我们描述每一种可能的状态,规划状态间的转换方式,并定义机器运行过程中可以存储的所有数据。这三个参数在机器初始化之前就已经完全定义好了:
- 模式是对机器整体架构的概览。它定义了应用程序在任何给定时刻可能处于的所有状态。
- 状态转换允许我们从一种状态过渡到另一种状态。它们可以通过用户界面中的事件处理程序触发。事件处理程序本身并不包含状态逻辑,它们只是将状态转换的类型以及任何相关数据发送给机器,机器随后会根据该类型转换到下一个状态
schema。 - 上下文(Context)是一个数据存储,它会被传递到你的状态机中。与Redux类似,上下文代表了程序生命周期中任何阶段(状态转换时)可能需要的所有数据。这意味着,虽然我们在初始化时可能无法获得所有实际数据,但我们确实需要
context预先定义数据存储的形状和结构。
让我们花点时间看一下状态机的初始配置:
const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
id: "Email Application",
initial: "HOME_PAGE",
context: {},
states: {}
};
- ID是一个字符串,指的是这台特定的机器。
- 初始状态指的是机器的初始状态。
- Context是一个定义
context数据存储初始状态和结构的对象,类似于Redux中的初始状态。在这里,我们将所有可能的状态数据作为该对象的键。我们会在适当的时候提供初始值,而未知或可能不存在的值可以在这里声明为 `null`undefined。
我们的机器已经拥有初始化所需的所有信息,我们已经绘制出了机器的各种状态图,机器的各个部件也正在运转。现在,让我们深入了解如何利用 XState 提供的各种工具来触发状态转换和处理数据。
各州
为了说明 XState 如何帮助我们管理应用程序状态,我们将构建一个简单的电子邮件应用程序状态机示例。假设有一个基本的电子邮件应用程序,HOME_PAGE我们可以从初始状态(或欢迎屏幕)转换到另一个INBOX状态(阅读电子邮件的屏幕)。我们可以使用这两个状态定义我们的模式,并定义一个名为 `transition` 的转换OPEN_EMAILS。
interface Schema {
states: {
HOME_PAGE: {};
INBOX: {};
};
};
type Transitions = { type: "OPEN_EMAILS" };
const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
id: "Email Application",
initial: "HOME_PAGE",
context: initialContext,
states: {
HOME_PAGE: {
id: "HOME_PAGE",
on: { OPEN_EMAILS: "INBOX" },
},
INBOX: {
id: "INBOX",
}
}
};
定义了两个状态和转换之后,可以清楚地看到我们的状态机是如何从状态开始HOME_PAGE,并且其转换是如何在on属性中定义的。
选项
1. 服务 + 操作
我们现在有了一个包含基本转换的状态机,但尚未存储任何数据context。一旦用户触发OPEN_EMAILS转换,我们需要调用一个操作service来获取该用户的所有电子邮件,并使用相应assign的操作将它们存储到我们的数据中context。这两个操作都在选项对象中定义。context由于在状态机初始化时我们尚未获取任何电子邮件,因此我们可以将电子邮件定义为一个可选数组。我们需要在模式中添加两个新状态:一个LOADING_EMAILS待处理状态和一个APPLICATION_ERROR错误状态(如果此请求失败)。我们可以调用此请求来获取新状态中的电子邮件LOADING_EMAILS。
type Context = {
emails?: [];
};
const initialContext: Context = {
emails: undefined,
};
interface Schema {
states: {
HOME_PAGE: {};
LOADING_EMAILS: {};
INBOX: {};
APPLICATION_ERROR: {};
};
};
type Transitions = { type: "OPEN_EMAILS"}
const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
id: "Email Application",
initial: "HOME_PAGE",
context: initialContext,
states: {
HOME_PAGE: {
on: { OPEN_EMAILS: "LOADING_EMAILS" },
},
LOADING_EMAILS: {
invoke: {
id: "LOADING_EMAILS",
src: (context, event) => 'fetchEmails',
onDone: {
actions: 'setEmails',
target: "INBOX",
},
onError: {
target: "APPLICATION_ERROR",
},
},
},
INBOX: {
id: "INBOX",
},
APPLICATION_ERROR: {
after: {
5000: `HOME_PAGE`,
},
},
},
};
const xStateOptions: Partial<MachineOptions<Context, any>> = {
services: {
fetchEmails: async () => {
return new Promise<void>((resolve, reject) =>{
resolve();
// reject();
})
},
},
actions: {
setEmails: assign({ emails: (context, event) => event.data }),
}
}
const xStateMachine = Machine<Context, Schema, Transitions>(
xStateConfig,
xStateOptions
);
配置中的四个键分别invoke是`get` id、src`get`、onDone`get` 和onError`get`,其中id`get` 是调用的标识符。`get`src是一个返回包含电子邮件数据的 Promise 的函数fetchEmails。成功获取数据后,我们将进入 `get` 阶段onDone,在那里我们可以使用 ` get`assign操作将获取到的电子邮件数据存储到我们的 `get`阶段中。如您所见,`get` 的两个参数是 ` get`和 `get` ,使其能够访问所有 `get`和 `get`值。我们还需要通过提供目标状态来告知机器下一步该做什么,在本例中,目标状态是我们的 `get` 阶段。对于失败的获取操作,我们有类似的结构,其中我们的目标状态是错误状态 `error`,它会在五秒后返回到`get` 阶段。contextsetEmailsfetchEmailscontexteventcontexteventINBOXAPPLICATION_ERRORHOME_PAGE
2. 卫兵
条件状态变更可以通过options对象中定义的守卫(guard)来处理。守卫是函数,其求值结果会返回一个布尔值。在 XState 中,我们可以使用键 cond 在转换中定义此守卫。
让我们添加一个用于撰写邮件的状态DRAFT_EMAIL。如果用户之前正在撰写邮件,而应用程序成功获取了邮件数据,则应用程序会将用户带回DRAFT_EMAIL页面,而不是跳转到当前状态INBOX。我们将使用一个isDraftingEmail函数来实现此条件逻辑。如果用户在成功获取数据时正在撰写邮件,则该函数isDraftingEmail将返回true并返回到DRAFT_EMAIL当前状态;如果返回错误false,则会将用户跳转到INBOX当前状态。我们的条件判断将在一个名为 `new_state` 的新状态中处理,ENTERING_APPLICATION该状态将负责检查此条件。通过always在定义此状态时使用 `key`,我们告诉 XState 在用户进入该状态时立即执行此条件逻辑。
const xStateConfig: MachineConfig<Context, Schema, Transitions> = {
id: "Email Application",
initial: "HOME_PAGE",
context: initialContext,
states: {
HOME_PAGE: {
on: { OPEN_EMAILS: "LOADING_EMAILS" },
},
LOADING_EMAILS: {
invoke: {
id: "LOADING_EMAILS",
src: 'fetchEmails',
onDone: {
actions: 'setEmails',
target: "ENTERING_APPLICATION",
},
onError: {
target: "APPLICATION_ERROR",
},
},
},
ENTERING_APPLICATION: {
id: "ENTERING_APPLICATION",
always:[
{
target: "DRAFT_EMAIL",
cond: 'isDraftingEmail',
},
{ target: "INBOX" }
]
},
INBOX: {
id: "INBOX",
},
DRAFT_EMAIL: {
id: "DRAFT_EMAIL",
},
APPLICATION_ERROR: {
after: {
5000: `HOME_PAGE`,
},
},
},
}
const xStateOptions: Partial<MachineOptions<Context, any>> = {
services: {
fetchEmails: async () => {
return new Promise<void>((resolve, reject) =>{
resolve();
// reject();
})
},
},
actions: {
setEmails: assign({ emails: (context, event) => event.data }),
},
guards: {
isDraftingEmail: () => {
return true;
// return false;
}
}
}
const xStateMachine = Machine<Context, Schema, Transitions>(
xStateConfig,
xStateOptions
);
XState 可视化工具
XState 最强大的功能之一是 XState 可视化工具,它能够接收我们的机器配置,并自动提供状态机的交互式可视化表示。这些可视化工具正是“状态机为设计人员和开发人员提供通用语言”的方式。
最后查看一下我们的 XState 可视化工具,它展示了整个邮件应用程序的拓扑图。使用下面的任一链接在新标签页中测试我们的机器!沙盒在新标签页中加载后,应该会打开第二个新标签页显示可视化工具。如果您没有看到可视化工具,请禁用弹出窗口拦截器并刷新沙盒。
在可视化工具中,点击OPEN_EMAILS转换即可运行状态机。要更改状态机的运行结果,请在沙盒环境中注释/取消注释fetchEmails`and`isDraftingEmails函数中的返回值。
结论
XState 通过其模式和可视化工具,让我们能够从宏观层面理解应用程序,同时还能通过其配置功能,对状态和数据进行更精细的可见性和控制。随着应用程序的增长,它的易用性帮助我们轻松应对复杂性,使其成为任何开发者的理想选择。非常感谢您的阅读,敬请期待第二部分:XState 和 React!
文章来源:https://dev.to/nkhosla91/an-introduction-to-xstate-in-typescript-1pdn