Skip to content
25 minute read

XState v5 发布了

David Khourshid

今天,我们很高兴终于发布了 XState v5!这是 XState 的一个新主要版本,专注于 actors,并帮助您比以前的版本更快、更轻松地开始使用 XState。

状态机转换可能需要零时间,但从 XState v4 过渡到 v5 花了很长时间。我们在 2018 年 10 月发布了 XState v4,并在此后的大部分时间里一直致力于下一个主要版本的开发。凭借 GitHub 上超过 25k 颗星、npm 上每周 100 万次下载以及一个了不起的社区,我们能够倾听和学习那些在生产中使用 XState 的人,并创建一个更强大但更简单(且更小!)的版本。

提醒一下,XState 是一个完全开源(MIT 许可)、零依赖的状态管理和编排解决方案,基于状态机、状态图和 actor 模型。XState 编排任何逻辑,从 promises 到状态机以及其他所有内容。它最适合管理和编排超越简单状态管理的复杂应用逻辑,并使应用逻辑在视觉上具有协作性,以便您的整个团队(技术和非技术人员)都能轻松理解和贡献。

您可以通过安装 xstate 来立即试用 XState v5:

npm i xstate

我们对 XState v5 的愿景

在 XState v4 中,状态机和状态图是主要焦点。通过状态机以事件驱动的方式建模复杂逻辑被证明是许多在生产中使用 XState 的公司的一种可靠策略。我们了解到,XState 不仅对管理前端逻辑(如复杂组件或多步骤表单)有用,团队还使用它来管理后端工作流和关键业务逻辑。

但随着用例的增加,很明显 XState 需要从管理系统某一部分的逻辑发展到协调需要相互通信的多个部分的逻辑。XState 最初有 活动,后来被 调用的 actors(在 v4 中称为“服务”)取代。状态机和 actor 模型自然地结合在一起,因为状态机可以建模单个 actor 的行为,而 actor 模型可以建模多个相互通信的 actor 的行为。

所以现在,在 XState v5 中,actors 是主要焦点。状态机和状态图仍然是 XState 的重要组成部分,但它们并不是建模 actor 行为的唯一方式(尽管它们可以说是最强大的方式)。我们希望 XState 成为一个多功能的状态协调库,使开发人员能够充分利用 actor 模型,无论他们选择如何编写逻辑。无论您是使用 promises 编写异步逻辑,使用 observables,使用 reducers 管理状态,还是使用回调函数处理任何其他类型的逻辑,您都可以使用 XState 以事件驱动的方式协调您的状态。

话虽如此,我们还:

  • 大大简化了 API 并减少了表面积
  • 引入了新的状态机功能,使强大的模式成为可能
  • 大幅改善了 TypeScript 开发者体验,提供了更好的推断
  • 大幅减少了包的大小
  • 改进了文档并添加了许多新示例

这个版本有很多新功能和改进。让我们来看看我最喜欢的一些新功能。

一切都是 actor

在 XState v5 中,Actor 是主要的抽象单位。actors 比你想象的要简单;它们是一些对象:

  • 有自己的内部状态
  • 可以发送和接收事件(或“消息”)并对其做出反应
  • 可以创建其他 actors

如果你使用过 Redux 或 Zustand 等库,你可能会认为这听起来有点像“store”。你是对的!就像 store 有自己的内部状态并且在接收到事件时可以改变其状态一样,actors 也可以做到这一点甚至更多。

在 XState v5 中,有几个 新的 actor 逻辑创建器 用于创建:

Promise actor logic

import { fromPromise } from 'xstate';

const promiseLogic = fromPromise(async ({ input }) => {
const user = await getUser(input.userId);

return user;
});

Transition function actor logic

import { fromTransition } from 'xstate';

const transitionLogic = fromTransition((state, event) => {
switch (event.type) {
// reducer logic; you know the drill
}
}, { count: 0 });

Observable actor logic

import { fromObservable } from 'xstate';
import { interval } from 'rxjs';

const intervalLogic = fromObservable(() => interval(1000));

Callback actor logic

