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

Material UI (MUI) 基于谷歌的Material Design语言,提供了一套可定制主题的组件,你可以立即开始构建 UI。通过在 Storybook 中独立开发 UI,你可以更快地入门。以下是如何配置 Storybook 来加载 Material UI 组件并与 MUI 的 API 进行动态交互。
- 📦 打包你的字体,实现快速一致的渲染
- 🎨 加载你的自定义主题并添加主题切换器
- ♻️ 重用 Material UI 类型来自动生成 Story 控件

开始构建吧
本指南假定你已经有一个使用 @mui/material
包,并已配置 Storybook 6.0 或更高版本。如果你没有现成的项目,可以克隆我的示例仓库跟着做。
打包字体和图标以获得更好的性能
Material UI 依赖于两种字体才能正常渲染,分别是谷歌的Roboto
和Material 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 正在提供我们的自定义深色主题。
使用 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 表格。

我们以以下 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开发的精彩插件,开发者分别是Usulpro和Smartlight。
接下来你想看什么?
期待你的反馈!
你喜欢这篇指南吗?你还想看哪些 Storybook 集成的指南?
在推特上联系@storybookjs或在Storybook Discord 服务器上联系我们!我们期待与你相见 🤩
📦 打包字体和图标以获得更好的性能。为什么?
— Storybook (@storybookjs) October 6, 2022
🏎️ 字体加载更快 因为它们与你的应用来自同一位置
✈️ 字体将离线加载 这样你就可以在任何地方继续开发你的 stories
📸 不再有不一致的快照测试 因为字体会立即加载
2/ pic.twitter.com/gPuXx8q4Vt