@blockquote-web-components/blockquote-controller-xstate

中文文档
2.10.0发布于 2026/10/31MITTypeScriptES Module构建来源已验证

用于构建可组合和可重用Web组件的现代体系结构

中文使用文档
文档目录5 节
  1. 连接XState机与Lit
  2. 演示
  3. 使用
  4. src/BlockquoteControllerXstate.ts :
  5. src/index.ts :

BlockquoteControllerXstate

Lit

连接XState机与Lit

blockquoteconcontrollerxstate是一个Lit Reactive Controller,专门设计用于与XState集成。该控制器提供订阅XState参与者的功能。它还调用 onSnapshot 随着状态的变化。


演示

Open in StackBlitz

Open in Stately.ai

使用

counterMachine.ts

import {setup, types} from 'xstate';

const counterSetup = setup({
  // v6: `schemas` (Standard Schema). `types<T>()` is type-only, no runtime validation.
  // Zod, Valibot, ArkType... can be used instead to validate at runtime.
  schemas: {
    context: types<{counter: number}>(),
    events: {
      INC: types<void>(),
      DEC: types<void>(),
      TOGGLE: types<void>(),
    },
    // The element's public events, dispatched on the host with `dispatchEmitted`
    emitted: {
      counterchange: types<{counter: number}>(),
    },
  },
  guards: {
    canIncrement: ({context}) => context.counter < 10,
    canDecrement: ({context}) => context.counter > 0,
  },
  delays: {
    backoff: ({context}) => context.counter * 1000,
  },
});

export const counterMachine = counterSetup.createMachine({
  context: {counter: 0},
  initial: 'enabled',
  states: {
    enabled: {
      on: {
        // Transition functions (typed from `schemas`): return the next `context`, or
        // `undefined` to ignore the event. The guard comes first: a function that calls
        // `enq` handles the event even when it returns nothing.
        INC: ({context, guards}, enq) => {
          if (!guards.canIncrement({context})) {
            return undefined;
          }
          const counter = context.counter + 1;
          enq.emit({type: 'counterchange', counter});
          return {context: {counter}};
        },
        DEC: ({context, guards}, enq) => {
          if (!guards.canDecrement({context})) {
            return undefined;
          }
          const counter = context.counter - 1;
          enq.emit({type: 'counterchange', counter});
          return {context: {counter}};
        },
        // v6 types do not accept the string shorthand (`TOGGLE: 'disabled'`)
        TOGGLE: {target: 'disabled'},
      },
    },
    disabled: {
      after: {
        backoff: {target: 'enabled'},
      },
      on: {
        TOGGLE: {target: 'enabled'},
      },
    },
  },
});

new BlockquoteControllerXstate(this, {machine, options, onSnapshot, onError, reconnect, dispatchEmitted})

  • this:主机组件(a) ReactiveControllerHost,通常是 LitElement 本身)。
  • machine (必选):XState机。
  • options: createActor 选项(input, inspect,……)。可选,除非机器需要 输入:那 {input} 或者 {snapshot} 必须提供,当呼唤 createActor.
  • onSnapshot (可选):每次新快照时调用:每次连接的第一个快照 最后一个(snapshot.status === 'stopped'),此时主机已断开连接。
  • onError (可选):当参与者出错时调用 'error' 快照也被发送到 onSnapshot). 如果省略,XState将错误报告为未处理,除非另一个订阅者 演员处理它。
  • reconnect (可选): 'restart' (默认)或 'restore'新演员的出发点是什么 当主机重新连接时。
  • dispatchEmitted (可选):调度机器作为DOM事件在 主人。 true 使用 { composed: true }.

需要输入的机器

const userMachine = setup({
  schemas: {
    input: types<{userId: string}>(),
    context: types<{userId: string}>(),
  },
}).createMachine({
  context: ({input}) => ({userId: input.userId}),
});

new BlockquoteControllerXstate(this, {machine: userMachine}); // type error: `input` is required
new BlockquoteControllerXstate(this, {machine: userMachine, options: {input: {userId: '123'}}});

输入可接受的机器 undefined (或者声明没有输入)可以省略 options.

actor是在构造函数中创建的,所以 snapshot, actor, send, trigger 和 can 在连接主机之前可用。它开始于 hostConnected 停了下来 hostDisconnected;如果主机再次连接,将创建一个新的参与者。与 reconnect: 'restore' 它恢复前参与者的状态(等待计时器保持其状态) 最后期限);默认情况下('restart'),它从初始状态开始。

xstate-counter.js

