Docs
Storybook Docs

组件故事格式 (CSF)

Watch a video tutorial

组件故事格式 (CSF) 是推荐的 写故事 方式。它是基于 ES6 模块的 开放标准,可移植到 Storybook 之外。

¥Component Story Format (CSF) is the recommended way to write stories. It's an open standard based on ES6 modules that is portable beyond Storybook.

如果你的故事是用较旧的 storiesOf() 语法编写的,它在 Storybook 8.0 中已被删除,并且不再维护。我们建议将你的故事迁移到 CSF。有关更多信息,请参阅 迁移指南

¥If you have stories written in the older storiesOf() syntax, it was removed in Storybook 8.0 and is no longer maintained. We recommend migrating your stories to CSF. See the migration guide for more information.

在 CSF 中,故事和组件元数据被定义为 ES 模块。每个组件故事文件都包含一个必需的 默认导出 和一个或多个 命名导出

¥In CSF, stories and component metadata are defined as ES Modules. Every component story file consists of a required default export and one or more named exports.

默认导出

¥Default export

默认导出定义有关你的组件的元数据,包括 component 本身、它的 title(它将显示在 导航 UI 故事层次结构 中)、decorators参数

¥The default export defines metadata about your component, including the component itself, its title (where it will show up in the navigation UI story hierarchy), decorators, and parameters.

component 字段是必需的,插件使用它来自动生成 prop 表和显示其他组件元数据。title 字段是可选的,应该是唯一的(即,不会在文件之间重复使用)。

¥The component field is required and used by addons for automatic prop table generation and display of other component metadata. The title field is optional and should be unique (i.e., not re-used across files).

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta } from '@storybook/your-framework';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/configure/#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'Path/To/MyComponent',
  component: MyComponent,
  decorators: [ ... ],
  parameters: { ... },
};
 
export default meta;

有关更多示例,请参阅 写故事

¥For more examples, see writing stories.

命名故事导出

¥Named story exports

对于 CSF,文件中的每个命名导出默认都代表一个故事对象。

¥With CSF, every named export in the file represents a story object by default.

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const Basic: Story = {};
 
export const WithProp: Story = {
  render: () => <MyComponent prop="value" />,
};

导出的标识符将使用 Lodash 的 startCase 函数转换为 "start case"。例如:

¥The exported identifiers will be converted to "start case" using Lodash's startCase function. For example:

标识符转换
name名称
someName一些名称
someNAME一些名称
some_custom_NAME一些自定义名称
someName1234一些名称 1 2 3 4

我们建议所有导出名称都以大写字母开头。

¥We recommend that all export names to start with a capital letter.

故事对象可以用几个不同的字段进行注释,以定义故事级 decorators参数,也可以定义故事的 name

¥Story objects can be annotated with a few different fields to define story-level decorators and parameters, and also to define the name of the story.

Storybook 的 name 配置元素在特定情况下很有用。常见用例是带有特殊字符或 Javascript 限制词的名称。如果未指定,Storybook 默认为命名导出。

¥Storybook's name configuration element is helpful in specific circumstances. Common use cases are names with special characters or Javascript restricted words. If not specified, Storybook defaults to the named export.

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const Simple: Story = {
  decorators: [],
  name: 'So simple!',
  parameters: {},
};

参数故事输入

¥Args story inputs

从 SB 6.0 开始,故事接受名为 Args 的命名输入。Args 是 Storybook 及其插件提供(并可能由其更新)的动态数据。

¥Starting in SB 6.0, stories accept named inputs called Args. Args are dynamic data that are provided (and possibly updated by) Storybook and its addons.

考虑 Storybook 的 "按钮" 示例,该按钮记录其点击事件:

¥Consider Storybook’s "Button" example of a text button that logs its click events:

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { action } from '@storybook/addon-actions';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Basic: Story = {
  render: () => <Button label="Hello" onClick={action('clicked')} />,
};

现在考虑使用 args 重写的相同示例:

¥Now consider the same example, re-written with args:

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { action } from '@storybook/addon-actions';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Text = {
  args: {
    label: 'Hello',
    onClick: action('clicked'),
  },
  render: ({ label, onClick }) => <Button label={label} onClick={onClick} />,
};