import { fromCallback } from 'xstate';

const callbackLogic = fromCallback(({ sendBack, receive }) => {
const handler = (event) => {
sendBack(event);
}

window.addEventListener('message', handler);

return () => { window.removeEventListener('message', handler); }
});

要从这些逻辑创建 actors,您可以使用 createActor(logic) 函数(在 XState v4 中称为 interpret()):

import { createActor } from 'xstate';

// ...

const actor = createActor(someLogic);

actor.subscribe(snapshot => {
console.log(snapshot);
});

actor.start();

actor.send({ type: 'greet', greeting: 'hello world' });

无论您创建哪种逻辑,创建 actors 的方式都是完全相同的。actors 是一个强大的抽象单元,因为它们不仅代表了处理应用程序中几乎所有事情的单一接口,而且 actor 之间的通信也可以在序列图中清晰地可视化(我们将很快发布)。此外,这种简单的抽象使您能够创建可组合的 actor 逻辑

function withLogging(actorLogic) {
return {
...actorLogic,
transition: (state, event, actorScope) => {
console.log('State:', state);
return actorLogic.transition(state, event, actorScope);
}
}
}

const actor = createActor(withLogging(someLogic));

有了这些构建块,您可以创建更高级的抽象,例如 withUndoRedowithDebounce,甚至自定义 actor 逻辑,如 fromGeneratorfromWebSocket 等。

Inspect API

有一种新的、更简洁的方法来检查不仅是状态机的状态转换,还有 actor 系统中 actor 的各个方面:

  • Actor 生命周期
  • Actor 事件通信
  • Actor 快照更新

与其神奇地设置 devTools: true,不如使用 Inspect API 将“检查器”(只是一个观察者,观察检查事件)附加到 actor 系统的根部:

const actor = createActor(machine, {
inspect: (inspectionEvent) => {
// type: '@xstate.actor' or
// type: '@xstate.snapshot' or
// type: '@xstate.event'
console.log(inspectionEvent);
}
});

检查器将接收系统中每个 actor 的检查事件,让您对发生的一切有细粒度的可见性,从单个 actor 如何变化到 actor 之间如何相互通信。

我们将很快发布检查开发工具,以状态机图、序列图等形式可视化这些信息。

深度持久化

我们已经写过如何持久化状态,而 XState v5 将持久化进一步提升。Actor 持久化是一种模式,其中 actor 的内部状态可以随时持久化和恢复。在 XState v4 中,被调用/生成的 actor 不会被持久化,而在 XState v5 中,actors 现在被深度(递归地)持久化。被调用/生成的 actors 将被持久化,以及从这些 actors 调用/生成的 actors,依此类推。

在以下示例中,mainActor 的状态将被持久化,以及被调用的 someCounter actor 的状态。当 restoredActor 启动时,它将从 mainActor 的持久化状态开始,其中包括 someCounter 的持久化状态:

import { setup, createActor } from 'xstate';

const machine = setup({
actors: {
counter: fromTransition(/* ... */)
}
}).createMachine({
invoke: {
// 这也将被持久化!
src: 'counter',
id: 'someCounter',
},
// ...
});

const mainActor = createActor(machine);
mainActor.start();

// 深度持久化状态
// 也持久化了 "someCounter" actor!
const persistedState = mainActor.getPersistedSnapshot();

// 恢复状态
const restoredActor = createActor(machine, {
snapshot: persistedState,
});

// 从递归持久化的状态开始
restoredActor.start();

这对于客户端(例如处理页面刷新)和服务器端(例如持久化工作流状态)用例都很有用。在我们的文档中阅读更多关于 actor 持久化的信息

在任何地方引用 actors

由于 actors 可以生成其他 actors,而这些 actors 又可以生成其他 actors,这些连接的 actors 形成了一个自然的层次结构,称为“actor 系统”。在 XState v4 中,actors 只能通过 sendTo('child-id', ...)sendParent(...) 在父子关系中轻松通信。从一个任意 actor 向同一系统中的另一个任意 actor 发送事件是困难且过于复杂的。

