npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@dao3fun/fsm

v0.0.5

Published

多实体共享配置的轻量模型动画有限状态机

Readme

@dao3fun/fsm — 多实体共享配置的模型动画轻量有限状态机

本 FSM 可视化编辑器: https://box3lab.com/tools/fsm-editor/ 使用说明在最底下~

用一张“简单、类型安全”的表,统一描述状态变化;在生命周期里自动注入安全的动画工具,降低出错概率,让多人协作有清晰边界。

  • 避免状态分散:逻辑散落在多个脚本和 if-else。
  • 事件统一:同语义多名字,重构成本高。
  • 动画脆弱:初始化时序、片段缺失易崩。
  • 多实体复用难:复制-粘贴导致分叉和倾斜。

为什么需要状态机(Before → After)

在实际项目中,我们常见到以下现象:

  • 同一个实体的行为被写在多个脚本里,靠事件回调“互相调用”,流程不透明。
  • 事件命名不统一,startFetch/tryLoad/doLoad 表达同一语义,换人维护代价大。
  • 动画播放分散在各处,稍有顺序/资源问题就容易崩或卡住。
  • 复制一份逻辑给另一个实体,随着时间推移产生分叉,回归困难。

your-fsm 的目标是把“状态变化”收敛到一张表:

  • MachineConfig 明确“当前状态对哪些事件如何响应”,让迁移路径一目了然;
  • 用生命周期钩子 onEnter/onExit/onUpdate 集中管理动画与持续逻辑;
  • 同一份配置可复用到 N 个实体,既统一又易于替换;
  • 订阅与全局回调 subscribe/onTransition 方便做日志与埋点。

对比示例

  • ❌ Before(分散的 if-else + 回调 + 重试/时序/命名不一致):
// 逻辑散落在不同文件/模块中(简化在一处展示)
let isBusy = false;
let tries = 0;
const MAX_TRY = 3;

// 各处都在直接操作动画,名称也不统一
function playSpin() {
  motion.play?.('旋转');
}
function stopSpin() {
  motion.stop?.('旋转');
}
function playStart() {
  motion.play?.('启动');
}
function playOk() {
  motion.play?.('成功');
}
function playFail() {
  motion.play?.('失败');
}

// 不同事件命名表达同一语义:startFetch / tryLoad / doLoad
function startFetch() {
  if (isBusy || entity.status === 'loading') return;
  isBusy = true;
  entity.status = 'loading';

  // 有时先播放“启动”,有时直接“旋转”,时序不一致
  if (Math.random() > 0.5) playStart();
  playSpin();

  // 多处复制的重试逻辑
  fetchData()
    .then((res) => {
      if (!res.ok) throw new Error('bad');
      stopSpin();
      playOk();
      isBusy = false;
      entity.status = 'success';
      // 另一个监听器里也会把 status 改回 idle,产生竞争条件
    })
    .catch((err) => {
      console.warn('fetch error', err);
      stopSpin();
      playFail();
      tries++;
      if (tries < MAX_TRY) {
        // 分散的退避策略(可能还有 setTimeout 嵌套)
        setTimeout(
          () => {
            // 命名不一致:这里叫 tryLoad
            tryLoad();
          },
          500 + tries * 200
        );
      } else {
        isBusy = false;
        entity.status = 'failure';
      }
    });
}

function tryLoad() {
  // 另一路也会播动画,造成重复/中断
  playSpin();
  doLoad()
    .then(() => {
      stopSpin();
      playOk();
      isBusy = false;
      entity.status = 'success';
    })
    .catch(() => {
      stopSpin();
      playFail();
      // 又一层 setTimeout 再试,逻辑分叉增多
      setTimeout(() => startFetch(), 1000);
    });
}

// UI/输入层又来一套 if-else,重复判断与动画
button.on('click', () => {
  if (entity.status === 'idle' || !isBusy) {
    startFetch();
  } else if (entity.status === 'failure') {
    playStart();
    startFetch();
  } else {
    // 正在 loading 时,某些情况下也允许再次触发,导致并发
    if (Math.random() < 0.2) startFetch();
  }
});

