Livre d'histoire Feuille de chaleur¶
Storybook - Construire des UI dans l'isolement
Storybook est un atelier de façade pour construire des composants d'interface utilisateur et des pages en isolement. Il vous aide à développer et partager des états difficiles à atteindre et des cas de bord sans avoir besoin d'exécuter toute votre application. Des milliers d'équipes l'utilisent pour le développement de l'interface utilisateur, les tests et la documentation.
Sommaire¶
- [Installation] (#installation)
- [Pour commencer] (#getting-started)
- [Histoires écrites] (#writing-stories)
- [Formulaires de l'histoire] (#story-formats)
- [Contrôles et actions] (#controls--actions)
- [Addons] (#addons)
- [Configuration] (#configuration)
- [Theming] (#theming)
- [Essais] (#testing)
- [Documentation] (#documentation)
- [Déploiement] (#deployment)
- [Caractéristiques avancées] (#advanced-features)
- [Performance] (#performance)
- [Intégration] (#integration)
- [Dépannage] (#troubleshooting)
- [Meilleures pratiques] (#best-practices)
Installation¶
Démarrer rapidement¶
# Initialize Storybook in existing project
npx storybook@latest init
# Create new project with Storybook
npx create-react-app my-app
cd my-app
npx storybook@latest init
# Start Storybook
npm run storybook
Installation manuelle¶
# Install Storybook CLI
npm install -g @storybook/cli
# Initialize in project
sb init
# Or specify framework
sb init --type react
sb init --type vue3
sb init --type angular
```_
### Cadre spécifique
```bash
# React
npx storybook@latest init --type react
# Vue 3
npx storybook@latest init --type vue3
# Angular
npx storybook@latest init --type angular
# Svelte
npx storybook@latest init --type svelte
# Web Components
npx storybook@latest init --type web-components
# HTML
npx storybook@latest init --type html
```_
### Structure du projet
## Commencer
### Histoire fondamentale
```javascript
// src/components/Button/Button.stories.js
import { Button } from './Button';
export default {
title: 'Example/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
backgroundColor: { control: 'color' },
},
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};
Exemple de composant¶
// src/components/Button/Button.js
import React from 'react';
import PropTypes from 'prop-types';
import './button.css';
export const Button = ({ primary = false, backgroundColor = null, size = 'medium', label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};
Button.propTypes = {
primary: PropTypes.bool,
backgroundColor: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
Button.defaultProps = {
primary: false,
onClick: undefined,
};
Histoire de course¶
# Start development server
npm run storybook
# Build static Storybook
npm run build-storybook
# Serve built Storybook
npx http-server storybook-static
# Start with specific port
npm run storybook -- --port 9001
# Start in quiet mode
npm run storybook -- --quiet
Écrire des histoires¶
Structure de l'histoire¶
// Component.stories.js
import { Component } from './Component';
// Default export - story metadata
export default {
title: 'Path/To/Component',
component: Component,
parameters: {
// Story-level parameters
},
argTypes: {
// Control definitions
},
args: {
// Default args for all stories
},
};
// Named exports - individual stories
export const Default = {};
export const WithProps = {
args: {
prop1: 'value1',
prop2: 'value2',
},
};
export const CustomStory = {
render: (args) => <Component {...args} />,
args: {
// Story-specific args
},
};
Nom de l'histoire¶
// Organize stories with hierarchy
export default {
title: 'Design System/Components/Button',
// Creates: Design System > Components > Button
};
export default {
title: 'Pages/Dashboard/UserProfile',
// Creates: Pages > Dashboard > UserProfile
};
export default {
title: 'Example/Button',
// Creates: Example > Button
};
Composants multiples¶
// stories/Forms.stories.js
import { Input } from '../components/Input';
import { Button } from '../components/Button';
import { Form } from '../components/Form';
export default {
title: 'Forms/LoginForm',
};
export const LoginForm = () => (
<Form>
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button primary>Login</Button>
</Form>
);
export const SignupForm = () => (
<Form>
<Input placeholder="Full Name" />
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Input type="password" placeholder="Confirm Password" />
<Button primary>Sign Up</Button>
</Form>
);
Paramètres de l'histoire¶
export default {
title: 'Example/Button',
component: Button,
parameters: {
// Layout
layout: 'centered', // 'centered' | 'fullscreen' | 'padded'
// Backgrounds
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
],
},
// Viewport
viewport: {
defaultViewport: 'mobile1',
},
// Documentation
docs: {
description: {
component: 'Button component for user interactions',
},
},
},
};
Formats d'histoire¶
CCA 3.0 (actuel)¶
// Modern format with object syntax
export default {
title: 'Example/Button',
component: Button,
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
primary: false,
label: 'Button',
},
};
CCA 2.0 (Legacy)¶
// Function-based format
export default {
title: 'Example/Button',
component: Button,
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
primary: false,
label: 'Button',
};
Fonctions de rendu personnalisées¶
export const CustomRender = {
render: (args) => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<Button
{...args}
onClick={() => setCount(count + 1)}
/>
</div>
);
},
args: {
label: 'Increment',
},
};
Fonctions de jeu¶
import { userEvent, within } from '@storybook/testing-library';
export const InteractiveExample = {
args: {
label: 'Click me',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
// Simulate user interaction
await userEvent.click(button);
// Add assertions if needed
// expect(button).toHaveClass('clicked');
},
};
Contrôles et actions¶
Configuration ArgTypes¶
export default {
title: 'Example/Button',
component: Button,
argTypes: {
// Text input
label: {
control: 'text',
description: 'Button label text',
},
// Boolean
primary: {
control: 'boolean',
description: 'Primary button style',
},
// Select dropdown
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Button size',
},
// Radio buttons
variant: {
control: 'radio',
options: ['primary', 'secondary', 'danger'],
},
// Color picker
backgroundColor: {
control: 'color',
},
// Number input
borderRadius: {
control: { type: 'number', min: 0, max: 50, step: 1 },
},
// Range slider
opacity: {
control: { type: 'range', min: 0, max: 1, step: 0.1 },
},
// Object editor
style: {
control: 'object',
},
// Array editor
items: {
control: 'object',
},
// Date picker
createdAt: {
control: 'date',
},
// Disable control
onClick: {
action: 'clicked',
control: false,
},
},
};
Actions¶
import { action } from '@storybook/addon-actions';
export default {
title: 'Example/Button',
component: Button,
argTypes: {
onClick: { action: 'clicked' },
onMouseEnter: { action: 'mouse-enter' },
onMouseLeave: { action: 'mouse-leave' },
},
};
// Or in individual stories
export const WithActions = {
args: {
label: 'Button',
onClick: action('button-click'),
onMouseEnter: action('mouse-enter'),
},
};
// Multiple actions
export const MultipleActions = {
args: {
label: 'Multi Action Button',
onClick: (...args) => {
action('primary-click')(...args);
action('analytics-track')('button-clicked');
},
},
};
Contrôles avancés¶
export default {
title: 'Example/Form',
component: Form,
argTypes: {
// Conditional controls
showAdvanced: {
control: 'boolean',
},
advancedOptions: {
control: 'object',
if: { arg: 'showAdvanced', truthy: true },
},
// Custom control
theme: {
control: {
type: 'select',
labels: {
light: 'Light Theme',
dark: 'Dark Theme',
auto: 'Auto (System)',
},
},
options: ['light', 'dark', 'auto'],
},
// Multi-select
features: {
control: 'multi-select',
options: ['feature1', 'feature2', 'feature3'],
},
},
};
Addons¶
Addons essentiels¶
// .storybook/main.js
module.exports = {
addons: [
'@storybook/addon-essentials', // Includes most common addons
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-design-tokens',
],
};
// Individual addons (if not using essentials)
module.exports = {
addons: [
'@storybook/addon-controls',
'@storybook/addon-actions',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-docs',
'@storybook/addon-toolbars',
],
};
Ajout d'accessibilité¶
// .storybook/preview.js
export const parameters = {
a11y: {
// Optional selector to inspect
element: '#storybook-root',
config: {
rules: [
{
// Disable specific rules
id: 'color-contrast',
enabled: false,
},
],
},
// Show violations in action panel
manual: true,
},
};
// In stories
export const AccessibleButton = {
args: {
label: 'Accessible Button',
},
parameters: {
a11y: {
// Story-specific a11y config
config: {
rules: [
{ id: 'button-name', enabled: true },
],
},
},
},
};
Ajout d'un port de vue¶
// .storybook/preview.js
export const parameters = {
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: {
width: '375px',
height: '667px',
},
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '1024px',
},
},
desktop: {
name: 'Desktop',
styles: {
width: '1024px',
height: '768px',
},
},
},
defaultViewport: 'mobile',
},
};
Marques de conception Addon¶
// design-tokens.json
{
"colors": {
"primary": "#007bff",
"secondary": "#6c757d",
"success": "#28a745",
"danger": "#dc3545"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px"
},
"typography": {
"fontFamily": {
"primary": "Inter, sans-serif",
"mono": "Monaco, monospace"
},
"fontSize": {
"sm": "14px",
"md": "16px",
"lg": "18px",
"xl": "24px"
}
}
}
// .storybook/main.js
module.exports = {
addons: [
{
name: '@storybook/addon-design-tokens',
options: {
designTokenGlob: '**/design-tokens.json',
},
},
],
};
Addons personnalisés¶
// .storybook/addons/theme-switcher/register.js
import { addons, types } from '@storybook/addons';
import { ADDON_ID, TOOL_ID } from './constants';
import { Tool } from './Tool';
addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
type: types.TOOL,
title: 'Theme Switcher',
render: Tool,
});
});
// .storybook/addons/theme-switcher/Tool.js
import React from 'react';
import { useGlobals } from '@storybook/api';
import { IconButton } from '@storybook/components';
export const Tool = () => {
const [globals, updateGlobals] = useGlobals();
const theme = globals.theme || 'light';
const toggleTheme = () => {
updateGlobals({
theme: theme === 'light' ? 'dark' : 'light',
});
};
return (
<IconButton
key="theme-switcher"
title="Toggle theme"
onClick={toggleTheme}
>
{theme === 'light' ? '🌙' : '☀️'}
</IconButton>
);
};
Configuration¶
Configuration principale¶
// .storybook/main.js
module.exports = {
// Stories location
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)',
],
// Addons
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
// Framework
framework: {
name: '@storybook/react-vite',
options: {},
},
// Features
features: {
buildStoriesJson: true,
storyStoreV7: true,
},
// TypeScript
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
// Webpack customization
webpackFinal: async (config) => {
// Add custom webpack config
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
});
return config;
},
// Vite customization
viteFinal: async (config) => {
// Customize Vite config
config.define = {
...config.define,
global: 'globalThis',
};
return config;
},
// Documentation
docs: {
autodocs: 'tag',
},
// Static directories
staticDirs: ['../public'],
};
Aperçu de la configuration¶
// .storybook/preview.js
import '../src/index.css'; // Global styles
export const parameters = {
// Actions
actions: { argTypesRegex: '^on[A-Z].*' },
// Controls
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
expanded: true,
sort: 'requiredFirst',
},
// Layout
layout: 'centered',
// Backgrounds
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
{ name: 'gray', value: '#f8f9fa' },
],
},
// Viewport
viewport: {
viewports: {
mobile1: {
name: 'Small mobile',
styles: { width: '320px', height: '568px' },
},
mobile2: {
name: 'Large mobile',
styles: { width: '414px', height: '896px' },
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
},
},
},
// Documentation
docs: {
theme: themes.light,
source: {
state: 'open',
},
},
};
// Global decorators
export const decorators = [
(Story) => (
<div style={{ margin: '3em', fontFamily: 'Inter, sans-serif' }}>
<Story />
</div>
),
];
// Global args
export const args = {
// Default args for all stories
};
// Global arg types
export const argTypes = {
// Global arg types
};
Variables d'environnement¶
// .storybook/main.js
module.exports = {
env: (config) => ({
...config,
API_URL: 'https://api.example.com',
FEATURE_FLAG: true,
}),
webpackFinal: async (config) => {
config.plugins.push(
new webpack.DefinePlugin({
'process.env.STORYBOOK': JSON.stringify(true),
})
);
return config;
},
};
// In components
const apiUrl = process.env.STORYBOOK
? 'https://api-mock.example.com'
: process.env.API_URL;
Thèmes¶
Personnalisé Thème¶
// .storybook/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light', // 'light' | 'dark'
// Brand
brandTitle: 'My Company Storybook',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo.svg',
brandTarget: '_self',
// Colors
colorPrimary: '#FF4785',
colorSecondary: '#029CFD',
// UI
appBg: '#F6F9FC',
appContentBg: '#FFFFFF',
appBorderColor: '#E6ECF0',
appBorderRadius: 4,
// Typography
fontBase: '"Inter", sans-serif',
fontCode: 'Monaco, monospace',
// Text colors
textColor: '#2E3438',
textInverseColor: '#FFFFFF',
textMutedColor: '#798186',
// Toolbar
barTextColor: '#798186',
barSelectedColor: '#029CFD',
barBg: '#FFFFFF',
// Form
inputBg: '#FFFFFF',
inputBorder: '#E6ECF0',
inputTextColor: '#2E3438',
inputBorderRadius: 4,
});
// .storybook/manager.js
import { addons } from '@storybook/addons';
import theme from './theme';
addons.setConfig({
theme,
});
Thème sombre¶
// .storybook/dark-theme.js
import { create } from '@storybook/theming';
export default create({
base: 'dark',
brandTitle: 'My Company Storybook',
brandUrl: 'https://example.com',
brandImage: 'https://example.com/logo-white.svg',
colorPrimary: '#FF4785',
colorSecondary: '#029CFD',
appBg: '#1A1A1A',
appContentBg: '#2D2D2D',
appBorderColor: '#404040',
textColor: '#FFFFFF',
textInverseColor: '#1A1A1A',
textMutedColor: '#CCCCCC',
barTextColor: '#CCCCCC',
barSelectedColor: '#029CFD',
barBg: '#2D2D2D',
inputBg: '#404040',
inputBorder: '#666666',
inputTextColor: '#FFFFFF',
});
Changement de thème¶
// .storybook/preview.js
import { themes } from '@storybook/theming';
export const parameters = {
docs: {
theme: themes.light,
},
darkMode: {
// Override the default dark theme
dark: { ...themes.dark, appBg: 'black' },
// Override the default light theme
light: { ...themes.normal, appBg: 'white' },
// Set the initial theme
current: 'light',
// Disable the addon for specific stories
stylePreview: true,
},
};
// Global decorator for theme switching
export const decorators = [
(Story, context) => {
const theme = context.globals.theme || 'light';
return (
<div className={`theme-${theme}`}>
<Story />
</div>
);
},
];
// Global types for theme switcher
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
showName: true,
},
},
};
Essais¶
Essais visuels¶
// .storybook/test-runner.js
const { getStoryContext } = require('@storybook/test-runner');
module.exports = {
async postRender(page, context) {
const storyContext = await getStoryContext(page, context);
// Skip visual tests for specific stories
if (storyContext.parameters?.skipVisualTest) {
return;
}
// Take screenshot
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotIdentifier: context.id,
});
},
};
// In stories
export const VisualTest = {
args: {
label: 'Visual Test Button',
},
parameters: {
// Skip this story in visual tests
skipVisualTest: true,
},
};
Test d'interaction¶
// Button.stories.js
import { userEvent, within, expect } from '@storybook/test';
export const InteractionTest = {
args: {
label: 'Click me',
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Find elements
const button = canvas.getByRole('button', { name: /click me/i });
// Simulate interactions
await userEvent.click(button);
// Assertions
await expect(args.onClick).toHaveBeenCalled();
// Check DOM changes
await expect(button).toHaveClass('clicked');
// Wait for async operations
await canvas.findByText('Success!');
},
};
export const FormInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Fill form
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/password/i);
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.type(emailInput, 'user@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);
// Check results
await expect(canvas.getByText(/success/i)).toBeInTheDocument();
},
};
Essai en unité avec des histoires¶
// Button.test.js
import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';
const { Primary, Secondary, Large } = composeStories(stories);
describe('Button', () => {
test('renders primary button', () => {
render(<Primary />);
expect(screen.getByRole('button')).toHaveClass('storybook-button--primary');
});
test('renders secondary button', () => {
render(<Secondary />);
expect(screen.getByRole('button')).not.toHaveClass('storybook-button--primary');
});
test('renders large button', () => {
render(<Large />);
expect(screen.getByRole('button')).toHaveClass('storybook-button--large');
});
});
Essais d'accessibilité¶
// a11y.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';
expect.extend(toHaveNoViolations);
const { Primary, Secondary } = composeStories(stories);
describe('Button Accessibility', () => {
test('Primary button should not have accessibility violations', async () => {
const { container } = render(<Primary />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('Secondary button should not have accessibility violations', async () => {
const { container } = render(<Secondary />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Documentation¶
Documentation automatique¶
// Component with JSDoc
/**
* Primary UI component for user interaction
*/
export const Button = ({
/**
* Is this the principal call to action on the page?
*/
primary = false,
/**
* What background color to use
*/
backgroundColor,
/**
* How large should the button be?
*/
size = 'medium',
/**
* Button contents
*/
label,
/**
* Optional click handler
*/
onClick,
...props
}) => {
// Component implementation
};
// Stories with auto-generated docs
export default {
title: 'Example/Button',
component: Button,
tags: ['autodocs'], // Enable auto-documentation
parameters: {
docs: {
description: {
component: 'Button component for user interactions. Supports multiple sizes and variants.',
},
},
},
};
Documentation personnalisée¶
// Button.stories.js
export default {
title: 'Example/Button',
component: Button,
parameters: {
docs: {
description: {
component: `
# Button Component
The Button component is a fundamental UI element used throughout the application.
## Usage
\`\`\`jsx
import { Button } from './Button';
<Button primary onClick={handleClick}>
Click me
</Button>
\`\`\`
## Design Guidelines
- Use primary buttons for main actions
- Use secondary buttons for supporting actions
- Ensure sufficient color contrast for accessibility
`,
},
},
},
};
export const Primary = {
args: {
primary: true,
label: 'Button',
},
parameters: {
docs: {
description: {
story: 'Primary buttons are used for the main call-to-action on a page.',
},
},
},
};
MDX Documentation¶
<!-- Button.stories.mdx -->
import { Meta, Story, Canvas, ArgsTable, Description } from '@storybook/addon-docs';
import { Button } from './Button';
<Meta title="Example/Button" component={Button} />
# Button
<Description of={Button} />
## Examples
### Primary Button
<Canvas>
<Story name="Primary" args={{ primary: true, label: 'Button' }}>
{(args) => <Button {...args} />}
</Story>
</Canvas>
### Secondary Button
<Canvas>
<Story name="Secondary" args={{ label: 'Button' }}>
{(args) => <Button {...args} />}
</Story>
</Canvas>
## Props
<ArgsTable of={Button} />
## Usage Guidelines
- Use primary buttons sparingly, typically one per page
- Secondary buttons can be used multiple times
- Always provide meaningful labels
- Consider accessibility when choosing colors
## Code Example
```jsx
importer le bouton {} de './Button';
fonction MyComponent() {
retour (
<div>
<Button primaire onClick={() => alerte('Primary clicked!')}>
Action primaire
</Button>
<Button onClick={() => alerte('Secondary clicked!')}>
Action secondaire
</Button>
</div>
);
}
### Pages de documentation
```javascript
// .storybook/main.js
module.exports = {
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx)',
'../docs/**/*.stories.mdx', // Documentation stories
],
};
// docs/Introduction.stories.mdx
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Design System
Welcome to our design system documentation.
## Getting Started
This Storybook contains all the components, patterns, and guidelines for our design system.
### Installation
```bash
npm install @company/design-system
Usage¶
Principles¶
- Consistency - Maintain visual and functional consistency
- Accessibility - Ensure all components are accessible
- Performance - Optimize for speed and efficiency
- Flexibility - Support customization and theming
Déploiement net¶
# netlify.toml
[build]
command = "npm run build-storybook"
publish = "storybook-static"
[build.environment]
NODE_VERSION = "16"
# Deploy
npm install -g netlify-cli
netlify deploy --prod --dir=storybook-static
Déploiement de Vercel¶
// vercel.json
{
"buildCommand": "npm run build-storybook",
"outputDirectory": "storybook-static",
"framework": null
}
Pages GitHub¶
# .github/workflows/storybook.yml
name: Build and Deploy Storybook
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Storybook
run: npm run build-storybook
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-static
Déploiement Docker¶
# Dockerfile
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build-storybook
FROM nginx:alpine
COPY --from=builder /app/storybook-static /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
AWS S3 + CloudFront¶
# Build and deploy to S3
npm run build-storybook
aws s3 sync storybook-static/ s3://my-storybook-bucket --delete
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/*"
Caractéristiques avancées¶
Configuration personnalisée du paquet Web¶
// .storybook/main.js
const path = require('path');
module.exports = {
webpackFinal: async (config) => {
// Add alias
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../src'),
'@components': path.resolve(__dirname, '../src/components'),
};
// Add custom loader
config.module.rules.push({
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
],
});
// Add plugin
config.plugins.push(
new webpack.DefinePlugin({
__VERSION__: JSON.stringify(process.env.npm_package_version),
})
);
return config;
},
};
Configuration personnalisée de Babel¶
// .storybook/main.js
module.exports = {
babel: async (options) => ({
...options,
plugins: [
...options.plugins,
['babel-plugin-styled-components', { displayName: true }],
],
}),
};
Configuration spécifique à l'environnement¶
// .storybook/main.js
const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
addons: [
'@storybook/addon-essentials',
...(isDevelopment ? ['@storybook/addon-a11y'] : []),
],
features: {
storyStoreV7: !isDevelopment, // Disable in development for faster builds
},
webpackFinal: async (config) => {
if (isDevelopment) {
// Development-specific webpack config
config.optimization.minimize = false;
}
return config;
},
};
UI de gestionnaire personnalisé¶
// .storybook/manager.js
import { addons } from '@storybook/addons';
addons.setConfig({
// Show/hide panels
showPanel: true,
panelPosition: 'bottom', // 'bottom' | 'right'
// Sidebar
showNav: true,
showToolbar: true,
// Initial active panel
selectedPanel: 'storybook/controls/panel',
// Sidebar tree expansion
initialActive: 'sidebar',
sidebar: {
showRoots: false,
collapsedRoots: ['other'],
},
// Toolbar
toolbar: {
title: { hidden: false },
zoom: { hidden: false },
eject: { hidden: false },
copy: { hidden: false },
fullscreen: { hidden: false },
},
});
Rendement¶
Chargement paresseux¶
// .storybook/main.js
module.exports = {
features: {
storyStoreV7: true, // Enable lazy loading
},
stories: [
{
directory: '../src/components',
files: '**/*.stories.*',
titlePrefix: 'Components',
},
{
directory: '../src/pages',
files: '**/*.stories.*',
titlePrefix: 'Pages',
},
],
};
Optimisation des ensembles¶
// .storybook/main.js
module.exports = {
webpackFinal: async (config) => {
// Code splitting
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
};
// Tree shaking
config.optimization.usedExports = true;
config.optimization.sideEffects = false;
return config;
},
};
Optimisation de la mémoire¶
// .storybook/preview.js
export const parameters = {
// Reduce memory usage
options: {
storySort: {
method: 'alphabetical',
order: ['Introduction', 'Components', 'Pages'],
locales: 'en-US',
},
},
// Disable source code addon for better performance
docs: {
source: {
state: 'closed',
},
},
};
// Cleanup decorators
export const decorators = [
(Story, context) => {
// Cleanup on story change
React.useEffect(() => {
return () => {
// Cleanup logic
};
}, [context.id]);
return <Story />;
},
];
Intégration¶
Intégration des outils de conception¶
// Figma integration
export default {
title: 'Example/Button',
component: Button,
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/ABC123/Design-System?node-id=123%3A456',
},
},
};
// Sketch integration
export const SketchExample = {
parameters: {
design: {
type: 'sketch',
url: 'https://sketch.cloud/s/abc123',
},
},
};
// Adobe XD integration
export const XDExample = {
parameters: {
design: {
type: 'adobe-xd',
url: 'https://xd.adobe.com/view/abc123',
},
},
};
Tester l'intégration¶
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
testPathIgnorePatterns: ['/node_modules/', '/storybook-static/'],
// Test stories
testMatch: [
'<rootDir>/src/**/__tests__/**/*.(js|jsx|ts|tsx)',
'<rootDir>/src/**/?(*.)(spec|test).(js|jsx|ts|tsx)',
'<rootDir>/src/**/*.stories.test.(js|jsx|ts|tsx)',
],
};
// Chromatic integration
// .github/workflows/chromatic.yml
name: 'Chromatic'
on: push
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: yarn
- uses: chromaui/action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
Intégration CI/CD¶
# .github/workflows/storybook-tests.yml
name: Storybook Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- run: npm ci
- run: npm run build-storybook --quiet
- run: npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on http://127.0.0.1:6006 && npm run test-storybook"
Dépannage¶
Questions communes¶
# Port already in use
npm run storybook -- --port 9001
# Clear cache
rm -rf node_modules/.cache/storybook
# Webpack issues
npm run storybook -- --no-manager-cache
# TypeScript issues
npm run storybook -- --no-dll
# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm run storybook
Mode de débogage¶
# Enable debug logging
DEBUG=storybook:* npm run storybook
# Webpack debug
npm run storybook -- --debug-webpack
# Verbose output
npm run storybook -- --verbose
Problèmes de performance¶
// .storybook/main.js
module.exports = {
// Disable source maps in development
webpackFinal: async (config) => {
if (process.env.NODE_ENV === 'development') {
config.devtool = false;
}
return config;
},
// Reduce bundle size
features: {
storyStoreV7: true,
buildStoriesJson: true,
},
};
Créer des problèmes¶
# Clear all caches
rm -rf node_modules/.cache
rm -rf storybook-static
npm run build-storybook
# Check for conflicting dependencies
npm ls
# Update Storybook
npx storybook@latest upgrade
Meilleures pratiques¶
Histoire Organisation¶
- ** Structure hiérarchique**: Utiliser des patrons de noms cohérents
- Groupement logistique: composantes liées au groupe
- Désignation claire: Utiliser des noms descriptifs d'histoires
- Documentation: Inclure une documentation complète
Développement des composantes¶
- Isolation: Développer des composants isolés
- Interface Props: Conception d'interfaces prop claires et cohérentes
- Valeurs par défaut: Fournir des valeurs par défaut raisonnables
- Manipulation de l'erreur: Boîtes de bord de poignée gracieusement
Stratégie d'essai¶
- Essais visuels: Utiliser des tests de régression visuelle
- Essais d'interaction: Tester les interactions avec l'utilisateur
- ** Tests d'accessibilité** : s'assurer que les composants sont accessibles
- Essais d'unité: Logique du composant d ' essai
Optimisation des performances¶
- ** Chargement paresseux** : Utiliser story store v7 pour le chargement paresseux
- Scission du bassin: Optimiser la configuration du webpack
- Gestion de la mémoire : Nettoyer correctement les ressources
- ** Optimisation de la construction** : Minimiser les temps de construction
Collaboration d'équipe¶
- Système de conception: Maintenir un système de conception cohérent
- Documentation: Conserver la documentation à jour
- Avis de code: Récapitulation des histoires et des composants
- Automation: Automatiser les essais et le déploiement
Résumé¶
Storybook est un outil essentiel pour le développement moderne de frontend qui permet:
- Isolation des composants: Développer et tester les composants de l'assurance-chômage indépendamment
- ** Documentation visuelle** : Créer une documentation vivante pour les systèmes de conception
- Développement interactif: Construire des composants avec rétroaction en temps réel
- Test de l'intégration: Tests visuels, d'interaction et d'accessibilité
- ** Collaboration avec l'équipe** : Partager les composantes entre les équipes et les parties prenantes
- Gestion du système de conception : maintenir des modèles d'assurance-chômage cohérents
Les principaux avantages comprennent des cycles de développement plus rapides, une meilleure qualité des composants, une meilleure collaboration d'équipe et une documentation complète. Storybook prend en charge tous les principaux cadres frontend et fournit des options de personnalisation étendues via des addons et la configuration.