Storybook 的 10 个最佳实践

2025-06-10

Storybook 的 10 个最佳实践

Storybook 最佳实践封面图片

这篇文章是关于 Storybook 和约定的,它基于我多年来使用 Storybook 的经验以及其他开发者的经验。我在这里定义的最佳实践并非万能的灵丹妙药,无法解决和改进每个项目。它们是我认为值得尝试的理念和约定的基准。希望它们能够帮助您和您的团队获得更好的开发体验,并最终向世界交付更好的软件。
我假设您了解 Storybook 是什么,并且有一定的使用经验。这里的想法可以应用于任何规模的应用程序,并且不局限于任何特定的框架(例如 React 或 Vue),但我在撰写这些实践时,是考虑到一个拥有庞大团队和众多贡献者的大型应用程序。


1. 每个组件一个 Storybook 文件

故事文件应该包含:

  • 一个默认故事
  • 游乐场的故事
  • 以及反映组件特定状态或 API 的其他故事。

默认故事仅显示定义了必需属性的组件。它为每个人创建了一个视觉基准表示。因此,理想情况下,当人们想到某个特定组件时,他们会记住默认故事所重现的内容。Playground故事用于帮助组件的使用者尝试不同的属性组合,并观察组件的响应。这可以在 Storybook 5 及更低版本中使用旋钮插件
实现。您可以为组件的所有属性提供旋钮:

import React from "react";
import {storiesOf} from "@storybook/react";
import { withKnobs, text } from '@storybook/addon-knobs';
import Button from ".";
const storiesOf("Button", module).
addDecorator(withKnobs)
add("Default", () => {
return <Button type="button" text="Click me" />
}).
add("Playground", () => {
const typePropLabel = 'type';
const typePropOptions = {
submit: 'submit',
input: 'input'
};
const typePropDefaultValue = typePropOptions.submit;
const textPropLabel = "text";
const textDefaultValue = "Click me";
return (
<Button type={select(typePropLabel, typePropOptions, typePropDefaultValue)} text={text(textPropLabel, textDefaultValue)} />
)
})
import React from "react";
import {storiesOf} from "@storybook/react";
import { withKnobs, text } from '@storybook/addon-knobs';
import Button from ".";
const storiesOf("Button", module).
addDecorator(withKnobs)
add("Default", () => {
return <Button type="button" text="Click me" />
}).
add("Playground", () => {
const typePropLabel = 'type';
const typePropOptions = {
submit: 'submit',
input: 'input'
};
const typePropDefaultValue = typePropOptions.submit;
const textPropLabel = "text";
const textDefaultValue = "Click me";
return (
<Button type={select(typePropLabel, typePropOptions, typePropDefaultValue)} text={text(textPropLabel, textDefaultValue)} />
)
})

对于最新版本的 Storybook(版本 6),可以使用新的Args 功能编写游乐场故事。它看起来像这样:

import React from "react";
import Button from ".";
// Args Setup
const Template = (args) => <Button {...args} />;
export const Playground = Template.bind({});
const buttonTypes = {
SUBMIT: 'submit',
INPUT: 'input'
}
Playground.args = {
type: buttonTypes.INPUT,
text: "Primary",
};
Playground.argTypes = {
type: {
control: {
type: "select",
options: [buttonTypes.INPUT, buttonTypes.SUBMIT]
}
},
text: {
control: "text"
}
}
export const DefaultStory = () => <Button type="button" text="Click me" />;
DefaultStory.storyName = "Default";
export default {
title: "Components/Button",
component: DefaultStory,
};
import React from "react";
import Button from ".";
// Args Setup
const Template = (args) => <Button {...args} />;
export const Playground = Template.bind({});
const buttonTypes = {
SUBMIT: 'submit',
INPUT: 'input'
}
Playground.args = {
type: buttonTypes.INPUT,
text: "Primary",
};
Playground.argTypes = {
type: {
control: {
type: "select",
options: [buttonTypes.INPUT, buttonTypes.SUBMIT]
}
},
text: {
control: "text"
}
}
export const DefaultStory = () => <Button type="button" text="Click me" />;
DefaultStory.storyName = "Default";
export default {
title: "Components/Button",
component: DefaultStory,
};

最后,其他故事应该反映组件的特定状态或 API。例如,如果我们有一个按钮组件,其 type 属性接受以下值primarysecondarytertiary、 或error。那么,我们将有四个故事:Button/PrimaryButton/SecondaryButton/TertiaryButton/Error。遵循这种模式有几个原因:

  • 更容易共享一个精确定义您想要引用的状态的组件的链接,这在与 QA 和设计师沟通时很有用。
  • 如果 Storybook 与快照测试或可视化回归测试等测试工具结合使用,每个故事都会变成一个单元测试。如果其中一个失败了,你就能准确地知道是哪一个。
  • 通过明确故事,我们避免隐藏旋钮下的组件状态。

2. 共置:Storybook 文件应与其组件共存

出于相同原因而更改的代码应放在一起。从这个意义上讲,给定组件的 Storybook 文件很可能会在组件更改时发生变化,因此请将它们放在一起。此外,如果组件文件夹移动到项目中的其他位置,甚至移动到其他项目,Storybook 文件也会更容易移动。


3. 命名约定

为 Storybook 文件命名[ComponentName].stories.[js|jsx|tsx]。说实话,重要的是你和你的团队达成一致的命名约定,并且每个人都遵循它。我喜欢在文件名中包含组件的名称,因为这样更容易在代码编辑器中找到。否则,我可能会调用五个文件index.stories.tsx,,然后不得不逐个打开或执行搜索才能找到正确的文件。


4. 新组件必须有故事书