// 另一个系统监听网络变化,直接改状态,未与上面流程对齐
network.on('online', () => {
  if (entity.status === 'failure') {
    entity.status = 'idle';
    // 没停动画,可能造成“失败动画”与“启动动画”交错
    playStart();
  }
});
  • ✅ After(统一事件命名 + 配置集中 + 动画在生命周期 + 重试与外部输入事件都通过 send):
type S = 'idle' | 'loading' | 'success' | 'failure';
type E = 'FETCH' | 'RESOLVE' | 'REJECT' | 'RESET';

// 在实体上记录重试次数,集中处理退避
declare const entity: GameEntity & { tries?: number };
const MAX_TRY = 3;

const config: MachineConfig<S, E> = {
  initial: 'idle',
  states: {
    // 空闲:统一从此处发起 FETCH
    idle: {
      on: { FETCH: 'loading' },
    },

    // 加载:只负责“转圈圈 + 等待结果事件”,不自行调接口
    loading: {
      on: { RESOLVE: 'success', REJECT: 'failure' },
      onEnter: (_, motion) =>
        motion.loadByName([{ name: '旋转', iterations: Infinity }]).play(),
      onExit: (_, motion) => motion.pause?.(),
    },

    // 成功:进入即播成功动画
    success: { onEnter: (_, motion) => motion.loadByName('成功').play() },

    // 失败:集中处理“失败动画 + 退避重试 or 归零等待外部 RESET/ONLINE”
    failure: {
      on: {
        // 允许用户/系统重置到 idle(对应 Before 里 network online 直接改状态)
        RESET: 'idle',
        // 也允许直接再次尝试(按钮点一次就再试一次)
        FETCH: {
          target: 'loading',
          guard: (e) => (e.tries ?? 0) < MAX_TRY,
          action: (e) => {
            e.tries = (e.tries ?? 0) + 1; // 记录重试次数
          },
        },
      },
      onEnter: (e, motion) => {
        motion.loadByName('失败').play();
        // 集中退避策略:如果还有额度,延迟自动再发起 FETCH(替代 Before 里分散的 setTimeout)
        if ((e.tries ?? 0) < MAX_TRY) {
          const backoff = 500 + (e.tries ?? 0) * 200;
          setTimeout(() => fsm.send(e, 'FETCH'), backoff);
        }
      },
    },
  },
};

// 创建状态机实例
const fsm = new StateMachine<S, E>(config);

// 注册实体
const entity = world.querySelector('#海盗船-1');
if (entity) {
  // 注册实体
  fsm.register(entity);

  // 统一的 UI 入口:按钮点击仅发送 FETCH,不直接碰动画/状态
  button.on('click', () => fsm.send(entity, 'FETCH'));

  // 统一的系统入口:网络恢复只发送 RESET,不直接改状态/动画
  network.on('online', () => fsm.send(entity, 'RESET'));
}

收益:

  • 迁移规则集中、可视;动画在对应状态的生命周期中,减少时序错误;
  • 更易复用,多实体共享一份配置;
  • 可观测、可埋点,问题定位快;
  • 类型安全让错误更早暴露。

安装(Install)

npm i @dao3fun/fsm

导出:StateMachine、类型(MachineConfig/StateConfig/...)。

上手(Getting Started)

import { StateMachine, type MachineConfig } from '@dao3fun/fsm';

// 定义状态和事件
type S = 'idle' | 'loading' | 'success' | 'failure';
type E = 'FETCH' | 'RESOLVE' | 'REJECT';

// 定义状态机配置
const config: MachineConfig<S, E> = {
  // 初始状态
  initial: 'idle',
  // 状态配置
  states: {
    // 空闲状态
    idle: {
      // 事件迁移表
      // - FETCH -> loading
      on: { FETCH: 'loading' },
      // 进入状态时触发
      onEnter: (_, motion) => {
        // 与实际项目中保持一致:按名称加载动画并播放
        motion.loadByName('启动').play();
      },
      onExit: () => {
        // 可选:做收尾或日志
      },
      onUpdate: () => {
        // 可选:帧更新逻辑
      },
    },
    // 加载状态
    loading: {
      // 事件迁移表:
      // - RESOLVE -> success
      // - REJECT -> failure
      on: { RESOLVE: 'success', REJECT: 'failure' },
      // 进入状态时触发
      onEnter: (_, motion) => {
        // 加载并播放动画,无限循环
        motion.loadByName([{ name: '旋转', iterations: Infinity }]).play();
      },
    },
    // 成功状态
    success: {},
    // 失败状态
    failure: {},
  },
};

// 创建状态机实例
const fsm = new StateMachine<S, E>(config, {
  // 状态迁移时触发
  onTransition: (entity, next, prev, event) => {
    console.log('[onTransition]', { prev, next, event });
  },
});

// 注册实体
const entity = world.querySelector('#海盗船-1');
if (entity) {
  // 初始状态为 idle
  fsm.register(entity);

  // 3s后发送 FETCH 事件
  setTimeout(() => fsm.send(entity, 'FETCH'), 3000);
}

执行以上代码后,实体将3s后从 idle 状态迁移至 loading,播放旋转动画。

帧驱动

setInterval(() => fsm.update(entity, 0.016), 16); // dt 按秒

执行以上代码后,实体将每帧更新一次,onUpdate 钩子会被调用。

配置入门(Step by Step)

用一个最小可跑的例子,逐步加到“动画 + 守卫/动作 + 帧驱动 + 订阅”。

  1. 定义状态与事件(S/E)
type S = 'idle' | 'loading' | 'success' | 'failure';
type E = 'FETCH' | 'RESOLVE' | 'REJECT';
  1. 写出最小配置(只有 initial 与一个事件迁移)
const config: MachineConfig<S, E> = {
  initial: 'idle',
  states: {
    idle: { on: { FETCH: 'loading' } },
    loading: {},
    success: {},
    failure: {},
  },
};
  1. 给进入状态加动画(onEnter 自动注入 motion)
const config: MachineConfig<S, E> = {
  initial: 'idle',
  states: {
    idle: {
      on: { FETCH: 'loading' },
      onEnter: (_, motion) => motion.loadByName('启动').play(),
    },
    loading: {
      on: { RESOLVE: 'success', REJECT: 'failure' },
      onEnter: (_, motion) =>
        motion.loadByName([{ name: '旋转', iterations: Infinity }]).play(),
    },
    success: {},
    failure: {},
  },
};
  1. 给事件加“守卫/动作”(需要时再用完整写法)
idle: {
  on: {
    FETCH: {
      target: 'loading',
      guard: (entity) => entity.canFetch === true, // 不满足则不跳转
      action: async (entity) => entity.log?.('enter loading'), // 切换前执行
    },
  },
},
  1. 驱动与订阅(让它真实跑起来)
const fsm = new StateMachine<S, E>(config, {
  onTransition: (entity, next, prev, event) => {
    console.log('[onTransition]', { prev, next, event });
  },
});

const entity = world.querySelector('#海盗船-1') as GameEntity;
fsm.register(entity);
fsm.send(entity, 'FETCH'); // 触发一次迁移

// 帧驱动(dt=秒)
setInterval(() => fsm.update(entity, 0.016), 16);
  1. 扩展 onUpdate(做计时/渐变/轮询)
loading: {
  on: { RESOLVE: 'success', REJECT: 'failure' },
  onUpdate: (entity, motion, current, dt) => {
    entity.timer = (entity.timer ?? 0) + dt;
    if (entity.timer > 2) {
      // 两秒后自动成功
      fsm.send(entity, 'RESOLVE');
      entity.timer = 0;
    }
  },
},

常见问题(Troubleshooting)

  • 动画不播放?
    • 确认 onEnter 里使用的是 motion.loadByName(...).play(),名称与资源一致;
    • 若在首帧触发,关注引擎侧“资源是否已就绪/可见”。
  • 事件发送无效?
    • 当前状态下是否存在该事件的迁移;
    • guard 是否返回了 false
  • onUpdate 未执行?
    • 是否调用了 update(entity, dt)dt 单位为秒。

核心概念(State / Event / Transition 等)

概念清晰,配起来就不抽象。

  • State(状态)

    • 描述实体在某一时刻所处的“行为阶段”,如 idle(空闲)、loading(加载)、successfailure
    • MachineConfig.states 中逐个定义,每个状态可配置事件表 on 和生命周期钩子 onEnter/onExit/onUpdate
    • 命名建议:短小、动名词或形容状态的形容词,统一小写,例如 idlerunningdead
  • Event(事件)

    • 触发状态迁移的“信号”。你通过 fsm.send(entity, 'FETCH') 发送事件,驱动状态图流转。
    • 命名建议:统一风格,常用全大写动词/动宾,如 FETCHRESOLVEREJECTDIERESPAWN
    • 一个事件只有在“当前状态的 on 中有定义”时才生效;否则被忽略(或由 guard 返回 false)。
  • Transition(迁移)

    • 从“当前状态”响应某个事件跳到“目标状态”。
    • 配置写在 on 表里:
      • 简写:on: { EVENT: 'target' }
      • 完整:on: { EVENT: { target, guard?, action? } }
  • Guard(守卫)

    • 类型:(entity, event) => boolean
    • 用来判定此次事件是否允许跳转,比如“冷却未结束”“血量不足”。返回 false 则不发生迁移。
    • 建议把“条件判断”集中到 guard,避免在业务处处 if-else。
  • Action(迁移动作)

    • 在“状态切换之前”执行(发生在 onExit 之前),可异步;内部已 try/catch,失败不会阻断后续流程。
    • 常见用途:上报埋点、预取资源、写入日志等“与切换紧密相关但非动画控制”的动作。
  • onEnter / onExit(生命周期)

    • onEnter(entity, motion, prev, next): 进入新状态时调用,常用于加载并播放动画、复位变量;
    • onExit(entity, motion, prev, next): 离开旧状态时调用,常用于停止动画、释放资源。
    • 动画控制通过 motion 提供的 API 进行,与你在示例中的 motion.loadByName(...).play() 一致。
  • onUpdate(帧更新)

    • onUpdate(entity, motion, current, dt)fsm.update(entity, dt) 驱动,dt 单位为“秒”。
    • 适合做:时间累计、缓动/渐变、轮询检查、自动超时切换等。
  • Entity(实体)

    • 运行中被状态机管理的对象(如游戏中的一个 NPC/模型)。
    • 同一个 StateMachine 可以注册多个实体,每个实体维护自己的当前状态,但共用同一份配置。
  • Motion(动画控制器)

    • 在生命周期钩子里自动注入,用于安全地播放/暂停动画片段:
      • 单个名称:motion.loadByName('启动').play()
      • 批量配置:motion.loadByName([{ name: '旋转', iterations: Infinity }]).play()
    • 建议把“进入某状态该放哪段动画”放在对应状态的 onEnter,离开时在 onExit 停止或切换。
  • Subscription / onTransition(订阅/全局回调)

    • fsm.subscribe((entity, next, prev) => { ... }):用于调试输出或统一埋点;返回 off 函数用于取消订阅。
    • 构造器里的 onTransition 也会在迁移后触发,位置在 listeners 通知之后。
  • 时序(再强调一次)

    1. action(若有)
    2. onExit(旧状态)
    3. 切换当前状态
    4. onEnter(新状态)
    5. 通知订阅者(subscribe)
    6. 触发 options.onTransition(若提供)

命名与建模建议

  • 状态是“阶段”,事件是“触发器”。避免状态名中出现“事件语义”,也避免事件名中出现“目标状态”。
  • 保持 S/E 为“字符串字面量联合”,让编辑器在配置里获得最强的类型提示与校验。
  • 尽量用“简写迁移”,需要条件或副作用时再切换到“完整写法”。

反例(Anti-pattern)

  • 在业务处四处 if (current === 'x') { ... } 决策,而不是把跳转规则放进 on
  • onEnter 里做复杂的条件判断,本该是 guard 的逻辑;
  • onUpdate 里频繁地直接操纵外部全局状态,导致难以维护和测试。

配置详解(MachineConfig/StateConfig/TransitionConfig)

以下解释基于源码类型定义 server/src/lib/types.ts

/**
 * 迁移动作:在状态切换前执行(发生在 onExit 之前)。
 * - 可异步;失败不会阻止后续 onExit/onEnter(内部已 try/catch)。
 */
export type TransitionAction<E> = (
  entity: GameEntity,
  event: E
) => void | Promise<void>;

/**
 * 迁移守卫:返回 true 才允许从当前状态按该事件跳转。
 * - 典型用途:冷却、资源条件、死亡保护等。
 */
export type TransitionGuard<E> = (entity: GameEntity, event: E) => boolean;

/**
 * 完整迁移配置(当事件需要 guard/action 时使用)。
 * 否则可使用简写:on: { EVENT: 'targetState' }
 */
export interface TransitionConfig<S extends string, E extends string> {
  /** 目标状态 */
  target: S;
  /** 可选守卫:返回 true 才允许跳转 */
  guard?: TransitionGuard<E>;
  /** 可选动作:在状态实际切换前执行 */
  action?: TransitionAction<E>;
}

/**
 * 状态生命周期钩子:onEnter/onExit。
 * - prev/next:用于区分是初次进入(prev===next===initial)还是普通迁移。
 */
export type StateHook<S extends string> = (
  /** 实体 */
  entity: GameEntity,
  /** 动画控制 */
  motion: GameMotionController<GameEntity>,
  /** 上一状态 */
  prev: S,
  /** 新状态 */
  next: S
) => void | Promise<void>;

/**
 * 单个状态的配置。
 */
export interface StateConfig<S extends string, E extends string> {
  /** 事件迁移表 */
  on?: Partial<Record<E, TransitionConfig<S, E> | S>>;
  /** 进入状态时的回调 */
  onEnter?: StateHook<S>;
  /** 离开状态时的回调 */
  onExit?: StateHook<S>;
  /** 每帧更新时的回调 */
  onUpdate?: (
    /** 实体 */
    entity: GameEntity,
    /** 动画控制 */
    motion: GameMotionController<GameEntity>,
    /** 当前状态 */
    current: S,
    /** 时差 */
    deltaTime: number
  ) => void | Promise<void>;
  // 显式终止态(可选)。若不写 on 或 on 为空也会被视为“自然终止态”。
  final?: boolean;
}

/**
 * 整个状态机的配置对象。
 */
export interface MachineConfig<S extends string, E extends string> {
  /** 初始状态 */
  initial: S;
  /** 各状态的事件表与生命周期钩子 */
  states: Record<S, StateConfig<S, E>>;
}

/**
 * 单实例状态机的订阅函数签名(当前仓库仅使用了多实体版本)。
 */
export type TransitionListener<S extends string> = (
  /** 下一状态 */
  next: S,
  /** 上一状态 */
  prev: S
) => void;

/**
 * 多实体状态机的订阅函数签名:实体、下一状态、上一状态。
 */
export type MultiTransitionListener<S extends string> = (
  /** 实体 */
  entity: GameEntity,
  /** 下一状态 */
  next: S,
  /** 上一状态 */
  prev: S
) => void;

/**
 * 额外可选项:全局迁移回调等(可不传)。
 */