import {html, LitElement} from 'lit';
import {BlockquoteControllerXstate} from '@blockquote-web-components/blockquote-controller-xstate';
import {counterMachine} from './counterMachine.js';
import {styles} from './styles/xstate-counter-styles.css.js';

export class XstateCounter extends LitElement {
  static styles = [styles];

  constructor() {
    super();
    // The controller re-renders the host on every snapshot. The machine emits the element's
    // public events, and `dispatchEmitted` dispatches them on it: `counterchange` with
    // `detail: {counter}`, `{composed: true}` (listen on the element, or capture).
    this.counterController = new BlockquoteControllerXstate(this, {
      machine: counterMachine,
      // Moving the element in the DOM keeps its state
      reconnect: 'restore',
      dispatchEmitted: true,
      options: {
        inspect: this._inspectEvents,
      },
    });
  }

  // xstate v6 inspection events: '@xstate.actor' | '@xstate.transition'
  _inspectEvents = (inspEvent) => {
    console.info('inspect event', inspEvent);
  };

  get #disabled() {
    return this.counterController.snapshot.matches('disabled');
  }

  render() {
    // `can.X()`: `false` when the event would be ignored (guards, `disabled` state) or the
    // actor has finished. `trigger` and `can` always target the current actor.
    const {can, trigger, snapshot} = this.counterController;
    return html`
      <slot></slot>
      <div data-disabled="${this.#disabled}">
        <span>
          <button
            ?disabled="${!can.INC()}"
            data-counter="increment"
            \@click=${() => trigger.INC()}>
            Increment
          </button>
          <button
            ?disabled="${!can.DEC()}"
            data-counter="decrement"
            \@click=${() => trigger.DEC()}>
            Decrement
          </button>
        </span>
        <p>${snapshot.context.counter}</p>
      </div>
      <div>
        <button \@click=${() => trigger.TOGGLE()}>
          ${this.#disabled ? 'Enabled counter' : 'Disabled counter'}
        </button>
      </div>
    `;
  }
}

trigger 和 can

trigger 中生成的类型化事件 schemas.events: trigger.INC(), trigger.RENAME({label}),就像 send({type: 'RENAME', label}). 它的签名和 XState v6的 actor.trigger,但有一点不同:XState属于一个参与者,而 控制器是一个稳定的代理(ControllerTrigger),它总是发送给当前actor。

这很重要,因为每次主机再次连接(停止)时都会创建一个新的actor 演员无法重启)。 actor.trigger 保持从第一个连接将发送到一个停止 行动者,事件就会失去; controller.trigger 可安全保存或销毁。

can (ControllerCan)有相同的形状,也是一个稳定的代理,和回报 snapshot.can(event) 对于现任演员: can.INC(), can.RENAME({label}). 它是 false 当转换函数忽略事件(返回 undefined)或者演员已经演完了 ('stopped', 'done' 或者 'error'). 机器仍然拒绝无效事件: can 只有 驱动UI。

断开连接和重新连接: restart 或者 restore

一个Lit元素被断开并再次连接的频率比看起来更频繁:移动它 DOM,重新排序 repeat,改变它的父级。actor在断开连接时停止,并且 停止的actor不能重新启动,因此在重新连接时创建一个新的actor。这个 reconnect 选项 决定它从哪里开始:

reconnect新演员从
'restart' (默认)初始状态,或者 options.snapshot
'restore'前一个参与者的状态:它的持久化快照

与 'restore',则快照在参与者停止之前被持久化 (actor.getPersistedSnapshot()),新的演员由此诞生 (createActor(machine, {snapshot})). 它使:

  • context,国家价值和子女。
  • 挂起计时器,具有其原始截止日期:在断开连接触发时过期的计时器 马上。
  • 调用的工作在飞行中,如 createAsyncLogic 请求:停止中止它,而新的 Actor再次运行它。

演员演完了('done' 或者 'error')重新启动,而另一个 machine 是 与此同时生效。 onSnapshot 仍然收到 'stopped' 快照断开,然后 恢复一个。

发射事件: on 和 dispatchEmitted

机器发出事件 enq.emit(...) 表示某事发生过一次:计数器 更改,提交了一个表单。中声明 schemas.emitted,元素的公共事件直播 在机器中,没有DOM的类型和可测试的。

dispatchEmitted: true 将主机上发出的每个事件分派为 CustomEvent: 发出 type 是事件名,其余是其名 detail, {composed: true} (通过一个 对象来更改它)。事件不会冒泡:监听元素本身,或者监听对象 捕获阶段的父节点({capture: true})。 EmittedEventMap 类型 addEventListener:

