
如何在 Storybook 中构建连接组件
了解如何使用装饰器模拟上下文、应用状态和 API 请求

像 Avatar
、Button
和 Tooltip
这样的展示型组件完全通过 props 接收输入,并且没有内部状态。这使得隔离和为它们编写 stories 非常简单。然而,应用程序树中更高级别的组件在 Storybook 中以隔离方式构建起来更棘手。
像 Forms
、List
和 Cards
这样的连接组件跟踪应用程序状态,然后将行为向下传递到树中。它们通常需要一个“harness/wrapper”才能以有用的方式呈现。
- 样式:
ThemeProvider
和全局样式 - 布局: 模仿布局的 DOM 结构
- 数据获取: GraphQL providers 或 hooks 以进行 API 调用。
- 状态管理: Redux、MobX、Recoil 等的状态存储 provider。
本文展示了如何使用装饰器来隔离连接组件。您将学习构建 装饰器,使用参数控制其行为,并使用它们来模拟组件依赖项。
为什么要在隔离状态下构建连接组件?
每个组件都有无数个变体,基于应用状态、主题、响应式行为、设备功能、国际化等等。开发人员编写 stories 以覆盖所有这些用例。这使他们能够立即查看任何变体,然后验证其外观和感觉。


虽然 Storybook 广泛用于设计系统,但前端团队为应用程序组件编写 stories 也很常见。这些组件“连接”到应用状态、上下文和 hooks,从而产生更复杂的变体。开发人员选择在 Storybook 中构建连接组件,因为开发难以触及的用例(如加载、错误和空状态)更容易。
Codeacademy, Gitlab, IBM, DC/OS Labs 和 Monday.com 只是其中的一些示例。




使用装饰器隔离连接组件
UI 组件需要数据和操作处理程序才能呈现。这些通常作为 props 传入,但连接组件也直接通过上下文、API 请求和 hooks 访问它们。
要隔离连接组件,您必须模拟其依赖项。在 Storybook 中,您可以使用 装饰器 来提供模拟上下文,并为不同的组件变体编写 stories。

Storybook 分为两个部分:manager,它呈现 Storybook UI(搜索、导航、工具栏和插件)和 preview,您的 stories 在其中呈现。
装饰器是在 preview iframe 内运行的包装代码。它们使您能够控制 story 布局、其呈现方式以及提供模拟上下文。让我们通过几个示例探索所有这些可能性。
控制 stories 的布局
装饰器最基本的使用场景是为组件提供布局约束。假设您正在构建一个 Sidebar
组件。默认情况下,它会扩展以填充其父容器。但是,它旨在用于页面布局中,其中仅占用视口宽度的一小部分。我们可以使用装饰器来模仿该页面结构,如下所示
// Sidebar.stories.js
import { Sidebar } from './Sidebar';
const withLayout = (Story) => (
<div style={{ display: 'flex' }}>
<div style={{ flex: '0 0 240px', marginRight: 16 }}>{Story()}</div>
<div style={{ display: 'flex', flex: '1 1 auto' }}>children</div>
</div>
);
export default {
title: 'Sidebar',
component: Sidebar,
decorators: [withLayout],
};
export const Base = () => { /* ... */ };
export const NonLatestVersion = () => { /* ... */ };

加载全局 providers
许多库依赖于全局 providers 进行配置。例如,Styled Components 和 Chakra UI 使用 provider 自定义主题。而 React Intl 使用 provider 传入特定于语言环境的翻译。

我们可以向 .storybook/preview.js
添加单个全局装饰器来加载这些 providers。以下是如何使用 Storybook 设置 React Intl 的示例。
// .storybook/preview.js
import React from 'react';
import { IntlProvider } from 'react-intl';
import messages from './compiled-lang/fr.json';
const withIntl = (StoryFn) => (
<IntlProvider locale="en" timeZone="Asia/Tokyo" messages={messages}>
{Story()}
</IntlProvider>
);
export const decorators = [withIntl];
通过参数控制装饰器
除了 story 函数之外,装饰器还接收 story 上下文 对象,其中包含 story 的 args、参数、globals 等。这意味着您可以使用参数配置插件。
例如,此 withTheme
装饰器为您的所有组件提供主题并加载全局样式。此外,您可以通过 story 参数控制哪个主题处于活动状态。有关此技术的完整概述,请查看:如何在 Storybook 中添加主题切换器。
// .storybook/preview.tsx
import { ThemeProvider } from 'styled-components'
import { GlobalStyle } from '../src/styles/GlobalStyle'
import { darkTheme, lightTheme } from '../src/styles/theme'
const withTheme = (Story, context) => {
// Get the active theme value from the story parameter
const { theme } = context.parameters
const storyTheme = theme === 'dark' ? darkTheme : lightTheme
return (
<ThemeProvider theme={storyTheme}>
<GlobalStyle />
<Story />
</ThemeProvider>)
}
export const decorators = [withTheme]
模拟状态管理库的上下文
provider 模式也被状态管理库(如 Redux, MobX 和 Recoil)广泛使用,以使组件能够访问状态存储。在这种情况下,我们可以使用 story 装饰器提供模拟存储来呈现不同的组件变体。