它有助于创建组件库并获得其带来的好处。如果您的团队有 PR 清单,那么 Storybook 可以作为在将代码合并到主分支之前需要检查的项目之一。


5. 更喜欢组件故事格式

根据 Storybook 维护者的建议,组件故事格式 (CSF)storiesOf是编写故事的推荐方式。它本质上是一组在故事文件中使用的约定。您可以编写常规 JavaScript 函数并将其导出,而无需使用 API。Storybook 会将命名和默认导出转换为故事。CSF 格式的一大优势是代码看起来更简洁,更易于阅读。您可以专注于代码的实际功能,而无需使用 Storybook 样板代码。


6. 在构建代码库时构建故事

使用 Storybook 时,你应该清楚地了解应用的组织结构。我从Loïc Goyet的精彩文章《如何使我的 Storybook 项目尽可能高效》中了解到了这一点。他的想法是让故事菜单反映故事在应用中的位置:

故事书菜单结构

你看到上面 Storybook 中的菜单是如何与应用文件夹结构对齐的了吗?
这种结构将帮助你:

  • 更轻松地查找故事
  • 了解代码的组织方式。

如果您的应用使用了共置模式,将相关项目放在一起,那么文件夹结构可以让您大致了解应用的结构。但请不要将文件夹结构与架构混淆。它们不是一回事。


7.一致的环境

在 Storybook 中开发时,我们力求独立,但很可能仍会使用一些与应用共享的资源,例如图片、数据、CSS、图标、翻译等等。这很好,因为我们希望确保组件在应用环境中使用时的行为保持一致。例如,如果应用中使用了本地化库,那么在 Storybook 中,它可能可以使用相同的配置重复使用。再比如:如果使用了第三方 CSS,则应该将其包含在 Storybook 中,因为我们想确定该 CSS 是否会与我们的 CSS 冲突。这样做的目的是避免在应用中使用组件时出现意外。


8. 控制数据

如果你注意到在许多不同的故事中都需要相同的数据,那么创建一个 mocks 文件夹并添加一些 JavaScript 文件,这些文件导出一些工厂函数来创建可复用的数据,可能会是一个不错的选择。假设我们有一个名为 mocks/user.js 的组件,用于显示用户图像、姓名和锚点,并且该组件在多个地方使用。我们可以创建一个名为 mocks/user.js 的文件,其中包含以下内容:



const getUser = (overrides = {}) => {
    const defaultValues = {
        username: "Some User",
        anchor: "@someuser",
        image: "https://webapp/static/images/someuser.png"
    };
    return Object.assign(defaultValues, overrides);
};
export default getUser;


Enter fullscreen mode Exit fullscreen mode

为什么要使用工厂函数?为了确保每次都能获取到新的对象。如果我们导入一个对象,可能会意外修改它并导致错误的结果。我见过这种情况。另外,这里我举个例子,Object.assign你可能需要更复杂的函数来处理数组和对象的合并。LodashRamdaJS都有相应的函数——RamdaJS 太棒了!


9. 创建自己的装饰器和附加组件(当它有意义时)

装饰器本质上是包装另一段代码并赋予其额外功能的函数。在 Storybook 中,装饰器可以应用于单个故事(称为故事装饰器)、组件的所有故事(称为组件装饰器)或项目中的所有故事(称为全局装饰器)。基本原理如下:



const myDecorator = (Story) => (
    <div>
        <Story />
    </div>
);


Enter fullscreen mode Exit fullscreen mode

在 React 应用中,使用提供程序包装整个应用或部分内容是很常见的。例如,如果你需要将组件包装在提供程序中,那么装饰器就是一个不错的选择。或者,如果你想给某个组件添加边距,使其不接触画布的边框,那么你可以使用如下装饰器:



const withMargin = (Story) => (
    <div style={{ margin: '3em' }}>
        <Story/>
    </div>
);


Enter fullscreen mode Exit fullscreen mode

插件是 Storybook 的扩展,可以帮助您以多种方式配置和扩展 Storybook。开发插件需要更多时间,但并不难,而且您可以获得更强大的功能和灵活性。


10.认真对待 Storybook 的使用和维护

Storybook 在开发 UI 方面能提供巨大的帮助,因为它鼓励你专注于组件的界面,从而帮助你拥有更多通用且精简的组件。通用且精简的组件非常灵活,可以在不同的环境中使用。最终,如果你拥有一些灵活的组件,你可能需要更少的组件。更少的组件意味着更少的代码;更少的代码意味着更少的 bug;更少的 bug 意味着更快乐的用户和开发者。因此,请维护并保持 Storybook 的运行和良好运转,不要让支离破碎的故事残留,并在它们变得混乱时进行重构和重新整理。
根据我的经验,只有当人们承担起责任时,事情才会有所改善。如果没有一个团队负责维护 Storybook,那么保持其不断发展并从中受益将并非易事。每个人都有责任贡献代码并遵守团队的惯例,但指定一个人或一组人作为 Storybook 维护者可能会有所帮助。Storybook 维护者可以督促其他人遵守惯例——他们可以改善团队中 Storybook 的使用。


结论

我使用 Storybook 五年,并从其他比我更聪明的开发人员的经验中总结了一些想法。我真诚地希望您能从中学到一些新东西,并乐于尝试 Storybook,或者让它变得更好,造福您和您的团队。如果您有任何问题或建议,希望本文能有所改进,请在下方评论区留言。

谢谢!

参考

鏂囩珷鏉ユ簮锛�https://dev.to/rafaelrozon/10-storybook-best-practices-5a97
PREV
使用 React.js 创建天气应用 - 第一部分
NEXT
我应该从学习原始 Javascript 还是框架开始?