在 XState v5 中,调用 createActor(...) 创建一个根 actor 也将创建一个隐式的 actor 系统。这启用了一个关键特性,称为 接待员模式。接待员模式意味着 actors 可以通过其 systemId 注册和查找,这对于需要相互通信但彼此不直接了解的 actors(即,不在父子关系中的 actors)非常有用。

例如,假设您有一个 checkoutMachine 来协调在线商店的状态。如果您希望一个通知器 actor 在 checkoutMachine 系统内的任何生成的机器中都可用,您可以通过提供 systemId 来注册它:

import { notifierMachine } from '../notifierMachine';
import { shippingMachine } from '../shippingMachine';

const checkoutMachine = createMachine({
invoke: {
src: notifierMachine,
systemId: 'notifier',
},
// ...
states: {
// ...
shipping: {
invoke: {
src: shippingMachine,
},
},
},
});

const checkoutActor = createActor(checkoutMachine);
checkoutActor.start();

现在,checkoutActor 系统中的任何 actor 都可以通过调用 system.get("notifier") 访问通知器 actor:

const shippingMachine = createMachine({
// ...
on: {
'address.updated': {
actions: sendTo(({ system }) => system.get('notifier'), {
type: 'notify',
message: 'Shipping address updated',
}),
},
},
});

隐式系统和接待员模式使得建模任意 actor 之间的通信、事件总线和其他事件驱动模式变得更加容易。

在某些情况下,您可能希望为 actors 指定初始“输入数据”。在 XState v4 中提供这些输入数据并不容易。您必须:

  • 创建一个工厂机器函数,该函数接受一些输入数据并返回一个包含该输入数据的机器。
  • 使用 machine.withContext(...) 创建一个新机器,并将整个上下文与输入数据一起传递。

由于只有机器应该最初确定 context,这并不理想,因为可能会在某个不可能的状态下初始化机器。此外,您可能希望将某些 context 属性视为 私有(机器内部)且不可外部配置。

在 XState v5 中,您现在可以通过将输入数据作为第二个参数传递给 createActor(machine, { input }) 来为机器提供输入数据。机器可以在 context 初始化函数中读取这些输入数据:

const greetingMachine = createMachine({
context: ({ input }) => ({
greeting: `Hello, ${input.name}!`,
}),
});

const greetingActor = createActor(greetingMachine, {
input: {
name: 'David',
},
});

此外,这适用于任何 actor 逻辑,而不仅仅是状态机:

const promiseLogic = fromPromise(({ input }) =>
fetch(`https://api.example.com/users/${input.id}`).then((res) => res.json()),
);

// 独立的 promise actor
const promiseActor = createActor(promiseLogic, {
input: {
id: 42,
},
});

// 从一个机器
const machine = setup({
actors: { promiseLogic }
}).createMachine({
invoke: {
src: 'promiseLogic',
input: {
id: 42,
},
}
})

actors 还可以有 output,它表示它们到达最终状态时的“完成数据”。不仅仅是状态机可以有输出;promise 逻辑自然会产生输出,并且将来可能为其他 actor 逻辑类型指定输出。

阅读更多关于 input 的信息outputcontext 的信息

const processMachine = createMachine({
id: 'some-process',
initial: 'pending',
context: {/* ... */},,
states: {
pending: {/* ... */},
transforming: {/* ... */},
done: {
type: 'final'
},
},
output: ({ context }) => ({
status: 200,
result: context.transformedData,
})
});

更强的类型推断

XState 的一个最大需求是改进 TypeScript 体验。考虑到状态图(分层状态机)的偶然复杂性,以及需要以声明方式表示它们,以便可以可视化、静态分析和强类型化,这并非易事。任何一个这些约束都已经很难;所有三个约束几乎是不可能的。

Mateusz Burzyński 拯救了我们,他不仅在 XState 中展示了令人难以置信的 TypeScript 工程和魔法,还直接对 TypeScript 本身做出了重要贡献!新的 setup API 是一个真正突显这些改进的领域:

import { setup, fromPromise } from 'xstate';