export interface MachineOptions<S extends string, E extends string> {
  /** 发生迁移后触发(在 onEnter/onExit 执行完毕、listeners 通知之后) */
  onTransition?: (
    /** 实体 */
    entity: GameEntity,
    /** 下一状态 */
    next: S,
    /** 上一状态 */
    prev: S,
    /** 触发迁移的事件 */
    event: E
  ) => void | Promise<void>;
}
  • initial(必填)

    • 初始状态,必须是 S 联合类型中的一个。
  • states(必填)

    • 每个状态名对应一个 StateConfig;在该状态下定义可响应的事件、生命周期钩子。
  • on(可选)

    • 事件迁移表。两种写法:
      • 简写:on: { EVENT: 'targetState' }
      • 完整:on: { EVENT: { target: 'targetState', guard?, action? } }
    • guard:返回 true 才允许跳转(冷却/条件检查等)。
    • action:在状态切换前执行(发生在 onExit 之前),可异步。
  • onEnter(entity, motion, prev, next)(可选)

    • 进入状态时触发,常用于加载并播放动画、初始化变量、打印日志等。
    • 示例:motion.loadByName('启动').play() 或批量 { name, iterations }
  • onExit(entity, motion, prev, next)(可选)

    • 离开状态时触发,常用于停止动画、资源清理等。
  • onUpdate(entity, motion, current, deltaTime)(可选)

    • 每帧由 update(entity, dt) 驱动触发,dt 单位为秒。
    • 适合做计时、渐变、轮询检查等连续性逻辑。
  • 终止态(final / 自然终止)

    • final: true 或该状态 on 未定义/为空时,视为终止态;
    • 终止态将忽略 send 事件且不会再执行 onUpdate
    • 建议在该状态的 onEnter 中做资源回收(停动画、清理计时器、取消订阅等)。

示例:

type S = 'idle' | 'done' | 'error';
type E = 'START' | 'FAIL';

const cfg: MachineConfig<S, E> = {
  initial: 'idle',
  states: {
    idle: { on: { START: 'done' } },
    done: { final: true, onEnter: (_, m) => m.loadByName('完成').play() },
    error: {}, // 自然终止态(无 on)
  },
};

生命周期执行顺序(一次合法迁移)

  1. action(若存在,发生在切换之前)
  2. onExit(旧状态)
  3. 切换当前状态
  4. onEnter(新状态)
  5. 通知订阅者(subscribe
  6. 触发构造参数中的 onTransition(若提供)

FSM 可视化编辑器 - 使用说明

本工具用于可视化编辑有限状态机(FSM),支持直接导入/导出与代码一致的 MachineConfig 配置,所见即所得地维护状态、事件、迁移与生命周期函数预览。

快速上手

  1. 在画布空白处右键 → 新增状态。
  2. 右键某状态 → 新增迁移 → 选择源/事件/目标。
    • 如需新事件,事件下拉选择“自定义事件…”,在出现的输入框填写事件名。
  3. 右键状态底部可直接编辑 onEnter/onExit/onUpdate,自动保存。
  4. 点击“复制 TS 代码”按钮,可生成 TypeScript 配置预览,或下载 TS 文件。

顶部操作区

  • 按钮
    • 导出 JSON:导出为 MachineConfig 结构的 fsm-config.json(含 initialstates)。
    • 导入 JSON:从文件选择器导入 JSON。支持两种格式:
      • MachineConfig JSON(推荐)。
      • 兼容旧版“内部模型 JSON”(含 states/events/transitions/lifecycle)。
    • 粘贴 Config:弹出对话框,支持粘贴整段 TypeScript/JavaScript 代码或对象字面量:
      • 可粘贴完整的 const config: MachineConfig<S, E> = { ... } 代码片段(包含注释与类型也可)。
      • 也可直接粘贴对象字面量或 JSON(例如 { initial: 'idle', states: {...} })。
      • 导入后自动重新生成图与列表,并自动保存。
    • 下载 TS 文件:生成 TypeScript 配置预览,可下载保存。
    • 清空:清空当前模型(localStorage 也会被清空)。

画布与右键菜单

  • 右键画布空白:仅包含“新增状态”。
  • 右键状态节点:
    • 重命名新增迁移设为初始删除(已移除“新增事件”,在“新增迁移”里可选“自定义事件…”)。
    • 菜单底部可直接编辑该状态的生命周期:onEnter/onExit/onUpdate
      • 支持自动保存:失焦、关闭菜单或 Cmd/Ctrl+Enter 即保存。
      • 保存后会自动更新 TS 预览与本地存储。
  • 右键事件箭头(边):
    • 重命名事件:仅修改该条迁移的事件名。
    • 反转方向:将 from/to 对调。
    • 删除迁移:仅删除该条边。

生命周期函数在编辑器内以字符串形式保存与展示,方便预览与导出 MachineConfig;真正的代码落地建议在你的工程代码中实现与维护。

列表区

  • 左侧“状态列表”:
    • 展示所有状态,可点击高亮并在图中定位。
    • 支持“重命名”“删除”。
  • 中部“事件列表”(迁移列表):
    • 展示每一条迁移(形如 EVENT from -> to)。
    • 支持“选中”“重命名(该条迁移的事件名)”“删除(该条迁移)”。

导入/导出的格式

  • MachineConfig JSON(推荐)
{
  "initial": "idle",
  "states": {
    "idle": {
      "on": { "FETCH": "loading" },
      "onEnter": "(_, motion) => { /* 建议在工程代码中维护 */ }",
      "onExit": "() => {}",
      "onUpdate": "() => {}"
    },
    "loading": {
      "on": { "RESOLVE": "success", "REJECT": "failure" },
      "onEnter": "(_, motion) => {}"
    },
    "success": {},
    "failure": {}
  }
}
  • 粘贴 Config(对象字面量/整段 TS)
    • 可粘贴:
      • 完整 TS 片段(例如含 type S/Econst config: MachineConfig<S,E> = { ... }、注释、函数)。
      • 只包含 { initial, states } 的对象字面量,允许函数值(会以字符串保存)。

自动保存

  • 每次编辑(增删改状态/事件/迁移,或修改生命周期文本)后,都会自动保存到浏览器 localStorage
  • 生命周期编辑支持失焦/关闭菜单自动保存,Cmd/Ctrl+Enter 快捷保存。
  • 下次打开页面会自动恢复,无需手动导入。

快捷键

  • 画布/列表通用
    • Enter:在对话框中确认
    • Esc:关闭当前对话框
  • 生命周期编辑
    • Cmd/Ctrl + Enter:保存当前编辑内容(同时系统也会在失焦或关闭菜单时自动保存)

新增迁移与自定义事件

  • 在“新增迁移”弹窗中:
    • 选择 源状态目标状态
    • 事件下拉可选择已有事件,或选择“自定义事件…”,此时会出现“新事件名”输入框。
    • 未填写自定义事件名前,“确定”按钮会禁用,避免空值提交。
    • 弹窗内提供即时的 from/event/to 预览(若你在页面中开启了预览区域)。

行为与校验

  • 去重与清理:
    • 自动去重 (from + event),若重复则更新其目标状态 to
    • 自动清理未使用的事件(当没有任何迁移引用某事件时,将其从事件集合中移除)。
  • 布局与位置:
    • 首次使用自动布局,之后记忆节点位置,增删元素时尽量保持手动布局不被打乱。

界面与交互优化

  • 对话框与右键菜单内的输入控件已统一样式(圆角、阴影、聚焦高亮)。
  • 生命周期编辑区使用等宽字体与浅色背景,阅读与编写更舒适。

小技巧

  • 快速新增状态:在画布空白处右键 → “新增状态”。
  • 定位节点/事件:在左/中列表点击项,画布会自动选中并定位。
  • 避免命名冲突:新增状态或事件时会校验重复,避免覆盖既有配置。

安全与注意事项

  • “粘贴 Config”支持对粘贴的对象字面量进行求值(以支持函数值)。为安全起见:
    • 仅在本地浏览器内求值,不会上传你的内容。
    • 请仅粘贴来自可信来源的片段,避免执行潜在恶意代码。
  • 生命周期函数建议在工程仓库的 TS/TSX 源码中维护,编辑器内的预览仅为参考。

常见问题(FAQ)

  • Q:粘贴整段 TS 报“解析失败”?
    • A:请确认包含 const config = { ... } 字面量;若有语法错误或花括号不匹配,工具将无法识别。你也可以尝试只粘贴 { initial, states } 对象部分。
  • Q:导入 JSON 后生命周期函数不生效?
    • A:JSON 不能保存函数值。若需要函数,请使用“粘贴 Config”粘贴对象字面量(允许函数),或在导出后手动回填到你的工程代码中。