如何为Storybook添加一个主题切换器(详细教程)

951 阅读4分钟

主题控制着用户界面的视觉特征--调色板、排版、留白、边框样式、阴影、弧度等。主题越来越受欢迎,因为应用程序需要支持多种颜色模式和品牌要求。

但主题开发可能很乏味。你必须跟踪你的应用程序中的无数个状态,然后乘以你支持的主题数量。同时不断地在不同的主题之间来回切换,以检查用户界面是否正确。

有了Storybook,你可以控制哪个主题应用于你的组件,并通过工具栏点击在不同的主题之间切换。这篇文章告诉你如何做:

  • 🎁 使用装饰器将主题对象传递给你的组件
  • 🎛 从工具栏或使用故事参数动态地切换主题
  • 🖍 自动更新故事背景以匹配主题
  • 🍱 在一个故事中并排渲染多个主题

我们正在建立什么?

与作为组件输入的数据不同,主题是通过上下文提供的,或者作为CSS变量全局配置的。

我们将建立一个主题切换工具,允许你在Storybook中为你的所有组件提供主题对象。你将能够通过参数或工具栏上的按钮来控制哪个主题处于活动状态:

How to add a theme switcher to Storybook

我们将使用这个徽章组件进行演示--它来自使用React和styled-components构建的Maldrop应用

How to add a theme switcher to Storybook

它使用主题对象的变量来设置边界半径、背景和颜色值。主题对象是通过上下文API传递给组件的:

// src/components/Badge/Badge.tsx
import styled, { css } from 'styled-components'

import { Body } from '../typography'

const Container = styled.div(
  ({ theme }) => css`
    padding: 3px 8px;
    background: ${theme.color.badgeBackground};
    border-radius: ${theme.borderRadius.xs};
    display: inline-block;
    text-transform: capitalize;
    span {
      color: ${theme.color.badgeText};
    }
  `
)

type BadgeProps = {
  text: string
  className?: string
}

export const Badge = ({ text, className }: BadgeProps) => (
  <Container className={className}>
    <Body type="span" size="S">
      {text}
    </Body>
  </Container>
)

克隆回购

让我们开始吧!Clone repo,安装依赖性,然后跟着做:

# Clone the template
npx degit yannbf/mealdrop#theme-switcher-base mealdrop

cd mealdrop

# Install dependencies
yarn

使用装饰器为你的组件提供主题

第一步是为我们的组件提供主题。我们将使用一个装饰器来做到这一点,它将用ThemeProvider 来包装每个故事,并传入lightTheme 对象。

装饰器是一种Storybook机制,允许你用额外的渲染功能来增强故事。例如,你可以提供一个组件所依赖的上下文或其他全局配置。

让我们把withTheme 装饰器添加到.storybook/preview.tsx 文件中:

// .storybook/preview.tsx
import { ThemeProvider } from 'styled-components'
import { DecoratorFn } from '@storybook/react'

import { GlobalStyle } from '../src/styles/GlobalStyle'
import { lightTheme } from '../src/styles/theme'

const withTheme: DecoratorFn = (StoryFn) => {
  return (
    <ThemeProvider theme={lightTheme}>
      <GlobalStyle />
      <StoryFn />
    </ThemeProvider>
  )
}

// export all decorators that should be globally applied in an array
export const decorators = [withTheme]

.storybook/preview.js|tsx 文件中定义的装饰器是全局的。也就是说,它们将被应用于你所有的故事。因此,它也是加载这些组件所使用的GlobalStyle 的完美位置。

运行yarn storybook ,启动Storybook,你应该看到Badge组件在应用了浅色主题的情况下正确渲染。

How to add a theme switcher to Storybook

通过参数设置活动主题

现在,我们的withTheme 装饰器只向组件提供了浅色主题。为了测试浅色和深色模式,我们需要在它们之间进行动态切换。我们可以使用参数来指定启用哪个主题。

