Storybook 使用指南:借助 Story 的 `parameters` 配置 Mock Provider,为每个故事提供不同上下文数据
在 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.mdx、decorators.mdx 与 parameters.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(如useArgs、useGlobals);parameters:故事的静态元数据,常用于控制 Storybook 功能与插件的行为——这正是本方案的核心开关;viewMode:当前激活的窗口(如canvas、docs)。
所谓"配置 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-components 的 ThemeProvider 为例,定义一个全局装饰器:每次渲染故事前,先从 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>
);
},
],
});
需要注意两点:
- 文件扩展名:由于装饰器内含 JSX,可能需要把 preview 文件命名为
.jsx/.tsx,具体视项目构建配置而定(mocking-providers.mdx 中的 Callout 特别提示了这一点); - 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.theme 的 Basic 故事会走装饰器中的默认值 '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.docs、parameters.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 的关键路径可以归纳为三步:
- 在
.storybook/preview中定义一个全局装饰器,让它解构装饰器第二参数(story context)里的parameters,并按约定字段构造 Provider 的值; - 在每条 Story 的
parameters中声明所需取值(如theme: 'dark'),未声明的故事自然落到装饰器里的默认值; - 借助
combineParameters(parameters.ts)的逐级合并语义,在全局、组件级、Story 级按需编排默认值与覆盖值。
这种"Provider 定义一次、取值逐 Story 调整"的方式(官方文档原文表述为 flexible and maintainable),避免了为每种场景复制粘贴装饰器的样板代码,让你的故事集既覆盖全面又易于维护,也保证了被测组件在故事中保持"纯净渲染"。
关联文档与源码索引
- 主题文档(代码片段主体):configure-mock-provider-with-story-parameter.md
- 配套装饰器实现片段:mock-provider-in-preview.md
- 完整指南(英文原版 MDX):mocking-providers.mdx
- 装饰器与 story context 说明:decorators.mdx
parameters定义与用法:parameters.mdxparameters合并实现源码:code/core/src/preview-api/modules/store/parameters.ts- 参数化装饰器同类示例:decorator-parameterized-in-preview.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00