文档
Storybook 文档

Component Story Format (CSF)

CSF 3CSF 工厂 (实验性)

这是一个实验性功能,(虽然不太可能)API 在未来版本中可能会更改。我们欢迎反馈和贡献,以帮助改进此功能。

CSF 工厂是 Storybook 组件 Story Format (CSF) 的下一步演进。这个新的 API 使用一种称为工厂函数的模式,为你的 Storybook stories 提供完全的类型安全,使其更容易正确配置插件,并释放 Storybook 功能的全部潜力。

本参考将概述 API 和从 CSF 3 升级的迁移指南。

概述

CSF 工厂 API 由四个主要函数组成,以帮助你编写 stories。请注意,其中三个函数作为工厂运行,每个工厂都生成链中的下一个函数(definePreviewpreview.metameta.story),在每个步骤提供完全的类型安全。

defineMain

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

.storybook/main.js|ts
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, experimental-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 函数指定项目的 story 配置。此函数也是类型安全的,并将推断整个项目的类型。

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

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

.storybook/preview.js|ts
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, experimental-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 函数用于定义你的 stories 的元数据。它接受一个包含 componenttitleparameters 和其他 story 属性的对象。

Button.stories.js|ts
// Learn about the # subpath import: https://storybook.org.cn/docs/api/csf/csf-factories#subpath-imports
import preview from '#.storybook/preview';
 
import { Button } from './Button';
 
const meta = preview.meta({
  component: Button,
  parameters: {
    // type-safe!
    layout: 'centered',
  }
});
export default meta;

meta.story

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

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

子路径导入

CSF 工厂利用子路径导入来简化从预览文件导入构造。虽然你仍然可以使用相对路径导入,但子路径导入提供了一种更方便和可维护的方法

// ✅ Subpath imports won't break if you move story files around
import preview from '#.storybook/preview';
 
// ❌ Relative imports will break if you move story files around
import preview from '../../../.storybook/preview';

有关配置必要的子路径导入的详细信息,请参阅手动迁移步骤。

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

从 CSF 1、2 或 3 升级

你可以逐步或一次性升级项目的 story 文件到 CSF 工厂。但是,在 story 文件中使用 CSF 工厂之前,你必须升级你的 .storybook/main.js|ts.storybook/preview.js|ts 文件。

1. 在 package.json 中添加子路径导入

为了能够从项目中的任何位置一致地导入预览文件,你需要在 package.json 中添加子路径导入。有关更多信息,请参阅子路径导入文档

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

2. 更新你的主要 Storybook 配置文件

更新你的 .storybook/main.js|ts 文件以使用新的 defineMain 函数。

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

3. 更新你的预览配置文件

更新你的 .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, experimental-nextjs-vite)
+ import { definePreview } from '@storybook/your-framework';
- import { Preview } from '@storybook/your-renderer';
// 👇 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;

4. 更新您的 story 文件

Story 文件已更新,以提高可用性。使用新格式

  • 从 Storybook preview 文件导入 preview 构造
  • meta 对象现在通过 preview.meta 函数创建,并且不必作为默认导出导出
  • Stories 现在从 meta 对象创建,通过 meta.story 函数

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

Button.stories.js|ts
// Learn about the # subpath import: https://storybook.org.cn/docs/api/csf/csf-factories#subpath-imports
+ import preview from '#.storybook/preview';
- import { Meta, StoryObj } from '@storybook/your-renderer';
 
import { Button } from './Button';
 
+ const meta = preview.meta({
- const meta = {
    // ...current meta
+ });
- } satisfies Meta<typeof Button>;
- export default meta;
 
- type Story = StoryObj<typeof meta>;
 
+ export const Primary = meta.story({
- export const Primary: Story = {
    // ...current story
+ });
- };

请注意,不再需要导入或手动将任何类型应用于 meta 或 stories。 感谢工厂函数模式,现在可以自动推断类型。

4.1 重用 story 属性

以前,当在另一个 story 中重用 story 属性(例如 Story.argsStory.parameters)时,会直接访问它们。虽然仍然支持像这样访问它们,但在 CSF Factories 中已弃用。

所有 story 属性现在都包含在一个名为 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、其组件 meta 和 preview 配置组合而成的。

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

5. 更新您的 Vitest 设置文件

无论您是使用 Storybook 的 Test 插件 还是 Vitest 中的 portable stories,您都可以使用 Vitest 设置文件来配置您的 stories。此文件必须更新为使用新的 CSF Factories 格式。

请注意,这仅在您对所有测试的 stories 使用 CSF Factories 时适用。如果您混合使用 CSF 1、2 或 3 以及 CSF Factories,则必须维护两个单独的设置文件。

vitest.setup.js|ts
import { beforeAll } from 'vitest';
// 👇 No longer necessary
- import { setProjectAnnotations } from '@storybook/react';
- 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);

6. 在测试文件中重用 stories

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

如果您不能使用 Storybook Test,您仍然可以使用 portable stories 在您的测试文件中重用 stories。在之前的 story 格式中,您必须先组合 stories,然后在测试文件中渲染它们。 使用 CSF Factories,您现在可以直接重用 stories。

Button.test.js|ts
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
- import { composeStories } from '@storybook/react';
 
// 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 等)。

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

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)

我是否必须将所有 stories 迁移到这种新格式?

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

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

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

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

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

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