或者更简单:

¥Or even more simply:

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 Text: Story = {
  args: {},
};

这些版本不仅比无参数版本更短、更容易编写,而且由于代码不依赖于操作插件,因此它们的可移植性也更高。

¥Not only are these versions shorter and more accessible to write than their no-args counterparts, but they are also more portable since the code doesn't depend on the actions addon specifically.

有关设置 文档操作 的更多信息,请参阅其各自的文档。

¥For more information on setting up Docs and Actions, see their respective documentation.

播放函数

¥Play function

Storybook 的 play 函数是故事在 UI 中渲染时执行的小代码片段。它们是方便的辅助方法,可帮助你测试原本不可能或需要用户干预的用例。

¥Storybook's play functions are small snippets of code executed when the story renders in the UI. They are convenient helper methods to help you test use cases that otherwise weren't possible or required user intervention.

play 函数的一个很好的用例是表单组件。使用以前的 Storybook 版本,你需要编写一组故事,并且必须与组件交互以验证它。使用 Storybook 的播放函数,你可以编写以下故事:

¥A good use case for the play function is a form component. With previous Storybook versions, you'd write your set of stories and had to interact with the component to validate it. With Storybook's play functions, you could write the following story:

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();
  },
};

当故事在 UI 中渲染时,Storybook 会执行 play 函数中定义的每个步骤并运行断言,而无需用户交互。

¥When the story renders in the UI, Storybook executes each step defined in the play function and runs the assertions without the need for user interaction.

自定义渲染函数

¥Custom render functions

从 Storybook 6.4 开始,你可以将故事编写为 JavaScript 对象,从而减少测试组件所需生成的样板代码,从而提高功能性和可用性。Render 函数是有用的方法,可让你额外控制故事的渲染方式。例如,如果你正在编写一个作为对象的故事,并且想要指定组件的渲染方式,你可以编写以下内容:

¥Starting in Storybook 6.4, you can write your stories as JavaScript objects, reducing the boilerplate code you need to generate to test your components, thus improving functionality and usability. Render functions are helpful methods to give you additional control over how the story renders. For example, if you were writing a story as an object and you wanted to specify how your component should render, you could write the following:

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Layout } from './Layout';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
// This story uses a render function to fully control how the component renders.
export const Example: Story = {
  render: () => (
    <Layout>
      <header>
        <h1>Example</h1>
      </header>
      <article>
        <MyComponent />
      </article>
    </Layout>
  ),
};

当 Storybook 加载这个故事时,它将检测 render 函数的存在,并根据定义的内容相应地调整组件渲染。

¥When Storybook loads this story, it will detect the existence of a render function and adjust the component rendering accordingly based on what's defined.

Storybook 导出与名称处理

¥Storybook export vs. name handling

Storybook 处理命名导出和 name 选项的方式略有不同。什么时候应该使用一个而不是另一个?

¥Storybook handles named exports and the name option slightly differently. When should you use one vs. the other?

Storybook 将始终使用命名导出来确定故事 ID 和 URL。

¥Storybook will always use the named export to determine the story ID and URL.

如果你指定 name 选项,它将用作 UI 中的故事显示名称。否则,它默认为命名导出,通过 Storybook 的 storyNameFromExportlodash.startCase 函数处理。

¥If you specify the name option, it will be used as the story display name in the UI. Otherwise, it defaults to the named export, processed through Storybook's storyNameFromExport and lodash.startCase functions.

MyComponent-test.js
it('should format CSF exports with sensible defaults', () => {
  const testCases = {
    name: 'Name',
    someName: 'Some Name',
    someNAME: 'Some NAME',
    some_custom_NAME: 'Some Custom NAME',
    someName1234: 'Some Name 1234',
    someName1_2_3_4: 'Some Name 1 2 3 4',
  };
  Object.entries(testCases).forEach(([key, val]) => {
    expect(storyNameFromExport(key)).toBe(val);
  });
});

当你想更改故事的名称时,请重命名 CSF 导出。它将更改故事的名称,并更改故事的 ID 和 URL。

