返回博客

如何在 Storybook 中构建连接组件

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

loading
Varun Vachhar
@winkerVSbecks
上次更新

AvatarButtonTooltip 这样的展示型组件完全通过 props 接收输入,并且没有内部状态。这使得隔离并为它们编写故事变得简单直接。然而,应用程序树中更上层的组件在使用 Storybook 进行隔离构建时则更加棘手。

FormsListCards 这样的连接组件跟踪应用程序状态,然后将行为向下传递。它们通常需要一个“线束/包装器”才能以有效的方式渲染。

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

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

为什么要隔离构建连接组件?

每个组件根据应用状态、主题、响应行为、设备特性、国际化等有无数种变体。开发者编写故事来涵盖所有这些用例。这使他们能够即时查看任何变体,然后验证其外观和感受。

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

CodeacademyGitlabIBMDC/OS LabsMonday.com 只是其中几个示例。

使用装饰器隔离连接组件

UI 组件需要数据和动作处理程序才能渲染。这些通常作为 props 传入,但连接组件也通过上下文、API 请求和 hooks 直接访问它们。

要隔离连接组件,必须模拟其依赖项。在 Storybook 中,可以使用装饰器来提供模拟上下文,并为不同的组件变体编写故事。

Storybook 分为两个部分:manager,它渲染 Storybook UI(搜索、导航、工具栏和插件),以及 preview,您的故事在此渲染。

一个装饰器是运行在预览 iframe 内的包装代码。它们使您能够控制故事布局、渲染方式,并提供模拟上下文。让我们通过一些示例来探索所有这些可能性。

控制故事的布局

装饰器最基本的用例是为组件提供布局约束。假设您正在构建一个 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 = () => { /* ... */ };

加载全局 provider

许多库依赖全局 provider 进行配置。例如,Styled ComponentsChakra UI 使用 provider 定制主题。而 React Intl 则使用 provider 传递本地化翻译。

我们可以向 .storybook/preview.js 添加一个全局装饰器来加载这些 provider。以下是如何使用 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];

通过参数控制装饰器

除了故事函数外,装饰器还接收包含故事的 args、parameters、globals 等的故事上下文对象。这意味着您可以使用参数配置插件。

例如,这个 withTheme 装饰器为您的所有组件提供主题并加载全局样式。此外,您可以通过故事参数控制哪个主题处于活动状态。有关此技术的完整概述,请参阅:如何在 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 等状态管理库,以为组件提供状态存储的访问权限。在这种情况下,我们可以使用故事装饰器提供模拟存储来渲染不同的组件变体。

考虑这个连接到 Redux 存储的 TaskList 组件。该存储中的应用程序状态决定了 TaskList 的哪个变体被渲染。

为了控制该应用程序状态,我们将使用 @reduxjs/toolkit 中的工具创建一个模拟存储。然后使用故事装饰器将不同的状态对象应用于模拟存储。这使我们能够将难以触及的组件状态复制为故事。

// 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 和 context 的组合代替状态管理库。在这种情况下,您可以使用 React Context 插件为您的组件提供和操作上下文。

模拟 REST 和 GraphQL API 请求

随着您继续向上遍历组件树,您开始将 UI 连接到后端 API 和服务。我们可以直接在 Storybook 中模拟这些请求。

JavaScript 生态系统提供了许多出色的工具用于模拟 API 请求。更重要的是,这些工具中的大多数都作为 Storybook 插件提供。因此,与其构建自定义装饰器,不如使用插件快速入门。

Mock Service Worker (MSW) 是一个多功能插件,它使用 service worker 在网络层面拦截请求并返回模拟数据。它适用于 REST 和 GraphQL 后端。

在底层,MSW 插件由装饰器驱动。它会自动将您的故事包装在一个 MSW 装饰器中,并允许您通过参数在故事层面提供请求处理程序。

// 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 是一个非常实用的选择,但您也可以找到针对特定库的插件,例如 ApolloURQLGraphQL KitAxios

使用装饰器扩展 Storybook 功能

除了模拟组件的依赖项外,装饰器还使您能够向 Storybook 添加额外的功能。例如,MeasureOutlineBackgrounds 插件使用装饰器将代码注入预览 iframe,从而使 CSS 调试更容易。有关此技术的更多信息,请参阅创建插件教程。

结论

UI 需要考虑语言、设备、用户偏好和应用状态的无数种组合。使用 Storybook,可以将这些变体捕获为故事,并在开发和测试期间重新查看它们。

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

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

加入 Storybook 邮件列表

获取最新新闻、更新和发布信息

7,180及更多开发者

我们正在招聘!

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

查看职位

热门文章

Storybook 7.0 设计抢先看

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

用 TypeScript 编写故事

学习如何为故事添加类型,使其更易于编写和更健壮
loading
Kyle Gach

社区案例展示 #2

VSCode 插件。Variants、Recoil 和 Code Editor 插件。此外还有大量新的学习资源。
loading
João Cardoso
加入社区
7,180及更多开发者
为何为何选择 Storybook组件驱动 UI
文档指南教程更新日志遥测
社区插件参与其中博客
案例展示探索项目组件术语表
开源软件
Storybook - Storybook 中文

特别鸣谢 Netlify CircleCI