关于 storybook-setup
为前端开发团队提供 Storybook 配置指南,覆盖组件故事、MDX 文档、交互与可视化测试,以及静态发布。
主要功能
- 01Storybook 初始化与配置
- 02CSF3 组件故事
- 03MDX 组件文档
- 04交互与无障碍测试
- 05Chromatic 可视化回归测试
- 06静态构建与发布
适用场景
- 01为组件建立可交互文档
- 02测试表单交互和无障碍性
- 03发布 Storybook 静态站点
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill storybook-setup安装后在 Claude Code 中输入/storybook-setup确认已加载。
详细介绍
Storybook 配置
配置 Storybook,以全面记录和测试组件。
核心工作流
- 初始化 Storybook:配置框架
- 配置附加组件:Controls、actions、a11y
- 编写 stories:记录组件
- 添加文档:MDX 页面
- 配置测试:视觉回归测试
- 部署文档:静态托管
安装
# Initialize Storybook
npx storybook@latest init
# Or with specific framework
npx storybook@latest init --type react
npx storybook@latest init --type nextjs
npx storybook@latest init --type vue3
配置
主配置
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-designs',
'@storybook/addon-coverage',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
viteFinal: async (config) => {
// Customize Vite config
return config;
},
};
export default config;
预览配置
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import { themes } from '@storybook/theming';
import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'gray', value: '#f5f5f5' },
],
},
layout: 'centered',
docs: {
theme: themes.light,
},
a11y: {
config: {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'label', enabled: true },
],
},
},
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: { width: '375px', height: '667px' },
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
},
desktop: {
name: 'Desktop',
styles: { width: '1440px', height: '900px' },
},
},
},
},
globalTypes: {
theme: {
description: 'Global theme',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => {
const theme = context.globals.theme;
return (
<div className={theme === 'dark' ? 'dark' : ''}>
<div className="bg-white dark:bg-gray-900 p-4">
<Story />
</div>
</div>
);
},
],
};
export default preview;
编写 Stories
组件 Story 格式(CSF3)
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'A versatile button component with multiple variants and sizes.',
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost'],
description: 'Visual style variant',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'primary' },
},
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
description: 'Button size',
},
disabled: {
control: 'boolean',
description: 'Disable the button',
},
loading: {
control: 'boolean',
description: 'Show loading state',
},
children: {
control: 'text',
description: 'Button content',
},
},
args: {
onClick: fn(),
children: 'Button',
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Basic stories
export const Primary: Story = {
args: {
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
},
};
export const Outline: Story = {
args: {
variant: 'outline',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
},
};
// Size variants
export const Small: Story = {
args: {
size: 'sm',
},
};
export const Large: Story = {
args: {
size: 'lg',
},
};
// States
export const Disabled: Story = {
args: {
disabled: true,
},
};
export const Loading: Story = {
args: {
loading: true,
},
};
// With icons
export const WithIcon: Story = {
args: {
children: (
<>
<PlusIcon className="mr-2 h-4 w-4" />
Add Item
</>
),
},
};
// All variants showcase
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};
交互式 Stories
// src/components/Form/Form.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect, fn } from '@storybook/test';
import { Form } from './Form';
const meta: Meta<typeof Form> = {
title: 'Components/Form',
component: Form,
args: {
onSubmit: fn(),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const FilledForm: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Fill out the form
const emailInput = canvas.getByLabelText('Email');
await userEvent.type(emailInput, '[email protected]', { delay: 50 });
const passwordInput = canvas.getByLabelText('Password');
await userEvent.type(passwordInput, 'password123', { delay: 50 });
// Submit the form
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
// Assert the form was submitted
await expect(args.onSubmit).toHaveBeenCalled();
},
};
export const ValidationError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Submit without filling
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
// Check for error messages
await expect(canvas.getByText('Email is required')).toBeInTheDocument();
},
};
MDX 文档
{/* src/components/Button/Button.mdx */}
import { Meta, Story, Canvas, Controls, ArgTypes } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
import { Button } from './Button';
<Meta of={ButtonStories} />
# Button
The Button component is used to trigger actions or navigation.
## Import
```tsx
import { Button } from '@/components/Button';
用法
变体
按钮有四种变体,用于表达不同程度的强调。
主要
用于页面上作为主要号召性用语的主要操作。
次要
用于补充主要操作的次要操作。
描边
用于第三级操作,或希望视觉强调程度较低的场景。
幽灵
用于导航或非常轻量的操作。
尺寸
小
中
大
状态
已禁用
加载中
属性
无障碍
- 按钮使用原生
<button>元素 - 加载状态会向屏幕阅读器播报
- 焦点状态清晰可见
- 已禁用的按钮会保留正确的 ARIA 属性
设计指南
- 使用描述性按钮文本
- 每个部分最多使用一个主要按钮
- 按钮文本应简洁(2-4 个词)
- 使用图标强化含义,而非替代文本
## Testing Integration
### Visual Regression with Chromatic
```typescript
// .storybook/main.ts
const config: StorybookConfig = {
addons: [
'@chromatic-com/storybook',
],
};
// package.json
{
"scripts": {
"chromatic": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
}
}
测试运行器
npm install @storybook/test-runner -D
// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
import { injectAxe, checkA11y } from 'axe-playwright';
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page, context) {
// Run accessibility tests
const storyContext = await getStoryContext(page, context);
if (!storyContext.parameters?.a11y?.disable) {
await checkA11y(page, '#storybook-root', {
detailedReport: true,
detailedReportOptions: { html: true },
});
}
},
};
export default config;
// package.json
{
"scripts": {
"test-storybook": "test-storybook",
"test-storybook:ci": "test-storybook --ci"
}
}
设计令牌集成
// .storybook/preview.ts
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/styles/theme';
const preview: Preview = {
decorators: [
(Story) => (
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
),
],
};
// src/styles/theme.ts (document in Storybook)
export const theme = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
radii: {
sm: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
full: '9999px',
},
};
发布
静态导出
// package.json
{
"scripts": {
"build-storybook": "storybook build",
"storybook:serve": "npx http-server storybook-static"
}
}
GitHub Pages
# .github/workflows/storybook.yml
name: Deploy Storybook
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build-storybook
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-static
最佳实践
- 每个文件放一个组件:组织清晰
- 使用 autodocs:生成文档
- 添加 controls:支持交互式探索
- 加入 a11y 附加组件:进行无障碍测试
- 编写 play 函数:进行交互式测试
- 使用 MDX 编写文档:提供丰富的文档
- 使用 decorators:保持上下文一致
- 进行视觉回归测试:捕捉 UI 变化
输出检查清单
每个 Storybook 配置都应包括:
- 包含附加组件的主配置
- 带有全局 decorators 的预览配置
- 使用 CSF3 格式编写的 stories
- 已启用 autodocs
- 为所有属性配置 controls
- 无障碍附加组件
- 深色模式支持
- 视口预设
- MDX 文档
- 测试运行器配置
- CI 部署
- 静态构建脚本
评论
支持 Markdown