Storybook 如何用 Decorator 模拟 Context Provider 并为不同 Story 配置不同 Provider 值
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
在 Storybook 中,很多组件不是只靠 props 渲染的:主题组件从 ThemeProvider 读取当前主题,Redux 类应用通过 context 向组件提供应用数据。这类组件在 Storybook 里单独渲染时会因为缺少 Provider 而无法工作。本文的任务是:用全局 Decorator 在 Story 外面包一层 mock 的 Provider,并通过 story 的parameters让同一个 Provider 对不同 Story 提供不同的值(例如 light / dark 两套主题),避免给每个 Story 各写一份 decorator。
该写法只适用于使用 JSX 的 renderer,文档以 React 或 Solid 为例;Preact 也属于同一适用范围。下文代码以 React + TypeScript 为主路径,示例来自文档的 CSF 3 写法。
准备条件
- 项目已初始化 Storybook,存在
.storybook/preview.ts|tsx预览配置文件(该文件用于统一配置所有 stories 的渲染行为,见 configure 文档)。 - 预览文件中要写 JSX,所以文件扩展名需要是
.tsx或.jsx。文档明确提醒:如果项目设置不允许,你可能需要把 preview 文件改成.tsx/.jsx扩展名。 - 代码中
@storybook/your-framework是文档示例的占位写法,需要替换为你实际使用的框架包,文档给出的例子有react-vite、nextjs、nextjs-vite等。 - 示例中的
ThemeProvider来自styled-components;如果你使用自己的 Provider 组件,换成自己的即可,写法不变。
第一步:在全局 Decorator 中包一层 Provider
在.storybook/preview.tsx的decorators中定义一个 decorator,用它包住所有 story:
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'; import { ThemeProvider } from 'styled-components'; const preview: Preview = { decorators: [ (Story) => ( <ThemeProvider theme="default"> {/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */} <Story /> </ThemeProvider> ), ], }; export default preview;定义在 preview 文件里的 decorator 对所有stories 生效(全局 decorator)。此时所有 Story 都用同一个 Provider 值渲染——如果组件依赖 ThemeProvider 才能取到主题,到这里它已经能正常渲染了。
第二步:让 Decorator 从 parameters 读取值
如果只想给个别 Story 换一套 Provider 值,为每个 Story 单独定义 decorator 也可以,但文档指出:当你要给每个组件都创建 light 和 dark 两套主题的 stories 时,这种做法会很快变得难以维护。文档推荐的方式是利用 decorator 函数的第二个参数——story context——访问当前 story 的parameters,从而"只定义一次 Provider,按 story 调整它的值"。
把上面的 decorator 改成下面这样,从parameters.theme读取要提供的主题:
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'; import { ThemeProvider } from 'styled-components'; // themes = { light, dark } import * as themes from '../src/themes'; const preview: Preview = { decorators: [ // 👇 Defining the decorator in the preview file applies it to all stories (Story, { parameters }) => { // 👇 Make it configurable by reading the theme value from parameters const { theme = 'light' } = parameters; return ( <ThemeProvider theme={themes[theme]}> <Story /> </ThemeProvider> ); }, ], }; export default preview;执行这段代码前需要明确两处替换:
import * as themes from '../src/themes'是文档示例假设的路径:注释themes = { light, dark }表示该模块按主题名导出主题对象。实际项目中请替换为你自己导出主题对象的模块路径,导出的键名要与后面 story 里写的parameters.theme值对应(示例中是light、dark)。const { theme = 'light' } = parameters中的'light'是文档示例的默认值:story 没有配置parameters.theme时,Provider 提供themes.light。
story context 的完整结构(args、argTypes、globals、hooks、parameters、viewMode)见 Decorators 文档的 "Context" for mocking 一节。其中parameters是 story 的静态元数据,这里正好用来控制 Provider 值。
第三步:在不同 Story 上配置不同的 Provider 值
现在只需在 story 上声明一个theme参数即可切换 Provider 提供的主题。文档示例(Button.stories.ts):
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import type { Meta, StoryObj } from '@storybook/your-framework'; import { Button } from './Button'; const meta = { component: Button, } satisfies Meta<typeof Button>; export default meta; type Story = StoryObj<typeof meta>; // Wrapped in light theme export const Basic: Story = {}; // Wrapped in dark theme export const Dark: Story = { parameters: { theme: 'dark', }, };对应关系很直接:
Basic没有声明parameters.theme,走 decorator 里的默认值,文档标注它是 "Wrapped in light theme";Dark声明了parameters.theme: 'dark',decorator 读取到dark后提供themes.dark,文档标注它是 "Wrapped in dark theme"。
parameters的继承规则见 Parameters 文档:story 参数覆盖组件参数、组件参数覆盖全局参数;参数是合并的,只有同名键被覆盖,其他键保留。这意味着你只覆盖theme一个键时,全局或组件层配置的其他参数不受影响。
验证方式
在 Storybook 中打开这两个 story 做判断:Basic应在 light 主题下渲染,Dark应在 dark 主题下渲染——即各自被对应主题的 Provider 包住,这正是文档示例给出的两条注释所描述的结果。如果组件读不到 Provider 数据(报错或主题不生效),先检查两点:decorator 是否定义在 preview 文件(或 story / 组件级)并正确包裹了<Story />;story 上parameters.theme的取值是否能在themes模块中找到对应导出的键。
另外注意 decorator 的执行顺序:全局 decorators 按定义顺序执行,然后是组件级 decorators,最后是 story 级 decorators。如果你的项目里还有其他全局 decorator,Provider 包装会按上述顺序与其他包装层叠加,见 Decorators 文档。
限制与可选分支
- 按 Story 单独定义 decorator:也可以不给全局 decorator,而是在某个 story 上用
decorators键定义包装(如 button-story-decorator 示例所示)。这条路可行,但文档明确说它在"每个组件都要 light/dark 两套 stories"的场景下会迅速变得繁琐,因此本文以 parameters 方案为主路径。 - 用 globals 代替 parameters 切换值:story context 中的
globals是 Storybook 全局值,文档指出可以通过 toolbars 特性在 Storybook UI 里直接改这些值。把 decorator 中的parameters.theme换成globals.theme后,即可通过工具栏在运行时切换主题,而不需要为每个值各写一个 story。文档中 Angular、Vue、Svelte、Web Components 的 provider 示例均采用globals.theme的读法(见 decorators 文档)。 - 这套"Decorator 包 Provider + 按 story 调值"的手法不限于主题:文档原文说它同样适用于提供用户角色(user role)、mock 数据等任意值。
参考
- Mocking providers:本场景的主文档,含 "Configuring the mock provider" 一节
- Decorators:story context 结构、各级 decorator 定义与继承顺序
- Parameters:参数层级与合并规则
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考