组件故事格式 (CSF)
这是一个实验性功能,其 API 在未来的版本中可能会发生变化(尽管可能性不大)。我们欢迎提供反馈和贡献,以帮助改进此功能。
CSF 工厂是 Storybook 的组件故事格式 (CSF) 的下一次演进。这个新 API 使用一种称为工厂函数的模式,为 Storybook 故事提供完整的类型安全,使正确配置插件变得更容易,并释放 Storybook 功能的全部潜力。
本参考将提供 API 概述以及从 CSF 3 升级的迁移指南。
概述
CSF 工厂 API 由四个主要函数组成,可帮助您编写故事。请注意,其中三个函数作为工厂运行,每个函数在链中生成下一个函数(definePreview
→ preview.meta
→ meta.story
),从而在每个步骤提供完整的类型安全。
defineMain
使用 CSF 工厂,您的主要 Storybook 配置由 defineMain
函数指定。此函数是类型安全的,并将自动推断您的项目类型。
// 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
,以定义组件元数据。
// 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
函数用于定义故事的元数据。它接受一个对象,其中包含 component
、title
、parameters
以及其他故事属性。
// 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
函数定义了故事。此函数接受一个对象,其中包含名称、args、parameters 以及其他故事属性。
// ...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
中添加一个子路径导入。有关更多信息,请参阅子路径导入文档。
{
"imports": {
"#*": ["./*", "./*.ts", "./*.tsx"],
},
}
2. 更新您的主要 Storybook 配置文件
更新您的 .storybook/main.js|ts
文件以使用新的defineMain
函数。
// 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
导出),可以通过两种方式导入
-
对于官方 Storybook 插件,您可以导入默认导出:
import addonName from '@storybook/addon-name'
-
对于社区插件,您应该导入整个模块并从那里访问插件:
import * as addonName from 'community-addon-name'
// 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 升级。
// 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.args
或 Story.parameters
)时,是直接访问的。虽然仍然支持这种访问方式,但在 CSF 工厂中已弃用。
现在所有故事属性都包含在一个名为 composed
的新属性中,应该从该属性访问它们。例如,Story.composed.args
或 Story.composed.parameters
。
// ...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 工厂,则必须维护两个单独的设置文件。
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 工厂,您现在可以直接重用故事。
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
属性访问其组合属性(args
、parameters
等)。
以下是如何在测试文件中通过渲染组件来重用故事的示例
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 2 和 CSF 3。这些之前的格式都没有被弃用。
在使用 CSF 工厂的同时,您仍然可以使用旧格式,只要它们不混杂在同一文件中。如果您想将现有文件迁移到新格式,请参阅上面的升级部分。
这种格式适用于 MDX 文档页面吗?
是的,用于在 MDX 文件中引用故事的 doc blocks 支持 CSF 工厂格式,无需任何更改。
如何了解更多关于此格式并提供反馈?
有关此实验性格式最初提案的更多信息,请参阅其 GitHub 上的 RFC。我们欢迎您的评论!