# Storybook

现在前端开发越来越复杂,组件越来越多,成熟的项目可能包含上百个组件,而每个组件又有很多不同的变化,调试这些组件将是非常繁重的任务。

Storybook 定义每个组件的不同状态为 story。Storybook render 这些 story,并提供很多的 addons (opens new window) 帮助你调试组件、撰写组件文档以及测试组件。

# 安装

在已有的工程里使用下面的命令安装 Storybook

$ npx storybook@latest init
1

Storybook 会根据你的工程依赖(比如你用的是 React 还是 Vue),提供最优的配置。

这个命令主要做四件事:

  • 安装依赖包,比如 @storybook/addon-essentials
  • 添加 script 命令,比如 "storybook": "storybook dev 6006"
  • 创建配置文件,在 .storybook 目录下,有两个文件 main.jspreview.js
  • 创建示例,在 src/stories 目录下

# 升级

$ npx storybook@latest upgrade
1

运行上面的命令升级 Storybook 相关的包至最新版本,同时检查是否有机会运行自动更新配置

# 自动更新配置

$ npx storybook@next automigrate
1

# Stories

本文档是在 Storybook 6.5 版本下编写,然后更新到 Storybook 7.4

Story 是一个函数,根据不同的 props 返回组件不同的 render 状态。一个组件可以定义多个 story,表示组件的多种 render 状态。 Storybook 通过 Component Story Format (opens new window) (CSF, Storybook 7.4 使用 CSF 3.0) 定义 story。

在 story 文件里,默认导出组件描述,命名导出 story 描述(推荐使用 UpperCamelCase)。

# Args

定义 story 最简单的方式是使用 Args (opens new window)。可以定义全局的、组件的、story 的 args.

// src/stories/Button.stories.jsx
import React from "react";
import { Button } from "./Button";

// 默认导出组件描述 
export default {
  component: Button
};

// 命名导出 story 描述
// More on args: https://storybook.js.org/docs/react/writing-stories/args
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',
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

Controls addon (opens new window) 通过 Args 可以让你很方便地修改组件的参数,从而方便地调试组件不同的状态

# ArgTypes

Storybook 自动从组件的代码中推断出组件参数的信息,包括参数类型、描述、默认值。

Storebook 的 addons 可以使用这些信息,比如 Controls addon (opens new window) 根据不同的参数类型,提供不同的控制组件。

同时,组件的参数信息也可以通过 ArgTypes (opens new window) 重写,例如

// Button.stories.js|jsx|ts|tsx
export default {
  title: 'Button',
  component: Button,
  argTypes: {
    label: {
      name: 'label',
      type: {
        name: 'string',
        required: true
      },
      description: 'overwritten description',
      defaultValue: 'Button',
      table: {
        type: { 
          summary: 'something short', 
          detail: 'something really really long' 
        },
        defaultValue: {
          summary: 'something short default value', 
          detail: 'something really really long default value' 
        }
      },
      control: {
        type: 'string'
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

其中 table 对应 ArgsTable,详情请参考 ArgsTable Customizing (opens new window)

control 对应 Controls addon,详情请参考 Controls Annotation (opens new window)

# Parameters

Parameters (opens new window) 是一组关于 story 的静态元数据,通常用于控制组件或者 story 的特性和 addon 的行为。

和 Args 一样可以定义全局的、组件的、 story 的 Parameters.

例如下面配置 story 的背景色,更多配置项请参考下面的 Configuration

// Button.stories.js|ts|jsx|tsx

import { Button } from './Button';

export default {
  component: Button,
};

export const Primary = {
  parameters: {
    backgrounds: {
      values: [
        { name: 'red', value: '#f00' },
        { name: 'green', value: '#0f0' },
        { name: 'blue', value: '#00f' },
      ],
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Decorators

Decorator (opens new window) 包装 story 进行额外的渲染,比如提供全局的 Context (opens new window).

和 Args、Parameters 一样可以定义全局的、组件的、 story 的 decorators. 优先级从高到低依次是 story、组件、全局 decorator,而且 decorators 数组中后面定义的 decorator 比前面定义的 decorator 优先级高。下面定义一个全局的 decorators。

// .storybook/previews.js
import { ThemeContext, themes } from './theme-context';
export const decorators = [
  Story => (
    <ThemeContext.Provider value={themes.dark}>
      <Story />
    </ThemeContext.Provider>
  )
];
1
2
3
4
5
6
7
8
9

Decorators 的第二个参数是 story context (opens new window).

# Loaders

Loaders (opens new window) 是为 story 和 decorator 加载数据的异步函数。Story 的 loaders 在 story 渲染之前运行,加载的数据通过 story 的 render context 注入到 story 中。

和 Args、Parameters、Decorators 一样可以定义全局的、组件的、 story 的 loaders.

下面通过 remote API 加载 currentUser, 然后通过组件或者 story 的第二个参数 story context (context.loaded.currentUser) 注入到 story 中。

// .storybook/preview.js
import fetch from 'node-fetch';
export const loaders = [
  async () => ({
    currentUser: await (await fetch('https://jsonplaceholder.typicode.com/users/1')).json(),
  }),
];

// Component.stories.js
export const Primary = (args, { loaded: { currentUser } }) => <Component {...args} user={currentUser} />;
1
2
3
4
5
6
7
8
9
10

# Naming components and hierarchy

下面是 storybook 的层级结构

Storybook sidebar hierarchy

可以通过设置 title 设置层级结构

// Button.stories.js|jsx

import { Button } from './Button';

export default {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'Design System/Atoms/Button',
  component: Button,
};
1
2
3
4
5
6
7
8
9
10
11
12

# Documents

Storybook 支持两种撰写文档的方法:Autodocs (opens new window)MDX (opens new window)

# Autodocs

Autodocs 是开箱即用的零配置默认文档。它将 story、文本描述、组件中的 docgen 注释、参数表和代码示例聚合在一起,生成关于组件的文档

# Overriding description

// 修改组件描述
export default {
  title: 'Example/Button',
  component: Button,
  parameters: {
    docs:
      description: {
        component: 'This is a button',
      },
    },
  },
};

// 修改 stroy 描述
Primary.parameters = {
  docs: {
    description: {
      story: 'This is a primary button',
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Doc 注释也能修改 story 的描述

/**
 * This is a large button
 */
export const Large = {
  args: {
    size: 'large',
    label: 'Button',
  },
}
1
2
3
4
5
6
7
8
9

# MDX

Storybook 默认使用 Autodocs 文档,但是当你想要自定义文档格式或者创建更加详细的文档时,可以使用 MDX。

MDX 是一个 标准文件格式 (opens new window),它结合了 Markdown 和 JSX。

可以在 MDX 中使用 Doc Blocks (opens new window) 来快速构建文档和定义 story。

📢:这里官方文档有错,Canvas 里只能有一个 Story,所以需要拆成三个 Canvas

<!-- Checkbox.stories.mdx -->
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Checkbox } from './Checkbox';

<Meta title="MDX/Checkbox" component={Checkbox} />
export const Template = (args) => <Checkbox {...args} />;

# Checkbox
With `MDX`, we can define a story for `Checkbox` right in the middle of our
Markdown documentation.

<!-- Stories -->
<Canvas>
  <Story 
    name="Unchecked"
    args={{ 
      label: 'Unchecked',
    }}>
    {Template.bind({})}
  </Story>
</Canvas>

<Canvas>
	<Story 
    name="Checked"
    args={{ 
      label: 'Unchecked', 
      checked: true,
    }}>
    {Template.bind({})}
  </Story>
</Canvas>

<Canvas>
	<Story 
    name="Secondary"
    args={{
      label: 'Secondary', 
      checked: true, 
      appearance: 'secondary',
    }}>
    {Template.bind({})}
  </Story>
</Canvas>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# Documentation-only MDX







 
 
 



// Button.stories.mdx 
import CustomMDXDocumentation from './Custom-MDX-Documentation.mdx';
export default {
  title: 'Button',
  component: Button,
  parameters: {
    docs: {
      page: CustomMDXDocumentation
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
  • 当 MDX 文件中定义了 <Meta>,但是没有定义 story 时,可以作为该组件的一个文档节点
import { Meta } from '@storybook/addon-docs';
<Meta title="Button/Intro" component={Button}  />

# This is Button introduction
1
2
3
4

# Embedding stories

通过 <Story id="" /> 嵌入其它 stories






 

<!-- MyComponent.stories.mdx -->
import { Story } from '@storybook/addon-docs';
# Some header
And Markdown here

<Story id="some--id" />
1
2
3
4
5
6

通过[title](link)链接到其它 stories 和 pages

[Go to specific story canvas](?path=/story/some--id)
[Go to specific documentation page](?path=/docs/some--id)
1
2

# Syntax Highlighting

Storybook 的 MDX 自带 Javascript, Markdown, CSS, HTML, Typescript, GraphQL 语言的语法高亮,但是要支持其它语言的语法高亮,需要自己进行扩展,比如使用 react-syntax-highlighter (opens new window).

// .storybook/preview.js
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import scss from 'react-syntax-highlighter/dist/esm/languages/prism/scss';

// Registers and enables scss language support
SyntaxHighlighter.registerLanguage('scss', scss);
1
2
3
4
5
6

也可以进行 单文件配置 (opens new window)

# Preview and build docs

package.json 文件中添加下面两个 script 来 preview 和 build 文档

{
  "scripts": {
    "storybook-docs": "start-storybook --docs --no-manager-cache",
    "build-storybook-docs": "build-storybook --docs",
  }
}
1
2
3
4
5
6

Build 生成的文件放在 storybook-static 文件夹里

# Docs configuration summary

可以在 MDX 中通过 parameters.docs 来配置文档

export default {
  parameters: {
    docs: {
      page: CustomMDXDocumentation
    }
  }
};
1
2
3
4
5
6
7

通过官方文档归纳出有以下这些配置项:

选项 说明
page 自定义文档或文档模版(在 preview.js 中),可以是一个 MDX 文件或者一个返回 React 组件的函数
description {component, story},修改组件或者 stroy 的描述
inlineStories 渲染 story 的方式:true(inline) / false (iframe)
prepareForInline 一个函数,将 story 的内容从给定的框架转换为 React 可以渲染的内容
disable 禁止 story 出现在 Docs 中
theme 文档主题色,详情请参考 Theming (opens new window)
source 用于 Source Doc Block,详情请参考 Source (opens new window)
controls 用于 Controls Doc Block,请求请参考 Controls (opens new window)
argTypes 用于 ArgTypes Doc Block
canvas 用于 Canvas Doc Block
toc 文档内容目录,详情请参考 Configure the table of contents (opens new window)
container 自定义文档容器组件,详情请参考 Customize the Docs Container (opens new window)
autodocs 配置自动生成文档,可选值为:true, false, tag,当配置tag时,通过在 story 中添加 tag: ['autodocs']为组件自动生成文档,详情请参考 Configure (opens new window)
components 修改文档中的组件,详情请参考 MDX component overrides (opens new window)
subtitle 副标题,组标题取值 Meta title 属性的最后部分,描述取自组件的 JSDoc 注释

# Configuration

Storybook 的配置文件在 .storybook 文件夹里,主要包括下面这些配置文件。

# main.js

main.js 控制 Storybook 服务器的行为,当你修改这个文件之后,必须重启服务。主要包括下面这些配置:

// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials'],
  framework: '@storybook/react',
  core: {
    builder: "@storybook/builder-webpack5",
    disableTelemetry: false, // https://storybook.js.org/docs/react/configure/telemetry#how-to-opt-out
    enableCrashReports: true
  },
  staticDirs: ["../public"],
  webpackFinal: async (config, { configType }) => {
    // Make whatever fine-grained changes you need
    // Return the altered config
    return config;
  },
  viteFinal: viteFinal(config) {},
  babel: async (options) => ({
    // Update your babel configuration here
    ...options,
  }),
  typescript: {},
  features: {},
  refs: {},
  env: (config) => ({...config, EXAMPLE_VAR: 'Example var' }),
  logLevel: 'debug'
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

通过官方文档归纳出有以下这些配置项:

更多详情请参考 Configure your Storybook project (opens new window)Main configuration (opens new window)

选项 说明
stories 确定哪些文件为 story 文件,文件的匹配使用 picomatch (opens new window) 支持的语法,详情请参考 Configure story loading (opens new window)
addons 设置 Storybook 加载的 addons (opens new window) 列表
framework 基于项目使用的框架来配置 Storybook。安装 Storybook 时,它是自动推断出工程使用的框架,目前 Storybook 主要支持这些 框架 (opens new window)
core 配置 Storybook 的内部特性,比如使用哪个构建工具,{ builder: "@storybook/builder-webpack5" }
staticDirs 设置 Storybook 要加载的静态文件的目录列表,详情请参考 Images, fonts, and assets (opens new window)
webpackFinal 定制 Webpack 的配置,详情请参考 Webpack (opens new window)
viteFinal 定制 Vite 的配置,详情请参考 Vite (opens new window)
babel 定制 Babel 的配置,详情请参考 Babel (opens new window)
typescript 定制 Typescript 的配置,详情请参考 TypeScript (opens new window)
features 启用 Storybook 额外的一些配置,详情请参考 Feature flags (opens new window)
refs 配置 Storybook composition (opens new window)
env 自定义 Storybook 环境变量,详情请参考 Environment variables (opens new window)
logLevel 控制日志输出,有这些选项:silly, verbose, info (默认), warn, error, silent
docs 配置 Storybook 自动生成文档,详情请参考 Automatic documentation and Storybook (opens new window)

# preview.js

preview.js 通过命名导出,来控制 story 怎样被渲染。可以把它当做 Storybook 的入口文件,可以添加全局样式,也可以配置全局 Parameters (opens new window)Decorator (opens new window)Loaders (opens new window)Globals (opens new window)ArgTypes (opens new window)

# 引入 CSS 文件

// .storybook/preview.js
import '../src/styles/global.css';

export default {
  parameters: {},
};
1
2
3
4
5
6

# CSS 预处理

如果需要在 Webpack 中使用 Sass, Less,可以使用 addon-styling-webpack (opens new window) 或者修改 storybook 的 webpack 配置。

# Parameters

Parameters (opens new window) 是一组关于 story 的静态元数据,通常用于控制组件或者 story 的特性和 addon 的行为。下面是 Essential addons (opens new window) 的一些配置:

// .storybook/preview.js
import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport";
import CustomMDXDocumentation from './Custom-MDX-Documentation.mdx';

export const parameters = {
  // https://storybook.js.org/docs/react/essentials/controls
  controls: {
    expanded: true,
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/
    },
    presetColors: [
      { color: "#ff4785", title: "Coral" },
      "rgba(0, 159, 183, 1)",
      "#fe4a49"
    ]
  },
  // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args
  actions: { argTypesRegex: "^on[A-Z].*" },
  // https://storybook.js.org/docs/react/essentials/viewport#configuration
  viewport: {
    viewports: INITIAL_VIEWPORTS
  },
  // https://storybook.js.org/docs/react/essentials/backgrounds#configuration
  backgrounds: {
    values: [
      { name: "red", value: "#f00" },
      { name: "green", value: "#0f0" },
      { name: "blue", value: "#00f" }
    ]
  },
  // https://storybook.js.org/docs/react/writing-docs/autodocs#with-mdx-documentation
  docs: {
    page: CustomMDXDocumentation
  },
  // https://storybook.js.org/docs/react/configure/story-layout
  layout: "fullscreen" // "centered", "fullscreen", "padded"(默认)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

通过官方文档归纳出有以下这些配置项:

选项 Addon
actions Actions (opens new window)文档 (opens new window)
backgrounds Backgrounds (opens new window)文档 (opens new window)
controls Controls (opens new window)文档 (opens new window)
docs Docs (opens new window)文档 (opens new window)
layout 文档 (opens new window)
viewport Viewport (opens new window)文档 (opens new window)
msw Mock Service Worker (opens new window)文档 (opens new window)

# GlobalTypes

Globals (opens new window) 是 Storybook 的全局输入,不特定于任何 story。它的一个用途是用于配置额外的 toolbar menus,例如下面创建一个 theme menu:

// .storybook/preview.js
export const globalTypes = {
  theme: {
    name: 'Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      icon: 'circlehollow',
      // Array of plain string values or MenuItem shape (see below)
      items: ['light', 'dark'],
      // Property that specifies if the name of the item will be displayed
      showName: true,
      // Change title based on selected value
      dynamicTitle: true,
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# preview-head.html & preview-body.html

在 Storybook 中,story 是在特定 "preview iframe" 中呈现,如果你想要在 "preview iframe" head 或者 body 中添加额外的元素,比如样式表,可以通过 preview-head.html / preview-body.html

// font
<link rel=”preload” href=”your/font” />

//js
<script src="xxx.js"></script>

// stylesheets
<link href="xx.css" rel="stylesheet" type="text/css">
<style>
  #app {}
<style>
1
2
3
4
5
6
7
8
9
10
11

# manager.js

manager.js 控制 Storybook 的 UI,详情请参考 Features and behavior (opens new window)。比如我们可以修改 Storybook UI 的 theme.

首先创建 YourTheme.js 文件

// .storybook/YourTheme.js

import { create } from '@storybook/theming';

export default create({
  base: 'light',
  brandTitle: 'My custom storybook',
  brandUrl: 'https://example.com',
  brandImage: 'https://place-hold.it/350x150',
  brandTarget: '_blank',
});
1
2
3
4
5
6
7
8
9
10
11

然后在 manager.js 引入这个 theme

// .storybook/manager.js

import { addons } from '@storybook/addons';
import yourTheme from './YourTheme';

addons.setConfig({
  theme: yourTheme,
});
1
2
3
4
5
6
7
8

# Existing problems

  • ArgTypes 没有 Array 数据类型,在 Controls addon 设置值时,默认给的值是 {},导致 Storybook crash。关于这个我提了一个 issue (opens new window).
  • 在 Canvas tab 不能查看 source code,关于这个我提了一个 discussion (opens new window).
  • 自动生成的代码还是存在很多问题

# References