declare global {
  interface HTMLElementEventMap extends EmittedEventMap<typeof counterMachine> {}
}

el.addEventListener('counterchange', (ev) => ev.detail.counter); // `number`

要处理代码中发出的事件,请使用 controller.on(type, handler) (或 '*'). 它有 的签名 actor.on,但XState删除 actor.on 处理程序,当参与者停止时 controller.on 处理程序不断从重新连接时创建的参与者接收事件。

交付时:

  • 转换应用后: controller.snapshot 已具有新状态(主机) 尚未重新渲染)。
  • 当主机断开连接(停止actor不运行)时不会 exit),当演员也是如此 恢复为 reconnect: 'restore' (entry (不再运行):移动元素 DOM什么也不分派。
  • can.X() 不触发:它只计算转换。

类中同步运行侦听器 trigger (或 send)导致事件发生;

  • controller.snapshot 已经是新的状态,但是主机还没有重新渲染。阅读 它的渲染DOM, await el.updateComplete 第一。
  • 一、 trigger 从侦听器中调用是排队的:当当前事件完成时,它被处理; 所以 controller.snapshot 之后它仍然是当前状态。它自己发出的事件 跟着,按顺序。
  • 抛出的侦听器被报告为未捕获的错误。其他听众仍然收到 事件,actor继续运行。

controller.trigger vs controller.actor.trigger

发送给重新连接主机后
controller.trigger现在的演员,每次打电话都抬头看到达新演员
controller.actor.trigger读取时当前的演员到达停止的参与者:事件被丢弃(开发版本中的警告)

使用 controller.trigger 获取来自UI的事件。的请求的响应 组件,这取决于 reconnect:

  • 'restore':新的参与者继续相同的会话,因此在 重新连接仍然有效。使用 controller.trigger.
  • 'restart':新actor启动新会话,因此响应属于旧会话。读 controller.actor.trigger 之前 await:响应被丢弃,而不是到达 新演员。
// `reconnect: 'restart'`
async #onInput(ev: InputEvent) {
  const query = (ev.target as HTMLInputElement).value;
  this.search.trigger.typed({query});

  // Bound to this session: read before the `await`
  const session = this.search.actor.trigger;
  const items = await fetchSuggestions(query);

  // Dropped if the actor that asked was stopped in the meantime.
  // `this.search.trigger.results({items})` would show results for `query` in the new session.
  session.results({items});
}

将请求移动到机器中(被调用) createAsyncLogic)避免选择:the 请求属于actor,停止actor将取消它 AbortSignal 是流产)。 与 'restore' 新演员再演一遍;与 'restart' 它随着旧的会议而消失了。


src/BlockquoteControllerXstate.ts:

类: BlockquoteControllerXstate

字段
名字隐私类型默认的描述继承自
machineTMachinemachine
optionsControllerActorOptions<TMachine>['options']options“createActor”选项。类型与构造函数参数类似,因此不能重新赋值 当机器需要输入时,没有输入(在重新连接时创建一个新的actor)。
onSnapshot(snapshot: SnapshotFrom<TMachine>) => void | undefinedonSnapshot
onError(error: unknown) => void | undefinedonError是否设置它决定,当主机连接时,如果XState报告错误为未处理: 添加或删除它适用于下一个连接。
reconnect'restart' | 'restore'reconnect当主机再次连接时读取。
actorRefActor<TMachine>
subscriptionSubscription | undefined
currentSnapshotSnapshotFrom<TMachine> | undefined
hostTHost
triggerControllerTrigger<TMachine>从模式生成的类型化事件发送器。“:”触发事件。公司()”, ”引发的反馈。更新‘]({value}) ’,如' send({type,…有效载荷})”。 稳定代理:它总是发送给当前参与者(每次创建一个新参与者) 主机再次连接),因此可以保留或销毁它。 不是“演员。Trigger ',它绑定到一个actor。使用‘ reconnect: ’restart' ',用于事件 这属于一个会话,例如对请求的响应,read ' actor。触发之前 ‘ await ’:如果主机同时断开连接,响应将被丢弃,而不是 接触新演员。使用“restore”,会话将继续,因此使用“trigger”。
canControllerCan<TMachine>每个事件类型键入‘ snapshot.can(…)’,形状类似‘ trigger ’: “可以。公司()”、“可以['反馈。更新‘]({value}) ’。 ‘ false ’表示事件将被忽略或actor已经完成('stopped' ', 'done' ')。 或“错误的”)。在主机连接之前,它应答初始状态。 一个稳定的代理,像“触发器”:它总是检查当前的参与者。
onActor<TMachine>['on']((type: string, handler: (event: EventObject) => void): Subscription => { const entry = {type, handler}; this.#emittedHandlers.add(entry); return { unsubscribe: () => { this.#emittedHandlers.delete(entry); }, }; }) as unknown处理机器发出的事件(' enq。Emit ‘),就像’ actor '。On (type, handler) ‘,带有’*' ' for 每一个事件。不同的演员。当actor停止时,其处理程序将被删除 当主机再次连接时,继续从创建的actor接收事件。
actorActor<TMachine>来自XState的底层actor。每次创建主机时都会创建一个新的actor 再次连接,所以不要跨连接保持对它的引用。
snapshotSnapshotFrom<TMachine>actor状态的最新快照
onNext状态更改的内部订阅者
isActiveboolean‘ true ’,而当前actor可以(或正在)运行。 快照在start()之前和运行时都是“活动的”;后“停止()” 它是“停止”(当它完成或失败时是“完成”/“错误”)。
方法
名字隐私描述参数返回继承自
send向actor服务发送事件ev: EventFrom<typeof this.machine>void
unsubscribevoid
startService启动演员。如果前一个参与者不再活动,则创建一个新参与者 (例如,当主机断开连接时停止),因为停止的actor无法重新启动。void
stopServicevoid
hostConnectedvoid
hostDisconnectedvoid
私有API
字段
名字隐私类型默认的描述继承自
#dispatchInit私人DispatchEmittedOptions | undefined‘ dispatchemited ’解析为‘ EventInit ’,或‘ undefined ’当它关闭时。
#emittedHandlers私人new Set<{ type: string; handler: (event: EventObject) => void; }>()
#emittedSubscription私人Subscription | undefined
#persisted私人”{
machine: TMachine;
snapshot: ActorOptions<TMachine>['snapshot'];

} |未定义 | | Persisted on disconnect with \与它所属的机器重新连接:‘restore’ '。| | | #onEmitted | private | | |将当前actor发出的事件传递给DOM和‘ on ’处理程序。| |

方法
名字隐私描述参数返回继承自
#createActor私人snapshot: ActorOptions<TMachine>['snapshot']Actor<TMachine>
#takePersisted私人在重新连接时要恢复的快照:仅使用‘ reconnect: ’restore',并且仅用于 被持久化的机器(‘ Machine ’可能已被重新分配)。ActorOptions<TMachine>['snapshot']

出口

种类名字宣言模块包
jsBlockquoteControllerXstateBlockquoteControllerXstatesrc / BlockquoteControllerXstate.ts

src/index.ts:

出口

种类名字宣言模块包
jsBlockquoteControllerXstateBlockquoteControllerXstate。/ BlockquoteControllerXstate.js
jsBlockquoteControllerXstateOptionsBlockquoteControllerXstateOptions。/ BlockquoteControllerXstate.js
jsControllerCanControllerCan。/ BlockquoteControllerXstate.js
jsControllerTriggerControllerTrigger。/ BlockquoteControllerXstate.js
jsDispatchEmittedOptionsDispatchEmittedOptions。/ BlockquoteControllerXstate.js
jsEmittedEventMapEmittedEventMap。/ BlockquoteControllerXstate.js
jsUseMachineOptionsUseMachineOptions。/ BlockquoteControllerXstate.js

版本与发布记录

@blockquote-web-components/blockquote-controller-xstate 共发布 70 个版本,首次发布于 2023/11/26;过去 12 个月发布了 21 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.10.0latest2026/10/312 天前
2.9.02026/10/312 天前
2.8.12026/10/313 天前
2.8.02026/10/1024 天前
2.7.02026/09/261 个月前
2.6.02026/09/251 个月前

关于 @blockquote-web-components/blockquote-controller-xstate 的常见问题

@blockquote-web-components/blockquote-controller-xstate 是什么?

用于构建可组合和可重用Web组件的现代体系结构

如何安装 @blockquote-web-components/blockquote-controller-xstate?

在项目目录运行 npm install @blockquote-web-components/blockquote-controller-xstate,或使用 pnpm add / yarn add / bun add。

@blockquote-web-components/blockquote-controller-xstate 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@blockquote-web-components/blockquote-controller-xstate 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@blockquote-web-components/blockquote-controller-xstate 可以商用吗?

@blockquote-web-components/blockquote-controller-xstate 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@blockquote-web-components/blockquote-controller-xstate 还在维护吗?

最新版本 2.10.0 发布于 2026/10/31(2 天前);至今共发布 70 个版本,其中过去 12 个月发布了 21 个;首次发布于 2023/11/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。