Docs 故事
Storybook Docs

如何撰写故事

Watch a video tutorial

故事捕获 UI 组件的渲染状态。它是一个带有注释的对象,根据一组参数描述组件的行为和外观。

¥A story captures the rendered state of a UI component. It's an object with annotations that describe the component's behavior and appearance given a set of arguments.

Storybook 在谈论 React 的 props、Vue 的 props、Angular 的 @Input 和其他类似概念时使用通用术语参数(简称 args)。

¥Storybook uses the generic term arguments (args for short) when talking about React’s props, Vue’s props, Angular’s @Input, and other similar concepts.

故事放在哪里

¥Where to put stories

组件的故事在与组件文件共存的故事文件中定义。故事文件仅用于开发,不会包含在你的生产包中。在你的文件系统中,它看起来像这样:

¥A component’s stories are defined in a story file that lives alongside the component file. The story file is for development-only, and it won't be included in your production bundle. In your filesytem, it looks something like this:

components/
├─ Button/
│  ├─ Button.js | ts | jsx | tsx | vue | svelte
│  ├─ Button.stories.js | ts | jsx | tsx

组件故事格式

¥Component Story Format

我们根据 组件故事格式 (CSF) 定义故事,这是一种基于 ES6 模块的标准,易于编写且可在工具之间移植。

¥We define stories according to the Component Story Format (CSF), an ES6 module-based standard that is easy to write and portable between tools.

关键成分是描述组件的 默认导出 和描述故事的 命名导出

¥The key ingredients are the default export that describes the component, and named exports that describe the stories.

默认导出

¥Default export

默认导出元数据控制 Storybook 如何列出你的故事并提供插件使用的信息。例如,这是故事文件 Button.stories.js|ts 的默认导出:

¥The default export metadata controls how Storybook lists your stories and provides information used by addons. For example, here’s the default export for a story file Button.stories.js|ts:

Button.stories.ts|tsx
import type { Meta } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;

从 Storybook 7.0 版开始,故事标题作为构建过程的一部分进行静态分析。默认导出必须包含一个可以静态读取的 title 属性或一个可以计算自动标题的 component 属性。使用 id 属性自定义故事 URL 也必须是静态可读的。

¥Starting with Storybook version 7.0, story titles are analyzed statically as part of the build process. The default export must contain a title property that can be read statically or a component property from which an automatic title can be computed. Using the id property to customize your story URL must also be statically readable.

定义故事

¥Defining stories

使用 CSF 文件的命名导出来定义组件的故事。我们建议你使用 UpperCamelCase 导出故事。以下是如何在“主要”状态下渲染 Button 并导出名为 Primary 的故事。

¥Use the named exports of a CSF file to define your component’s stories. We recommend you use UpperCamelCase for your story exports. Here’s how to render Button in the “primary” state and export a story called Primary.

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
};

使用 React Hooks

¥Working with React Hooks

React Hooks 是使用更简化的方法创建组件的便捷辅助方法。如果需要,你可以在创建组件的故事时使用它们,尽管你应该将它们视为高级用例。我们尽可能推荐你在编写自己的故事时使用 args。作为示例,这是一个使用 React Hooks 更改按钮状态的故事:

¥React Hooks are convenient helper methods to create components using a more streamlined approach. You can use them while creating your component's stories if you need them, although you should treat them as an advanced use case. We recommend args as much as possible when writing your own stories. As an example, here’s a story that uses React Hooks to change the button's state:

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
/*
 *👇 Render functions are a framework specific feature to allow you control on how the component renders.
 * See https://storybook.js.org/docs/api/csf
 * to learn how to use render functions.
 */
export const Primary: Story = {
  render: () => <Button primary label="Button" />,
};

重命名故事

¥Rename stories

你可以重命名你需要的任何特定故事。例如,给它一个更准确的名称。以下是如何更改 Primary 故事的名称:

¥You can rename any particular story you need. For instance, to give it a more accurate name. Here's how you can change the name of the Primary story:

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Primary: Story = {
  // 👇 Rename this story
  name: 'I am the primary',
  args: {
    label: 'Button',
    primary: true,
  },
};

你的故事现在将与给定的文本一起显示在侧边栏中。

¥Your story will now be shown in the sidebar with the given text.

如何撰写故事

¥How to write stories

故事是一个描述如何渲染组件的对象。每个组件可以有多个故事,这些故事可以相互构建。例如,我们可以根据上面的主要故事添加次要和第三级故事。

¥A story is an object that describes how to render a component. You can have multiple stories per component, and those stories can build upon one another. For example, we can add Secondary and Tertiary stories based on our Primary story from above.

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Primary: Story = {
  args: {
    backgroundColor: '#ff0',
    label: 'Button',
  },
};
 
export const Secondary: Story = {
  args: {
    ...Primary.args,
    label: '😄👍😍💯',
  },
};
 
export const Tertiary: Story = {
  args: {
    ...Primary.args,
    label: '📚📕📈🤓',
  },
};

此外,在为其他组件编写故事时,你可以导入 args 以重用,这在构建复合组件时很有帮助。例如,如果我们制作一个 ButtonGroup 故事,我们可能会从其子组件 Button 中重新混合两个故事。

¥What’s more, you can import args to reuse when writing stories for other components, and it's helpful when you’re building composite components. For example, if we make a ButtonGroup story, we might remix two stories from its child component Button.

ButtonGroup.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { ButtonGroup } from '../ButtonGroup';
 
//👇 Imports the Button stories
import * as ButtonStories from './Button.stories';
 
const meta: Meta<typeof ButtonGroup> = {
  component: ButtonGroup,
};
 
export default meta;
type Story = StoryObj<typeof ButtonGroup>;
 
export const Pair: Story = {
  args: {
    buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
    orientation: 'horizontal',
  },
};

当 Button 的签名发生变化时,你只需要更改 Button 的故事以反映新的架构,ButtonGroup 的故事将自动更新。此模式允许你在组件层次结构中重用数据定义,使你的故事更易于维护。

¥When Button’s signature changes, you only need to change Button’s stories to reflect the new schema, and ButtonGroup’s stories will automatically be updated. This pattern allows you to reuse your data definitions across the component hierarchy, making your stories more maintainable.

这还不是全部!故事函数中的每个参数都可以使用 Storybook 的 控件 面板进行实时编辑。这意味着你的团队可以动态更改 Storybook 中的组件以进行压力测试并查找边缘情况。

¥That’s not all! Each of the args from the story function are live editable using Storybook’s Controls panel. It means your team can dynamically change components in Storybook to stress test and find edge cases.

你还可以在调整其控制值后使用“控件”面板编辑或保存新故事。

¥You can also use the Controls panel to edit or save a new story after adjusting its control values.

插件可以增强参数。例如,操作 自动检测哪些参数是回调并将日志记录函数附加到它们。这样,交互(如点击)就会记录在操作面板中。

¥Addons can enhance args. For instance, Actions auto-detects which args are callbacks and appends a logging function to them. That way, interactions (like clicks) get logged in the actions panel.

使用播放函数

¥Using the play function

Storybook 的 play 函数和 @storybook/addon-interactions 是方便的辅助方法,用于测试需要用户干预的组件场景。它们是故事渲染后执行的小代码片段。例如,假设你想要验证表单组件,你可以使用 play 函数编写以下故事,以检查组件在填写信息输入时如何响应:

¥Storybook's play function and the @storybook/addon-interactions are convenient helper methods to test component scenarios that otherwise require user intervention. They're small code snippets that execute once your story renders. For example, suppose you wanted to validate a form component, you could write the following story using the play function to check how the component responds when filling in the inputs with information:

LoginForm.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { userEvent, within, expect } from '@storybook/test';
 
import { LoginForm } from './LoginForm';
 
const meta: Meta<typeof LoginForm> = {
  component: LoginForm,
};
 
export default meta;
type Story = StoryObj<typeof LoginForm>;
 
export const EmptyForm: Story = {};
 
