返回博客

Storybook 中的 Material UI

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

loading
Shaun Evening
@Integrayshaun
上次更新
⚠️
更新,2023 年: 自 Storybook 7.0 正式发布以来,一些事项已经发生了变化。有关在 Storybook 中集成 Material UI 的最新指南,请查看我们在集成目录中的配方

Material UI (MUI) 基于 Google 的 Material Design 语言,提供了一组可主题化的组件,您可以立即使用它们开始构建 UI。通过在 Storybook 中隔离构建 UI,您可以更快地开始。以下是如何配置 Storybook 以加载 Material UI 组件并动态地与 MUI 的 API 交互。

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

开始构建吧

此配方假设您已经有一个使用 @mui/material 包并设置了 Storybook 6.0 或更高版本的 React 应用程序。如果您还没有准备好项目,请克隆我的示例存储库以跟随学习。

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

Material UI 依赖两种字体才能按预期渲染:Google 的 RobotoMaterial Icons。虽然您可以直接从 Google Fonts CDN 加载这些字体,但将字体与 Storybook 打包在一起可以获得更好的性能。

  • 🏎️ 字体加载更快,因为它们与您的应用程序来自同一位置
  • ✈️ 字体将离线加载,因此您可以在任何地方继续开发故事
  • 📸 不再有不一致的快照测试,因为字体即时加载

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

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"],
    },
  },
});

要将自定义主题应用于我们的故事,我们需要使用装饰器将它们包装在 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 装饰器正在提供我们的自定义暗黑主题。

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

使用 globalTypes 添加主题切换器

为了使这个装饰器更进一步,让我们添加一种在多个主题之间切换的方法。

为此,我们可以在 .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" },
      ],
    },
  },
};

现在我们可以更新我们的装饰器,以提供在我们新的下拉菜单中选择的主题。

// .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 属性类型以获得更好的控件和文档

Storybook 控件为您提供图形控件来操作组件的属性。它们对于查找组件的边缘情况和在浏览器中进行原型设计非常方便。

通常,您必须手动配置控件。但是,如果您使用 Typescript,您可以重用 Material UI 的组件属性类型来自动生成故事控件。作为奖励,这还将自动填充文档选项卡中的属性表。

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 属性作为 MuiButton 的子元素,并将所有其他属性传递过去。但是,当我们在 Storybook 中渲染它时,我们的控件面板只允许我们更改我们自己声明的 label 属性。

这是因为 Storybook 只将组件属性类型或 Story Args 中显式声明的属性添加到控件表中。让我们更新 Storybook 的 Docgen 配置,以便也将 Material UI 的 Button 属性带入控件表中。

// .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 中的参数,以显示控件表的描述和默认列。

// .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 属性扩展,从而将所有这些属性添加到控件中。

// 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 属性的控件。

选择哪些控件可见

我们的按钮现在有 27 个属性,这对于您的用例来说可能有点多。为了控制哪些属性可见,我们可以使用 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 属性。

📣 感谢 Eric Mudrak 出色的 Storybook with React & TypeScript 文章,它启发了这个技巧。

总结

Material UI 提供了很多功能——大量的组件、强大的主题引擎和图标系统。与其重新发明轮子,不如使用这些构建块快速入门。通过一些配置调整,您的 Storybook 可以释放 Material UI 的全部潜力。

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

如果您正在寻找我在此处介绍的代码示例,请查看我在 GitHub 上的 storybook-mui-example 存储库。如果您正在寻找一个 Storybook 插件来为您处理主题,请查看由优秀的社区成员 UsulproSmartlight 提供的 React Theming Addon

您接下来想看什么?

我们想听取您的意见!

您喜欢这个配方吗?您还想看到其他 Storybook 集成配方的吗?

在 Twitter 上 @storybookjs 或在 Storybook Discord 服务器上联系我们!我们迫不及待想见到您 🤩

加入 Storybook 邮件列表

获取最新的新闻、更新和发布

6,730位开发者及更多

我们正在招聘!

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

查看职位

热门文章

Storybook 中一流的 Vite 支持

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

Storybook 新网站

2022 年的全新外观
loading
Dominic Nguyen

Storybook 的 I18n

如何使用 Storybook 将组件国际化
loading
Shaun Evening
加入社区
6,730位开发者及更多
为什么为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI