Storybook 全局装饰器(Global Decorators)实战指南:在 .storybook/preview 中统一包装所有 Story
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
导读
全局装饰器(Global Decorators)是 Storybook 在.storybook/preview文件中声明的一类装饰器,可以作用于项目中每一个 Story,是统一注入布局、间距、Provider、主题或 Mock 数据的最直接手段。本文以当前仓库 Storybook 源码为据,系统讲解全局装饰器的定义方式、跨框架(Angular / React / Solid / Svelte / Vue / Web Components)的写法差异、与组件级、Story 级装饰器的执行顺序,以及底层渲染机制。
一、什么是装饰器与全局装饰器
装饰器(Decorator)是一种将 Story 包裹在额外"渲染"功能中的机制。在 docs/writing-stories/decorators.mdx 中,Storybook 官方将其定义为:装饰器用于给 Story 包裹额外的标记(markup)或上下文 Mock,许多插件(addon)正是通过定义装饰器来增强 Story 的渲染行为。
按照作用范围,Storybook 装饰器分为三层:
| 层级 | 定义位置 | 作用范围 |
|---|---|---|
| 全局装饰器 | .storybook/preview.ts\|tsx中的decorators导出 | 项目中所有 Story |
| 组件级装饰器 | CSF 默认导出(default export)的decorators键 | 该组件下的所有 Story |
| Story 级装饰器 | CSF 命名导出(named export)的decorators键 | 单个 Story |
本文聚焦第一层——全局装饰器。根据 docs/configure/index.mdx 的说明,.storybook/preview.js负责控制 Story 的渲染方式,它被加载到渲染组件预览的 Canvas iframe 中,其decorators导出即全局装饰器数组。除decorators外,preview文件还可以导出parameters(全局参数)和globalTypes(全局类型定义)。
二、在不同框架中定义全局装饰器
关联文档 docs/_snippets/storybook-preview-global-decorator.md 提供了 6 大渲染器下的完整示例。核心场景是:当组件"顶到边缘"时,用一个带margin: 3em的包裹层为所有 Story 增加间距("harness")。
2.1 React(CSF 3)
import React from 'react'; // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import type { Preview } from '@storybook/your-framework'; const preview: Preview = { decorators: [ (Story) => ( <div style={{ margin: '3em' }}> {/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */} <Story /> </div> ), ], }; export default preview;在 React 中,装饰器接收一个Story组件并返回 JSX。官方注释特别提醒:装饰器也接受函数形式,将<Story />替换为Story()即可启用函数调用方式,以配合某些需要直接调用渲染函数的场景。JS 版本(.storybook/preview.jsx)写法与 TS 版本等价。
2.2 Angular
import { type Preview, componentWrapperDecorator } from '@storybook/angular'; const preview: Preview = { decorators: [componentWrapperDecorator((story) => `<div style="margin: 3em">${story}</div>`)], }; export default preview;Angular 框架推荐使用componentWrapperDecorator工具函数,它接收一个返回模板字符串的回调,${story}为被包裹的 Story 占位符,从而以 Angular 模板语法完成包裹。
2.3 Svelte
// Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite import type { Preview } from '@storybook/your-framework'; import MarginDecorator from './MarginDecorator.svelte'; const preview: Preview = { decorators: [() => MarginDecorator], }; export default preview;Svelte 略有不同:需要先创建一个独立的 Svelte 组件作为装饰器(如MarginDecorator.svelte),再在decorators中返回该组件。如需向装饰器组件传递 props,可返回包含Component与props键的对象,以便根据 Story 上下文(如parameters.pageLayout)动态定制行为。
2.4 Vue(CSF 3 与 CSF Next)
import type { Preview } from '@storybook/vue3-vite'; const preview: Preview = { decorators: [ (story) => ({ components: { story }, template: '<div style="margin: 3em;"><story /></div>', }), ], }; export default preview;Vue 装饰器返回一个组件选项对象,将 Story 注册为局部组件story并在模板中渲染。CSF Next 实验性语法则使用definePreview包裹:
import { definePreview } from '@storybook/vue3-vite'; export default definePreview({ decorators: [ (story) => ({ components: { story }, template: '<div style="margin: 3em;"><story /></div>', }), ], });2.5 Web Components(Lit)
import { html } from 'lit'; export default { decorators: [(story) => html`<div style="margin: 3em">${story()}</div>`], };Web Components 渲染器使用lit的html标签模板,story()以函数形式调用。TS 版本引入Preview类型并声明const preview: Preview = {...},CSF Next 版本则使用definePreview。
2.6 Solid
export default { decorators: [ (Story) => ( <div style={{ margin: '3em' }}> <Story /> </div> ), ], };Solid 写法与 React 高度相似(TS 版导入storybook-solidjs-vite的Preview类型)。
三、全局装饰器的底层执行机制
3.1 合并:composeConfigs 中的数组拼接
所有 Story 的装饰器在项目注解(project annotations)合并阶段被统一收集。composeConfigs.ts 通过getArrayField(moduleExportList, 'decorators', { reverseFileOrder: ... })从各个配置模块中抽取decorators数组,并默认按文件顺序反转合并,以保证先加载的文件装饰器位于外层。这一行为可通过features.legacyDecoratorFileOrder特性开关回退到旧版顺序(参见 main-config-features-legacy-decorator-file-order.md)。
3.2 组合:defaultDecorateStory 的洋葱模型
decorators.ts 中的defaultDecorateStory使用decorators.reduce(...)将所有装饰器逐层组合成一个"洋葱"式渲染链:最外层装饰器先执行,逐层向内,最终由 Story 本身的渲染函数收尾。源码中bindWithContext将部分装饰后的 storyFn 绑定上下文,并sanitizeStoryContextUpdate过滤掉componentId、title、id、parameters等只读静态键,防止装饰器内部调用storyFn({ ... })时覆盖这些保留字段。
3.3 归一化:Story 级与组件级装饰器
在 normalizeStory.ts 中,单个 Story 的装饰器由storyObject.decorators(组件级)与story?.decorators(Story 级)拼接而成。结合全局装饰器,一个 Story 最终命中的装饰器链为:
- 全局装饰器,按定义顺序执行;
- 组件级装饰器,按定义顺序执行;
- Story 级装饰器,按定义顺序执行(由最内层向外)。
这与 docs/writing-stories/decorators.mdx 描述的继承规则完全一致。
四、全局装饰器的进阶用法:基于上下文的参数化
装饰器函数的第二个参数是story context,包含args、argTypes、globals、hooks、parameters、viewMode等属性。利用它可以让同一个全局装饰器根据 Story 元数据动态切换行为,例如通过parameters.pageLayout决定是否应用页面级布局:
import { withLayout } from './withLayout'; export default { decorators: [withLayout], };其中withLayout读取context.parameters.pageLayout(取值为'page'或'page-mobile')来决定包裹方式。完整示例见 decorator-parameterized-in-preview.md。同样的技术也用于配置 Mock Provider 以切换组件获得的主题(参见 mocking-providers 配置章节)。
在 Vue 渲染器中,若装饰器需要读取globals,必须经由setup函数透传以保证响应式,并可结合computed派生值(示例见 decorator-with-reactive-globals.md);调用 Storybook 的 API hooks(如useArgs、useGlobals)时同样应在装饰器中通过storybook/preview-api导入对应 hook 以避免重渲染错误(参见 decorator-with-updateArgs.md)。
五、最佳实践与注意事项
- 保持 Story 纯净:装饰器之外的组件应保持为被测组件的"纯粹"渲染,额外的 HTML 或包装组件只应出现在装饰器中。这样 Source Doc Block 等文档块才能正确提取源码。
- "连接型"组件的数据注入:如果组件依赖外部加载的数据,可以用全局装饰器以 Mock 方式提供数据,无需把数据重构为 args;具体策略可参考 building pages in Storybook。
- 慎用全局作用域:全局装饰器作用于所有 Story,因此只放置真正普适的包装(间距、主题 Provider、路由/Store Provider)。针对单个组件或 Story 的定制优先使用低层级装饰器。
- 顺序敏感:由于洋葱模型的存在,多个全局装饰器的定义顺序就是它们的执行顺序,调整数组顺序即可改变包裹层次。
结语
全局装饰器是 Storybook 配置层最常用的扩展点之一。通过.storybook/preview中的decorators导出,开发者可以用各框架惯用的语法为全部 Story 统一注入布局与上下文;理解其背后的composeConfigs合并与defaultDecorateStory洋葱组合机制,则能在多装饰器叠加、文件加载顺序等复杂场景下准确预判渲染结果。相关完整代码可继续阅读 preview-api 模块 及其测试 decorators.test.ts。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考