如何在Storybook中建立连接组件(详细教程)

587 阅读7分钟

Avatar,Button, 和Tooltip 这样的展示型组件只通过道具接收输入,没有内部状态。这使得为它们隔离和编写故事变得简单明了。然而,在应用树中更高的组件在使用Storybook进行隔离时就比较棘手。

Forms,List, 和Cards 这样的连接组件跟踪应用程序的状态,然后在树上传递行为。它们通常需要一个 "束缚/包装器",以便以有用的方式呈现:

  • 样式ThemeProvider 和全局样式
  • 布局:模仿布局的DOM结构
  • 数据获取:GraphQL提供者或钩子来进行API调用。
  • 状态管理:Redux、MobX、Recoil等的存储提供者。

这篇文章展示了如何使用装饰器来隔离连接的组件。你将学会构建装饰器,使用参数控制其行为,并使用它们来模拟组件的依赖性。

为什么要孤立地构建连接的组件?

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

How to build connected components in Storybook

How to build connected components in Storybook

Supabase的Button组件和Storybook设计系统的Cardinal组件的众多变体。

虽然Storybook被广泛用于设计系统,但对于前端团队来说,为应用程序组件编写故事也很常见。这些组件与应用程序的状态、上下文和钩子 "相连",产生更复杂的变化。开发人员选择在Storybook中构建连接的组件,因为它更容易开发难以触及的用例,如加载、错误和空状态:

CodeacademyGitlabIBMDC/OS LabsMonday.com只是其中的几个例子:

How to build connected components in Storybook

How to build connected components in Storybook

How to build connected components in Storybook

How to build connected components in Storybook

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

UI组件需要数据和动作处理程序来渲染。这些通常是作为道具传入的,但连接的组件也通过上下文、API请求和钩子直接访问它们。

为了隔离一个连接的组件,你必须模拟它的依赖关系。在Storybook中,你可以使用装饰器来提供模拟的上下文,并为不同的组件变化编写故事。

How to build connected components in Storybook

Storybook分为两部分:管理器,它渲染Storybook的用户界面(搜索、导航、工具栏和附加组件)和预览,你的故事在这里被渲染。

装饰器是在预览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 = () => { /* ... */ };

How to build connected components in Storybook

加载全局提供者

许多库依靠全局提供者进行配置。例如,Styled ComponentsChakra UI使用一个提供者来定制主题。而React Intl使用一个提供者来传递特定于本地的翻译。

How to build connected components in Storybook

我们可以在.storybook/preview.js ,添加一个全局装饰器来加载这些提供者。下面是一个如何用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、参数、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]

状态管理库的模拟上下文

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

How to build connected components in Storybook

考虑这个TaskList 组件与Redux商店的连接。该存储中的应用状态决定了TaskList 的变体被呈现出来:

How to build connected components in Storybook

为了控制应用状态,我们将使用@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生态系统中,使用钩子和上下文的组合而不是状态管理库的情况越来越普遍。在这种情况下,你可以使用React Context插件来为你的组件提供和操作上下文。

模拟REST和GraphQL API请求

当你继续往上看组件树时,你开始将UI与后端API和服务进行连接。我们可以在Storybook中模拟这些请求。

JavaScript生态系统提供了许多优秀的工具来模拟API请求。更重要的是,这些工具中的大多数都可以作为Storybook的附加组件使用。因此,与其建立一个自定义的装饰器,不如使用一个附加组件来快速入门。

Mock Service Worker (MSW)是一个多功能插件,使用服务工作者在网络层面拦截请求并返回模拟数据。它可以与REST和GraphQL后端一起工作:

How to build connected components in Storybook

How to build connected components in Storybook

在引擎盖下,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的特定库附加组件:

How to build connected components in Storybook

使用装饰器来扩展Storybook的功能

除了模拟一个组件的依赖性,装饰器还可以使你为你的Storybook添加额外的功能位。例如,MeasureOutlineBackgrounds插件使用装饰器将代码注入预览iframe中,使CSS调试更容易。关于这一技术的更多信息,请查看创建附加组件教程:

How to build connected components in Storybook

How to build connected components in Storybook

How to build connected components in Storybook

结论

UIs考虑到了语言、设备、用户偏好和应用程序状态的无尽变化。通过Storybook,你可以将这些变化捕捉为故事,并在开发和测试过程中重新审视它们。

你可以通过道具提供模拟数据来重现一个展示性组件的不同状态。但要隔离连接的组件就更有挑战性了,因为它们与应用程序的状态、交互和API请求是相连的。