文档
Storybook 文档

组件 Story 格式 (CSF)

CSF 3CSF Next (预览版)

这是一个 预览 功能,并且(虽然不太可能)API 在未来的版本中可能会发生变化。我们 欢迎反馈 和贡献,以帮助改进此功能。

CSF Next 是 Storybook 组件故事格式 (CSF) 的下一代版本。这个新的 API 使用一种称为工厂函数(factory functions)的模式,为您的 Storybook 故事提供完整的类型安全,从而更轻松地正确配置插件并释放 Storybook 功能的全部潜力。

本参考指南提供了 API 的概述以及从先前 CSF 版本升级的迁移指南。

概览

CSF Next API 由函数组成,用于帮助您编写故事。请注意,其中三个函数作为工厂函数运行,每个函数生成链中的下一个函数(definePreviewpreview.metameta.story),在每个步骤提供完整的类型安全。

defineMain

使用 CSF Next,您的 主 Storybook 配置defineMain 函数指定。此函数是类型安全的,并将自动推断您项目的类型。

.storybook/main.js|ts
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { defineMain } from '@storybook/your-framework/node';
 
export default defineMain({
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: ['@storybook/addon-a11y'],
});

definePreview

同样,definePreview 函数指定您项目的故事配置。此函数也是类型安全的,并将推断您整个项目的类型。

重要的是,通过在此处指定插件,它们的类型将在您的项目中可用,从而实现自动完成和类型检查。

您将在故事文件中导入此函数的结果 preview,以定义组件元数据。

.storybook/preview.js|ts
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { definePreview } from '@storybook/your-framework';
import addonA11y from '@storybook/addon-a11y';
 
export default definePreview({
  // 👇 Add your addons here
  addons: [addonA11y()],
  parameters: {
    // type-safe!
    a11y: {
      options: { xpath: true },
    },
  },
});

在安装插件(通过 npx storybook add <addon-name>)或运行 storybook dev 时,预览配置将自动更新以引用 必需的插件

preview.meta

preview 对象上的 meta 函数用于定义 故事的元数据。它接受一个包含 componenttitleparameters 和其他故事属性的对象。

Button.stories.js|ts
import preview from '../.storybook/preview';
 
import { Button } from './Button';
 
const meta = preview.meta({
  component: Button,
  parameters: {
    // type-safe!
    layout: 'centered',
  }
});

如果您想在预览配置中使用绝对导入而不是相对导入(如下所示),则可以使用子路径导入或别名进行配置。

// ✅ Absolute imports won't break if you move story files around
import preview from '#.storybook/preview';
 
// ❌ Relative imports can break if you move story files around
import preview from '../../../.storybook/preview';
配置

子路径导入是 Node.js 的标准,允许您在项目中定义自定义导入路径,然后可以在整个代码库中使用它们。

要配置子路径导入,请在您的 package.json 中添加以下内容:

package.json
{
  "imports": {
    "#*": ["./*", "./*.ts", "./*.tsx"],
  },
}

有关更多信息,请参阅 子路径导入文档


或者,您可以在 构建器(Vite 或 Webpack)中配置别名

meta.story

最后,meta 对象上的 story 函数定义了故事。此函数接受一个包含 nameargsparameters 和其他故事属性的对象。

Button.stories.js|ts
// ...from above
const meta = preview.meta({ /* ... */ });
 
export const Primary = meta.story({
  args: {
    primary: true,
  },
});

<Story>.extend

您可以使用 .extend 方法基于现有故事创建新故事,可以选择覆盖或添加新属性。

属性合并详情

属性会智能合并

Button.stories.js|ts
// ...from above
const meta = preview.meta({ /* ... */ });
 
export const Example = meta.story({
  args: {
    primary: true,
    exampleArray: ['a', 'b'],
    exampleObject: { a: 'a', b: 'b' },
  },
  parameters: {
    exampleArray: ['a', 'b'],
    exampleObject: { a: 'a', b: 'b' },
  },
  tags: ['a'],
});
 
/* 
 * 👇 Final values applied:
 *    {
 *      args: {
 *        primary: true,
 *        disabled: true,
 *        exampleArray: ['c'],
 *        exampleObject: { a: 'c' }
 *      },
 *      parameters: {
 *        exampleArray: ['c'],
 *        exampleObject: { a: 'c', b: 'b' }
 *      },
 *      tags: ['a', 'b']
 *    }
 */
