尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Storybook Addon API 事件通道解析:从 addons.getChannel() 到管理器与预览的双向通信

Storybook Addon API 事件通道解析:从 addons.getChannel() 到管理器与预览的双向通信 Storybook Addon API 事件通道解析从 addons.getChannel() 到管理器与预览的双向通信【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook在 Storybook 中addon 要想把自定义能力嵌入 UI 组件开发流程前提是能拿到一条通信总线它连接着负责界面的manager管理器与负责渲染故事的preview预览两侧运行时。官方 Addon API 提供的addons.getChannel()正是这条总线的入口本文基于仓库内 Addon API 官方文档 docs/addons/addons-api.mdx 与其示例代码结合核心源码讲解其用途、事件模型与实际用法让读者可以据此编写出能与 Storybook 双向通信、可刷新界面或同步状态的 addon。一、背景Addon API 的两大运行时与通道的作用Storybook 的扩展能力通过两个职责不同的包暴露给开发者这也是理解getChannel()的前提storybook/manager-api用于与 Storybook 管理器界面交互或访问 Storybook APIstorybook/preview-api用于控制和配置 addon 在预览侧的行为。manager 与 preview 是相互独立的运行时它们之间需要一条实时通信通道来同步事件。addons.getChannel()正是获取这条通道实例的标准方式。在 Addon API 文档 docs/addons/addons-api.mdx 中官方对它的描述是获取一个通道实例用于与 manager 和 preview 通信。你既可以在 addon 的注册代码中、也可以在 addon 的包装组件在 story 内部使用时里调用它。也就是说无论你的 addon 逻辑跑在哪一侧工具栏组件、面板组件、还是作用于故事渲染的装饰器/包装组件都可以通过addons.getChannel()拿到同一条通道并向另一端广播或监听事件。通道的 NodeJS EventEmitter 兼容 API官方文档特别强调该通道具备与NodeJS EventEmitter 兼容的 API因此你可以通过.emit(eventName, ...args)发出事件通过.on(eventName, listener)/.once()监听事件通过.off()移除监听。由于它是标准的发布/订阅pub/sub事件模型addon 之间、addon 与 Storybook 核心之间的解耦非常干净发布者无需知道谁在监听只需要约定事件名与消息结构。二、示例解读用 FORCE_RE_RENDER 主动刷新预览 UI仓库文档提供的getChannel核心示例位于 storybook-addons-api-getchannel.md它是一个典型的工具栏 addon切换一个自定义 global 值然后强制触发 UI 刷新。完整代码如下import React, { useCallback } from react; import { OutlineIcon } from storybook/icons; import { useGlobals } from storybook/manager-api; import { addons } from storybook/preview-api; import { ToggleButton } from storybook/internal/components; import { FORCE_RE_RENDER } from storybook/internal/core-events; const ExampleToolbar () { const [globals, updateGlobals] useGlobals(); const isActive globals[my-param-key] || false; // Function that will update the global value and trigger a UI refresh. const refreshAndUpdateGlobal () { updateGlobals({ [my-param-key]: !isActive, }); // Invokes Storybooks addon API method (with the FORCE_RE_RENDER) event to trigger a UI refresh addons.getChannel().emit(FORCE_RE_RENDER); }; const toggleToolbarAddon useCallback(() refreshAndUpdateGlobal(), [isActive]); return ( ToggleButton keyExample paddingsmall variantghost pressed{isActive} onClick{toggleToolbarAddon} ariaLabelAddon feature tooltipToggle addon feature OutlineIcon / /ToggleButton ); };逐步拆解这段代码做了什么事读取与管理全局状态通过useGlobals()拿到当前 globals 与updateGlobals更新函数isActive读取键为my-param-key的自定义 global 值。这样 addon 的状态能够跟随 Storybook 的 URL 与故事切换而持久化。更新 global 值点击时先用updateGlobals({ my-param-key: !isActive })切换该值。globals 变化本身会触发一次渲染但对于某些需要强制整帧重绘的场景仍不够。发出事件强制刷新最关键的一行是addons.getChannel().emit(FORCE_RE_RENDER)。它通过通道发出 Storybook 内置的FORCE_RE_RENDER事件告诉预览侧重新渲染当前故事。在仓库事件常量表 code/core/src/core-events/index.ts 中可以看到它的真实定义FORCE_RE_RENDER forceReRender并作为coreEvents的命名导出供各运行时共享。这里体现了emit()的最典型用法addon 无法直接调用预览侧的函数但可以通过通道广播一个双方约定好的事件名来请求某种行为。除FORCE_RE_RENDER外Storybook 还内置了大量类似事件如STORY_CHANGED、SET_CONFIG、SET_CURRENT_STORY等它们都集中在同一个核心事件文件中定义addon 开发者应优先复用这些既有事件避免为同一语义自造事件名。与 useChannel 的对比何时用 getChannel何时用 Hook官方在 addons-api.mdx 中同时提供了useChannel允许设置对事件的订阅并获得可向通道发出自定义事件的 emitter。其配套示例见 storybook-addons-api-usechannel.mdimport React from react; import { useChannel } from storybook/manager-api; import { AddonPanel, Button } from storybook/internal/components; import { STORY_CHANGED } from storybook/internal/core-events; export const Panel () { // Creates a Storybook API channel and subscribes to the STORY_CHANGED event const emit useChannel({ STORY_CHANGED: (...args) console.log(...args), }); return ( AddonPanel keycustom-panel activetrue Button onClick{() emit(my-event-type, { sampleData: example })} Emit a Storybook API event with custom data /Button /AddonPanel ); };二者的分工可以这样理解场景推荐 API理由在React 组件工具栏、面板等内部需要订阅事件并在组件卸载时自动清理useChannelHook 会在卸载时自动取消订阅无需手动管理生命周期同时返回emit供组件发送事件在普通函数 / 非组件环境注册回调、runner、装饰器逻辑等需要按需取通道收发事件addons.getChannel()返回的是底层Channel实例是纯粹的 EventEmitter不依赖 React 渲染上下文需要同步读取历史事件快照如初始化状态时读取上一个事件携带的数据addons.getChannel()Channel实例额外提供last(eventName)能力见下文官方 addon 实例三、源码级原理getChannel 的实现与单例存储addons对象本身是单例的 AddonStore。manager 侧实现在 code/core/src/manager-api/lib/addons.ts其中与通道相关的核心逻辑如下通过模块级KEY __STORYBOOK_ADDONS_MANAGER挂载到globalThis上保证整个 manager 运行时只存在一份addons见getAddonsStore()getChannel()的取值顺序是先返回已缓存的this.channel未缓存时尝试读取共享通道插槽中已安装的通道若该运行时还没有安装任何通道则返回一个一次性 mockChannel避免调用方因拿不到通道而崩溃同时不会错误地缓存它或提前 resolve 就绪 Promise从而防止一次过早读取污染整个运行时见 addons.ts 第 52-69 行源码注释setChannel(channel)负责安装真实通道同时把它写入共享通道插槽并 resolveready()返回的 Promiseready()可用于在通道尚未就绪时异步等待hasChannel()用于判断通道是否已安装。预览侧则维护了独立但同构的 AddonStore见 code/core/src/preview-api/modules/addons/main.ts其挂载键为__STORYBOOK_ADDONS_PREVIEW。也就是说manager 与 preview 各自暴露同名addons但逻辑对称两侧都通过getChannel()指向各自运行时内那条已经被安装的共享通道。通道插槽跨运行时共享的规范来源通道真正被安装到哪里由通道插槽模块决定见 code/core/src/channels/channel-slot.ts每个运行时manager、preview、dev server都会在入口处安装一个通道实例setChannel除了更新模块内的变量外还会把通道镜像到全局槽globalThis.__STORYBOOK_ADDONS_CHANNEL__使旧版代码与 builder 前缀脚本能够读取到同一份实例getChannel()读取时全局槽优先于模块缓存——这样即使同一模块因打包被复制多份例如 dev-server preset 代码与.storybook服务注册各自加载了不同 bundle仍能观测到由另一份拷贝通过setChannel安装的活的通道当通道尚未安装时模块返回nullrequireChannel()则要求通道必须已存在否则抛出异常提示应在运行时入口通过setChannel()或addons.setChannel()安装针对非浏览器环境Node 服务端、无 DOM 的 Vitest模块在导入时自动ensureChannel()引导安装一个 noop进程内通道而浏览器预览侧不会在导入时自行引导因为真实通道由 builder 在预览配置加载之前安装保证 addon 拿到的一定是带有 websocket 传输的真实通道。这一设计解释了官方文档那句你既可以在 addon 注册代码中、也可以在包装组件中使用它只要运行时入口已经安装了通道这是 Storybook 各运行时自身的启动契约任何代码位置调用getChannel()拿到的都是同一条有实际传输能力的通道。四、真实世界佐证官方 addons 如何消费通道仓库中多个官方 addon 的源码可以直接验证上述 API 的实际用法适合作为参考模板。themes addon读取历史事件快照 订阅事件在 code/addons/themes/src/theme-switcher.tsx 中主题切换器组件同时演示了getChannel()与useChannel()const channel addons.getChannel(); const fromLast channel.last(THEMING_EVENTS.REGISTER_THEMES); const initializeThemeState Object.assign({}, DEFAULT_ADDON_STATE, { themesList: fromLast?.[0]?.themes || [], themeDefault: fromLast?.[0]?.defaultTheme || , });组件初始化时先用getChannel()拿到通道实例调用channel.last(THEMING_EVENTS.REGISTER_THEMES)同步读取最近一次该事件的载荷用于初始化主题列表与默认主题——这是纯 EventEmitter 接口之外、Channel类型额外提供的便利能力随后再用useChannel({ [THEMING_EVENTS.REGISTER_THEMES]: (...) {...} })订阅后续的注册事件并更新组件状态。也就是说官方 addon 的实际姿势是用getChannel()读历史、用useChannel()订阅未来。二者针对的都是预览侧或对端运行时通过同一通道发来的事件。a11y addon在 runner 中用 on/emit 打通两侧在 code/addons/a11y/src/a11yRunner.ts 中无障碍检测 runner 以普通模块非 React 组件身份工作在预览侧const channel addons.getChannel(); channel.on(EVENTS.MANUAL, async (storyId, input DEFAULT_PARAMETERS) { // 执行扫描… channel.emit(EVENTS.RESULT, resultJson, storyId); // 出错时 channel.emit(EVENTS.ERROR, error); });它用on()监听来自 manager 面板的EVENTS.MANUAL用户点了运行扫描把扫描结果通过emit(EVENTS.RESULT, ...)/emit(EVENTS.ERROR, ...)广播回 manager 侧。这正是manager 面板 ↔ preview 运行时双向通信的完整闭环也印证了getChannel()并不只属于 React 组件任何模块内拿到通道即可收发。五、动手实践构造你自己的事件协议综合上述文档与源码一个可复制的最小实践路径如下第 1 步定义双方共享的事件常量。仿照 code/core/src/core-events/index.ts 的做法在 addon 内集中定义事件名尽量以字符串字面量如my-addon/request-run保证跨 bundle 一致性并复用 Storybook 内置事件FORCE_RE_RENDER、STORY_CHANGED、SET_CONFIG等代替自行造轮子。第 2 步发送侧调用addons.getChannel().emit(...)。例如在 manager 侧工具栏点击后广播请求事件若发送逻辑在 React 组件内且需要随组件卸载清理监听优先用useChannel否则直接getChannel()。第 3 步接收侧监听。在另一端用channel.on(EVENT_NAME, handler)响应对端如果是装饰器/包装组件等非组件代码对应官方文档所述addon 的 wrapper component场景直接使用addons.getChannel()即可因为此时你处于能够渲染 story 的运行时上下文。第 4 步触发 UI 刷新或同步状态。需要强制重绘时发射FORCE_RE_RENDER需要把对端数据带回界面时用事件载荷携带数据并用useAddonState/updateGlobals落为 UI 状态。一个需要留意的运行时约束非浏览器环境如 Node 服务端、无 DOM 的 Vitest下通道为进程内 noop 实现只有浏览器中 Storybook 才会安装带真实传输websocket的通道见 channel-slot.ts 中的引导逻辑因此依赖跨端实时通信的 addon 应在真实 Storybook 运行环境storybook dev/storybook build后访问页面中验证行为。六、总结addons.getChannel()返回 Storybook 的通信通道实例API 与 NodeJS EventEmitter 兼容是 addon 与 manager/preview 之间收发事件的统一入口可在注册代码与 addon 包装组件中任意使用通道的安装与共享由 channel-slot.ts 负责manager 与 preview 两侧各自拥有单例addonsmanager 侧、preview 侧并在通道未就绪时优雅降级为 mock 通道面向 React 组件的高层封装是useChannel普通函数/非组件场景与需要读取事件历史快照channel.last()的场景直接用getChannel()官方 themes 与 a11y addon 分别提供了读历史 订阅未来与on/emit 双向闭环的完整范例是编写自定义 addon 事件协议时最值得对照的仓库内样本。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表