拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 XState v5 与 Vue 构建 7GUIs Counter:从状态机到组件的完整实战

用 XState v5 与 Vue 构建 7GUIs Counter:从状态机到组件的完整实战
  • 前端
  • 后端

【免费下载链接】xstate

State machines, statecharts, and actors for complex logic

项目地址:https://gitcode.com/gh_mirrors/xs/xstate
点击查看免费下载

XState 是一个用于建模复杂逻辑的 JavaScript/TypeScript 库,其核心理念是状态机(state machines)、状态图(statecharts)与 Actor 模型。本篇文章基于当前仓库xs/xstate中examples/7guis-1-counter-vue这一示例,讲解如何用XState v5 + Vue 3 + TypeScript + Vite构建 7GUIs("7 Graphical User Interfaces" 基准测试套件)中的第一个任务 —— 一个仅含"显示计数 + 递增按钮"的 Counter。读完后你将掌握:用setup()定义类型安全的机器、用assign内建动作更新context、用@xstate/vue的useMachine()在 Vue 组件中接入状态机,以及如何在本地运行和在线预览这个示例。

说明:仓库中该示例的依赖版本为xstate ^5.28.0、@xstate/vue ^3.1.4、vue ^3.5.29、vite ^5.4.21(见 examples/7guis-1-counter-vue/package.json),下文所有代码均以此为准。

1. 示例概览与 7GUIs 背景

7GUIs 是 Eugen Kiss 提出的一组经典 GUI 基准任务,用于检验不同 UI 框架/工具在典型交互场景下的表达能力,Counter 是其第 1 个任务(原文档在 examples/7guis-1-counter-vue/README.md 中明确了这一点)。任务需求很简单:显示一个数值,点击按钮使其加一。

本示例把它实现为一个状态机驱动的 Vue 应用,文件结构如下(全部位于examples/7guis-1-counter-vue/):

  • src/counterMachine.ts—— 定义 Counter 状态机(业务逻辑核心)
  • src/Counter.vue—— 使用useMachine接入状态机并渲染 UI 的组件
  • src/App.vue—— 仅负责挂载Counter的根组件
  • src/main.ts—— Vue 应用入口
  • src/style.css—— 7GUIs 风格的全局样式
  • vite.config.ts、package.json、tsconfig.json等工程配置

2. 用setup()定义类型安全的计数器状态机

counterMachine.ts是本例的灵魂,完整代码如下(见 examples/7guis-1-counter-vue/src/counterMachine.ts):

import { setup, assign } from 'xstate'; export const counterMachine = setup({ types: { context: {} as { count: number }, events: {} as { type: 'increase' } } }).createMachine({ context: { count: 0 }, id: 'Counter', initial: 'ready', states: { ready: { on: { increase: { target: 'ready', actions: assign({ count: ({ context }) => context.count + 1 }) } } } } });

2.1setup():XState v5 的类型安全入口

XState v5 引入了setup()作为创建机器的新推荐方式。从源码看(packages/core/src/setup.ts),setup接受一个包含schemas、types、actors、actions、guards、delays的配置对象,返回一个SetupReturn对象,其上挂有createMachine、createAction、createStateConfig、extend以及assign、sendTo、raise、log、emit、spawnChild等内建动作(见 packages/core/src/setup.ts)。

在本例中,setup的types字段声明了两类类型信息:

  • context: {} as { count: number }—— 声明机器上下文中有一个count: number字段;
  • events: {} as { type: 'increase' }—— 声明该机器唯一可接收的事件是{ type: 'increase' }。

之后调用.createMachine({ ... })生成实际机器。这种"先声明类型、再定义配置"的方式,让后续配置中的context、on、actions等全部获得编译期类型检查:比如向send传递{ type: 'decrease' }这类未声明的事件会直接报错,这正是 XState v5 强类型体验的核心。

2.2context:机器的可读状态

context: { count: 0 }定义了机器的初始上下文。在状态机术语中,context是"与状态正交的可变数据",与"当前处于哪个状态(state value)"共同构成快照(snapshot)。本例只关心计数数值,因此把count放进context,而状态本身仅有一个ready。

2.3id与initial/states

  • id: 'Counter'给机器一个稳定标识,便于调试与引用;
  • initial: 'ready'指定初始状态;
  • states定义状态集合,这里只有一个ready状态,其中on声明了事件到转移的映射。

