Storybook 插件开发:用 useParameter 钩子读取与响应当前 Story 参数
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
Storybook 的 addon 体系允许开发者在 Manager 侧(面板、工具栏、标签页等 UI 区域)读取当前选中 Story 的元数据。本文聚焦storybook/manager-api提供的useParameter钩子,讲解其签名、默认值行为、基于事件通道的自动刷新机制,并结合 manager-api 源码 与官方文档示例,说明如何在自定义插件面板中安全地消费自定义参数。读完你将能写出随 Story 切换实时刷新的参数面板,并理解官方文档片段背后的实现原理。
useParameter 在插件架构中的定位
Storybook 的插件(addon)界面运行在独立的Manager进程中,而 Story 本体渲染在Preview(iframe)中。插件 UI 无法直接访问 Preview 内部的 React context,而是通过storybook/manager-api暴露的一组 React hooks 获取状态与数据。
其中useParameter专门用于读取当前选中 Story 的 parameters。一个典型的使用场景是在自定义面板中展示某个自定义参数的取值。它的官方说明见 docs/addons/addons-api.mdx 的 "Storybook hooks" 章节:
The
useParameterretrieves the current story's parameters. If the parameter's value is not defined, it will automatically default to the second value defined.
也就是说:传入一个参数键名,返回当前 Story 中该参数的值;若值为定义(undefined),自动回落到传入的第二个默认值参数。
API 签名与参数说明
useParameter是对storybook/manager-api模块的扩展,与useStorybookState、useChannel、useAddonState、useGlobals等 hooks 并列(详见 docs/addons/addons-api.mdx)。它的完整签名如下:
function useParameter<S>(parameterKey: string, defaultValue?: S): S;| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
parameterKey | string | 是 | 要读取的自定义参数键名,例如'custom-parameter'、'my-addon/theme' |
defaultValue | S(泛型) | 否 | 当当前 Story 未定义该参数时返回的默认值 |
| 返回值 | S | — | 当前 Story 中该参数的实际值;未定义时返回defaultValue |
由于是泛型函数,也支持显式指定类型,例如官方 TypeScript 类型指南中的用法:
import { useParameter } from 'storybook/manager-api'; const paramData = useParameter<string>(PARAM_KEY, '');官方示例:自定义面板中的参数读取
本文关联的官方代码片段 docs/_snippets/storybook-addons-api-useparameter.md 展示了在面板组件中使用useParameter的完整写法,通常放在插件的manager.js|ts入口中:
import React from 'react'; import { AddonPanel } from 'storybook/internal/components'; import { useParameter } from 'storybook/manager-api'; export const Panel = () => { // Connects to Storybook's API and retrieves the value of the custom parameter for the current story const value = useParameter('custom-parameter', 'initial value'); return ( <AddonPanel key="custom-panel" active="true"> {value === 'initial value' ? ( <h2>The story doesn't contain custom parameters. Defaulting to the initial value.</h2> ) : ( <h2>You've set {value} as the parameter.</h2> )} </AddonPanel> ); };这段代码包含三个关键动作:
- 从
storybook/manager-api引入useParameter—— 它是 Manager 侧 UI 组件的专属钩子; - 以
'custom-parameter'为键读取当前 Story 参数,并以'initial value'作为兜底值; - 渲染到
storybook/internal/components提供的AddonPanel容器中,并依据返回值是"默认值"还是"用户设置值"分支展示不同文案。
要在 Storybook 中看到该面板,还需在 manager 入口注册对应的面板(完整注册流程见 docs/addons/writing-addons.mdx):
import { addons, types } from 'storybook/manager-api'; import { Panel } from './panel'; addons.register('my-addon', () => { addons.add('custom-panel', { type: types.PANEL, title: 'Custom Panel', render: () => <Panel />, }); });自定义参数从哪里来:parameters 的层级与合并
useParameter('custom-parameter', ...)读取的并非孤立数据,而是当前 Story 经过合并后的parameters对象。在 CSF / 文档中,开发者可以从多个层级写入自定义参数,作用域由大到小依次为:
- 全局 Preview 层:在
.storybook/preview.js|ts的export default { parameters: { ... } }中设置,作用于所有 Story; - 组件元数据层(Meta):在
Button.stories.js的export default { parameters: { ... } }中设置,作用于该文件下所有 Story; - 单个 Story 层:在
export const Primary = { parameters: { ... } }中设置,仅作用于该 Story。
Storybook 会将这些层的对象自下而上做浅合并,最终得到一个"当前 Story 参数快照",存储在索引条目(story entry)的parameters字段中。useParameter读取的正是这个合并后的最终值,因此传入的键即使只定义在 Preview 或 Meta 层,也能被面板正确读到。
源码解析:useParameter 如何工作
了解 API 用法后,深入源码能帮你预判它的行为边界。useParameter的实际实现在 code/core/src/manager-api/root.tsx#L380-L398:
export function useParameter<S>(parameterKey: string, defaultValue?: S) { const api = useStorybookApi(); const [parameter, setParameter] = useState(api.getCurrentParameter<S>(parameterKey)); const handleParameterChange = useCallback(() => { const newParameter = api.getCurrentParameter<S>(parameterKey); setParameter(newParameter); }, [api, parameterKey]); useChannel( { [STORY_PREPARED]: handleParameterChange, [DOCS_PREPARED]: handleParameterChange, }, [handleParameterChange] ); return orDefault<S>(parameter, defaultValue!); }结合这段源码可归纳出四条实现要点:
1. 初始快照由 getCurrentParameter 提供
组件挂载时,useState(api.getCurrentParameter(parameterKey))以"当前选中 Story"为基准立刻读取一次参数作为初始状态。这里的useStorybookApi()从 Manager 的 React context 中取出 API 实例。
2. 通过 Channel 事件实现随 Story 切换自动刷新
仅读一次初始值是不够的——用户会在侧边栏切换 Story。为此useParameter内部使用useChannel(同样是 root.tsx 导出的 hook)订阅了两个核心事件:
STORY_PREPARED(事件名为storyPrepared);DOCS_PREPARED(事件名为docsPrepared)。
这两个事件常量定义于 code/core/src/core-events/index.ts:
STORY_PREPARED = 'storyPrepared', DOCS_PREPARED = 'docsPrepared',每当一个新 Story 准备完成、参数被提交到 Manager 端,事件触发回调handleParameterChange,重新调用getCurrentParameter并用setParameter更新本地状态,从而驱动面板重渲染。这正是"面板内容随故事切换自动变化"的机制来源——无需你手动监听任何事件。同时,DOCS_PREPARED的订阅意味着在 Docs 模式页面间切换时,参数同样会刷新。
3. orDefault 兜底逻辑
返回语句orDefault<S>(parameter, defaultValue!)依赖文件顶部定义的工具函数:
function orDefault<S>(fromStore: S, defaultState: S): S { if (typeof fromStore === 'undefined') { return defaultState; } return fromStore; }即只有真实取到的参数为undefined时,才会返回第二个参数指定的默认值——这也对应官方文档描述的行为:"If the parameter's value is not defined, it will automatically default to the second value defined."
4. 读取链路的底层实现
api.getCurrentParameter的实现在 code/core/src/manager-api/modules/stories.ts#L561-L567:
getCurrentParameter: (parameterName) => { const { storyId, refId } = store.getState(); const parameters = api.getParameters({ storyId, refId: refId as string }, parameterName); // FIXME Returning falsey parameters breaks a bunch of toolbars code, // so this strange logic needs to be here until various client code is updated. return parameters || undefined; },它先从 Manager 全局状态中取出当前选中的storyId与refId(跨组合 refs 时指向来源仓库),再调用getParameters查询参数。而getParameters(见同文件 stories.ts 上游逻辑)会校验当前条目类型为story或docs,随后在条目携带的parameters上按键取值:传入parameterName返回单值,不传则返回完整参数对象。
需要注意的行为细节
结合上述源码,以下几点对实际开发非常关键,建议在使用前建立预期:
- 默认值只在"未定义"时生效:只有 Story 完全没设置该参数、或取值为
undefined时,orDefault才会回落到默认值;若参数被显式设置为其他值(包括空字符串、0、false),面板拿到的是真实值。 - 假值参数目前的兼容性折衷:从 stories.ts#L564-L566 的注释
FIXME Returning falsey parameters breaks a bunch of toolbars code可以看出,为了兼容既有 toolbar 代码,getCurrentParameter会对假值统一返回undefined。也就是说在源码修正之前,即使某个 Story 显式把参数设为0、''或false,useParameter也倾向于返回默认值。若你的参数语义依赖假值,需要留意这一历史行为,并让默认值与真实值的类型保持可区分。 - 面板应基于"选中 Story"消费参数:
useParameter与侧边栏当前高亮的 Story 强绑定,适合面板、工具条、标签页这类"跟随当前故事"的 UI。若你需要长期跨 Story 持久化的数据,应改用useAddonState(内部基于useSharedState与插件状态 API,实现见 root.tsx#L492-L494)。 - 它读取的是 Story 元数据而非渲染结果:如果插件需要知道 iframe 里实际渲染时用到的参数,需要结合事件通道在 Preview 侧读取后再回传;
useParameter只反映 Manager 侧已索引好的参数快照。
与其他 Manager 侧 Hooks 的配合
useParameter是 Storybook hooks 家族的一员,官方文档 docs/addons/addons-api.mdx 的 "Storybook hooks" 一节(其使用方式与各自动机定位)中与之并列的还有:
| Hook | 用途 |
|---|---|
useStorybookState | 读取 Storybook 内部 UI 状态 |
useStorybookApi | 获得完整 API 方法句柄 |
useChannel | 订阅 / 发送 Channel 事件 |
useAddonState | 插件持久化状态(含 HMR 缓存,见STORYBOOK_ADDON_STATE) |
useGlobals | 读取与更新全局 globals |
useArgs | 读取或更新当前 Story 的 args |
这些 hooks 都从storybook/manager-api导出。若你的面板需要"参数 + 其他状态"组合驱动,一个常见模式是:useParameter负责描述当前 Story 的静态元数据,useAddonState负责面板自身的交互状态,二者互不干扰。
实战建议小结
在插件中使用useParameter时,建议遵循以下约定:
- 参数键使用带命名空间的稳定字符串,如
'my-addon/custom-parameter',避免与其它插件冲突; - 总是提供与真实取值同类型的默认值,并处理好上述"假值被回退"的历史边界;
- 让面板 UI 以默认值分支呈现空态引导(如官方示例中的提示文案),在 Story 未配置参数时给出友好反馈;
- 如果你要同时支持多个参数,可在 Preview 或 Meta 层统一配置一个对象型参数,再通过类型化工具函数读取,减少键散落。
延伸阅读
- docs/addons/addons-api.mdx:完整 Storybook API 与 hooks 参考文档;
- docs/addons/configure-addons.mdx:addon 配置总览,含对
useParameter的指引; - docs/_snippets/storybook-addon-toolkit-types.md:TypeScript 下类型化使用
useParameter<string>(...)的示例; - code/core/src/manager-api/root.tsx:
useParameter及全部 Manager 侧 hooks 实现; - code/core/src/manager-api/modules/stories.ts:
getCurrentParameter/getParameters的参数查询实现; - code/core/src/core-events/index.ts:
STORY_PREPARED、DOCS_PREPARED等核心事件定义。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考