首页
/ Storybook 使用指南:借助 Story 的 `parameters` 配置 Mock Provider,为每个故事提供不同上下文数据

Storybook 使用指南:借助 Story 的 `parameters` 配置 Mock Provider,为每个故事提供不同上下文数据

2026-09-07 13:58:09作者:瞿蔚英Wynne

在 Storybook 中,组件往往需要从 React Context / Context Provider(如 ThemeProvider、Redux Provider、用户会话 Provider)中读取数据与配置。与其为每个组件分别编写带 Provider 的故事,更高效的做法是在 .storybook/preview 里定义一个全局可配置的 Mock Provider 装饰器(decorator),再通过每条 Story 的 parameters 字段按需切换 Provider 提供的值(例如主题、用户角色、Mock 数据)。读完本文,你将掌握"装饰器读取 parameters → Provider 动态取值"这一组合技,并用最少的重复代码为组件编写暗色/亮色主题、不同角色等场景下的全部故事。

本文内容以仓库文档中的 configure-mock-provider-with-story-parameter.md(配套 mock-provider-in-preview.md)为主体,结合 mocking-providers.mdxdecorators.mdxparameters.mdx 展开讲解。

一、问题背景:为什么组件在 Storybook 里需要 Mock Provider

许多"连接型"组件并不通过 props 接收全部数据,而是从 context provider 读取。文档 mocking-providers.mdx 中列举了典型场景:

  • styled-components 组件通过 ThemeProvider 读取主题变量;
  • Redux 通过 React Context 将应用状态提供给组件;
  • 类似的还有用户权限、国际化语言、鉴权会话等 Provider。

要在 Storybook 中渲染这些组件,就"必须"用包含所需 context 的**装饰器(decorator)**把故事包起来。装饰器是 Storybook 用来给故事"套上一层额外渲染"的机制,常见于添加 context、包裹布局等场景(参见 decorators.mdx)。

需要指出的是,Context Provider 的模拟方式仅适用于使用 JSX 的渲染器,如 React、Preact、Solid(对 Svelte、Vue 等渲染器,文档采用各自不同的 context 机制)。

二、朴素方案及其缺陷:为每条故事单独写装饰器

Mock Provider 最直接的做法是在每个故事文件里为每个故事单独声明 decorators。但如果你的目标是"为所有组件分别创建暗色、亮色主题的故事":

export const Dark = {
  decorators: [
    (Story) => (
      <ThemeProvider theme={themes.dark}>
        <Story />
      </ThemeProvider>
    ),
  ],
};

一旦组件数量增多,这种写法会迅速变得冗长、重复且难以维护——这是 mocking-providers.mdx 中明确指出的痛点。因此,官方推荐改用"定义一次、按 Story 调参"的模式。

三、关键机制:装饰器的第二个参数(Story Context)中的 parameters

要让"一个全局装饰器"按故事提供不同值,必须借助装饰器函数的第二个参数——story context(故事上下文)。依据 decorators.mdx 中的 "Context for mocking" 小节,该上下文对象包含以下常用字段:

  • args:该故事的参数(arguments),可用于在装饰器中消费部分 args;
  • argTypes:Storybook 的 argTypes,用于描述与约束 args;
  • globals:Storybook 全局级变量,配合 Toolbars 功能可在 UI 上动态切换;
  • hooks:Storybook 的 API hooks(如 useArgsuseGlobals);
  • parameters故事的静态元数据,常用于控制 Storybook 功能与插件的行为——这正是本方案的核心开关;
  • viewMode:当前激活的窗口(如 canvasdocs)。

所谓"配置 Mock Provider 由 Story 参数驱动",本质就是让装饰器读取 parameters 中约定的字段(例如 theme),再据其构造 Provider 的值。

关联内容出处:decorators.mdx 中"Context for mocking"一节明确指出,可以通过 parameters.pageLayout = 'page' 让一个装饰器按故事切换页面布局,并交叉引用了本文讲解的 "configuring the mock provider" 案例。

四、核心实现(一):在 .storybook/preview 中定义"可配置的 Mock Provider"

