Storybook 高级主题 #
高级装饰器 #
装饰器执行顺序 #
javascript
export default {
decorators: [
// 执行顺序:从外到内
(Story) => (
<div className="outer">
<Story />
</div>
),
(Story) => (
<div className="middle">
<Story />
</div>
),
(Story) => (
<div className="inner">
<Story />
</div>
),
],
};
// 渲染结果:
// <div className="outer">
// <div className="middle">
// <div className="inner">
// <Component />
// </div>
// </div>
// </div>
参数化装饰器 #
javascript
// 创建可配置的装饰器
const withPadding = (padding = 20) => (Story) => (
<div style={{ padding: `${padding}px` }}>
<Story />
</div>
);
// 使用
export default {
decorators: [withPadding(40)],
};
// Story 级别
export const SmallPadding = {
decorators: [withPadding(10)],
};
Context Provider 装饰器 #
javascript
import { ThemeProvider, ThemeContext } from '../theme';
import { UserProvider } from '../user-context';
import { RouterProvider } from '../router-context';
// 组合多个 Provider
const withProviders = (initialState = {}) => (Story) => (
<ThemeProvider initialTheme={initialState.theme}>
<UserProvider initialUser={initialState.user}>
<RouterProvider>
<Story />
</RouterProvider>
</UserProvider>
</ThemeProvider>
);
export default {
decorators: [withProviders()],
};
// 自定义初始状态
export const WithDarkTheme = {
decorators: [withProviders({ theme: 'dark' })],
};
export const WithLoggedInUser = {
decorators: [
withProviders({
user: { id: 1, name: 'Test User' },
}),
],
};
条件装饰器 #
javascript
const withConditionalDecorator = (condition, decorator) => (Story, context) => {
if (condition(context)) {
return decorator(Story, context);
}
return <Story />;
};
export default {
decorators: [
withConditionalDecorator(
(context) => context.args.darkMode,
(Story) => (
<div className="dark-theme">
<Story />
</div>
)
),
],
};
全局配置 #
Manager 配置 #
javascript
// .storybook/manager.js
import { addons } from '@storybook/manager-api';
import theme from './theme';
addons.setConfig({
// 主题
theme: theme,
// 侧边栏配置
sidebar: {
showRoots: true,
collapsedRoots: ['other'],
},
// 工具栏配置
toolbar: {
title: { hidden: false },
zoom: { hidden: false },
eject: { hidden: true },
copy: { hidden: false },
fullscreen: { hidden: false },
},
// 面板配置
panelPosition: 'bottom',
enableShortcuts: true,
// 关于页面
about: {
name: 'My Component Library',
version: '1.0.0',
githubUrl: 'https://github.com/example/components',
},
});
Preview 配置 #
javascript
// .storybook/preview.js
import '../src/styles/global.css';
export default {
// 全局参数
parameters: {
layout: 'centered',
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
options: {
storySort: {
order: [
'Introduction',
'Design System',
['Colors', 'Typography', 'Spacing'],
'Components',
['Atoms', 'Molecules', 'Organisms'],
'Pages',
],
},
},
},
// 全局装饰器
decorators: [
(Story) => (
<ThemeProvider>
<Story />
</ThemeProvider>
),
],
// 全局类型
argTypes: {
// 隐藏某些属性
className: {
table: {
disable: true,
},
},
},
// 全局标签
tags: ['autodocs'],
};
Story 排序 #
javascript
// .storybook/preview.js
export default {
parameters: {
options: {
storySort: {
// 方法一:指定顺序
order: [
'Introduction',
'Getting Started',
['Installation', 'Configuration'],
'Components',
['Button', 'Input', 'Select'],
'*',
],
// 方法二:自定义排序函数
method: 'alphabetical',
locales: 'zh-CN',
},
},
},
};
国际化 #
i18n 装饰器 #
javascript
import { I18nProvider } from '../i18n';
const withI18n = (locale = 'zh-CN') => (Story) => (
<I18nProvider locale={locale}>
<Story />
</I18nProvider>
);
export default {
decorators: [withI18n()],
};
// 不同语言的 Story
export const English = {
decorators: [withI18n('en-US')],
};
export const Chinese = {
decorators: [withI18n('zh-CN')],
};
export const Japanese = {
decorators: [withI18n('ja-JP')],
};
语言切换工具 #
javascript
// .storybook/manager.js
import { addons, types } from '@storybook/manager-api';
import { LocaleSelector } from './LocaleSelector';
addons.register('my-i18n', () => {
addons.add('i18n-selector', {
type: types.TOOL,
title: 'Language',
match: ({ viewMode }) => viewMode === 'story',
render: LocaleSelector,
});
});
javascript
// .storybook/LocaleSelector.js
import React from 'react';
import { useGlobals } from '@storybook/api';
export const LocaleSelector = () => {
const [{ locale }, updateGlobals] = useGlobals();
return (
<select
value={locale}
onChange={(e) => updateGlobals({ locale: e.target.value })}
>
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
);
};
主题定制 #
完整主题配置 #
javascript
// .storybook/theme.js
import { create } from '@storybook/theming';
export default create({
// 基础主题
base: 'light',
// 品牌颜色
colorPrimary: '#1890ff',
colorSecondary: '#52c41a',
// UI 颜色
appBg: '#f5f5f5',
appContentBg: '#ffffff',
appBorderColor: '#e8e8e8',
appBorderRadius: 8,
// 文字颜色
textColor: '#333333',
textInverseColor: '#ffffff',
// 工具栏
barTextColor: '#666666',
barSelectedColor: '#1890ff',
barBg: '#ffffff',
// 输入框
inputBg: '#ffffff',
inputBorder: '#d9d9d9',
inputTextColor: '#333333',
// 品牌信息
brandTitle: 'My Design System',
brandUrl: 'https://example.com',
brandImage: '/logo.svg',
brandTarget: '_self',
// 字体
fontBase: '"PingFang SC", "Microsoft YaHei", sans-serif',
fontCode: '"Fira Code", "Source Code Pro", monospace',
});
动态主题 #
javascript
// .storybook/manager.js
import { addons } from '@storybook/manager-api';
import { themes } from '@storybook/theming';
const getTheme = (isDark) => ({
...(isDark ? themes.dark : themes.light),
brandTitle: 'My Design System',
colorPrimary: '#1890ff',
});
addons.setConfig({
theme: getTheme(false),
});
// 监听主题变化
addons.register('theme-switcher', () => {
const channel = addons.getChannel();
channel.on('DARK_MODE', (isDark) => {
addons.setConfig({
theme: getTheme(isDark),
});
});
});
性能优化 #
懒加载 Stories #
javascript
// .storybook/main.js
export default {
stories: async (list) => [
...list,
// 动态导入
...(await import('./additional-stories')).default,
],
};
减少构建时间 #
javascript
// .storybook/main.js
export default {
// 使用 Vite 构建
core: {
builder: '@storybook/builder-vite',
},
// 禁用不需要的功能
features: {
storyStoreV7: true,
buildStoriesJson: false,
},
// 优化 Vite 配置
async viteFinal(config) {
return {
...config,
build: {
...config.build,
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
};
},
};
缓存优化 #
javascript
// .storybook/main.js
export default {
features: {
// 启用现代 Story 存储
storyStoreV7: true,
// 启用构建缓存
build: {
cache: true,
},
},
};
静态资源优化 #
javascript
// .storybook/main.js
export default {
// 静态文件目录
staticDirs: ['../public'],
async viteFinal(config) {
config.build = {
...config.build,
assetsInlineLimit: 8192,
};
return config;
},
};
组件库发布 #
构建配置 #
javascript
// .storybook/main.js
export default {
// 输出配置
docs: {
autodocs: true,
},
// 构建优化
async viteFinal(config) {
config.build = {
...config.build,
outDir: './dist',
emptyOutDir: true,
};
return config;
},
};
发布脚本 #
json
// package.json
{
"scripts": {
"build-storybook": "storybook build -o dist",
"deploy-storybook": "storybook build && gh-pages -d dist",
"publish-docs": "storybook build && npm run deploy"
}
}
版本化文档 #
javascript
// .storybook/main.js
const version = process.env.STORYBOOK_VERSION || 'latest';
export default {
docs: {
autodocs: true,
},
async viteFinal(config) {
config.define = {
...config.define,
'process.env.VERSION': JSON.stringify(version),
};
return config;
},
};
Mock 数据 #
API Mock #
javascript
// .storybook/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
])
);
}),
rest.post('/api/users', (req, res, ctx) => {
return res(
ctx.status(201),
ctx.json({ id: 3, name: 'New User' })
);
}),
];
javascript
// .storybook/preview.js
import { initialize, mswLoader } from 'msw-storybook-addon';
import { handlers } from './mocks/handlers';
initialize();
export default {
loaders: [mswLoader],
parameters: {
msw: {
handlers: {
user: handlers,
},
},
},
};
数据生成器 #
javascript
// .storybook/data/generators.js
import { faker } from '@faker-js/faker';
export const generateUser = () => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
role: faker.helpers.arrayElement(['admin', 'user', 'guest']),
});
export const generateUsers = (count = 10) =>
Array.from({ length: count }, generateUser);
export const generateProduct = () => ({
id: faker.string.uuid(),
name: faker.commerce.productName(),
price: faker.number.int({ min: 100, max: 10000 }),
description: faker.commerce.productDescription(),
image: faker.image.url(),
});
javascript
// 使用生成的数据
import { generateUsers } from '../.storybook/data/generators';
export const WithUsers = {
args: {
users: generateUsers(5),
},
};
自动化测试 #
Playwright 集成 #
javascript
// playwright.config.js
module.exports = {
use: {
baseURL: 'http://localhost:6006',
},
webServer: {
command: 'npm run storybook',
port: 6006,
timeout: 120000,
reuseExistingServer: !process.env.CI,
},
};
javascript
// tests/visual.spec.js
const { test, expect } = require('@playwright/test');
test('Button visual test', async ({ page }) => {
await page.goto('/iframe.html?id=components-button--primary');
await expect(page.locator('.storybook-wrapper')).toHaveScreenshot(
'button-primary.png'
);
});
E2E 测试 #
javascript
// tests/e2e.spec.js
const { test, expect } = require('@playwright/test');
test('Button interaction', async ({ page }) => {
await page.goto('/iframe.html?id=components-button--interactive');
const button = page.getByRole('button');
await button.click();
await expect(page.getByText('Clicked!')).toBeVisible();
});
最佳实践总结 #
1. 文件组织 #
text
.storybook/
├── main.js # 主配置
├── preview.js # 预览配置
├── manager.js # 管理器配置
├── theme.js # 主题配置
├── decorators/ # 装饰器
│ ├── withTheme.js
│ ├── withI18n.js
│ └── withRouter.js
├── mocks/ # Mock 数据
│ └── handlers.js
└── data/ # 测试数据
└── generators.js
2. 命名规范 #
javascript
// Story 文件命名
Button.stories.jsx // 组件 Stories
Button.test.jsx // 组件测试
Button.docs.mdx // 组件文档
// Story 命名
export const Primary = {};
export const Secondary = {};
export const WithIcon = {};
export const LoadingState = {};
3. 配置分层 #
javascript
// 全局配置 → 组件配置 → Story 配置
// 全局(preview.js)
export default {
parameters: { layout: 'centered' },
decorators: [withTheme],
};
// 组件
export default {
parameters: { layout: 'padded' },
decorators: [withPadding],
};
// Story
export const Custom = {
parameters: { layout: 'fullscreen' },
};
4. 文档完善 #
javascript
export default {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: `
## Button 组件
### 使用场景
- 表单提交
- 页面导航
- 触发操作
### 最佳实践
- 使用语义化的 variant
- 保持按钮文本简洁
`,
},
},
},
};
总结 #
恭喜你完成了 Storybook 完全指南的学习!你现在应该已经掌握了:
- Storybook 的基本概念和安装配置
- Stories 的编写方法和最佳实践
- 插件系统的使用和扩展
- 组件测试、交互测试和视觉测试
- 高级装饰器和全局配置
- 国际化、主题定制和性能优化
继续实践,构建你的组件库!
最后更新:2026-03-29