返回博客

如何在 Storybook 中构建连接的组件

学习如何使用装饰器模拟 context、app 状态和 API 请求

loading
Varun Vachhar
@winkerVSbecks
最后更新

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

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

  • 样式ThemeProvider和全局样式
  • 布局:模仿布局的DOM结构
  • 数据获取:GraphQL提供商或钩子以发出API请求。
  • 状态管理:Redux、MobX、Recoil等的store提供商。

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

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

每个组件都有无数种变体,具体取决于应用程序状态、主题、响应式行为、设备功能、国际化等。开发者编写故事来涵盖所有这些用例。这使他们能够立即查看任何变体,然后验证其外观和感觉。

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

CodeacademyGitlabIBMDC/OS LabsMonday.com只是其中的一些例子。

使用装饰器来隔离连接型组件

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

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

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

一个装饰器是运行在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 = () => { /* ... */ };

加载全局提供者

许多库依赖于全局提供者进行配置。例如,Styled ComponentsChakra UI使用提供者来自定义主题。而React Intl使用提供者来传递特定于区域设置的翻译。

我们可以向.storybook/preview.js添加一个全局装饰器来加载这些提供者。下面是如何使用React Intl与Storybook进行设置的示例。

// .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、参数、全局变量等。这意味着您可以使用参数来配置插件。

例如,这个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]

模拟状态管理库的上下文

提供者模式也被广泛用于状态管理库,如ReduxMobXRecoil,以使组件能够访问状态存储。在这种情况下,我们可以使用故事装饰器提供一个模拟存储来渲染不同的组件变体。

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

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

// 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生态系统中,越来越普遍的是结合使用钩子和上下文而不是状态管理库。在这种情况下,您可以使用React Context addon来为您的组件提供和操纵上下文。

模拟REST和GraphQL API请求

当您继续深入组件树时,您开始将UI连接到后端API和服务。我们可以在Storybook中直接模拟这些请求。

JavaScript生态系统提供了许多优秀的API请求模拟工具。此外,其中大多数工具都可用作Storybook插件。因此,与其构建自定义装饰器,不如使用插件来快速入门。

Mock Service Worker (MSW)是一个多功能插件,它使用服务工作线程在网络级别拦截请求并返回模拟数据。它同时支持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插件使用装饰器将代码注入preview iframe,从而简化CSS调试。有关更多关于这种技术的信息,请参阅创建插件教程。

结论

UI包含无数种语言、设备、用户偏好和应用程序状态的排列组合。使用Storybook,您可以将这些变体捕获为故事,并在开发和测试期间重新访问它们。

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

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

加入 Storybook 邮件列表

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

7,468开发者及更多

我们正在招聘!

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

查看职位

热门帖子

Storybook 7.0 设计预览

视觉更新、用户体验调整和更快的性能
loading
Dominic Nguyen

用 TypeScript 编写 Stories

了解如何为您的 Story 添加类型,使它们更容易编写且更健壮
loading
Kyle Gach

社区作品集 #2

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

特别感谢 Netlify CircleCI