下面这段代码来自 mock-provider-in-preview.md,它以 styled-componentsThemeProvider 为例,定义一个全局装饰器:每次渲染故事前,先从 parameters 中取出 theme(默认 'light'),再用 themes[theme] 选中对应的主题对象包裹故事。

由于装饰器定义在 preview 文件中,它会对所有故事生效,这也是"配置一次、处处复用"的前提。

CSF 3(.storybook/preview.js|jsx

import React from 'react';

import { ThemeProvider } from 'styled-components';

// themes = { light, dark }
import * as themes from '../src/themes';

export default {
  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>
      );
    },
  ],
};

CSF 3 + TypeScript(.storybook/preview.ts|tsx

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;

CSF Next(实验性,.storybook/preview.tsx

import React from 'react';

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { definePreview } from '@storybook/your-framework';

import { ThemeProvider } from 'styled-components';

// themes = { light, dark }
import * as themes from '../src/themes';

export default definePreview({
  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>
      );
    },
  ],
});

CSF Next(实验性,.storybook/preview.jsx

import React from 'react';

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { definePreview } from '@storybook/your-framework';

import { ThemeProvider } from 'styled-components';

// themes = { light, dark }
import * as themes from '../src/themes';

export default definePreview({
  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>
      );
    },
  ],
});

需要注意两点:

  1. 文件扩展名:由于装饰器内含 JSX,可能需要把 preview 文件命名为 .jsx/.tsx,具体视项目构建配置而定(mocking-providers.mdx 中的 Callout 特别提示了这一点);
  2. TypeScript 类型占位符:示例中的 @storybook/your-framework 需要替换为你实际使用的框架包,如 @storybook/react-vite@storybook/nextjs@storybook/nextjs-vite 等。

五、核心实现(二):在 Story 中通过 parameters 配置 Mock Provider

全局装饰器就绪后,每条 Story 只需声明自己的 parameters.theme 即可切换 Provider 提供的主题。以下四段代码完整取自 configure-mock-provider-with-story-parameter.md,覆盖 CSF 3 与 CSF Next 两种写法。

CSF 3(Button.stories.js

import { Button } from './Button';

export default {
  component: Button,
};

// Wrapped in light theme
export const Basic = {};

// Wrapped in dark theme
export const Dark = {
  parameters: {
    theme: 'dark',
  },
};

没有设置 parameters.themeBasic 故事会走装饰器中的默认值 'light';而 Dark 故事通过 parameters.theme = 'dark' 被包裹进暗色主题——两者共用同一份 Button 组件与同一个全局装饰器,组件本身保持"纯净",无需感知主题逻辑。

CSF 3 + TypeScript(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',
  },
};

TypeScript 版本借助 satisfies Meta<typeof Button>meta 获得组件相关的类型约束,StoryObj<typeof meta> 则保证每条导出的 Story 形状正确。若在 .storybook/preview.tsx 中给 decorators 使用了 Preview 类型标注,那么参数名 theme 目前是自由键(custom key),仍需要你自行保证类型与实现的一致性。

CSF Next(实验性,Button.stories.ts

import preview from '../.storybook/preview';

import { Button } from './Button';

const meta = preview.meta({
  component: Button,
});

// Wrapped in light theme
export const Basic = meta.story();

// Wrapped in dark theme
export const Dark = meta.story({
  parameters: {
    theme: 'dark',
  },
});

CSF Next(实验性,Button.stories.js

import preview from '../.storybook/preview';

import { Button } from './Button';

const meta = preview.meta({
  component: Button,
});

// Wrapped in light theme
export const Basic = meta.story();

// Wrapped in dark theme
export const Dark = meta.story({
  parameters: {
    theme: 'dark',
  },
});

在实验性的 CSF Next 语法中,故事文件不再使用独立的 default export,而是从 ../.storybook/preview 引入 preview 对象,用 preview.meta({...}) 生成类型完备的 meta,再以 meta.story({...}) 声明每条故事;meta.story() 未传参即为默认配置(对应亮色),传入含 parameters.theme 的对象即为暗色变体。由于该 API 尚处于实验阶段(仓库中以 🧪 标注),生产项目仍建议优先使用成熟的 CSF 3 语法。

六、底层原理:parameters 如何被逐级合并并最终交给装饰器

理解这套模式还需知道 parameters 的生命周期:它的值可以在三个层级分别声明——全局(preview 文件)、组件级(default export)、Story 级(命名导出)。Storybook 会在渲染某条故事时把它们合并为该故事最终可见的 parameters,装饰器上下文里的 parameters 正是合并后的结果。

这一合并逻辑的实现位于 code/core/src/preview-api/modules/store/parameters.ts,核心函数 combineParameters(见 #L11-L42)的语义可以概括为:

  • 基本规则是"覆盖":后出现的参数集覆盖先出现的同名键;
  • 例外是"纯对象键递归合并":若某个键在多层参数集中都是普通对象(如 parameters.docsparameters.a11y),则该键会递归合并,而不是整体覆盖;
  • 数组键直接替换Array.isArray(value) 时直接采用新值,不做数组合并。

因此,在 Story 上写 parameters: { theme: 'dark' },会被合并进该故事的最终参数对象;当 preview 中的全局装饰器解构 const { theme = 'light' } = parameters 时,读到的就是该 Story 专属的 theme。若未来你在组件级统一指定 parameters.theme(例如给某个组件所有故事都默认暗色),再在个别 Story 上覆盖它,同样遵循上述合并顺序,Story 级优先级最高。

与之配套的是装饰器的继承与执行顺序(decorators.mdx 的 "Decorator inheritance" 一节):装饰器可在全局、组件级、Story 级三层声明,渲染时先执行全局装饰器,再执行组件级装饰器,最后按由内到外的顺序执行 Story 级装饰器。Mock Provider 采用全局装饰器,即可天然成为包裹组件与 Story 装饰器的最外层。

七、扩展:同一个技巧可驱动任意 Provider 值

"通过 parameters 配置 Mock Provider"并不局限于主题。parameters 本质是任意 JSON 序列化友好的元数据键值集合,因此该模式可以平滑推广到:

场景 Story 中的写法 装饰器内的读取逻辑
主题切换(亮/暗) parameters: { theme: 'dark' } themes[parameters.theme]
用户角色 parameters: { user: { role: 'admin' } } 构造携带该 role 的会话对象注入 Provider
页面布局 parameters: { pageLayout: 'page' } pageLayout 返回不同的外层包裹
国际化 / 语言 parameters: { locale: 'zh-CN' } 为 i18n Provider 选择对应的资源包

文档 decorators.mdx 的 "Context for mocking" 一节还给出了同族示例:装饰器读取 parameters.pageLayout,并根据 'page'/'page-mobile' 等取值决定是否包裹布局容器(示例片段见 decorator-parameterized-in-preview.md)。可见,"上下文中的 parameters → 装饰器参数化"是 Storybook 文档体系中一种通用的参数化装饰器范式。

需要留意的一点是:若 Provider 承载的是异步加载的数据(而非同步对象),则通常不属于本方案的职责范围,应转向 loaders 或页面构建章节(如 build-pages-with-storybook.mdx)中的数据 Mock 策略。

八、小结

配置 Mock Provider 的关键路径可以归纳为三步:

  1. .storybook/preview 中定义一个全局装饰器,让它解构装饰器第二参数(story context)里的 parameters,并按约定字段构造 Provider 的值;
  2. 在每条 Story 的 parameters 中声明所需取值(如 theme: 'dark'),未声明的故事自然落到装饰器里的默认值;
  3. 借助 combineParametersparameters.ts)的逐级合并语义,在全局、组件级、Story 级按需编排默认值与覆盖值。

这种"Provider 定义一次、取值逐 Story 调整"的方式(官方文档原文表述为 flexible and maintainable),避免了为每种场景复制粘贴装饰器的样板代码,让你的故事集既覆盖全面又易于维护,也保证了被测组件在故事中保持"纯净渲染"。

关联文档与源码索引

登录后查看全文
热门项目推荐
相关项目推荐