ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从样板到源码:在 Storybook 中打造自定义 Panel 插件(addon)的完整指南

2026/9/9 12:26:19 拓冰建站 浏览量
从样板到源码:在 Storybook 中打造自定义 Panel 插件(addon)的完整指南 从样板到源码在 Storybook 中打造自定义 Panel 插件addon的完整指南【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook在 Storybook 中Panel面板是最常见的 UI 类插件形态——它是出现在 Storybook 预览区下方或侧边的可交互区域承载了无障碍检查a11y、Actions 事件日志、组件属性检查等一系列官方与社区功能。本文以 Storybook 官方 addon 文档中用于在 Storybook UI 中新增一个 Panel 的样板代码为骨架逐行讲解其运行机制并结合当前仓库中的真实源码a11y 插件实现、AddonPanel 组件与类型定义解释addons.register、types.PANEL、AddonPanel等核心概念帮助你写出可复用、可交互、可发布的自定义面板插件。一、先理解概念Panel 在 Storybook 插件体系中的位置Storybook 将插件addon划分为两大类这一分类在 docs/addons/addon-types.mdx 中有明确说明基于 UI 的插件UI-based addons负责定制界面、为常见操作提供快捷键或在 UI 中展示附加信息可进一步细分为Panels面板、Toolbars工具栏与Tabs标签页Preset 插件一组预配置的babel、webpack、addons配置集合用于将 Storybook 与其他技术栈集成。Panel addon 正是 UI 类插件中最普遍的一种。文档明确指出面板插件允许你在 Storybook 的 addon 面板中加入自己的 UI这是插件生态中数量最多的类型。文档以官方storybook/addon-a11y作为该模式的代表案例。当你需要展示与本条 story 相关的额外信息如测试结果、可访问性报告、数据日志、辅助操作表单时最自然的落点就是面板。二、面板样板代码逐行精解官方文档为向 Storybook UI 添加一个新的 Panel提供了一段最小可运行的样板代码完整内容位于 docs/_snippets/storybook-addon-panel-example.md也是storybook内置类型面板的标准写法import React from react; import { AddonPanel } from storybook/internal/components; import { useGlobals, addons, types } from storybook/manager-api; addons.register(my/panel, () { addons.add(my-panel-addon/panel, { title: Example Storybook panel, // Sets the type of UI element in Storybook type: types.PANEL, render: ({ active }) ( AddonPanel active{active} h2Im a panel addon in Storybook/h2 /AddonPanel ), }); });虽然只有不到二十行但它涵盖了面板插件的全部关键要素下面逐段拆解1. 三个 import 各自负责什么import React from reactPanel 的render是 JSX 组件显然需要 React。import { AddonPanel } from storybook/internal/componentsStorybook 暴露的内部 UI 组件库。AddonPanel是面板内容的标准外壳它处理了面板的滚动容器、active激活态切换等公共行为。import { useGlobals, addons, types } from storybook/manager-api这是 managerStorybook 的界面管理端的公共 API。addons对象负责注册与添加插件types枚举声明 UI 元素的类型useGlobals是读取/更新全局状态globals的 React Hook用于实现插件开关、联动等交互逻辑。值得注意样例代码同时引入了AddonPanel与useGlobals。在最小示例里useGlobals并未被真正使用但从源码实现来看绝大多数真实面板都靠它与 Storybook 的全局状态交互——官方 a11y 插件的面板与 Vision Simulator 工具栏正是通过useGlobals、useAddonState等 Hook 维持同一份运行状态。2. addons.register注册插件的入口addons.register(my/panel, () { // ... });addons.register的第一个参数是全局唯一标识符id用于在 Storybook 中标识整个插件第二个参数是初始化回调在 Storybook manager 启动时执行。这里的 idmy/panel只是字符串约定并不强制带/但用命名空间式写法scope/name有助于避免与其他插件冲突。注册回调里应完成该插件所需 UI 元素panel、tool、tab的逐一添加。3. addons.add types.PANEL声明一个面板类型条目addons.add(my-panel-addon/panel, { title: Example Storybook panel, type: types.PANEL, render: ({ active }) ( AddonPanel active{active} h2Im a panel addon in Storybook/h2 /AddonPanel ), });addons.add(id, config)为插件注册一个 UI 条目。这里的 idmy-panel-addon/panel同样需要唯一且习惯上以插件 id 前缀命名方便后续定位。title显示在面板页签上的标题文本。type: types.PANEL声明该条目属于面板。types来自storybook/manager-api其真实取值定义在 code/core/src/types/modules/addons.ts 的Addon_TypesEnum枚举中完整类型包括TAB tab画布上方的自定义标签页PANEL panel侧边栏中的插件面板本文所述类型TOOL tool画布上方工具栏左侧的工具项TOOLEXTRA toolextra画布上方工具栏右侧的工具项PREVIEW preview包裹画布/iframe 的包装组件experimental_PAGE、experimental_TEST_PROVIDER等实验性类型。render: ({ active }) ...渲染函数Storybook 会向它传入当前面板是否处于激活/选中状态active。当你切换面板页签时active随之变化使组件可以据此决定挂载或卸载内容。4. AddonPanel面板的官方外壳AddonPanel active{active} h2Im a panel addon in Storybook/h2 /AddonPanelAddonPanel是面板内容的容器组件实际实现位于 code/core/src/components/components/addon-panel/addon-panel.tsx。从源码可以确认其接口与行为export interface AddonPanelProps { active: boolean; children: ReactElement; /** Whether the panel has a vertical scrollbar, true by default. */ hasScrollbar?: boolean; /** Whether the panel has an horizontal scrollbar, false by default */ hasHorizontalScrollbar?: boolean; }实现要点包括通过Div hidden{!active}控制显隐——源码注释特别说明使用 HTML 标准的hidden属性而非单纯的 CSS 隐藏是为了保证可访问性accessible与可索引性同时仍能视觉上隐藏内容hasScrollbar默认truehasHorizontalScrollbar默认false。当任一滚动条开启时内容会被包裹进ScrollAreacode/core/src/components/components/ScrollArea获得滚动能力内部使用useUpdate/usePrevious缓存子元素仅当active更新时替换children引用避免面板在未激活时因父组件重渲染而丢失内部状态。这也解释了为何需要始终把面板内容放在AddonPanel内——它负责维持内容在失活再激活后的连续性。因此面板里的render不应只返回裸的h2而应返回被AddonPanel包裹的内容这样才会获得一致的样式、滚动与激活态处理。三、把它放进真实的插件工程文件结构与注册入口面板代码通常会位于插件的src/Panel.tsx对应工具栏Tool.tsx、标签页Tab.tsx然后在 manager 入口文件中完成注册。官方的 Writing addons 指南 明确说明了这条工程路径UI 类插件的代码默认放在src/Tool.tsx、src/Panel.tsx或src/Tab.tsx之一若用 Addon Kit 脚手架创建插件发布时的package.json会通过bundler字段声明构建入口参见 docs/addons/writing-addons.mdxbundler: { exportEntries: [src/index.ts], managerEntries: [src/manager.ts], previewEntries: [src/preview.ts] }面板在 manager 端运行因此对应的managerEntries构建产物会被注入 Storybook 的 manager 环境。Storybook 的 manager 环境会以全局作用域形式提供部分包插件无需也不应将它们打入产物或声明为依赖——这也是上节三个 import 直接从storybook/internal/components、storybook/manager-api导入的原因。四、源码佐证官方 a11y 插件是如何使用同一套 API 的样板代码只是骨架官方文档特意点名 storybook/addon-a11y 作为 Panel 模式的真实案例。查看其 manager 入口 code/addons/a11y/src/manager.tsx可以看到同样的骨架在真实插件中如何被扩展import { addons, types, useAddonState, useStorybookApi } from storybook/manager-api; const Title () { const api useStorybookApi(); const selectedPanel api.getSelectedPanel(); // ...根据 useAddonState 统计违规数量并渲染 Badge 徽标 }; addons.register(ADDON_ID, (api) { addons.add(PANEL_ID, { title: Title, // title 可以是一个 React 组件此处动态显示违规数量徽标 type: types.PANEL, render: ({ active true }) ( A11yContextProvider{active ? A11YPanel / : null}/A11yContextProvider ), paramKey: PARAM_KEY, // 将该面板与某个 story 参数绑定 }); });与样板对比真实插件的增量主要在三点你可以据此扩展自己的面板title不限于字符串可以是组件a11y 用Title函数组件 useAddonState面板级私有状态实时读取违规数量并把Badge徽标嵌在页签标题上实现标题上的动态角标。paramKey与 story 参数联动paramKey把面板与同名的 story/preview 参数关联起来Storybook 借此判断该面板在什么情况下可展示。在 manager 注册回调中同时添加多个 UI 元素a11y 在同一个addons.register(ADDON_ID, ...)内既addons.add了一个TOOL条目Vision Simulator又addons.add了一个PANEL条目。这印证了一个插件 一次 register 多个 UI 元素 add的组织方式。另外该插件的单元测试 code/addons/a11y/src/manager.test.tsx 展示了如何对面板注册结果做断言——测试通过find(({ type }) type api.types.PANEL)在注册列表中筛选出面板条目并校验其渲染行为这可以作为你为自己面板编写测试的参照。五、把面板做成可交互、可感知上下文样板中的面板是静态内容真实场景通常还需要读取当前 story 的信息、与全局工具联动。官方文档把下列 API 列入了 addon 开发者的基础工具箱见 docs/addons/addons-api.mdx 等参考资料useGlobals()读取并更新全局状态globals。比如某个工具栏按钮切换的开关面板内可用它读取同一状态从而展示联动结果这也解释了样板中为何引入该 Hook。useStorybookApi()拿到 Storybook 的 API 对象可调用getSelectedPanel()判断当前选中了哪个面板a11y 的Title就靠它决定徽标的 active 外观。useAddonState面板自己的持久化状态切换 story、收起再展开面板都不会丢失。match属性控制条目在什么视图模式下展示story/docs 画布或特定自定义 tab详见 writing-addons.mdx。一个典型的扩展思路是把样板中的AddonPanel内部替换成读取 story 参数的表单、把用户操作结果写入globals再配合一个 toolbar 按钮控制启停——这正是 a11y、themes 等大量官方插件的运作模式。六、同源扩展Tool、Tab 与预设类插件了解 Panel 后应知晓其余同类 UI 元素的样板差异完整对照见 docs/addons/addon-types.mdxToolbar工具栏把代码块放在 storybook-addon-toolbar-example.md条目type使用types.TOOL并为match、icon配置条件渲染与图标Tab标签页对应 storybook-addon-tab-example.md用于创建画布上方的自定义标签页Preset 插件非 UI 类属于babel/webpack/addons配置的集合官方文档以preset-create-react-app为例。若想系统掌握插件开发官方文档提供了一条完整的学习链从 Writing addons理解 addon 的解剖结构、本地调试与发布出发接着阅读 addon-types.mdx 与 writing-presets.mdx最后以 addons-api.mdx 作为 API 速查手册。小结最小骨架addons.register(唯一id, () addons.add(id, { type: types.PANEL, render: ({active}) AddonPanel active{active}...即构成一个可用面板。必须用AddonPanel包裹它提供滚动容器、hidden显隐与基于active的状态保持addon-panel.tsx。从样板到真实通过useAddonState、useStorybookApi、paramKey与动态title组件可以复现官方 a11y 插件的面板模式manager.tsx。入口位置把代码放入插件的 manager 入口由打包配置将 manager 产物注入 Storybook面板运行在 manager 端属于 UI 类插件的标准形态。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考