参数是元数据,你可以附加到一个故事或一个组件上。然后,withTheme 装饰器可以从故事上下文对象中访问它们并应用适当的主题。

更新你的装饰器以读取主题参数:

// .storybook/preview.tsx
import { ThemeProvider } from 'styled-components'
import { DecoratorFn } from '@storybook/react'

import { GlobalStyle } from '../src/styles/GlobalStyle'
import { darkTheme, lightTheme } from '../src/styles/theme'

const withTheme: DecoratorFn = (StoryFn, 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 />
      <StoryFn />
    </ThemeProvider>
  )
}

export const decorators = [withTheme]

当为一个组件编写故事时,你可以选择使用参数来应用哪个主题。像这样:

// src/components/Badge/Badge.stories.tsx
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { Badge } from './Badge'

export default {
  title: 'Components/Badge',
  component: Badge,
} as ComponentMeta<typeof Badge>

const Template: ComponentStory<typeof Badge> = (args) => <Badge {...args} />

export const Default = Template.bind({})
Default.args = {
  text: 'Comfort food',
}

export const LightTheme = Template.bind({})
LightTheme.args = Default.args
LightTheme.parameters = {
  theme: 'light',
}

export const DarkTheme = Template.bind({})
DarkTheme.args = Default.args
DarkTheme.parameters = {
  theme: 'dark',
}

切换回你的故事书,你会注意到,当你在这两个故事之间导航时,主题会更新:

How to add a theme switcher to Storybook

很好!这使我们可以灵活地设置每个故事的主题。

切换背景颜色以匹配主题

这是一个好的开始。我们可以灵活地控制每个故事的主题。然而,背景仍然是一样的。让我们更新我们的装饰器,使故事的背景颜色与活动主题相匹配。

我们现在用一个ThemeBlock 组件来包装每个故事,它根据活动主题来控制背景颜色:

// .storybook/preview.tsx
import React from 'react'
import styled, { css, ThemeProvider } from 'styled-components'
import { DecoratorFn } from '@storybook/react'

import { GlobalStyle } from '../src/styles/GlobalStyle'
import { darkTheme, lightTheme } from '../src/styles/theme'
import { breakpoints } from '../src/styles/breakpoints'

const ThemeBlock = styled.div<{ left?: boolean; fill?: boolean }>(
  ({ left, fill, theme }) =>
    css`
      position: absolute;
      top: 0;
      left: ${left || fill ? 0 : '50vw'};
      border-right: ${left ? '1px solid #202020' : 'none'};
      right: ${left ? '50vw' : 0};
      width: ${fill ? '100vw' : '50vw'};
      height: 100vh;
      bottom: 0;
      overflow: auto;
      padding: 1rem;
      background: ${theme.color.screenBackground};
      ${breakpoints.S} {
        left: ${left ? 0 : '50vw'};
        right: ${left ? '50vw' : 0};
        padding: 0 !important;
      }
    `
)

export const withTheme: DecoratorFn = (StoryFn, context) => {
  // Get values from story parameter first
  const { theme } = context.parameters
  const storyTheme = theme === 'dark' ? darkTheme : lightTheme
  return (
    <ThemeProvider theme={storyTheme}>
      <GlobalStyle />
      <ThemeBlock fill>
        <StoryFn />
      </ThemeBlock>
    </ThemeProvider>
  )
}

export const decorators = [withTheme]

现在,当你在这些故事之间切换时,主题和背景颜色都会更新:

How to add a theme switcher to Storybook

从工具条上切换主题

通过参数对主题进行硬编码只是一种选择。我们也可以定制Storybook的用户界面,添加一个下拉菜单,让我们可以切换哪个主题处于活动状态。

Storybook提供了工具条插件,使你能够定义一个全局值,并将其连接到工具条上的一个菜单。

为了创建一个控制活动主题的工具栏项目,我们需要在我们的.storybook/preview.tsx 文件中添加一个globalTypes 对象:

// .storybook/preview.tsx

// ...code ommited for brevity...

export const withTheme: DecoratorFn = (StoryFn, context) => {
  // Get values from story parameter first, else fallback to globals
  const theme = context.parameters.theme || context.globals.theme
  const storyTheme = theme === 'dark' ? darkTheme : lightTheme
  return (
    <ThemeProvider theme={storyTheme}>
      <GlobalStyle />
      <ThemeBlock fill>
        <StoryFn />
      </ThemeBlock>
    </ThemeProvider>
  )
}

export const globalTypes = {
  theme: {
    name: 'Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      // The icon for the toolbar item
      icon: 'circlehollow',
      // Array of options
      items: [
        { value: 'light', icon: 'circlehollow', title: 'light' },
        { value: 'dark', icon: 'circle', title: 'dark' },
      ],
      // Property that specifies if the name of the item will be displayed
      showName: true,
    },
  },
}

export const decorators = [withTheme]

我们还更新了withTheme 装饰器,以便首先从参数中获取主题值,如果它未被定义,则返回到全局值。

现在你应该看到一个切换主题的工具条项目:

How to add a theme switcher to Storybook

对于Default ,它没有指定一个主题参数,你可以使用工具栏切换主题。然而,LightThemeDarkTheme 故事将始终执行通过主题参数设置的值。

并排渲染主题

有时,如果你能同时看到一个组件的所有主题变体,就会更容易对其进行处理。你猜怎么着?你可以在一个装饰器中多次渲染一个故事,并为每个实例提供不同的主题对象。

更新withTheme 装饰器和globalTypes ,添加一个 "并排 "模式:

// .storybook/preview.tsx

// ...code ommited for brevity...

export const withTheme: DecoratorFn = (StoryFn, context) => {
  // Get values from story parameter first, else fallback to globals
  const theme = context.parameters.theme || context.globals.theme
  const storyTheme = theme === 'light' ? lightTheme : darkTheme

  switch (theme) {
    case 'side-by-side': {
      return (
        <>
          <ThemeProvider theme={lightTheme}>
            <GlobalStyle />
            <ThemeBlock left>
              <StoryFn />
            </ThemeBlock>
          </ThemeProvider>
          <ThemeProvider theme={darkTheme}>
            <GlobalStyle />
            <ThemeBlock>
              <StoryFn />
            </ThemeBlock>
          </ThemeProvider>
        </>
      )
    }
    default: {
      return (
        <ThemeProvider theme={storyTheme}>
          <GlobalStyle />
          <ThemeBlock fill>
            <StoryFn />
          </ThemeBlock>
        </ThemeProvider>
      )
    }
  }
}

export const globalTypes = {
  theme: {
    name: 'Theme',
    description: 'Theme for the components',
    defaultValue: 'light',
    toolbar: {
      // The icon for the toolbar item
      icon: 'circlehollow',
      // Array of options
      items: [
        { value: 'light', icon: 'circlehollow', title: 'light' },
        { value: 'dark', icon: 'circle', title: 'dark' },
        { value: 'side-by-side', icon: 'sidebar', title: 'side by side' },
      ],
      // Property that specifies if the name of the item will be displayed
      showName: true,
    },
  },
}

export const decorators = [withTheme]

这就是最后的结果:

How to add a theme switcher to Storybook

并排模式对于使用Chromatic等工具进行视觉回归测试也是非常方便的。你可以通过参数启用它,一次性测试一个组件的所有基于主题的变体。

总结

在构建UI时,你必须考虑到应用状态、地域、视口尺寸、主题等的无数种变化。Storybook使测试UI变体变得容易。你可以使用数以百计的附加组件之一,或者定制Storybook以满足你的需求。

How to add a theme switcher to Storybook

装饰器使你能够完全控制故事的渲染,并使你能够设置提供者,并使用参数或通过将它们连接到一个工具条项目来控制它们的行为。切换主题只是这项技术的一个应用。你可以用它来添加一个语言切换器或一个菜单来管理多租户的配置。