const getChatCompletion = fromPromise(async () => { ... });
const processResult = fromPromise(async () => { ... });
const sendToDiscord = fromPromise(async () => { ... });

const machine = setup({
actors: {
getChatCompletion,
processResult,
sendToDiscord
}
}).createMachine({
// ...
states: {
thinking: {
invoke: {
// 字符串源强类型!
src: 'getChatCompletion',
onDone: {
target: 'processing',
actions: assign({
// event.output 强类型!
completion: ({ event }) => event.output
})
}
}
},
processing: {
invoke: { src: 'processResult', /** ... **/ }
},
sending: {
invoke: { src: 'sendToDiscord', /** ... **/ }
},
done: { type: 'final' }
}
});

使用 setup(...),您不再需要做双重工作来指定 actors、actions、guards、delays 等的类型 并且 之后再提供它们;只需在 setup(...) 函数中完成这些操作,类型就会自动流动。这也更加安全,因为您可以保证这些实现存在,而不是希望它们存在(或依赖于 typegen)在之后提供。

setup API 还启用了另一个神奇的功能:强类型的状态值

const machine = setup({
// ...
}).createMachine({
initial: 'green',
states: {
green: {/* ... */},
yellow: {/* ... */},
red: {
initial: 'walk',
states: {
walk: {/* ... */},
run: {/* ... */},
stop: {/* ... */}
}
}
}
});

const actor = createActor(machine)
actor.start();

// 强类型的状态值!
// 自动补全将显示:
// - 'green'
// - 'yellow'
// - 'red'
// - { red: 'walk' }
// - { red: 'run' }
// - { red: 'stop' }
actor.getSnapshot().matches('green');

阅读更多关于 setup(...) 的信息

动态参数

在类型改进的主题上,动态 action 和 guard 参数现在使得创建独立于状态机的强类型 action 和 guard 实现成为可能:

const machine = setup({
actions: {
greet: (_, params: { name: string }) => {
console.log(`Hello, ${params.name}!`);
}
},
guards: {
isGreaterThan: (_, params: { value: number; min: number }) => {
return params.value > params.min;
}
}
}).createMachine({
context: ({ input }) => ({
user: input.user,
count: 0
}),
entry: {
type: 'greet',
params: ({ context }) => ({
name: context.user.name
})
},
on: {
dec: {
guard: {
type: 'isGreaterThan',
params: ({ context, event }) => ({
value: context.count,
min: 0
})
}
}
}
});

这消除了动作实现与机器的耦合,并允许更灵活地使用它们。它还减少了对类似 typegen 的依赖,因为我们不再需要预测动作可以调用的可能 contextevent 类型。

排队动作

enqueueActions() 动作创建器使得在单个动作创建器中协调复杂动作变得更加容易。可以将其视为 pure()choose() 的更直观组合,它们现在被这个新的动作创建器所取代:

const machine = createMachine({
// ...
entry: enqueueActions(({ context, event, enqueue, check }) => {
// 赋值动作
enqueue.assign({
count: context.count + 1
});

// 条件动作(替代 choose(...))
if (event.someOption) {
enqueue.sendTo('someActor', { type: 'blah', thing: context.thing });

// 其他动作
enqueue('namedAction');
// 带参数
enqueue({ type: 'greet', params: { message: 'hello' } });
} else {
// 内联
enqueue(() => console.log('hello'));

// 甚至内置动作
}

// 使用 check(...) 基于守卫条件性地排队动作
if (check({ type: 'someGuard' })) {
// ...
}

// 无返回值
})
});

这是一个更自然的编写效果的方法,因为您可以使用普通的 JavaScript 来构建效果。在我们的迁移指南中阅读更多关于 enqueueActions() 动作创建器的信息

自引用

在您可以在 XState v5 机器配置中提供的动态函数中,现在有一个 self 属性引用 actor 本身。这为 actor 通信启用了新的、灵活的模式,因为您可以在事件中将此 self 引用传递给其他 actors:

const pingMachine = createMachine({
invoke: {
src: 'pong',
id: 'pong',
},
on: {
ping: {
actions: sendTo('pong', ({ self }) => ({ type: 'ping', sender: self })),
},
},
});