考虑一下连接到 Redux 存储的 TaskList
组件。该存储中的应用程序状态决定了呈现哪个 TaskList
变体。

为了控制应用程序状态,我们将使用 @reduxjs/toolkit
中的实用程序创建一个模拟存储。然后使用 story 装饰器将不同的状态对象应用于模拟存储。这使我们能够将难以触及的组件状态复制为 stories。
// TaskList.stories.js
import React from 'react';
import { Provider } from 'react-redux';
import { configureStore, createSlice } from '@reduxjs/toolkit';
import TaskList from './TaskList';
export default {
component: TaskList,
title: 'TaskList',
};
// Mock state that'll be passed to the mock redux store
const MockState = {
tasks: [/*...code omitted for brevity */],
status: 'idle',
error: null,
};
// Mocked redux store
const Mockstore = ({ taskboxState, children }) => (
<Provider
store={configureStore({
reducer: {
taskbox: createSlice({
name: 'taskbox',
initialState: taskboxState,
}).reducer,
},
})}
>
{children}
</Provider>
);
const Template = () => <TaskList />;
export const Default = Template.bind({});
Default.decorators = [
(story) => <Mockstore taskboxState={MockedState}>{story()}</Mockstore>,
];
export const Loading = Template.bind({});
Loading.decorators = [
(story) => (
<Mockstore
taskboxState={{
...MockedState,
status: 'loading',
}}
>
{story()}
</Mockstore>
),
];
export const Empty = Template.bind({});
Empty.decorators = [
(story) => (
<Mockstore
taskboxState={{
...MockedState,
tasks: [],
}}
>
{story()}
</Mockstore>
),
];
在 React 生态系统中,使用 hooks 和上下文的组合来代替状态管理库正变得越来越普遍。在这种情况下,您可以使用 React Context 插件 为您的组件提供和操作上下文。
模拟 REST 和 GraphQL API 请求
当您继续向上移动组件树时,您开始将 UI 连接到后端 API 和服务。我们可以在 Storybook 中模拟这些请求。
JavaScript 生态系统提供了许多用于模拟 API 请求的优秀工具。更重要的是,这些工具中的大多数都作为 Storybook 插件提供。因此,与其构建自定义装饰器,不如使用插件快速入门。
Mock Service Worker (MSW) 是一个通用的插件,它使用 service workers 在网络级别拦截请求并返回模拟数据。它适用于 REST 和 GraphQL 后端。


在底层,MSW 插件由装饰器驱动。它自动将您的 stories 包装在 MSW 装饰器中,并允许您通过参数在 story 级别提供请求处理程序。
// CategoryDetailPage.stories.js
import { rest } from 'msw';
import { CategoryDetailPage } from './CategoryDetailPage';
import { restaurants } from '../../mocks/restaurants';
export default {
title: 'CategoryDetailPage',
component: CategoryDetailPage,
};
const Template = () => <CategoryDetailPage />;
export const Default = Template.bind({});
Default.parameters = {
msw: {
handlers: [
rest.get('/restaurants', (req, res, ctx) => res(ctx.json([restaurants[0]]))),
],
},
};
export const Loading = Template.bind({});
Loading.parameters = {
msw: {
handlers: [
rest.get('/restaurants', (req, res, ctx) => res(ctx.delay('infinite'))),
],
},
};
export const Missing = Template.bind({});
Missing.parameters = {
deeplink: { route: '/categories/wrong', path: '/categories/:id' },
msw: {
handlers: [rest.get('/restaurants', (req, res, ctx) => res(ctx.json([])))],
},
};
虽然 MSW 是一个非常实用的选项,但您也可以找到特定于库的插件,用于 Apollo, URQL, GraphQL Kit 和 Axios。

使用装饰器扩展 Storybook 功能
除了模拟组件的依赖项之外,装饰器还使您能够向 Storybook 添加额外的功能。例如,Measure, Outline 和 Backgrounds 插件使用装饰器将代码注入 preview iframe 中,从而简化 CSS 调试。有关此技术的更多信息,请查看 创建插件 教程。



结论
UI 考虑了语言、设备、用户偏好和应用程序状态的无尽排列组合。借助 Storybook,您可以将这些变体捕获为 stories,并在开发和测试期间重新访问它们。
您可以通过 props 提供模拟数据来重现展示型组件的不同状态。但是隔离连接组件更具挑战性,因为它们连接到应用状态、交互和 API 请求。
Storybook 装饰器使您能够模拟这些依赖项。您可以构建自定义装饰器以使用 providers 包装您的组件,或者 使用现成的插件 来模拟 API 请求。
Storybook 装饰器帮助您在隔离状态下构建和测试连接组件。
— Storybook (@storybookjs) July 7, 2022
🏗️ 控制 story 布局
💽 加载全局 providers
🎛️ 模拟状态管理库的上下文
📡 模拟 REST 和 GraphQL API 请求
开始使用我们的新教程: https://#/PgSyCiDw6V pic.twitter.com/eTnMkpUMsh