返回博客

如何在 Storybook 中构建连接组件

了解如何使用装饰器模拟上下文、应用状态和 API 请求

loading
Varun Vachhar
@winkerVSbecks
最后更新

AvatarButtonTooltip 这样的展示型组件完全通过 props 接收输入,并且没有内部状态。这使得隔离和为它们编写 stories 非常简单。然而,应用程序树中更高级别的组件在 Storybook 中以隔离方式构建起来更棘手。

FormsListCards 这样的连接组件跟踪应用程序状态,然后将行为向下传递到树中。它们通常需要一个“harness/wrapper”才能以有用的方式呈现。

  • 样式: ThemeProvider 和全局样式
  • 布局: 模仿布局的 DOM 结构
  • 数据获取: GraphQL providers 或 hooks 以进行 API 调用。
  • 状态管理: Redux、MobX、Recoil 等的状态存储 provider。

本文展示了如何使用装饰器来隔离连接组件。您将学习构建 装饰器,使用参数控制其行为,并使用它们来模拟组件依赖项。

为什么要在隔离状态下构建连接组件?

每个组件都有无数个变体,基于应用状态、主题、响应式行为、设备功能、国际化等等。开发人员编写 stories 以覆盖所有这些用例。这使他们能够立即查看任何变体,然后验证其外观和感觉。

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

Codeacademy, Gitlab, IBM, DC/OS LabsMonday.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 ComponentsChakra 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, MobXRecoil)广泛使用,以使组件能够访问状态存储。在这种情况下,我们可以使用 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 KitAxios

使用装饰器扩展 Storybook 功能

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

结论

UI 考虑了语言、设备、用户偏好和应用程序状态的无尽排列组合。借助 Storybook,您可以将这些变体捕获为 stories,并在开发和测试期间重新访问它们。

您可以通过 props 提供模拟数据来重现展示型组件的不同状态。但是隔离连接组件更具挑战性,因为它们连接到应用状态、交互和 API 请求。

Storybook 装饰器使您能够模拟这些依赖项。您可以构建自定义装饰器以使用 providers 包装您的组件,或者 使用现成的插件 来模拟 API 请求。

加入 Storybook 邮件列表

获取最新消息、更新和发布

6,730位开发者和计数中

我们正在招聘!

加入 Storybook 和 Chromatic 背后的团队。构建被数十万开发者在生产中使用的工具。远程优先。

查看职位

热门文章

Storybook 7.0 设计先睹为快

视觉更新、UX 调整和更快的性能
loading
Dominic Nguyen

在 TypeScript 中编写 stories

了解如何为您的 stories 输入类型,使其更易于编码且更健壮
loading
Kyle Gach

社区展示 #2

VSCode 扩展。Variants, Recoil 和 Code Editor 插件。以及许多新的学习资源。
loading
João Cardoso
加入社区
6,730位开发者和计数中
为什么为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
案例展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI