返回博客

Material UI 在 Storybook 中

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

loading
Shaun Evening
@Integrayshaun
最后更新
⚠️
更新,2023年:自Storybook 7.0正式发布以来,已经发生了一些变化。要获取更最新的Material UI在Storybook中集成指南,请查看我们集成目录中的食谱

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包的React应用程序,并且已安装Storybook 6.0或更高版本。如果您还没有准备好的项目,请克隆我的示例存储库进行跟随。

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

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的视觉测试插件

使用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中获取variantsizecolor属性。

📣 感谢Eric Mudrak的精彩Storybook与React和TypeScript文章,它启发了这个技巧。

总结

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

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

如果您正在寻找我在这里介绍的内容的代码示例,请查看我在GitHub上的storybook-mui-example存储库。如果您正在寻找一个可以为您处理主题的Storybook插件,请查看由了不起的社区成员UsulproSmartlight开发的React Theming Addon

您想看什么?

我们希望听到您的声音!

您喜欢这个食谱吗?您还想看其他Storybook集成食谱吗?

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

加入 Storybook 邮件列表

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

7,468开发者及更多

我们正在招聘!

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

查看职位

热门帖子

Storybook对Vite的一流支持

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

Storybook 新网站

2022年全新面貌
loading
Dominic Nguyen

使用 Storybook 进行国际化

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

特别感谢 Netlify CircleCI