
组件故事格式 3 来啦
下一代故事格式,助您提高工作效率

故事(Stories)可视化了组件在不同场景下的行为。组件故事格式(Component Story Format,CSF)是故事的通用文件格式。
组件故事格式 3(CSF3)标志着故事格式的一次演进,它减少了样板代码并改善了人体工程学。这使得故事更加简洁,编写速度更快。
我很高兴宣布 CSF3 的正式发布。在长达 18 个月的Beta 测试期间,社区帮助我们发现了问题并完善了格式。从 Storybook 7 开始,CSF3 将成为编写故事的默认方式。
改进包括
- ♻️ 可扩展的故事对象,轻松复用故事
- 🌈 默认渲染函数,简洁明了
- 📓 自动标题,方便省事
- ▶️ Play 函数,用于脚本化交互和测试
- ✅ 与 CSF 2 完全向后兼容
继续阅读,了解更多关于这种格式的信息,自最初发布以来的变化,以及如何在 Storybook 7 中充分利用它。
等等,为什么?
在应用程序之外开发 UI 组件是创建高质量组件的最佳方式。Storybook 开创了这种组件驱动开发(Component-driven Development,CDD)的风格。
故事现在被设计师和产品经理用于视觉评审,也被用于设计系统文档、自动化测试,甚至从生产组件生成设计资产。
毫无疑问,Shopify、IBM 和 Salesforce 等公司都在使用 Storybook 构建许多世界上最受欢迎的 UI。
CSF3 是故事的下一次演进——更易于编写和维护
与其前身非常相似,CSF3 基于 ESM。默认导出包含有关组件的信息,而一个或多个命名导出(即故事)捕获组件的不同状态。主要区别在于故事现在是对象,并且你可以为每个故事附加一个 play 函数来模拟用户交互。
CSF3 完全向后兼容 CSF2,并且 CSF2 在 Storybook 7 中仍然受支持。我们甚至将 play 函数反向移植到了 CSF2。
我们建议迁移,因为 CSF3 更具表现力且更易于维护,所需的样板代码更少。在大多数情况下,你可以使用 codemod 自动从 CSF 2 迁移到 3。

CSF3 功能回顾
大型项目可能包含数百个组件和数千个故事。当你编写如此多的故事时,人体工程学的改进会带来显著的生活质量提升。我们的目标是简化故事格式,让编写、阅读和维护故事变得更容易。
让我们看看 CSF3 在假设的 RegistrationForm
组件中是什么样子的。
默认导出声明了你正在为其编写故事的组件
// RegistrationForm.stories.js
import { RegistrationForm } from './forms/RegistrationForm';
export default {
title: 'forms/RegistrationForm',
component: RegistrationForm,
};
故事现在是对象
export const EmptyForm = {
render: (args) => <RegistrationForm {...args} />,
args: { /* ... */ },
parameters: { /* ... */ },
};
默认渲染函数,简洁明了。 90% 的时间里,编写故事只是以标准方式将一些输入传递给你的组件。
在 CSF3 中,如果你不指定渲染函数,每个故事都会渲染组件并传入所有参数。这大大简化了你的代码。
在我们的 RegistrationForm
示例中,渲染函数是样板代码
export const EmptyForm = {
// render: (args) => <RegistrationForm {...args} />, -- now optional!
args: { /* ... */ },
parameters: { /* ... */ },
};
可扩展的故事对象,方便复用。 当你试图模拟复杂状态时,能够扩展现有故事而不是重新编写它们是非常有用的。假设你想展示已填写完毕的表单,但在不同的视口下
export const FilledForm = {
args: {
email: 'marcus@acme.com',
password: 'j1287asbj2yi394jd',
}
};
export const FilledFormMobile = {
...FilledForm,
parameters: {
viewports: { default: 'mobile' }
},
};
Play 函数,用于脚本化交互。 有些 UI 状态在没有用户交互的情况下是无法捕获的。play 函数在故事渲染后运行,并使用 testing-library
来模拟用户交互。例如
// RegistrationForm.stories.ts|tsx
import { userEvent, within } from '@storybook/testing-library';
// ...
export const FilledForm = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emailInput = canvas.getByLabelText('email', {
selector: 'input'
});
await userEvent.type(emailInput, 'example-email@email.com', {
delay: 100
});
const passwordInput = canvas.getByLabelText('password', {
selector: 'input'
});
await userEvent.type(passwordInput, 'ExamplePassword', {
delay: 100
});
const submitButton = canvas.getByRole('button');
await userEvent.click(submitButton);
},
};

自动标题,方便省事。 在 CSF 中,故事的标题决定了它在 UI 导航层次结构中的位置。在 CSF3 中,标题可以根据文件相对于根目录的位置自动生成。需要输入的文字更少,而且如果你重新组织文件,也无需更新。
export default {
// title: 'forms/RegistrationForm' -- optional
component: RegistrationForm,
};

有关 CSF3 功能和原理的深入描述,以及它与 CSF2 的确切区别,请参阅原始 CSF3 文章。
对原文的更改
在过去的一年半里,用户一直在他们的项目中测试 CSF3。根据反馈,我们对原文做了一些更改。
更好的 TypeScript 类型。 我们更新了 7.0 中的 Meta/Story 类型,以支持故事的类型安全和自动补全。敬请关注未来几周内关于此主题的专门文章。
更新了自动标题启发式算法。 自动标题根据 CSF 文件的磁盘位置生成“标题”(Storybook 侧边栏中的路径)。例如,如果 /project/path/src
是故事根目录,则 /project/path/src/atoms/Button.stories.js
将获得标题 atoms/Button
。然而,朴素的启发式算法无法处理诸如 atoms/Button/Button.stories.js
或 atoms/Button/index.stories.js
等常见模式。我们更新了启发式算法以解决此问题。有关完整的迁移说明以及其他自动标题改进,包括更好的前缀处理和大小写,请参阅MIGRATION.md。
升级了文档和 CLI 模板。 最后但同样重要的是,我们升级了 7.0 的官方文档,增加了 CSF3 源代码片段。我们还更新了 CLI,以便为新项目生成 CSF3。
立即升级到 CSF3
CSF3 完全向后兼容,因此你现有的 CSF 故事无需修改即可正常工作。我们近期不会弃用旧格式。然而,CSF3 是一个巨大的进步,我们建议你在升级到 Storybook 7.0 (SB7) 时升级你的故事。
要升级到 SB7,请参阅我们的SB7 迁移指南。项目升级后,你可以选择使用以下 codemod 将你的 CSF 故事迁移到 CSF3。请务必更新 glob,使其包含你想要更新的文件。
npx storybook@next migrate csf-2-to-3 --glob="**/*.stories.js"

如果你无法使用 codemod,我们还提供了升级说明。
参与其中
组件故事格式(CSF)帮助你独立开发、测试和文档化组件。通过 CSF3 改进的人体工程学,可以帮助你用更少的精力编写更多故事。
CSF3 由Michael Shilman(我!)、Kasper Peulen、Tom Coleman 和 Pavan Sunkara 开发,并得到了整个 Storybook 社区的测试和反馈。
Storybook 是超过1600 位社区贡献者的成果。加入我们的GitHub 或在Discord 与我们聊天。最后,在 Twitter 上关注@storybookjs,获取最新消息。
组件故事格式 3 来啦!
— Storybook (@storybookjs) 2023 年 1 月 26 日
♻️ 可扩展的故事对象
🌈 默认渲染函数
📓 自动标题
⏯️ Play 函数,用于脚本化交互
✅ 与 CSF 2 完全向后兼容
🗞️ 阅读全文... pic.twitter.com/TaMIKrTNV9