¥When you want to change the name of your story, rename the CSF export. It will change the name of the story and also change the story's ID and URL.

最好在以下情况下使用 name 配置元素:

¥It would be best if you used the name configuration element in the following cases:

  1. 你希望名称以命名导出无法实现的方式显示在 Storybook UI 中,例如保留关键字(如 "default")、特殊字符(如表情符号)、空格/大写(除 storyNameFromExport 提供的内容外)。

    ¥You want the name to show up in the Storybook UI in a way that's not possible with a named export, e.g., reserved keywords like "default", special characters like emoji, spacing/capitalization other than what's provided by storyNameFromExport.

  2. 你希望独立于更改其显示方式来保留 Story ID。拥有稳定的 Story ID 有助于与第三方工具集成。

    ¥You want to preserve the Story ID independently from changing how it's displayed. Having stable Story IDs is helpful for integration with third-party tools.

非故事导出

¥Non-story exports

在某些情况下,你可能希望导出故事和非故事的混合(例如模拟数据)。

¥In some cases, you may want to export a mixture of stories and non-stories (e.g., mocked data).

你可以在默认导出中使用可选配置字段 includeStoriesexcludeStories 来实现这一点。你可以将它们定义为字符串或正则表达式的数组。

¥You can use the optional configuration fields includeStories and excludeStories in the default export to make this possible. You can define them as an array of strings or regular expressions.

考虑以下故事文件:

¥Consider the following story file:

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { MyComponent } from './MyComponent';
 
import someData from './data.json';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
  includeStories: ['SimpleStory', 'ComplexStory'], // 👈 Storybook loads these stories
  excludeStories: /.*Data$/, // 👈 Storybook ignores anything that contains Data
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const simpleData = { foo: 1, bar: 'baz' };
export const complexData = { foo: 1, foobar: { bar: 'baz', baz: someData } };
 
export const SimpleStory: Story = {
  args: {
    data: simpleData,
  },
};
 
export const ComplexStory: Story = {
  args: {
    data: complexData,
  },
};

当此文件在 Storybook 中渲染时,它会将 ComplexStorySimpleStory 视为故事,并忽略名为 data 的导出。

¥When this file renders in Storybook, it treats ComplexStory and SimpleStory as stories and ignores the data named exports.

对于这个特定的例子,你可以用不同的方式实现相同的结果,具体取决于方便的方式:

¥For this particular example, you could achieve the same result in different ways, depending on what's convenient:

  • includeStories: /^[A-Z]/

  • includeStories: /.*Story$/

  • includeStories: ['SimpleStory', 'ComplexStory']

  • excludeStories: /^[a-z]/

  • excludeStories: /.*Data$/

  • excludeStories: ['simpleData', 'complexData']

如果你遵循以大写字母开头故事导出的最佳实践(即使用 UpperCamelCase),则第一个选项是推荐的解决方案。

¥The first option is the recommended solution if you follow the best practice of starting story exports with an uppercase letter (i.e., use UpperCamelCase).

从 CSF 2 升级到 CSF 3

¥Upgrading from CSF 2 to CSF 3

在 CSF 2 中,命名的导出始终是实例化组件的函数,并且这些函数可以使用配置选项进行注释。例如:

¥In CSF 2, the named exports are always functions that instantiate a component, and those functions can be annotated with configuration options. For example:

CSF 2
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { Button } from './Button';
 
export default {
  title: 'Button',
  component: Button,
} as ComponentMeta<typeof Button>;
 
export const Primary: ComponentStory<typeof Button> = (args) => <Button {...args} />;
Primary.args = { primary: true };

这为按钮声明了一个主要故事,该按钮通过将 { primary: true } 展开到组件中来渲染自身。default.title 元数据说明在导航层次结构中放置故事的位置。

¥This declares a Primary story for a Button that renders itself by spreading { primary: true } into the component. The default.title metadata says where to place the story in a navigation hierarchy.

以下是 CSF 3 等效实现:

¥Here's the CSF 3 equivalent:

CSF 3
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 } };

让我们逐一了解这些变化,以了解发生了什么。