export const ExtendedExample = Example.extend({
  args: {
    disabled: true,
    exampleArray: ['c'],
    exampleObject: { a: 'c' },
  },
  parameters: {
    disabled: true,
    exampleArray: ['c'],
    exampleObject: { a: 'c' },
  },
  tags: ['b'],
});
Button.stories.js|ts
// ...from above
const meta = preview.meta({ /* ... */ });
 
export const Primary = meta.story({
  args: {
    primary: true,
  },
});
 
export const PrimaryDisabled = Primary.extend({
  args: {
    disabled: true,
  },
});

<Story>.test

(⚠️ 实验性)

定义 故事测试 的更符合人体工程学的方式。虽然此 API 仍处于实验阶段,但它 在 RFC 中进行了记录以收集反馈,并且必须通过 experimentalTestSyntax 功能标志 来启用。

Button.stories.js|ts
// ...from above
export const PrimaryDisabled = Primary.extend({ args: { disabled: true } });
 
// 👇 .test method: Attach tests to a story
//    The test function can run the same code as the play function
PrimaryDisabled.test('should be disabled', async ({ canvas, userEvent, args }) => {
  const button = await canvas.findByRole('button');
  await userEvent.click(button);
 
  await expect(button).toBeDisabled();
  await expect(args.onClick).not.toHaveBeenCalled();
});

升级到 CSF Next

您可以自动(从 CSF 3)或手动(从 CSF 1、2 或 3)将故事升级到 CSF Next。CSF Next 的设计理念是允许增量使用;您不必一次性升级所有故事文件。但是,您不能在同一个文件中混合使用不同的故事格式。

在继续之前,请确保您使用的是最新版本的 Storybook。您可以使用此命令自动升级您的 Storybook:

npx storybook@latest upgrade

自动

您可以使用此命令自动将项目的所有故事从 CSF 3 升级到 CSF Next:

npx storybook automigrate csf-factories

它将对您所有故事文件执行下面的所有手动升级步骤。

从 CSF 2 升级到 CSF 3

您必须使用 CSF 3 才能自动升级到 CSF Next。如果您使用的是 CSF 2,则可以首先使用此命令升级到 CSF 3:

npx storybook migrate csf-2-to-3 --glob="**/*.stories.tsx" --parser=tsx

手动

您也可以手动将项目的故事文件升级到 CSF Next。在使用 CSF Next 的故事文件之前,您必须升级您的 .storybook/main.js|ts.storybook/preview.js|ts 文件。

1. 更新您的主 Storybook 配置 f文件

将您的 .storybook/main.js|ts 文件更新为使用新的 defineMain 函数。

.storybook/main.js|ts
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
+ import { defineMain } from '@storybook/your-framework/node';
- import { StorybookConfig } from '@storybook/your-framework';
 
+ export default defineMain({
- export const config: StorybookConfig = {
    // ...config itself is unchanged
+ });
- };
- export default config;

2. 更新您的预览配置 f文件

将您的 .storybook/preview.js|ts 文件更新为使用新的 definePreview 函数。

哪些插件应在 preview 中指定?

插件提供注解类型(parametersglobals 等)的功能是新的,并非所有插件都支持。

如果插件提供注解(即,它分发了 ./preview 导出),则可以通过两种方式导入:

  1. 对于官方 Storybook 插件,导入默认导出:import addonName from '@storybook/addon-name'

  2. 对于社区插件,您应该导入整个模块,并从中访问插件:import * as addonName from 'community-addon-name'

.storybook/preview.js|ts
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
+ import { definePreview } from '@storybook/your-framework';
- import type { Preview } from '@storybook/your-framework';
// 👇 Import the addons you are using
+ import addonA11y from '@storybook/addon-a11y';
 
+ export default definePreview({
- export const preview: Preview = {
    // ...current config
    // 👇 Add your addons here
+   addons: [addonA11y()],
+ });
- };
- export default preview;

3. 更新您的故事文件

故事文件已更新以提高可用性。使用新格式:

  • 从 Storybook 预览文件中导入预览构造
  • 元数据对象现在通过 preview.meta 函数创建,并且不必导出为默认导出
  • 故事现在从元数据对象创建,通过 meta.story 函数

下面的示例显示了升级故事文件从 CSF 3 到 CSF Next 所需的更改。您也可以使用类似的步骤从 CSF 1 或 2 升级。

Button.stories.js|ts
+ import preview from '../.storybook/preview';
- import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { Button } from './Button';
 
+ const meta = preview.meta({
- const meta = {
    // ...meta object is unchanged
+ });
- } satisfies Meta<typeof Button>;
- export default meta;
 
- type Story = StoryObj<typeof meta>;
 
+ export const Primary = meta.story({
- export const Primary: Story = {
    // ...story object is unchanged
+ });
- };

