参加直播会话:美国东部时间周四上午 11 点,Storybook 9 发布及问答
文档
Storybook Docs

组件故事格式 (CSF)

CSF 3CSF 工厂 (实验性)

这是一个实验性功能,其 API 在未来的版本中可能会发生变化(尽管可能性不大)。我们欢迎提供反馈和贡献,以帮助改进此功能。

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

本参考将提供 API 概述以及从 CSF 3 升级的迁移指南。

概述

CSF 工厂 API 由四个主要函数组成,可帮助您编写故事。请注意,其中三个函数作为工厂运行,每个函数在链中生成下一个函数(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, 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
// 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 函数定义了故事。此函数接受一个对象,其中包含名称argsparameters 以及其他故事属性。

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 升级

您可以逐步或一次性将项目的故事文件升级到 CSF 工厂。但是,在故事文件中使用 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, 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 中指定?

插件提供注释类型(parameters、globals 等)的能力是新的,并非所有插件都支持此功能。

如果插件提供注释(即它分发一个 ./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;

4. 更新您的故事文件

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

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

下面的示例显示了将故事文件从 CSF 3 升级到 CSF 工厂所需的更改。您也可以使用类似的步骤从 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 type { Meta, StoryObj } from '@storybook/your-framework';
 
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 或故事。得益于工厂函数模式,类型现在会自动推断。

4.1 重用故事属性

以前,在另一个故事中重用故事属性(例如 Story.argsStory.parameters)时,是直接访问的。虽然仍然支持这种访问方式,但在 CSF 工厂中已弃用。

现在所有故事属性都包含在一个名为 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

5. 更新您的 Vitest 设置文件

如果您正在使用Storybook 的 Vitest 插件,可以移除您的 Vitest 设置文件。

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

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

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);

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

Storybook 的 Vitest 插件允许您直接在 Storybook 中测试组件。所有故事都会自动转换为 Vitest 测试,使集成在测试套件中无缝进行。

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

Button.test.js|ts
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
- // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
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 工厂的同时,您仍然可以使用旧格式,只要它们不混杂在同一文件中。如果您想将现有文件迁移到新格式,请参阅上面的升级部分

这种格式适用于 MDX 文档页面吗?

是的,用于在 MDX 文件中引用故事的 doc blocks 支持 CSF 工厂格式,无需任何更改。

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

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