组件故事格式 (CSF)
组件故事格式 (CSF) 是编写故事的推荐方式。它是一种基于 ES6 模块的开放标准,可移植到 Storybook 之外。
如果您使用较旧的storiesOf()
语法编写故事,该语法已在 Storybook 8.0 中移除且不再维护。我们建议将您的故事迁移到 CSF。请参阅迁移指南了解更多信息。
在 CSF 中,故事和组件元数据被定义为 ES 模块。每个组件故事文件都包含一个必需的默认导出和一个或多个命名导出。
默认导出
默认导出定义了组件的元数据,包括component
本身、它的title
(在导航界面故事层级中显示的位置)、装饰器和参数。
component
字段是必需的,插件使用它来自动生成 prop 表并显示其他组件元数据。title
字段是可选的,并且应该是唯一的(即不跨文件重复使用)。
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta = {
/* 👇 The title prop is optional.
* See https://storybook.org.cn/docs/configure/#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'Path/To/MyComponent',
component: MyComponent,
decorators: [
/* ... */
],
parameters: {
/* ... */
},
} satisfies Meta<typeof MyComponent>;
export default meta;
更多示例,请参阅编写故事。
命名故事导出
在 CSF 中,文件中的每个命名导出默认代表一个故事对象。
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {};
export const WithProp: Story = {
render: () => <MyComponent prop="value" />,
};
导出的标识符将使用 Lodash 的startCase函数转换为“start case”。例如
标识符 | 转换 |
---|---|
name | Name |
someName | Some Name |
someNAME | Some NAME |
some_custom_NAME | Some Custom NAME |
someName1234 | Some Name 1 2 3 4 |
我们建议所有导出名称都以大写字母开头。
故事对象可以用一些不同的字段进行注解,以定义故事级别的装饰器和参数,还可以定义故事的name
。
Storybook 的name
配置元素在特定情况下很有帮助。常见的用例是名称中包含特殊字符或 Javascript 保留字。如果未指定,Storybook 默认使用命名导出。
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Simple: Story = {
name: 'So simple!',
// ...
};
Args 故事输入
从 SB 6.0 开始,故事接受名为 Args 的命名输入。Args 是由 Storybook 及其插件提供(并可能更新)的动态数据。
考虑 Storybook 的“Button”示例,一个记录其点击事件的文本按钮
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { action } from 'storybook/actions';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
render: () => <Button label="Hello" onClick={action('clicked')} />,
};
现在考虑使用 args 重写的相同示例
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { action } from 'storybook/actions';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Text = {
args: {
label: 'Hello',
onClick: action('clicked'),
},
render: ({ label, onClick }) => <Button label={label} onClick={onClick} />,
};
或者更简单地
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Text: Story = {
args: {},
};
这些版本不仅比没有 args 的版本更短且更易于编写,而且它们也更具可移植性,因为代码不专门依赖于 actions 插件。
有关设置文档和Actions的更多信息,请参阅其各自的文档。
Play 函数
Storybook 的play
函数是当故事在 UI 中渲染时执行的小段代码。它们是方便的辅助方法,可以帮助您测试否则不可能或需要用户干预的用例。
play
函数的一个很好的用例是表单组件。在以前的 Storybook 版本中,您需要编写一组故事并必须与组件交互来验证它。使用 Storybook 的 play 函数,您可以编写以下故事
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { expect } from 'storybook/test';
import { LoginForm } from './LoginForm';
const meta = {
component: LoginForm,
} satisfies Meta<typeof LoginForm>;
export default meta;
type Story = StoryObj<typeof meta>;
export const EmptyForm: Story = {};
export const FilledForm: Story = {
play: async ({ canvas, userEvent }) => {
// 👇 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.org.cn/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
函数中定义的每个步骤并运行断言,而无需用户交互。
自定义渲染函数
从 Storybook 6.4 开始,您可以将故事编写为 JavaScript 对象,从而减少生成测试组件所需的样板代码,从而提高功能性和可用性。Render
函数是很有用的方法,可以帮助您更好地控制故事的渲染方式。例如,如果您将故事编写为对象,并且想要指定组件应如何渲染,您可以编写以下代码
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Layout } from './Layout';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
// 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
函数是否存在,并根据定义的内容相应地调整组件渲染。
Storybook 导出与名称处理
Storybook 处理命名导出和name
选项的方式略有不同。何时应该使用一个而不是另一个?
Storybook 始终使用命名导出确定故事 ID 和 URL。
如果指定了name
选项,它将用作 UI 中故事的显示名称。否则,它默认为命名导出,通过 Storybook 的storyNameFromExport
和lodash.startCase
函数处理。
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。
在以下情况下最好使用name
配置元素
- 您希望名称以命名导出无法实现的方式显示在 Storybook UI 中,例如,保留关键字如“default”、特殊字符如表情符号,以及除
storyNameFromExport
提供的间隔/大写之外的其他格式。 - 您希望在更改显示方式的同时保持故事 ID 不变。稳定的故事 ID 对于与第三方工具集成很有帮助。
非故事导出
在某些情况下,您可能希望混合导出故事和非故事内容(例如,模拟数据)。
您可以在默认导出中使用可选的配置字段includeStories
和excludeStories
来实现此目的。您可以将它们定义为字符串数组或正则表达式。
考虑以下故事文件
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
import someData from './data.json';
const meta = {
component: MyComponent,
includeStories: ['SimpleStory', 'ComplexStory'], // 👈 Storybook loads these stories
excludeStories: /.*Data$/, // 👈 Storybook ignores anything that contains Data
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
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 中渲染时,它会将ComplexStory
和SimpleStory
视为故事,并忽略data
命名导出。
对于此特定示例,您可以根据方便性以不同的方式实现相同的结果
includeStories: /^[A-Z]/
includeStories: /.*Story$/
includeStories: ['SimpleStory', 'ComplexStory']
excludeStories: /^[a-z]/
excludeStories: /.*Data$/
excludeStories: ['simpleData', 'complexData']
如果您遵循将故事导出以大写字母开头的最佳实践(即使用 UpperCamelCase),则第一个选项是推荐的解决方案。
从 CSF 2 升级到 CSF 3
在 CSF 2 中,命名导出总是实例化组件的函数,并且这些函数可以被配置选项注解。例如
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { ComponentStory, ComponentMeta } from '@storybook/your-framework';
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 };
这为 Button 声明了一个 Primary 故事,该故事通过将{ primary: true }
展开到组件中来渲染自身。default.title
元数据说明了在导航层级中放置故事的位置。
这是 CSF 3 的等效写法
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = { args: { primary: true } };
让我们逐个分析这些变化,以理解发生了什么。
可展开的故事对象
在 CSF 3 中,命名导出是对象,而不是函数。这使我们能够使用 JS 展开运算符更有效地重用故事。
考虑在入门示例中添加以下内容,它创建了一个在深色背景下渲染的PrimaryOnDark
故事
这是 CSF 2 的实现
export const PrimaryOnDark = Primary.bind({});
PrimaryOnDark.args = Primary.args;
PrimaryOnDark.parameters = { background: { default: 'dark' } };
Primary.bind({})
复制故事函数,但不复制函数上的注解,因此我们必须添加PrimaryOnDark.args = Primary.args
来继承 args。
在 CSF 3 中,我们可以展开Primary
对象来继承其所有注解
export const PrimaryOnDark: Story = {
...Primary,
parameters: { background: { default: 'dark' } },
};
了解更多关于命名故事导出的信息。
默认渲染函数
在 CSF 3 中,您通过render
函数指定故事如何渲染。我们可以通过以下步骤将 CSF 2 示例重写为 CSF 3。
让我们从一个简单的 CSF 2 故事函数开始
// Other imports and story implementation
export const Default: ComponentStory<typeof Button> = (args) => <Button {...args} />;
现在,让我们在 CSF 3 中将其重写为一个故事对象,其中包含一个明确的render
函数,该函数告诉故事如何渲染自身。就像 CSF 2 一样,这使我们能够完全控制如何渲染组件甚至组件集合。
// Other imports and story implementation
export const Default: Story = {
render: (args) => <Button {...args} />,
};
了解更多关于渲染函数的信息。
但在 CSF 2 中,许多故事函数是相同的:获取默认导出中指定的组件,并将 args 展开到其中。这些故事的有趣之处不在于函数,而在于传递给函数的 args。
CSF 3 为每个渲染器提供了默认渲染函数。如果您只是将 args 展开到组件中(这是最常见的情况),则完全不需要指定任何render
函数
export const Default = {};
更多信息,请参阅自定义渲染函数部分。
自动生成标题
最后,CSF 3 可以自动生成标题。
export default {
title: 'components/Button',
component: Button,
};
export default { component: Button };
您仍然可以像在 CSF 2 中一样指定标题,但如果您不指定,则可以从故事在磁盘上的路径推断出来。更多信息,请参阅配置故事加载部分。