
Storybook Addon 开发指南在自定义面板中使用 useGlobals 消费全局主题状态【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook导读本文基于 Storybook 官方文档中 addon-consume-globaltype.md 代码示例讲解如何在 Storybook Addon 的 Manager 端面板、工具条通过useGlobals()Hook 读取由工具栏/全局配置设置的globals例如当前选中的主题并据此渲染 UI。读完你将掌握useGlobals的完整用法、返回值语义、底层事件流与源码级工作原理并能将其与updateGlobals组合实现读取—更新—重渲染的闭环 Addon 能力。为什么 Addon 需要消费 globals在 Storybook 中globals代表全局的、与单个 Story 无关的渲染输入。它不像args那样传入 Story 函数而是通常被Decorator对所有 Story 生效或Addon 的 UI 组件所消费。globals 的核心使用场景包括主题切换、语言locale切换、背景色切换等——这些状态一旦改变Story 与相关 UI 会随之重新渲染。官方文档在 toolbars-and-globals.mdx 中明确Globals 在 Storybook 中代表对 Story 渲染的全局输入。由于它们并非 Story 特定不会通过args传入 Story 函数但可通过context.globals访问通常用于作用于所有 Story 的 Decorator 中。当 globals 变化时Story 会重新渲染Decorator 也会以新值重新执行。这里有一个重要区别值得注意消费于渲染链路Decorator / Story 内部时应使用context.globals.theme之类的上下文取值消费于 Manager UIAddon 面板、工具条时由于代码运行在 Manager 端、与 Preview iframe 隔离就必须通过storybook/manager-api提供的useGlobals()Hook 来读取。我们讨论的addon-consume-globaltype.md示例属于后一种它把一个主题全局变量theme作为面板内容展示出来。示例代码全解析把主题全局变量展示在 Addon 面板中先给出addon-consume-globaltype.md中的完整示例代码。它演示了一个名为ThemePanel的面板组件从theme全局变量中取出当前主题名从主题字典里取出完整的主题对象并以 JSON 源码块的形式渲染到面板中当没有选中任何主题时显示占位提示。import React from react; import { useGlobals } from storybook/manager-api; import { AddonPanel, Placeholder, Separator, Source, Spaced, Title, } from storybook/internal/components; import { MyThemes } from ../my-theme-folder/my-theme-file; // Function to obtain the intended theme const getTheme (themeName) { return MyThemes[themeName]; }; const ThemePanel (props) { const [{ theme: themeName }] useGlobals(); const selectedTheme getTheme(themeName); return ( AddonPanel {...props} {selectedTheme ? ( Spaced row{3} outer{1} Title{selectedTheme.name}/Title pThe full theme object/p Source code{JSON.stringify(selectedTheme, null, 2)} languagejs copyable padded showLineNumbers / /Spaced ) : ( PlaceholderNo theme selected/Placeholder )} /AddonPanel ); };下面按代码逻辑分几个关键环节展开。1. 关键导入useGlobals与 Manager UI 组件import { useGlobals } from storybook/manager-api;这是整段代码的灵魂。storybook/manager-api是面向 Addon 开发者的 Manager 端 API 模块useGlobals是其中负责读取与更新全局变量的 React Hook。需要说明的是该示例文件命名约定为your-addon-register-file.js意思是它应放在 Addon 中运行于 Manager 端的注册文件如preset/manager或 UI 入口中而不是 Preview 端。import { AddonPanel, Placeholder, Separator, Source, Spaced, Title, } from storybook/internal/components;storybook/internal/components提供了一套可直接复用的 Manager UI 组件组件作用AddonPanel面板容器{...props}透传 addon API 注入的属性如active、key等Title标题文本渲染Source源码展示块支持language、copyable复制按钮、padded、showLineNumbers等属性Spaced统一间距的布局容器row/outer控制内外部间距Placeholder空状态占位提示Separator分隔线该示例中导入未使用是演示代码中的常见保留写法这种从storybook/internal/components复用官方 UI 组件的做法是保证 Addon 面板视觉风格与 Storybook 一致的关键——AddonPanel、Placeholder在官方各 Addon如 a11y、backgrounds中同样被广泛使用。2. 解构读取全局useGlobals()返回当前生效的 globalsconst [{ theme: themeName }] useGlobals();useGlobals()返回的第一个元素就是当前生效的 globals 对象。代码通过解构{ theme: themeName }取出了theme这个全局变量并把键名重命名为themeName。注意这里的变量是此前在.storybook/preview.*中通过globalTypes与toolbar注解声明、并在工具条下拉菜单中可被用户切换的全局项。例如一个 light/dark 主题的全局配置形如// .storybook/preview.js export const initialGlobals { theme: light, }; export const globalTypes { theme: { name: Theme, description: Global theme for components, toolbar: { icon: paintbrush, items: [ { value: light, title: Light }, { value: dark, title: Dark }, ], }, }, };值得一提的限制是globalTypes与initialGlobals只能定义在项目的.storybook/preview.*中因为它们作用于所有 Story而在 Story 级别可以通过 Story 的globals注解为特定 Story 固定某个全局值并禁用对应工具条菜单。useGlobals读取到的就是用户选择 Story 覆盖叠加后的当前生效值这一点与下文介绍的源码实现完全对应。3. 主题对象映射与兜底渲染const getTheme (themeName) { return MyThemes[themeName]; };MyThemes是来自主题包示意路径../my-theme-folder/my-theme-file的主题名 → 主题对象映射表。getTheme仅做一次查表转换。渲染层随后根据查找结果分支命中主题展示selectedTheme.name标题、说明文字并用Source以格式化 JSONJSON.stringify(selectedTheme, null, 2)输出完整主题对象copyable让用户一键复制未命中例如theme全局尚未被设置、值为undefined渲染PlaceholderNo theme selected/Placeholder空态提示。这一段用最直白的代码说明了 Addon 面板消费 globals 的完整范式读全局 → 映射/计算 → 条件渲染。useGlobals 源码级原理返回值、store 语义与事件通道useGlobals并非魔法。从源码结构看它定义于 code/core/src/manager-api/root.tsxexport function useGlobals(): [ globals: Globals, updateGlobals: (newGlobals: Globals) void, storyGlobals: Globals, userGlobals: Globals, ] { const api useStorybookApi(); return [api.getGlobals(), api.updateGlobals, api.getStoryGlobals(), api.getUserGlobals()]; }可见useGlobals实际返回一个四元组除了示例中只用到的第一项之外还有三个globals第一项当前生效的 globals。按照 code/core/src/manager-api/modules/globals.ts 中SubAPI的注释它是userGlobals 叠加上 storyGlobals之后的结果updateGlobals第二项更新全局变量并触发 UI 同步的函数storyGlobals第三项当前 Story 自身通过 Story/组件globals注解声明的全局值userGlobals第四项用户在工具条等 UI 中选择设置的全局值。在底层模块 modules/globals.ts 中store 维护了三份状态globals、userGlobals、storyGlobals以及项目级定义的globalTypes。API 层直接透出对应 gettergetGlobals() { return store.getState().globals!; }, getUserGlobals() { return store.getState().userGlobals!; }, getStoryGlobals() { return store.getState().storyGlobals!; }, getGlobalTypes() { return store.getState().globalTypes!; }, updateGlobals(newGlobals) { // 仅向本地 ref 发送消息 provider.channel?.emit(UPDATE_GLOBALS, { globals: newGlobals, options: { target: storybook-preview-iframe }, }); },这个updateGlobals的实现还揭示了一条关键链路调用它并不会直接修改 Manager 本地 store而是通过 channel 发出UPDATE_GLOBALS事件并定向投递到preview iframepreview 端应用新全局后再回发GLOBALS_UPDATED事件让 Manager 端更新 store完成一次Manager → Preview → Manager的闭环。同时模块还在初始化阶段监听SET_GLOBALSpreview 初始化时广播初始 globals 与 globalTypes。在两处监听与 store 更新时代码使用dequaldeepEqual做深度比较以避免无意义的 setState。配套测试 code/core/src/manager-api/tests/globals.test.ts 也验证了这一行为it(emits UPDATE_GLOBALS when updateGlobals is called, () { ... (api as SubAPI).updateGlobals({ a: b });因此作为 Addon 作者可以放心依赖useGlobals()返回的当前值因为它在每个渲染周期都与 Manager store 保持同步任何来自 Preview 的全局变化都会触发组件重渲染。文档在 addons-api.mdx 中特别提醒useGlobals对依赖 Globals 的 Addon 极其有用但它会带来较多的重渲染周期建议配合React.memo、useMemo、useCallback优化 Addon 组件。从消费到更新补充 updateGlobals 的完整用法useGlobals的返回四元组中第二项updateGlobals用于在 Manager 端主动修改全局值。同目录下的姊妹示例 addon-consume-and-update-globaltype.md 给出了一个工具条类 Addon 的标准写法——点击按钮切换全局开关并强制刷新 UIimport 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 () { // Updates Storybook global value 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 toggleOutline useCallback(() refreshAndUpdateGlobal(), [isActive]); return ( ToggleButton keyExample paddingsmall variantghost pressed{isActive} onClick{toggleOutline} ariaLabelAddon feature tooltipToggle addon feature OutlineIcon / /ToggleButton ); };把它与消费型示例合在一起可以看到 Manager 端 globals 编程的完整面貌读取const [globals, updateGlobals] useGlobals();globals[my-param-key]取指定键更新updateGlobals({ my-param-key: !isActive })通过底层UPDATE_GLOBALS事件让 Preview 应用新值见 modules/globals.ts 中的updateGlobals刷新调用addons.getChannel().emit(FORCE_RE_RENDER)广播FORCE_RE_RENDER事件强制触发 UI 重渲染防抖优化使用useCallback包裹点击回调依赖项为[isActive]避免不必要的函数重建——这与 addons-api 文档建议的优化方向一致。面板注册与运行前提要让上面的ThemePanel真正出现在 Storybook 界面中还需要把它挂到 addon 面板机制上示例文件只聚焦面板组件本身。常规做法是在 Addon 的 Manager 注册文件中完成addons.register通过manager-api的useAddonState/useChannel或经典 API 注册 Panel例如import { addons, types } from storybook/manager-api; import { ThemePanel } from ./ThemePanel; addons.register(my-theme-addon, () { addons.add(my-theme-addon/panel, { type: types.PANEL, title: Active Theme, render: ({ active, key }) ThemePanel key{key} active{active} /, }); });运行前提有二项目中必须真正定义了对应的全局在本例中即theme在.storybook/preview.*中通过globalTypes/initialGlobals声明否则getTheme查表永远返回undefined面板只会停留在No theme selected的占位态Addon 包需要同时提供Preview 端与 Manager 端的构建入口本段代码运行于 Manager 端 bundle 中因此不能访问 Preview 端的 DOM 上下文。小结与最佳实践把addon-consume-globaltype.md示例及其上下文放到一起可以得到一组面向 Addon 开发者的 globals 消费最佳实践区分消费位置渲染链路内Decorator/Story用context.globalsManager UI面板/工具条内用storybook/manager-api的useGlobals()两者不要混用善用返回值useGlobals()实际返回[globals, updateGlobals, storyGlobals, userGlobals]其中第一项是叠加了 Story 级覆盖的当前生效值是绝大多数场景下的首选读取对象见 root.tsx处理空态全局未设置时应像示例那样通过Placeholder或条件渲染兜底而不是假设值永远存在控制渲染频率useGlobals会在全局变化时触发多次重渲染务必配合useCallback/useMemo/React.memo读写一体需要双向交互时把updateGlobals与FORCE_RE_RENDER事件结合使用即可实现面板内切换主题/语言并让所有 Story 实时响应的完整体验。继续深入阅读示例所在的完整功能文档Toolbars globalsHook API 参考addons-api.mdx#useglobals更新型示例addon-consume-and-update-globaltype.md源码实现useGlobals 定义、globals 状态模块、单元测试【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考