返回博客

在 Storybook 中使用 Material UI

充分利用 Storybook 和 Material UI 的三个技巧

loading
Shaun Evening
@Integrayshaun
最后更新于
⚠️
更新,2023年:自 Storybook 7.0 正式发布以来,一些内容有所变化。有关在 Storybook 中集成 Material UI 的最新指南,请查看我们的集成目录中的指南

Material UI (MUI) 基于谷歌的Material Design语言,提供了一套可定制主题的组件,你可以立即开始构建 UI。通过在 Storybook 中独立开发 UI,你可以更快地入门。以下是如何配置 Storybook 来加载 Material UI 组件并与 MUI 的 API 进行动态交互。

  • 📦 打包你的字体,实现快速一致的渲染
  • 🎨 加载你的自定义主题并添加主题切换器
  • ♻️ 重用 Material UI 类型来自动生成 Story 控件
A demo of the completed Storybook with a theme switcher and prop controls

开始构建吧

本指南假定你已经有一个使用 @mui/material 包,并已配置 Storybook 6.0 或更高版本。如果你没有现成的项目,可以克隆我的示例仓库跟着做。

打包字体和图标以获得更好的性能

Material UI 依赖于两种字体才能正常渲染,分别是谷歌的RobotoMaterial Icons。虽然你可以直接从 Google Fonts CDN 加载这些字体,但在 Storybook 中打包字体对性能更好。

  • 🏎️ 字体加载更快,因为它们与你的应用来自同一位置
  • ✈️ 字体将离线加载,这样你就可以在任何地方继续开发你的 stories
  • 📸 不再有不一致的快照测试,因为字体会立即加载

首先,将字体安装为依赖项

yarn add @fontsource/roboto @fontsource/material-icons

然后将 CSS 文件导入到 .storybook/preview.js 中,这是你的 Storybook 的入口文件

// .storybook/preview.js
 
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '@fontsource/material-icons';

加载自定义主题并添加主题切换器

Material UI 默认提供一个主题,但你也可以创建并提供自己的主题。考虑到深色模式的流行,你很可能会有不止一个自定义主题。让我们看看如何加载自定义主题并通过一次点击在它们之间切换。

例如,这个自定义的深色模式主题

// src/themes/dark.theme.js

import { createTheme } from "@mui/material";
import { blueGrey, cyan, pink } from "@mui/material/colors";

export const darkTheme = createTheme({
  palette: {
    mode: "dark",
    primary: {
      main: pink["A200"],
    },
    secondary: {
      main: cyan["A400"],
    },
    background: {
      default: blueGrey["800"],
      paper: blueGrey["700"],
    },
  },
});

要将自定义主题应用于我们的 stories,我们需要使用 decorator 将它们包裹在 Material UI 的 ThemeProvider

// .storybook/preview.js

import { CssBaseline, ThemeProvider } from "@mui/material";
import { darkTheme } from "../src/themes/dark.theme";

/* snipped for brevity */

export const withMuiTheme = (Story) => (
  <ThemeProvider theme={darkTheme}>
    <CssBaseline />
    <Story />
  </ThemeProvider>
);

export const decorators = [withMuiTheme];

太棒了!现在重新加载 Storybook 后,你会看到我们的 withMuiTheme decorator 正在提供我们的自定义深色主题。

为了自动捕捉所有主题 UI 变体中的视觉 bug,请查看 Storybook 的Visual Tests 插件

使用 globalTypes 添加主题切换器

为了进一步完善这个 decorator,让我们添加一种在多个主题之间切换的方法。

为此,我们可以在 .storybook/preview.js 中声明一个名为 theme 的全局变量,并提供一个支持的主题列表供选择。

// .storybook/preview.js

export const globalTypes = {
  theme: {
    name: "Theme",
    title: "Theme",
    description: "Theme for your components",
    defaultValue: "light",
    toolbar: {
      icon: "paintbrush",
      dynamicTitle: true,
      items: [
        { value: "light", left: "☀️", title: "Light mode" },
        { value: "dark", left: "🌙", title: "Dark mode" },
      ],
    },
  },
};

现在我们可以更新我们的 decorator,使其提供在新下拉菜单中选择的主题。

// .storybook/preview.js

import { useMemo } from "react";

/* Snipped for brevity */

// Add your theme configurations to an object that you can
// pull your desired theme from.
const THEMES = {
  light: lightTheme,
  dark: darkTheme,
};

export const withMuiTheme = (Story, context) => {
  // The theme global we just declared
  const { theme: themeKey } = context.globals;

  // only recompute the theme if the themeKey changes
  const theme = useMemo(() => THEMES[themeKey] || THEMES["light"], [themeKey]);

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Story />
    </ThemeProvider>
  );
};


现在我们有了一个功能齐全的 MaterialUI Storybook 主题切换器。如果你想了解更多关于切换器的信息,请查看Yann Braga关于添加主题切换器的文章。

使用 Material UI prop 类型以获得更好的控件和文档

Storybook controls 提供图形化控件来操作组件的 props。它们对于发现组件的边缘情况以及在浏览器中进行原型设计非常方便。

通常,你必须手动配置控件。但如果你使用 Typescript,你可以重用 Material UI 的组件 prop 类型来自动生成 story 控件。另外,这还会自动填充文档标签页中的 prop 表格。

Changing the button components props using Storybook controls

我们以以下 Button 组件为例。

// button.component.tsx
 
import React from 'react';
import { Button as MuiButton } from '@mui/material';
 
export interface ButtonProps {
  label: string;
}
 
export const Button = ({ label, ...rest }: ButtonProps) => (
  <MuiButton {...rest}>{label}</MuiButton>
);

这里我将 label prop 作为 MuiButton 的子元素,并将所有其他 props 透传下去。然而,当我们在 Storybook 中渲染它时,我们的 controls 面板只允许我们更改自己声明的 label prop。

这是因为 Storybook 只会将组件 prop 类型或 Story Args 中明确声明的 props 添加到 controls 表中。让我们更新 Storybook 的 Docgen 配置,将 Material UI 的 Button props 也带入 controls 表中。

// .storybook/main.ts
 
module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "@storybook/preset-create-react-app",
  ],
  framework: "@storybook/react",
  core: {
    builder: "@storybook/builder-webpack5",
  },
  typescript: {
    check: false,
    checkOptions: {},
    reactDocgen: "react-docgen-typescript",
    reactDocgenTypescriptOptions: {
      // speeds up storybook build time
      allowSyntheticDefaultImports: false,
      // speeds up storybook build time
      esModuleInterop: false,
      // makes union prop types like variant and size appear as select controls
      shouldExtractLiteralValuesFromEnum: true,
      // makes string and boolean types that can be undefined appear as inputs and switches
      shouldRemoveUndefinedFromOptional: true,
      // Filter out third-party props from node_modules except @mui packages
      propFilter: (prop) =>
        prop.parent
          ? !/node_modules\/(?!@mui)/.test(prop.parent.fileName)
          : true,
    },
  },
};


我们还需要更新 .storybook/preview.js 中的参数,以便在 controls 表中显示描述和默认值列。

// .storybook/preview.js
 
export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    expanded: true, // Adds the description and default columns
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}

最后,更新 ButtonProps 类型,使其扩展自 Material UI 的 Button props,以便将所有这些 props 添加到 controls 中。

// button.component.tsx
 
import React from "react";
import {
  Button as MuiButton,
  ButtonProps as MuiButtonProps,
} from "@mui/material";
 
export interface ButtonProps extends MuiButtonProps {
  label: string;
}
 
export const Button = ({ label, ...rest }: ButtonProps) => (
  <MuiButton {...rest}>{label}</MuiButton>
);

重启 Storybook 服务器,以便这些配置更改生效。现在你应该会看到 Button 也拥有了 MuiButton 的所有 props 的控件。

选择哪些控件可见

我们的 button 现在有27 个 props,对于你的用例来说可能有点多。要控制哪些 props 可见,我们可以使用 TypeScript 的Pick<type, keys>Omit<type, keys>工具类型。

// button.component.tsx

import React from "react";
import {
  Button as MuiButton,
  ButtonProps as MuiButtonProps,
} from "@mui/material";

// Only include variant, size, and color
type ButtonBaseProps = Pick<MuiButtonProps, "variant" | "size" | "color">;

// Use all except disableRipple
// type ButtonBaseProps = Omit<MuiButtonProps, "disableRipple">;

export interface ButtonProps extends ButtonBaseProps {
  label: string;
}

export const Button = ({ label, ...rest }: ButtonProps) => (
  <MuiButton {...rest}>{label}</MuiButton>
);

现在我们的 Button 将只接受来自 MuiButton 的 variant、size 和 color 这三个 props。

📣 特别感谢Eric Mudrak的精彩文章使用 React 和 TypeScript 的 Storybook,这篇文章启发了这个技巧。

总结

Material UI 提供了丰富的功能——广泛的组件、强大的主题引擎和图标系统。无需重复造轮子,你可以使用这些构建块快速启动。只需进行一些配置调整,你的 Storybook 就可以充分发挥 Material UI 的潜力。

可以使用 Storybook decorator 来提供自定义 Material UI 主题,并且通过添加一个工具栏项,你可以在多个主题之间切换。这使得在构建应用时轻松切换主题并验证 UI 的外观和感觉。此外,可以免费重用 Material UI 的 Typescript 类型为你的 stories 生成动态控件和文档。

如果你想找到我在这里介绍的代码示例,请查看我在 GitHub 上的storybook-mui-example仓库。如果你正在寻找一个帮你处理主题的 Storybook 插件,请查看社区成员React Theming Addon开发的精彩插件,开发者分别是UsulproSmartlight

接下来你想看什么?

期待你的反馈!

你喜欢这篇指南吗?你还想看哪些 Storybook 集成的指南?

在推特上联系@storybookjs或在Storybook Discord 服务器上联系我们!我们期待与你相见 🤩

加入 Storybook 邮件列表

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

7,180开发者(还在增加)

我们正在招聘!

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

查看职位

热门文章

Storybook 中对 Vite 的一流支持

Storybook 7.0 带来了许多 Vite 改进:预打包以提高速度、零配置设置、无需 Webpack 以及更小的安装包大小。
loading
Ian VanSchooten

Storybook 新网站

2022 年焕然一新的外观
loading
Dominic Nguyen

Storybook 中的国际化 (I18n)

如何使用 Storybook 对组件进行国际化
loading
Shaun Evening
加入社区
7,180开发者(还在增加)
为什么为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与其中博客
案例展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI