编写 Stories #
什么是 Story? #
Story 是组件的一个特定状态或变体。每个 Story 代表组件在不同条件下的展示方式。
text
┌─────────────────────────────────────────────────────────────┐
│ Story 的概念 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 组件 = Button │
│ │
│ Stories = 组件的各种状态 │
│ │
│ ├── Primary Story (主要按钮) │
│ ├── Secondary Story (次要按钮) │
│ ├── Disabled Story (禁用按钮) │
│ ├── Loading Story (加载中按钮) │
│ └── WithIcon Story (带图标按钮) │
│ │
└─────────────────────────────────────────────────────────────┘
CSF 格式 #
CSF(Component Story Format)是 Storybook 的标准格式。
基本结构 #
javascript
// Button.stories.jsx
import Button from './Button';
// 默认导出:组件配置
export default {
title: 'Components/Button', // Story 路径
component: Button, // 关联组件
tags: ['autodocs'], // 自动文档
argTypes: { // 参数类型定义
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
};
// 命名导出:各个 Story
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
默认导出配置 #
javascript
export default {
// Story 标题和路径
title: 'Components/Button',
// 关联的组件
component: Button,
// 子组件
subcomponents: { ButtonGroup },
// 自动文档标签
tags: ['autodocs'],
// 参数类型定义
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
description: '按钮变体类型',
},
},
// 参数默认值
args: {
variant: 'primary',
size: 'medium',
},
// 装饰器
decorators: [
(Story) => (
<div style={{ padding: 20 }}>
<Story />
</div>
),
],
// 参数配置
parameters: {
layout: 'centered',
backgrounds: {
default: 'light',
},
},
};
Story 定义方式 #
对象语法(推荐) #
javascript
// Storybook 7+ 推荐的对象语法
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const WithCustomRender = {
args: {
variant: 'primary',
},
render: (args) => (
<div>
<Button {...args}>Button 1</Button>
<Button {...args}>Button 2</Button>
</div>
),
};
函数语法 #
javascript
// 旧版函数语法(仍然支持)
export const Primary = (args) => <Button {...args} />;
Primary.args = {
variant: 'primary',
children: 'Primary Button',
};
// 带装饰器
export const WithDecorator = (args) => <Button {...args} />;
WithDecorator.decorators = [
(Story) => (
<div style={{ background: '#f0f0f0', padding: 20 }}>
<Story />
</div>
),
];
模板复用 #
javascript
// 创建模板
const Template = (args) => <Button {...args} />;
// 复用模板
export const Primary = Template.bind({});
Primary.args = {
variant: 'primary',
children: 'Primary Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
variant: 'secondary',
children: 'Secondary Button',
};
Args 参数 #
基本使用 #
javascript
export const Primary = {
args: {
// 字符串
children: 'Button',
// 布尔值
disabled: false,
// 数字
count: 5,
// 对象
style: { padding: '10px 20px' },
// 数组
items: ['Item 1', 'Item 2'],
// 函数
onClick: () => console.log('clicked'),
// React 节点
icon: <Icon name="plus" />,
},
};
共享默认参数 #
javascript
export default {
title: 'Components/Button',
component: Button,
// 所有 Story 共享的默认参数
args: {
size: 'medium',
disabled: false,
},
};
// 继承默认参数
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
// size 和 disabled 继承自默认
},
};
// 覆盖默认参数
export const Large = {
args: {
variant: 'primary',
size: 'large', // 覆盖默认
children: 'Large Button',
},
};
Args 组合 #
javascript
// 从其他 Story 继承参数
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const PrimaryDisabled = {
args: {
...Primary.args,
disabled: true,
},
};
ArgTypes 参数类型 #
控件配置 #
javascript
export default {
title: 'Components/Button',
component: Button,
argTypes: {
// 下拉选择
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'warning'],
description: '按钮变体类型',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'primary' },
},
},
// 单选按钮
size: {
control: 'radio',
options: ['small', 'medium', 'large'],
},
// 行内单选
shape: {
control: 'inline-radio',
options: ['default', 'circle', 'round'],
},
// 布尔开关
disabled: {
control: 'boolean',
},
// 文本输入
children: {
control: 'text',
},
// 数字输入
count: {
control: 'number',
},
// 范围滑块
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
// 颜色选择
color: {
control: 'color',
},
// 对象编辑
style: {
control: 'object',
},
// 数组编辑
items: {
control: 'object',
},
// 日期选择
date: {
control: 'date',
},
// 文件选择
image: {
control: 'file',
accept: '.png,.jpg,.jpeg',
},
// 隐藏控件
className: {
control: false,
},
},
};
映射值 #
javascript
argTypes: {
status: {
control: 'select',
options: ['pending', 'active', 'completed'],
mapping: {
pending: { color: 'orange', icon: 'clock' },
active: { color: 'blue', icon: 'play' },
completed: { color: 'green', icon: 'check' },
},
},
},
条件显示 #
javascript
argTypes: {
icon: {
if: { arg: 'showIcon', truthy: true },
},
iconPosition: {
if: { arg: 'showIcon', truthy: true },
control: 'radio',
options: ['left', 'right'],
},
},
Decorators 装饰器 #
组件级装饰器 #
javascript
export default {
title: 'Components/Card',
component: Card,
decorators: [
// 函数装饰器
(Story) => (
<div style={{ padding: 20, background: '#f5f5f5' }}>
<Story />
</div>
),
],
};
Story 级装饰器 #
javascript
export const WithDarkBackground = {
decorators: [
(Story) => (
<div style={{ padding: 20, background: '#333' }}>
<Story />
</div>
),
],
args: {
children: 'Dark Theme',
},
};
Provider 装饰器 #
javascript
// 提供上下文
import { ThemeProvider } from '../theme';
import { I18nProvider } from '../i18n';
export default {
title: 'Components/Button',
component: Button,
decorators: [
(Story) => (
<ThemeProvider theme="light">
<I18nProvider locale="zh-CN">
<Story />
</I18nProvider>
</ThemeProvider>
),
],
};
Router 装饰器 #
javascript
import { MemoryRouter } from 'react-router-dom';
export default {
title: 'Components/NavLink',
component: NavLink,
decorators: [
(Story) => (
<MemoryRouter initialEntries={['/home']}>
<Story />
</MemoryRouter>
),
],
};
Redux 装饰器 #
javascript
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
const mockStore = configureStore({
reducer: {
user: userReducer,
},
preloadedState: {
user: { name: 'Test User', isLoggedIn: true },
},
});
export default {
title: 'Components/UserProfile',
component: UserProfile,
decorators: [
(Story) => (
<Provider store={mockStore}>
<Story />
</Provider>
),
],
};
Parameters 参数 #
布局配置 #
javascript
export default {
title: 'Components/Button',
component: Button,
parameters: {
// 布局方式
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
},
};
// Story 级别覆盖
export const FullWidth = {
parameters: {
layout: 'fullscreen',
},
};
背景配置 #
javascript
export default {
parameters: {
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
{ name: 'brand', value: '#1890ff' },
],
},
},
};
// Story 级别背景
export const OnDark = {
parameters: {
backgrounds: { default: 'dark' },
},
};
视口配置 #
javascript
export default {
parameters: {
viewport: {
defaultViewport: 'mobile1',
viewports: {
mobile1: {
name: 'Mobile (375)',
styles: { width: '375px', height: '812px' },
},
tablet: {
name: 'Tablet (768)',
styles: { width: '768px', height: '1024px' },
},
},
},
},
};
文档配置 #
javascript
export default {
parameters: {
docs: {
description: {
component: `
## Button 组件
按钮用于触发操作。
### 使用场景
- 表单提交
- 页面导航
- 触发操作
`,
},
source: {
// 源码显示方式
type: 'code', // 'code' | 'auto'
},
},
},
};
禁用插件 #
javascript
export const WithoutControls = {
parameters: {
controls: {
disable: true,
},
actions: {
disable: true,
},
},
};
Render 函数 #
自定义渲染 #
javascript
// 完全自定义渲染
export const MultipleButtons = {
render: (args) => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button {...args}>Button 1</Button>
<Button {...args}>Button 2</Button>
<Button {...args}>Button 3</Button>
</div>
),
};
// 使用 args 和 argTypes
export const DynamicRender = {
args: {
count: 3,
},
render: ({ count }) => (
<div>
{Array.from({ length: count }).map((_, i) => (
<Button key={i}>Button {i + 1}</Button>
))}
</div>
),
};
组合渲染 #
javascript
export const AllVariants = {
render: () => (
<div style={{ display: 'grid', gap: '10px' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
</div>
),
};
export const AllSizes = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
</div>
),
};
Play 函数 #
交互测试 #
javascript
import { within, userEvent, expect } from '@storybook/test';
export const InteractiveButton = {
args: {
onClick: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
// 点击按钮
await userEvent.click(button);
// 验证点击被调用
await expect(args.onClick).toHaveBeenCalled();
},
};
复杂交互 #
javascript
export const FormSubmission = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// 填写表单
await userEvent.type(canvas.getByLabelText('用户名'), 'testuser');
await userEvent.type(canvas.getByLabelText('密码'), 'password123');
// 提交表单
await userEvent.click(canvas.getByRole('button', { name: '登录' }));
// 验证结果
await expect(canvas.getByText('登录成功')).toBeInTheDocument();
},
};
Story 组合 #
扩展 Story #
javascript
// 基础 Story
export const Base = {
args: {
children: 'Base Button',
},
};
// 扩展基础 Story
export const Primary = {
...Base,
args: {
...Base.args,
variant: 'primary',
},
};
// 进一步扩展
export const PrimaryLarge = {
...Primary,
args: {
...Primary.args,
size: 'large',
},
};
Story 模板 #
javascript
// 定义模板
const createButtonStory = (variant, size = 'medium') => ({
args: {
variant,
size,
children: `${variant} ${size} button`,
},
});
// 使用模板创建多个 Story
export const PrimarySmall = createButtonStory('primary', 'small');
export const PrimaryMedium = createButtonStory('primary', 'medium');
export const PrimaryLarge = createButtonStory('primary', 'large');
export const SecondarySmall = createButtonStory('secondary', 'small');
export const SecondaryMedium = createButtonStory('secondary', 'medium');
MDX Stories #
使用 MDX #
mdx
<!-- Button.stories.mdx -->
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import Button from './Button';
<Meta title="Components/Button" component={Button} />
# Button 组件
按钮用于触发操作。
## 基础用法
<Canvas>
<Story name="Primary">
<Button variant="primary">Primary Button</Button>
</Story>
</Canvas>
## 所有变体
<Canvas>
<Story name="AllVariants">
<div style={{ display: 'flex', gap: '10px' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
</div>
</Story>
</Canvas>
## API
<ArgsTable of={Button} />
MDX 与 CSF 混合 #
javascript
// Button.stories.js
import Button from './Button';
export default {
title: 'Components/Button',
component: Button,
};
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
mdx
<!-- Button.docs.mdx -->
import { Meta, Story, Canvas } from '@storybook/addon-docs';
import Button from './Button';
<Meta title="Components/Button/Docs" />
# Button 文档
这是 Button 组件的详细文档。
<Canvas>
<Story name="Example">
<Button variant="primary">Example</Button>
</Story>
</Canvas>
最佳实践 #
1. Story 命名 #
javascript
// ✅ 好的命名
export const Primary = {};
export const WithIcon = {};
export const LoadingState = {};
export const ErrorState = {};
export const WithLongText = {};
// ❌ 不好的命名
export const Story1 = {};
export const Test = {};
export const Default = {}; // 除非是默认状态
2. 组织结构 #
javascript
export default {
title: 'Components/Button',
component: Button,
};
// 基础状态
export const Primary = {};
export const Secondary = {};
export const Tertiary = {};
// 尺寸变体
export const Small = {};
export const Medium = {};
export const Large = {};
// 状态变体
export const Loading = {};
export const Disabled = {};
// 组合示例
export const WithIcon = {};
export const WithBadge = {};
export const InGroup = {};
3. 文档描述 #
javascript
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
},
parameters: {
docs: {
description: {
story: '主要操作按钮,用于页面的主要行动点。',
},
},
},
};
4. 类型安全 #
typescript
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
下一步 #
现在你已经掌握了 Stories 的编写技巧,接下来学习 插件系统 扩展 Storybook 的功能!
最后更新:2026-03-29