请注意,导入或手动应用任何类型到元数据或故事已不再是必需的。由于工厂函数模式,类型现在会自动推断。

3.1 重用故事属性

如果您正在重用故事属性以基于现有故事创建新故事,则 <Story>.extend 方法是推荐的实现方式。

以前,在故事属性(如 Story.argsStory.parameters)被重用于另一个故事时,会直接访问它们。虽然这样访问仍然受支持,但在 CSF Next 中已被弃用。

所有故事属性现在都包含在一个名为 composed 的新属性中,应该从该属性访问。例如,Story.composed.argsStory.composed.parameters

Button.stories.js|ts
// ...rest of file
 
+ export const Primary = meta.story({
- export const Primary: Story = {
    args: { primary: true },
+ });
- };
 
+ export const PrimaryDisabled = meta.story({
- export const PrimaryDisabled: Story = {
    args: {
+     ...Primary.composed.args,
-     ...Primary.args,
      disabled: true,
    }
+ });
- };

选择“composed”这个属性名是因为其中的值是从故事、其组件元数据和预览配置组合而成的。

如果您想访问故事的直接输入,可以使用 Story.input 而不是 Story.composed

4. 更新您的 Vitest 设置文件

如果您使用 Storybook 的 Vitest 插件,则可以移除 Vitest 设置文件(.storybook/vitest.setup.ts)。

如果您在 Vitest 中使用 可移植故事,则可以使用 Vitest 设置文件来配置您的故事。此文件必须更新为使用新的 CSF Next 格式。

请注意,这仅在您将 CSF Next 用于所有经过测试的故事时适用。如果您混合使用 CSF 1、2 或 3 和 CSF Next,则必须维护两个单独的设置文件。

vitest.setup.js|ts
import { beforeAll } from 'vitest';
// 👇 No longer necessary
- // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { setProjectAnnotations } from '@storybook/your-framework';
- import * as addonAnnotations from 'my-addon/preview';
+ import preview from './.storybook/preview';
- import * as previewAnnotations from './.storybook/preview';
 
// No longer necessary
- const annotations = setProjectAnnotations([previewAnnotations, addonAnnotations]);
 
// Run Storybook's beforeAll hook
+ beforeAll(preview.composed.beforeAll);
- beforeAll(annotations.beforeAll);

5. 在测试文件中重用故事

Storybook 的 Vitest 插件 允许您直接在 Storybook 中测试您的组件。所有故事都将自动转换为 Vitest 测试,从而在您的测试套件中实现无缝集成。

如果您无法使用 Storybook Test,仍然可以使用 可移植故事 在测试文件中重用故事。在以前的故事格式中,您必须在将故事渲染到测试文件中之前组合它们。使用 CSF Next,您现在可以直接重用故事。

Button.test.js|ts
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
- import { composeStories } from '@storybook/your-framework';
 
// Import all stories from the stories file
import * as stories from './Button.stories';
 
+ const { Primary } = stories;
- const { Primary } = composeStories(stories);
 
test('renders primary button with default args', async () => {
  // The run function will mount the component and run all of Storybook's lifecycle hooks
  await Primary.run();
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});

Story 对象还提供了一个 Component 属性,使您能够使用任何您选择的方法(例如 Testing Library)来渲染组件。您还可以通过 composed 属性访问其组合的属性(argsparameters 等)。

以下是如何通过渲染其组件在测试文件中重用故事的示例:

Button.test.tsx
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
 
// Import all stories from the stories file
import * as stories from './Button.stories';
 
const { Primary, Secondary } = stories;
 
test('renders primary button with default args', async () => {
  // Access the story's component via the .Component property
  render(<Primary.Component />);
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});
 
test('renders primary button with overridden props', async () => {
  // You can override props by passing them directly to the story's component
  render(<Primary.Component>Hello world</Primary.Component>);
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

常见问题解答 (FAQ)

我是否必须将我所有的故事迁移到这个新格式?

Storybook 将在可预见的未来继续支持 CSF 1、CSF 2CSF 3。这些旧格式均未被弃用。

在使用 CSF Next 时,您仍然可以使用旧格式,只要它们不混用在同一个文件中。如果您想将现有文件迁移到新格式,请参阅 上面的升级部分

这个格式是否适用于 MDX 文档页面?

是的,用于在 MDX 文件中引用故事的 文档块 支持 CSF Next 格式,无需任何更改。

我如何了解更多关于此格式的信息并提供反馈?

有关此实验性格式的原始提案的更多信息,请参阅其 GitHub 上的 RFC。我们欢迎您的评论!