/*
 * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const FilledForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // 👇 Simulate interactions with the component
    await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
 
    await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
 
    // See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await userEvent.click(canvas.getByRole('button'));
 
    // 👇 Assert DOM structure
    await expect(
      canvas.getByText(
        'Everything is perfect. Your account is ready and we should probably get you started!',
      ),
    ).toBeInTheDocument();
  },
};

如果没有 play 函数和 @storybook/addon-interactions 的帮助,你必须编写自己的故事并手动与组件交互以测试每个可能的用例场景。

¥Without the help of the play function and the @storybook/addon-interactions, you had to write your own stories and manually interact with the component to test out each use case scenario possible.

使用参数

¥Using parameters

参数是 Storybook 定义故事静态元数据的方法。故事的参数可用于在故事或故事组级别为各种插件提供配置。

¥Parameters are Storybook’s method of defining static metadata for stories. A story’s parameters can be used to provide configuration to various addons at the level of a story or group of stories.

例如,假设你想针对与应用中其他组件不同的一组背景测试你的按钮组件。你可以添加组件级 backgrounds 参数:

¥For instance, suppose you wanted to test your Button component against a different set of backgrounds than the other components in your app. You might add a component-level backgrounds parameter:

Button.stories.ts|tsx
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
  // 👇 Meta-level parameters
  parameters: {
    backgrounds: {
      default: 'dark',
    },
  },
};
export default meta;
 
type Story = StoryObj<typeof Button>;
 
export const Basic: Story = {};

Parameters background color

此参数将指示背景插件在选择按钮故事时重新配置自身。大多数插件都是通过基于参数的 API 配置的,并且可以在 globalcomponentstory 级别受到影响。

¥This parameter would instruct the backgrounds addon to reconfigure itself whenever a Button story is selected. Most addons are configured via a parameter-based API and can be influenced at a global, component and story level.

使用装饰器

¥Using decorators

装饰器是一种在渲染故事时将组件封装在任意标记中的机制。组件通常是在假设它们渲染的“位置”的情况下创建的。你的样式可能需要主题或布局封装器,或者你的 UI 可能需要特定的上下文或数据提供程序。

¥Decorators are a mechanism to wrap a component in arbitrary markup when rendering a story. Components are often created with assumptions about ‘where’ they render. Your styles might expect a theme or layout wrapper, or your UI might expect specific context or data providers.

一个简单的例子是向组件的故事添加填充。使用装饰器完成此操作,该装饰器将故事封装在带有填充的 div 中,如下所示:

¥A simple example is adding padding to a component’s stories. Accomplish this using a decorator that wraps the stories in a div with padding, like so:

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
  decorators: [
    (Story) => (
      <div style={{ margin: '3em' }}>
        {/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it  */}
        <Story />
      </div>
    ),
  ],
};
 
export default meta;

装饰器 可能更复杂addons 通常提供。你还可以在 storycomponentglobal 级别配置装饰器。

¥Decorators can be more complex and are often provided by addons. You can also configure decorators at the story, component and global level.

两个或更多组件的故事

¥Stories for two or more components

有时你可能需要创建两个或多个组件来协同工作。例如,如果你有一个父 List 组件,它可能需要子 ListItem 组件。

¥Sometimes you may have two or more components created to work together. For instance, if you have a parent List component, it may require child ListItem components.

List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
 
const meta: Meta<typeof List> = {
  component: List,
};
 
export default meta;
type Story = StoryObj<typeof List>;
 
//👇 Always an empty list, not super interesting
export const Empty: Story = {};

在这种情况下,为每个故事渲染不同的功能是有意义的:

¥In such cases, it makes sense to render a different function for each story:

List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
const meta: Meta<typeof List> = {
  component: List,
};
 
export default meta;
type Story = StoryObj<typeof List>;
 
export const Empty: Story = {};
 
/*
 *👇 Render functions are a framework specific feature to allow you control on how the component renders.
 * See https://storybook.js.org/docs/api/csf
 * to learn how to use render functions.
 */
export const OneItem: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem />
    </List>
  ),
};
 
export const ManyItems: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem />
      <ListItem />
      <ListItem />
    </List>
  ),
};

你还可以在 List 组件中重用子 ListItem 中的故事数据。这更容易维护,因为你不必在多个地方更新它。

¥You can also reuse story data from the child ListItem in your List component. That’s easier to maintain because you don’t have to update it in multiple places.

List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 We're importing the necessary stories from ListItem
import { Selected, Unselected } from './ListItem.stories';
 
const meta: Meta<typeof List> = {
  component: List,
};
 
export default meta;
type Story = StoryObj<typeof List>;
 
export const ManyItems: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem {...Selected.args} />
      <ListItem {...Unselected.args} />
      <ListItem {...Unselected.args} />
    </List>
  ),
};

请注意,像这样编写故事有缺点,因为你无法充分利用 args 机制并在构建更复杂的复合组件时组合 args。有关更多讨论,请参阅 多组件故事 工作流文档。

¥Note that there are disadvantages in writing stories like this as you cannot take full advantage of the args mechanism and composing args as you build even more complex composite components. For more discussion, see the multi component stories workflow documentation.