虽然这个例子只有单个状态、结构极简,但它完整展示了状态机的基本骨架。当业务变复杂时,可以在states中扩展多个状态与转移,这也是 XState 处理复杂逻辑的核心优势。

2.4assign:更新 context 的唯一内建方式

转移中的actions: assign({ count: ({ context }) => context.count + 1 })是理解本例的关键。assign是 XState 的内建动作(built-in action),专门用于不可变地更新机器的 context。

从实现看(packages/core/src/actions/assign.ts),assign接收一个"赋值器"(assignment),可以是:

  • 一个函数:({ context, event }) => partialContext;
  • 一个属性映射对象:{ count: ({ context }) => context.count + 1 }(本例所用形式),其中每个字段的值可以是函数(根据当前context/event计算新值),也可以是常量。

在resolveAssign中(packages/core/src/actions/assign.ts),最终通过Object.assign({}, snapshot.context, partialUpdate)生成一份新的 context,再调用cloneMachineSnapshot产出新快照 —— 也就是说,assign不会就地修改旧 context,而是返回不可变的新快照,这与 Vue 的响应式心智模型天然契合。

需要特别注意的是:assign是声明式动作,不能把它直接写进普通自定义动作里命令式调用(开发模式下会给出警告,见 packages/core/src/actions/assign.ts)。

2.5 一次点击背后的完整转移语义

当收到increase事件时,状态机执行一次转移:

  1. 事件匹配ready状态的on.increase;
  2. target: 'ready'表明转移目标仍是ready状态(即"自转移",self-transition),这是计数器的典型写法 —— 我们不离开当前状态,只更新数据;
  3. actions中的assign被解析执行,count从context.count加一;
  4. 状态机发出新的快照,@xstate/vue的订阅机制将其同步到组件。

3. 在 Vue 组件中接入状态机

3.1Counter.vue:useMachine 的两种关键返回值

examples/7guis-1-counter-vue/src/Counter.vue 完整代码如下:

<script setup lang="ts"> import { useMachine } from '@xstate/vue'; import { counterMachine } from './counterMachine'; const { snapshot, send } = useMachine(counterMachine); </script> <template> <main class="case center-children"> <span>{{ snapshot.context.count }}</span> <button @click="send({ type: 'increase' })">+</button> </main> </template>

useMachine来自@xstate/vue。从源码看(packages/xstate-vue/src/useMachine.ts),它本质上是useActor的别名,返回三个值:

  • snapshot: Ref<SnapshotFrom<TMachine>>—— 一个 Vue 响应式引用,存放机器当前快照;模板中通过snapshot.context.count读取计数;
  • send: (event) => void—— 发送事件的函数,模板中按钮点击时send({ type: 'increase' });
  • actorRef: Actor<TMachine>—— 底层 actor 引用,需要直接操作 actor(如订阅、停止)时才用到。

在useActor内部(packages/xstate-vue/src/useActor.ts),useActorRef负责创建并持有 actor,useSelector(actorRef, (s) => s)用于订阅快照变化并把最新快照写入snapshot这个 Ref —— 当assign产生新快照时,Vue 的响应式系统自动触发视图更新,<span>中的数字随之刷新。此外,useActor在开发模式下会校验传入的是"逻辑"而非"已创建的 ActorRef",传错会直接抛错提示(packages/xstate-vue/src/useActor.ts)。

3.2App.vue与main.ts

  • App.vue(examples/7guis-1-counter-vue/src/App.vue)只是简单地渲染<Counter />,保持组件层级清晰;
  • main.ts(examples/7guis-1-counter-vue/src/main.ts)是标准 Vue 入口:createApp(App).mount('#app')。

3.3 样式与 7GUIs 观感

examples/7guis-1-counter-vue/src/style.css 实现了 7GUIs 演示常见的"卡片 + 大按钮"视觉:.case是圆角卡片容器,span以大号等宽字体显示数字,button使用浅绿主题并带按下位移效果,hover/focus 也做了处理。这些细节并非功能必需,但让示例在演示时更接近 7GUIs 基准的呈现风格。

4. 运行、构建与在线预览

4.1 本地运行

仓库使用 pnpm 工作区管理,示例自身携带package.json与pnpm-lock.yaml(examples/7guis-1-counter-vue/package.json)。在示例目录内:

# 安装依赖(在仓库根目录或示例目录执行均可,工作区会解析 @xstate/vue 与 xstate) pnpm install # 启动 Vite 开发服务器 pnpm dev

dev脚本即vite,启动后按终端提示打开本地地址即可看到计数器页面。

4.2 生产构建与预览

package.json中的scripts提供了完整构建链路:

  • build:vue-tsc && vite build—— 先执行vue-tsc对.vue与.ts做类型检查,再打包到dist/。这也是验证"类型安全"设计的最佳实践:类型不匹配会在构建期暴露;
  • preview:vite preview—— 本地预览构建产物。

4.3 一键在线体验

原文档提供了两个在线预览入口(对应 examples/7guis-1-counter-vue/README.md 的 Live 小节):CodeSandbox 与 StackBlitz 均支持直接打开本示例仓库目录运行,无需本地安装,适合快速体验或分享演示。

5. 从源码看 XState v5 的完整链路

结合上面的讲解,把本例涉及的调用链串起来看:

  1. 定义机器:setup({ types: {...} }).createMachine({...})(packages/core/src/setup.ts)内部把配置连同schemas交给createMachine,生成StateMachine实例;
  2. 创建 actor:useActorRef将机器作为 actor logic 启动,得到一个可接收事件的 actor;
  3. 发送事件:send({ type: 'increase' })把事件送入机器;
  4. 执行转移:机器在ready状态命中on.increase,执行assign;resolveAssign计算新 context 并cloneMachineSnapshot出新快照(packages/core/src/actions/assign.ts);
  5. 通知订阅者:actor 发布新快照,useActor中注册的 listener 更新snapshotRef(packages/xstate-vue/src/useActor.ts);
  6. 视图刷新:Vue 响应式系统驱动模板重新渲染,页面上数字 +1。

这条链路展示了 XState 的一个核心心智模型:UI 只负责把事件交给 actor、把快照渲染出来;状态如何变化、数据如何更新,完全由状态机决定。这也是从 7GUIs Counter 走向更复杂状态图(并行状态、守卫、子状态机、actor 通信等)时的统一基础。

6. 延伸:把 Counter 泛化为你的第一个 XState 应用

本例虽小,但麻雀虽小五脏俱全。基于同一套代码骨架,你可以轻松扩展:

  • 增加事件:在types.events中加入{ type: 'decrease' }、{ type: 'reset' },再在ready.on中补对应转移;
  • 增加状态:在states中加入counting、paused等状态,用事件驱动状态切换;
  • 增加守卫与延迟:通过setup({ guards, delays })声明,并在转移中使用guard与after;
  • 复用机器:setup()返回对象上的extend可以基于已有类型声明扩展 actions/guards/delays(packages/core/src/setup.ts)。

仓库examples/目录还提供了大量同类参考:Vue 版的温度转换器(examples/7guis-2-temperature-vue)、React 版 Counter(examples/7guis-counter-react)、带本地存储的 React 计数器(examples/local-store-counter-react)、计时器(examples/timer)与秒表(examples/stopwatch)等,均采用同样的"机器文件 + 框架 hook"组织方式,是继续学习的极佳素材。

7. 小结

本文围绕examples/7guis-1-counter-vue完整拆解了"用 XState v5 状态机驱动 Vue 计数器"的实践:setup()提供类型安全入口,assign不可变地更新 context,useMachine/useActor把快照与事件桥接到 Vue 响应式系统。掌握这条链路,你就掌握了 XState v5 应用的最小完备单元 —— 无论是 7GUIs 的后续任务,还是真实业务中的复杂交互逻辑,都可以在此基础上逐步叠加状态、守卫、子状态机与 actor 协作来构建。

  • 前端
  • 后端

【免费下载链接】xstate

State machines, statecharts, and actors for complex logic

项目地址:https://gitcode.com/gh_mirrors/xs/xstate
点击查看免费下载
上一篇:Axis2 WebShell 实战:基于 config.aar 的 Axis2 服务后门(命令执行 / 反弹 Shell / 文件上传下载)
下一篇:Apereo CAS 委托认证(Delegated Authentication)完全指南:基于 Pac4j 的多协议外部身份源接入与治理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表