¥Let's go through the changes individually to understand what's going on.

可扩展故事对象

¥Spreadable story objects

在 CSF 3 中,命名的导出是对象,而不是函数。这使我们能够使用 JS 扩展运算符更有效地重用故事。

¥In CSF 3, the named exports are objects, not functions. This allows us to reuse stories more efficiently with the JS spread operator.

考虑对介绍示例进行以下补充,它创建了一个在深色背景下渲染的 PrimaryOnDark 故事:

¥Consider the following addition to the intro example, which creates a PrimaryOnDark story that renders against a dark background:

以下是 CSF 2 实现:

¥Here's the CSF 2 implementation:

CSF 2
export const PrimaryOnDark = Primary.bind({});
PrimaryOnDark.args = Primary.args;
PrimaryOnDark.parameters = { background: { default: 'dark' } };

Primary.bind({}) 复制故事函数,但不会复制函数上悬挂的注释,因此我们必须添加 PrimaryOnDark.args = Primary.args 来继承参数。

¥Primary.bind({}) copies the story function, but it doesn't copy the annotations hanging off the function, so we must add PrimaryOnDark.args = Primary.args to inherit the args.

在 CSF 3 中,我们可以传播 Primary 对象以继承其所有注释:

¥In CSF 3, we can spread the Primary object to carry over all its annotations:

CSF 3
export const PrimaryOnDark: Story = {
  ...Primary,
  parameters: { background: { default: 'dark' } },
};

了解更多有关 命名故事导出 的信息。

¥Learn more about named story exports.

默认渲染功能

¥Default render functions

在 CSF 3 中,你可以指定故事如何通过 render 函数渲染。我们可以通过以下步骤将 CSF 2 示例重写为 CSF 3。

¥In CSF 3, you specify how a story renders through a render function. We can rewrite a CSF 2 example to CSF 3 through the following steps.

让我们从一个简单的 CSF 2 故事功能开始:

¥Let's start with a simple CSF 2 story function:

CSF 2
// Other imports and story implementation
export const Default: ComponentStory<typeof Button> = (args) => <Button {...args} />;

现在,让我们将其重写为 CSF 3 中的故事对象,并使用显式 render 函数告诉故事如何渲染自身。与 CSF 2 一样,这让我们可以完全控制如何渲染组件甚至组件集合。

¥Now, let's rewrite it as a story object in CSF 3 with an explicit render function that tells the story how to render itself. Like CSF 2, this gives us full control of how we render a component or even a collection of components.

CSF 3 - explicit render function
// Other imports and story implementation
export const Default: Story = {
  render: (args) => <Button {...args} />,
};

了解更多有关 渲染函数 的信息。

¥Learn more about render functions.

但在 CSF 2 中,很多故事功能是相同的:获取默认导出中指定的组件并将参数传播到其中。这些故事的有趣之处不在于函数,而在于传递到函数中的参数。

¥But in CSF 2, a lot of story functions are identical: take the component specified in the default export and spread args into it. What's interesting about these stories is not the function, but the args passed into the function.

CSF 3 为每个渲染器提供默认渲染功能。如果你所做的只是将参数传播到你的组件中(这是最常见的情况),则根本不需要指定任何 render 函数:

¥CSF 3 provides default render functions for each renderer. If all you're doing is spreading args into your component—which is the most common case—you don't need to specify any render function at all:

CSF 3 - default render function
export const Default = {};

有关更多信息,请参阅有关 自定义渲染函数 的部分。

¥For more information, see the section on custom render functions.

自动生成标题

¥Generate titles automatically

最后,CSF 3 可以自动生成标题。

¥Finally, CSF 3 can automatically generate titles.

CSF 2
export default {
  title: 'components/Button',
  component: Button,
};
CSF 3
export default { component: Button };

你仍然可以像在 CSF 2 中一样指定标题,但如果你不指定标题,则可以从故事在磁盘上的路径中推断出来。有关更多信息,请参阅有关 配置故事加载 的部分。

¥You can still specify a title like in CSF 2, but if you don't specify one, it can be inferred from the story's path on disk. For more information, see the section on configuring story loading.