Storybook 插件内消费与更新 Globals:useGlobals、updateGlobals 与 FORCE_RE_RENDER 实战
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
本指南基于 Storybook 官方文档中“在 Addon 内部更新 Globals 并刷新界面”的权威示例(代码片段 docs/_snippets/addon-consume-and-update-globaltype.md,由 docs/essentials/toolbars-and-globals.mdx 在 "Updating globals from within an addon" 一节引入),并结合 Storybook 仓库的 manager-api、core-events 与 preview-api 源码进行纵深讲解。读完你将掌握:在自研 Storybook 工具栏型 Addon 中,如何用useGlobals()读取全局值、用updateGlobals()修改全局值,并通过FORCE_RE_RENDER事件强制触发 story 重新渲染,最终写出可复用的交互式 Toolbar 按钮。
一、场景背景:为什么 Addon 需要“读”和“改” Globals
在 Storybook 中,Globals 表示“全局的”(非某个 story 专属)渲染输入,例如主题(light/dark)、语言 locale、背景色等。它不同于args——不会作为 story 参数传入,而是在装饰器(decorator)与 story context(context.globals)中被消费,作用于所有 story。当 Globals 改变时,story 会随之重新渲染、装饰器也会以新值重跑。
官方推荐的使用路径有两种:
- 在
.storybook/preview.*中通过globalTypes+toolbar注解声明工具条,用户在 UI 下拉菜单里改变全局值; - 在 Addon(面板或工具栏)内部以代码方式读写 Globals——这是很多增强型插件(主题切换、无障碍模拟、伪状态注入等)的常见需求。
本指南的关联片段正是第 2 条路径的官方最小示范:一个工具栏按钮,点击后在“开/关”两种全局状态间切换,同时强制刷新当前渲染的 story。
二、完整示例:在 Addon 中读取并更新 Globals
关联文档 docs/_snippets/addon-consume-and-update-globaltype.md 给出了一个完整的 React 工具栏组件。以下代码为原文完整复刻(保存于你的 Addon 注册文件中,例如your-addon-register-file.js):
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 = () => { // Updates Storybook global value updateGlobals({ ['my-param-key']: !isActive, }); // Invokes Storybook's 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 key="Example" padding="small" variant="ghost" pressed={isActive} onClick={toggleOutline} ariaLabel="Addon feature" tooltip="Toggle addon feature" > <OutlineIcon /> </ToggleButton> ); };2.1 逐段拆解:四类导入各司其职
| 导入 | 来源模块 | 作用 |
|---|---|---|
React, { useCallback } | react | 编写 manager 侧 React 组件(Toolbar 即 manager UI) |
OutlineIcon | @storybook/icons | 按钮图标;FAQ 中列出了所有可用于 toolbar/addon 的图标名 |
useGlobals | storybook/manager-api | 读取当前 Globals,并拿到更新函数updateGlobals |
addons | storybook/preview-api | 拿到 Addon 通信 channel,用于发射事件 |
ToggleButton | storybook/internal/components | Storybook 内部按钮组件,支持pressed/padding/variant/tooltip等属性 |
FORCE_RE_RENDER | storybook/internal/core-events | 事件常量,值即字符串'forceReRender',用于强制刷新界面 |
2.2 读取:const [globals, updateGlobals] = useGlobals()
useGlobals()是从storybook/manager-api导出的 React Hook。在 manager 侧实现位于 code/core/src/manager-api/root.tsx#L515-L523:
export function useGlobals(): [ globals: Globals, updateGlobals: (newGlobals: Globals) => void, storyGlobals: Globals, userGlobals: Globals, ] { const api = useStorybookApi(); return [api.getGlobals(), api.updateGlobals, api.getStoryGlobals(), api.getUserGlobals()]; }注意返回值是四元组:第一个元素是当前全局值对象;第二个是更新函数;第三、四个分别是 story 级与用户级 Globals。官方示例中只解构前两者,即可满足“读全局、改全局”的需求。
globals['my-param-key']就是读取当前开关值,配合|| false做布尔兜底:
const isActive = globals['my-param-key'] || false;2.3 更新:updateGlobals({ ['my-param-key']: !isActive })
更新函数接收一个局部 globals 对象,Storybook 会把其中携带的键合并进当前全局状态。key 写成计算属性['my-param-key']是为了容纳任意自定义键名(例如带-或命名字典key时),其效果等同于{ 'my-param-key': !isActive }。点击后按钮将在两种状态之间反复切换。
作为佐证,仓库内真实 Addon 全部遵循这一模式:例如 code/addons/a11y/src/components/VisionSimulator.tsx#L54-L62 用updateGlobals({ [VISION_GLOBAL_KEY]: selected })同步视觉模拟选项;code/addons/pseudo-states/src/manager/PseudoStateTool.tsx#L30 用updateGlobals({ [PARAM_KEY]: {} })重置伪状态;code/addons/themes/src/theme-switcher.tsx#L83-L104 用updateGlobals({ theme: alternateTheme })切换主题。
2.4 刷新:addons.getChannel().emit(FORCE_RE_RENDER)
FORCE_RE_RENDER是 Storybook 内置核心事件之一,定义于 code/core/src/core-events/index.ts#L21:FORCE_RE_RENDER = 'forceReRender'。官方注释将其语义概括为 “re-render unchanged”(见 code/core/src/preview-api/README-preview-web.md#L47)。
addons.getChannel()返回 Storybook 的通信 channel(manager 与预览 iframe 之间的事件总线);.emit(FORCE_RE_RENDER)表示“请用当前状态原样重新渲染当前 story”。
预览端在 code/core/src/preview-api/modules/preview-web/Preview.tsx#L144-L153 的setupListeners()中注册了对该事件的监听:
setupListeners() { this.channel.on(STORY_INDEX_INVALIDATED, this.onStoryIndexChanged.bind(this)); this.channel.on(UPDATE_GLOBALS, this.onUpdateGlobals.bind(this)); // ... this.channel.on(FORCE_RE_RENDER, this.onForceReRender.bind(this)); this.channel.on(FORCE_REMOUNT, this.onForceRemount.bind(this)); // ... }也就是说:Addon 在 manager 侧updateGlobals修改全局值后,再通过 channel 发射FORCE_RE_RENDER,预览端收到事件即触发当前 story 以最新 Globals 重新渲染——这正是注释中所说的“trigger a UI refresh”。在 Storybook 内部机制中,preview hooks 在“非渲染阶段”触发更新时同样会走这条通道(见 code/core/src/preview-api/modules/addons/hooks.ts#L371-L383 的triggerUpdate():addons.getChannel().emit(FORCE_RE_RENDER)),相关行为在 code/core/src/preview-api/modules/store/hooks.test.ts#L463-L524 中有专门测试断言(expect(mockChannel.emit).toHaveBeenCalledWith(FORCE_RE_RENDER))。
2.5 UI:ToggleButton与事件绑定
const toggleOutline = useCallback(() => refreshAndUpdateGlobal(), [isActive]);- 用
useCallback把回调的依赖收敛为isActive,保证isActive变化后闭包中读到的是最新值; ToggleButton上pressed={isActive}让按钮呈现“按下/激活”态,onClick触发切换,ariaLabel与tooltip分别用于无障碍与悬停提示。
三、从“代码片段”到“可运行 Toolbar”:完整接线
3.1 第一步:在 preview 中声明 globalTypes 与 initialGlobals
Toolbar 消费的全局键需要先在.storybook/preview.*声明。参考官方片段 docs/_snippets/storybook-preview-configure-globaltypes.md(TS/React 版):
import type { Preview } from '@storybook/react-vite'; const preview: Preview = { globalTypes: { theme: { description: 'Global theme for components', toolbar: { title: 'Theme', // Toolbar 项的显示名 icon: 'circlehollow', // 未选中时显示的图标 items: ['light', 'dark'], dynamicTitle: true, // 根据当前选中值动态更新标题 }, }, }, initialGlobals: { theme: 'light', }, }; export default preview;需要强调的限制(官方明确提示):Globals 是“全局”的,因此globalTypes与initialGlobals只能写在.storybook/preview.*(即 docs/configure/index.mdx 的 “Configure story rendering” 所描述的项目级 preview 配置),不能在单个 story/meta 中声明。
toolbar.items支持两种形态:纯字符串值数组,或MenuItem对象数组。MenuItem 各字段见下表(摘自关联主文档):
| MenuItem | 类型 | 说明 | 是否必填 |
|---|---|---|---|
value | String | 设置到 globals 中的菜单值 | 是 |
title | String | 菜单项的主文本 | 是 |
right | String | 显示在菜单右侧的文本 | 否 |
icon | String | 该项被选中时 Toolbar 显示的图标 | 否 |
(图标须取自@storybook/icons中可用的图标名列表。)
3.2 第二步:注册 Addon
把ExampleToolbar导出后,通过 manager 入口(Addon 包中的manager.*或preset)注册到 Storybook,并在.storybook/main.ts的addons数组中加入你的 Addon:
// .storybook/main.ts const config = { // ... addons: ['your-addon-package-name'], }; export default config;启动 Storybook 后,Toolbar 中即可出现带OutlineIcon的切换按钮:点击一次 →updateGlobals置为true并强制重渲染;再点一次 → 恢复为false。若已有装饰器读取该 global(例如基于context.globals['my-param-key']开关大纲/网格),story 视觉会随之实时变化。消费 Globals 的装饰器写法可参考 docs/_snippets/storybook-preview-use-global-type.md(包含 ReactThemeProvider、Vue Vuetify、Angular、Web Components 等框架示例)。
四、延伸:在面板 Addon 中“只读” Globals(useGlobals 的另一半)
如果你做的是面板型(Panel)Addon,只想展示当前全局值而无需修改,官方配套片段 docs/_snippets/addon-consume-globaltype.md 展示了“读取并渲染主题对象”的用法。其核心只有一行:
const [{ theme: themeName }] = useGlobals();随后将themeName映射为完整主题对象,并用Source/Placeholder等组件在面板中渲染。这与工具栏示例形成互补:读取用useGlobals()[0],修改用useGlobals()[1](即updateGlobals)。
两段片段在官方文档中的位置(docs/essentials/toolbars-and-globals.mdx):
- "Consuming globals from within an addon" →
addon-consume-globaltype.md; - "Updating globals from within an addon" →
addon-consume-and-update-globaltype.md(本文主体)。
五、store 侧视角:preview hooks 中的另一套 useGlobals
需要区分的是,storybook/manager-api的useGlobals面向manager(Addon UI);而 preview 渲染侧还存在一套面向 story/decorator 的 hooks 实现,位于 code/core/src/preview-api/modules/addons/hooks.ts#L649,被 story 内部用于订阅/更新全局值并驱动重渲染。若你需要在story 内部按单 story 读取 Locale 之类的 global(而不用装饰器),官方片段 docs/_snippets/my-component-story-use-globaltype.md 展示了从context.globals解构的方式。两者适用层级不同,勿混用。
六、仓库内真实 Addon 佐证
这一“读 + 写 + 刷新”模式的工程可信度,可从本仓库内置 Addon 中直接验证:
- code/addons/pseudo-states/src/manager/PseudoStateTool.tsx:
const [globals, updateGlobals] = useGlobals(),再通过Select的onReset/onChange调updateGlobals写入PARAM_KEY; - code/addons/a11y/src/components/VisionSimulator.tsx#L54-L62:同样
useGlobals()+updateGlobals同步无障碍视觉模拟全局值; - code/addons/themes/src/theme-switcher.tsx#L83-L104:
updateGlobals({ theme: alternateTheme })切换主题。
这些官方向导 Addon 把文档示例固化成了线上代码,可作为你实现自研 Addon 的参考蓝本。
七、自查清单与关键结论
实现“在 Addon 中消费并更新 Globals”,牢记以下要点:
- 读取:
const [globals, updateGlobals] = useGlobals(),来自storybook/manager-api;返回值实为四元组(globals、updateGlobals、storyGlobals、userGlobals),依据 code/core/src/manager-api/root.tsx#L515-L523。 - 写入:
updateGlobals({ 'key': value })按键合并;写入本身会经 Storybook 全局状态流程同步到 story。 - 强制刷新 UI:
addons.getChannel().emit(FORCE_RE_RENDER);FORCE_RE_RENDER常量值'forceReRender'(code/core/src/core-events/index.ts#L21),预览端在setupListeners()中监听(code/core/src/preview-api/modules/preview-web/Preview.tsx#L150)。 - 声明前置:对应的
globalTypes/initialGlobals只能在.storybook/preview.*配置;story 级globals注解用于“锁定”某个 story 的取值,但会禁用该 global 的 Toolbar 交互(官方建议克制使用)。 - UI 状态一致:
ToggleButton的pressed与useCallback的依赖数组必须与实际全局值同步,避免闭包读到过期状态。
借助 manager-api、preview-api、core-events 与内置 Addon 源码,你可以把上面几十行示例扩展为具备完整状态读写与实时刷新能力的企业级 Storybook 插件功能。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考