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

Material UI (MUI) 基于 Google 的 Material Design 语言,提供了一组可主题化的组件,您可以立即使用它们开始构建 UI。通过在 Storybook 中隔离构建 UI,您可以更快地开始。以下是如何配置 Storybook 以加载 Material UI 组件并动态地与 MUI 的 API 交互。
- 📦 打包字体以实现快速且一致的渲染
- 🎨 加载您的自定义主题并添加主题切换器
- ♻️ 重用 Material UI 类型以自动生成故事控件

开始构建吧
此配方假设您已经有一个使用 @mui/material
包并设置了 Storybook 6.0 或更高版本的 React 应用程序。如果您还没有准备好项目,请克隆我的示例存储库以跟随学习。
打包字体和图标以获得更好的性能
Material UI 依赖两种字体才能按预期渲染:Google 的 Roboto
和 Material 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
装饰器正在提供我们的自定义暗黑主题。
使用 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 的组件属性类型来自动生成故事控件。作为奖励,这还将自动填充文档选项卡中的属性表。

让我们以以下 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 插件来为您处理主题,请查看由优秀的社区成员 Usulpro 和 Smartlight 提供的 React Theming Addon。
您接下来想看什么?
我们想听取您的意见!
您喜欢这个配方吗?您还想看到其他 Storybook 集成配方的吗?
在 Twitter 上 @storybookjs 或在 Storybook Discord 服务器上联系我们!我们迫不及待想见到您 🤩
📦 打包字体和图标以获得更好的性能。为什么?
— Storybook (@storybookjs) 2022 年 10 月 6 日
🏎️ 字体加载更快 ,因为它们与您的应用程序来自同一位置
✈️ 字体将离线加载 ,因此您可以在任何地方继续开发故事
📸 不再有不一致的快照测试 ,因为字体即时加载
2/ pic.twitter.com/gPuXx8q4Vt