
Storybook 实战用 Svelte 编写 MarginDecorator 装饰器组件为 Story 渲染添加外壳与画布间距本指南基于 Storybook 官方文档体系中的docs/_snippets/margindecorator.md代码片段展开。该片段演示了在Svelte 渲染器下如何编写一个名为MarginDecorator.svelte的装饰器组件用于给紧贴画布边缘的 Story 包裹一层带外边距的 harness外壳。读完本文你将掌握 Svelte 5 runes 语法下装饰器组件的写法、在*.stories.svelte与 CSF 3 两种文件形态中挂接装饰器的方法以及向装饰器传参、借助 story context 与 Svelte context API 定制渲染的进阶能力。装饰器Decorator是 Storybook 中给 Story 额外套一层渲染的标准手段许多插件通过它增强渲染或采集渲染信息而在手工编写 Story 时它最常用于补充标记结构或注入 context上下文。当某个组件顶着画布边缘渲染、难以观察整体效果时最经典的解法就是为它包一层带留白的容器。上面对比来自宿主文档 docs/writing-stories/decorators.mdx左侧组件与画布边缘零距离视觉上十分局促右侧组件被套进带有间距的容器后边界清晰、更便于审阅与截图。要实现右侧效果Svelte 渲染器需要比 JSX 渲染器多做一步——先创建一个独立的 Svelte 组件来充当装饰器这就是MarginDecorator.svelte的由来。一、创建 MarginDecorator.svelte装饰器组件本体原文档 docs/_snippets/margindecorator.md 提供了同一组件的 JavaScript 与 TypeScript 两个版本二者都使用 Svelte 5 的 runes 语法script let { children } $props(); /script div {render children()} /div style div { margin: 3em; } /stylescript import type { Snippet } from svelte; let { children }: { children: Snippet } $props(); /script div {render children()} /div style div { margin: 3em; } /style这段代码逐行拆解如下let { children } $props()Svelte 5 runes 中声明组件 props 的标准方式。装饰器组件接收一个名为children的 prop它将在后续被 Storybook 注入真正要渲染的 Story 内容。TS 版本额外用import type { Snippet } from svelte标注children的类型。Snippet是 Svelte 5 引入的代码片段类型代表可被{render ...}调用的一整段模板。{render children()}Svelte 5 中渲染 snippet 的专用指令等价于旧版 Slots 体系下slot /的角色。style中的div { margin: 3em }为内部 div 设置 3em 外边距从而把真正被渲染的 Story 组件与画布边缘推开。em是相对单位会随根字号缩放若希望间距与字号解耦可改用rem或px。一个值得注意的设计点是装饰器组件本身不关心被装饰的是谁。它只声明children片段并渲染它实际内容由 Storybook 在渲染期注入——这是装饰器组件与普通组件最本质的区别。二、把装饰器接进 Story 文件有了组件本体下一步是让 Storybook 在渲染该组件的所有 Story 时都把内容包进MarginDecorator。根据 docs/_snippets/your-component-with-decorator.mdSvelte 渲染器支持两种等价写法。Svelte CSF*.stories.svelte配合storybook/addon-svelte-csf——把decorators放进defineMetascript module import { defineMeta } from storybook/addon-svelte-csf; import YourComponent from ./YourComponent.svelte; import MarginDecorator from ./MarginDecorator.svelte; const { Story } defineMeta({ component: YourComponent, decorators: [() MarginDecorator], }); /scriptCSF 3普通*.stories.js|ts——把decorators作为默认导出的键import YourComponent from ./YourComponent.svelte; import MarginDecorator from ./MarginDecorator.svelte; export default { component: YourComponent, decorators: [() MarginDecorator], };两个例子中的写法decorators: [() MarginDecorator]需要特别留意数组元素是一个返回组件本身的箭头函数而不是直接写MarginDecorator。这样既保持了装饰器 API 的统一所有渲染器都约定 decorator 是一个可接收 story 的函数也为后续在函数体内读取 story context、返回{ Component, props }形态留出了扩展空间。三、需要传 props 时返回 { Component, props } 对象上面的MarginDecorator写死了margin: 3em。若想让间距可配置——例如根据 story 的parameters决定用 small 还是 medium——装饰器函数可以直接返回一个对象。这一点在宿主文档中被单独强调其配套片段见 docs/_snippets/your-component-with-decorator-with-props.mdimport YourComponent from ./YourComponent.svelte; import MarginDecorator from ./MarginDecorator.svelte; export default { component: YourComponent, decorators: [ (story, { parameters }) ({ Component: MarginDecorator, // Pass props to the MarginDecorator component props: { size: parameters.smallMargin ? small : medium }, }), ], };要点装饰器函数的第二个参数是 story context此处解构出了parametersstory 的静态元数据。返回值中Component指定要渲染的装饰器组件props则会被透传给该组件。此时你的MarginDecorator.svelte就需要配套声明sizeprop 并据此切换间距值——它的接收端本质上就是普通 Svelte 组件传参。把props绑定到parameters之后你可以在任意 story 上通过parameters: { smallMargin: true }零侵入地单独控制该 story 的边距也可以在.storybook/preview.js的全局parameters里统一配置。更进一步可以让 props 完全跟随 story 的args例如props: args从而让size、color等成为可在 Storybook UI 的 Controls 面板中实时调节的控件。这一点由仓库渲染器测试所证实参见下文第五节。同样的传参思路也适用于结合全局工具globals/toolbars动态切换读取globals.marginSize再把它换算成组件 props 或通过 Svelte context 下发。四、story context 里有什么装饰器函数第二参数装饰器函数的第二参数是story context。根据 docs/writing-stories/decorators.mdx 的完整清单它包含以下核心字段字段说明args该 story 的参数可在装饰器中消费一部分 args使 story 实现本身更纯净argTypesStorybook 的 argTypes 配置用于细调 story 的 args 及其控件globalsStorybook 全局变量尤其可配合 Toolbars 功能在 UI 中实时切换取值hooksStorybook 的 API hooks如useArgs、useGlobals装饰器与渲染函数中均可用parametersstory 的静态元数据常用于控制 Storybook 特性与插件行为viewModeStorybook 当前激活的视图窗口如 canvas、docs这些字段就是装饰器实现按需定制的依据。例如装饰器可以读取parameters.pageLayout page来动态决定是否应用整页布局而不是一味包固定容器详见片段 docs/_snippets/decorator-parameterized-in-preview.md。对 Svelte 渲染器而言story context 还有一个特殊用途它让装饰器在运行期调用 Svelte 自身的 context API实现上下文注入。五、从源码看 Svelte 装饰器返回值的三种归一化decorators: [() MarginDecorator]返回的是裸组件而第三节返回的是{ Component, props }对象——这两种形态是如何被统一处理的答案在 Svelte 渲染器的核心实现 code/renderers/svelte/src/decorators.ts。prepareStory函数同文件第 40–80 行对 decorator 返回值做了清晰的归一化源码注释第 26–39 行原样概括了三类输入() ({ Component: MyComponent, props: ... })——已经准备好的形态原样保留() MyComponent——被转换为() ({ Component: MyComponent })若返回空值或空对象则退回到使用 story context 中的component。随后源码第 65–76 行是关键一环当存在内层 story即装饰器链不是最后一层时渲染器会构造一个DecoratorHandler组件把内层 story 作为普通组件、当前装饰器作为decoratorprop一并注入return { Component: DecoratorHandler, props: { ...innerStory, decorator: preparedStory, }, };也就是说无论你写的是() MarginDecorator还是() ({ Component: MarginDecorator, props })最终都会被包进storybook/svelte/internal/DecoratorHandler.svelte由它负责把被装饰的 Story 作为childrensnippet 渲染进你的装饰器组件。这正是MarginDecorator.svelte中children{render children()}能收到 Story 内容的底层原因。渲染器自带的验收测试 code/renderers/svelte/template/stories/decorators.stories.js 恰好覆盖了上文讨论的所有形态可作为正确姿势的可运行样例第 10 行decorators: [() BorderDecoratorRed]——组件级装饰器返回裸组件第 19–24 行WithPreparedBlueBorder——显式返回{ Component: BorderDecoratorBlue }验证归一化等价性第 26–32 行WithPropsBasedBorder——props: { color: green }的静态传参形态第 33–42 行WithArgsBasedBorder——props: args把控件面板的 args 直接喂给装饰器组件第 46–55 行DecoratorsRunOnce——用play函数断言装饰器恰好执行一次验证渲染期语义。如果你的组件在.stories.svelte中编写还需在.storybook/main.js的addons中注册storybook/addon-svelte-csf与配套的 CSF 解析器参考片段 docs/_snippets/main-config-svelte-csf-register.md否则defineMeta与Story组件无法被识别。六、更进一步装饰器 Svelte context / globals除了包一层带边距的 div装饰器组件最常见的进阶用法是充当 context 提供者。宿主文档专门指出可以借助 story context 在装饰器中调用 Svelte 的setContext/getContextAPI把 Storybook 的 globals 桥接给组件树。配套片段 docs/_snippets/your-component-with-decorator-with-context.md 展示了把上文边距场景升级为 context 版本的做法——装饰器读取globals.marginSize调用setContext(marginSize, ...)下发import { setContext } from svelte; import YourComponent from ./YourComponent.svelte; import MarginDecorator from ./MarginDecorator.svelte; export default { component: YourComponent, decorators: [ (story, { globals }) { const marginSize globals.marginSize small ? small : medium; setContext(marginSize, marginSize); return { Component: MarginDecorator }; }, ], };配套的装饰器组件随之改为从 context 读取尺寸并用style属性动态设置边距script langts import { getContext, type Snippet } from svelte; interface Props { children?: Snippet; } let { children }: Props $props(); const size getContextsmall | medium(marginSize) ?? medium; const margin size small ? 1rem : 3rem; /script div stylemargin: {margin}; {render children?.()} /div注意两点细节TS 版本用getContextsmall | medium(marginSize)标注了 context 值的联合类型并通过?? medium提供缺省值避免空渲染。使用{render children?.()}的可选调用可以容忍children尚未就绪的场景比原版强制调用更健壮。这与 Svelte 5 中 snippet prop 为可选的约定一致。这样MarginDecorator就从写死 3em 边距的固定外壳进化成了由 Storybook 全局工具驱动、可实时切换布局密度的渲染组件UI 层切换marginSizeglobalStory 画布随之重排。七、控制装饰器的生效范围与执行顺序MarginDecorator既可以按上文放在组件的默认导出作用于该组件全部 Story也可以按需放在不同层级。基于 docs/writing-stories/decorators.mdxSvelte 渲染器支持三种层级Story 级作用于单个 story。Svelte CSF 中在Story组件的decoratorsprop 声明或在 CSF 具名导出上加decorators键。组件级作用于某组件全部 story。Svelte CSF 中放进defineMeta的decorators属性CSF 3 中作为默认导出的decorators键——前文两节示例均属此类。全局级作用于所有story。在.storybook/preview.js的decorators导出中声明写法参考片段 docs/_snippets/storybook-preview-global-decorator.md。这三层可以叠加使用。Story 一旦渲染所有相关装饰器按以下顺序依次执行全局装饰器按声明顺序组件级装饰器按声明顺序story 级装饰器按声明顺序从最内层开始向外逐层包裹。对MarginDecorator这类通用外壳而言最省事的做法是放到.storybook/preview.js作为全局装饰器——但要注意全局装饰器会作用于包括插件、Docs 页在内的所有渲染因此若只想影响某组件的预览布局组件级是更稳妥的选择。八、最佳实践让 Story 保持纯净渲染使用装饰器的核心理念正如宿主文档结尾所强调的尽量让 story 保持对被测组件的纯净渲染所有额外 HTML 或辅助组件都应只作为装饰器存在。以本文为例边距 div、context 注入都属于查看方式不该侵入YourComponent的实现或每个 story 的定义。这一实践还有一个实际收益Storybook 的 Source Doc Block 只有在 story 本身不被多余标记污染时生成的源代码示例才最干净、最可复用。把装饰职责收敛到MarginDecorator一处等于同时获得可维护的外壳与高质量的示例代码。小结本指南完整走通了 Svelte 渲染器下装饰器组件从编写到接入再到定制的链路MarginDecorator.svelte用 Svelte 5 runes 的$props(){render children()}定义一个可被注入 Story 内容的外壳组件通过decorators数组以() Component或() ({ Component, props })形态接入故事文件利用 story context 的parameters、globals等字段按需定制而 decorators.ts 中prepareStoryDecoratorHandler的实现则解释了这些形态如何被统一归一化、渲染为嵌套组件树。如需查阅本文全部配套代码片段与后续的 context mocking、数据注入等主题可直接深入本仓库的 docs/writing-stories/decorators.mdx 及其_snippets目录并对照渲染器测试 code/renderers/svelte/template/stories/decorators.stories.js 验证各种装饰器写法的真实行为。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考