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