// ...

const pongMachine = createMachine({
on: {
ping: {
actions: sendTo(({ event }) => event.sender, { type: 'pong' }),
},
},
});

阅读更多关于 self 属性的信息

高阶守卫

在 XState v4 中,守卫是 .cond 转换属性上的简单函数,返回 truefalse 以确定是否采取转换。要否定守卫或组合守卫,您必须创建一个新的守卫,这会导致代码重复或冗余。在 XState v5 中,您现在可以使用 高阶守卫,这些是接受守卫(引用和/或内联)并返回守卫函数的函数。有 3 个内置的高阶守卫函数:and([...guards])or([...guards])not(guard)

import { setup, and, not } from 'xstate';

const userMachine = setup({
guards: {
isAuthenticated: ({ context }) => context.user !== undefined,
isAdmin: ({ context }) => context.user.role === 'admin',
isBanned: ({ context }) => context.user.status === 'banned',
}
}).createMachine({
// ...
on: {
doSomething: {
// 高阶守卫
// 从 "cond" (v4) 重命名为 "guard" (v5)
guard: and(['isAuthenticated', 'isAdmin', not('isBanned')]),
},
},
});

这些高阶守卫可以以多种不同的方式组合,以表达任何复杂的条件。未来,Stately Studio 将能够可视化守卫中表达的复杂条件逻辑。在我们的文档中阅读更多关于高阶守卫的信息

部分事件描述符

部分事件描述符,也称为 部分通配符,是 XState v5 中的一个强大新功能,使处理事件组变得更加容易。在 XState v4 中,您可以使用通配符来处理任何未被其他转换匹配的事件,但您必须小心不要意外处理不打算处理的事件。在 XState v5 中,您可以使用部分事件描述符通过在分隔符后放置通配符 (.*) 来处理事件组,并且可以明确您想要处理的事件:

const machine = createMachine({
// ...
on: {
// 将处理任何以 "pointer." 开头的事件:
// "pointer.down", "pointer.up", "pointer.move" 等等。
'pointer.*': {
actions: 'logPointerEvent',
},
},
});

在我们的文档中阅读更多关于部分事件描述符的信息

哦,顺便说一句,它们是类型安全的!🎉

迁移和重大变化

与任何主要版本一样,有一些重大变化。我们尽量将这些变化保持在最低限度,但有些是必要的,以使 XState v5 尽可能强大、灵活、简单和强类型化。阅读我们的指南,了解从 XState v4 迁移到 v5 的方法以及重大变化的列表

最大的变化之一是将函数参数合并为单个“统一参数”。实现函数以前需要多个参数,这使得记住使用哪个参数或忽略某些参数变得困难。在 XState v5 中,所有实现函数现在都接受一个单一的统一参数对象,该对象包含 contextevent 和其他与实现函数相关的属性:

const machine = createMachine({
context: {
count: 0,
},
on: {
increment: {
// 单个参数,而不是:
// guard: (_, event) => ...
guard: ({ event }) => !Number.isNaN(event.value),
// 单个参数,而不是:
// actions: (context, event) => ...
actions: ({ context, event }) => {
console.log(context, event);
},
},
},
});

Stately Studio 对 v5 的支持

现在 XState v5 API 终于稳定了,我们正在努力在 Stately Studio 中添加对所有新功能和更新的支持。目前,Studio 已经可以导入和导出 XState v5 代码。即将推出的是对 inputoutput 和 action/guard 参数的支持。为了给您完全的控制权,我们还即将发布用于 actions、actors、guards 等的 Studio 内代码编辑器。通过 Stately AI,您甚至可以生成任何您想要实现的代码,完全符合 XState v5 代码。

为了庆祝 XState v5 的发布,使用代码 XSTATEV5 可享受 Stately Pro 订阅 35% 的折扣,并解锁 Stately Studio 中的众多精彩专业功能。

未来计划和想法

这不是我们的最终状态。XState v5 还有更多的功能和改进,例如: