工具栏与全局变量
Storybook 提供控制故事渲染视口和背景的功能。类似地,你可以使用内置功能创建工具栏项来控制特殊的“全局变量”。然后你可以读取全局变量的值,创建装饰器来控制故事渲染。
全局变量
Storybook 中的全局变量代表了故事渲染的“全局”(即非故事特定的)输入。由于它们不是故事特定的,因此不会作为 args
参数传递给故事函数(尽管可以通过 context.globals
访问)。相反,它们通常用于装饰器,装饰器适用于所有故事。
当全局变量改变时,故事会重新渲染,装饰器会使用新值重新运行。改变全局变量最简单的方法是为它们创建一个工具栏项。
全局类型和工具栏注解
Storybook 提供了一种简单、声明式的语法来配置工具栏菜单。在你的.storybook/preview.js|ts
中,你可以通过创建带有 toolbar
注解的 globalTypes
来添加自己的工具栏
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
const preview: Preview = {
globalTypes: {
theme: {
description: 'Global theme for components',
toolbar: {
// The label to show for this toolbar item
title: 'Theme',
icon: 'circlehollow',
// Array of plain string values or MenuItem shape (see below)
items: ['light', 'dark'],
// Change title based on selected value
dynamicTitle: true,
},
},
},
initialGlobals: {
theme: 'light',
},
};
export default preview;
由于全局变量是全局的,你只能在.storybook/preview.js|ts
中设置 globalTypes
和 initialGlobals
。
当你启动 Storybook 时,你的工具栏应该会有一个新的下拉菜单,包含 light
和 dark
选项。
创建装饰器
我们已经实现了一个全局变量
。现在让我们把它连接起来!我们可以在装饰器中利用新的 theme
全局变量,使用 context.globals.theme
的值。
例如,假设你正在使用 styled-components
。你可以在 .storybook/preview.js|ts
配置中添加一个主题提供者装饰器:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
import { MyThemes } from '../my-theme-folder/my-theme-file';
const preview: Preview = {
decorators: [
(story, context) => {
const selectedTheme = context.globals.theme || 'light';
const theme = MyThemes[selectedTheme];
// Your theme provider and other context providers goes in the return statement
return;
},
],
};
export default preview;
在故事中设置全局变量
当 Storybook 中通过工具栏菜单更改全局变量值时,该值在你切换故事时会继续使用。但有时故事需要特定的值才能正确渲染,例如在针对特定环境进行测试时。
为了确保故事始终使用特定的全局变量值,无论工具栏中选择了什么,你可以在故事或组件上设置 globals
注解。这会覆盖这些故事的全局变量值,并在查看这些故事时禁用该全局变量的工具栏菜单。
// Replace your-framework with the name of your framework (e.g., react-vite, vue3-vite, etc.)
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta = {
component: Button,
globals: {
// 👇 Set background value for all component stories
backgrounds: { value: 'gray', grid: false },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const OnDark: Story = {
globals: {
// 👇 Override background value for this story
backgrounds: { value: 'dark' },
},
};
在上面的示例中,Storybook 将强制所有 Button 故事使用灰色背景颜色,但 OnDark
故事除外,它将使用深色背景。对于所有 Button 故事,backgrounds
全局变量的工具栏菜单将被禁用,并带有工具提示,解释该全局变量已在故事级别设置。
将故事的 globals
注解配置为覆盖项目级全局设置很有用,但应适度使用。未在故事级别定义的全局变量可以在 Storybook 的 UI 中交互式选择,允许用户探索值的每种现有组合(例如,全局变量、args
)。在故事级别设置它们将禁用该控件,阻止用户探索可用选项。
高级用法
到目前为止,我们已经在 Storybook 内部创建并使用了全局变量。
现在,让我们看一个更复杂的例子。假设我们想为国际化实现一个新的名为 locale 的全局变量,并在工具栏右侧显示一个旗帜图标。
在你的 .storybook/preview.js|ts
中,添加以下内容:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
const preview: Preview = {
globalTypes: {
locale: {
description: 'Internationalization locale',
toolbar: {
icon: 'globe',
items: [
{ value: 'en', right: '🇺🇸', title: 'English' },
{ value: 'fr', right: '🇫🇷', title: 'Français' },
{ value: 'es', right: '🇪🇸', title: 'Español' },
{ value: 'zh', right: '🇨🇳', title: '中文' },
{ value: 'kr', right: '🇰🇷', title: '한국어' },
],
},
},
},
initialGlobals: {
locale: 'en',
},
};
export default preview;
示例中使用的 icon
元素从 @storybook/icons
包加载图标。可用的图标列表请参见这里。
一旦将其连接到装饰器,添加配置元素 right
将在工具栏菜单右侧显示文本。
以下是可用配置选项的列表。
菜单项 | 类型 | 描述 | 必需 |
---|---|---|---|
value | 字符串 | 在全局变量中设置的菜单字符串值 | 是 |
title | 字符串 | 标题的主文本 | 是 |
right | 字符串 | 显示在菜单右侧的字符串 | 否 |
icon | 字符串 | 如果此项被选中,则在工具栏中显示的图标 | 否 |
在故事中消费全局变量
我们建议在装饰器中消费全局变量,并为所有故事定义全局设置。
但我们也意识到有时按故事使用工具栏选项更有益。
使用上面的例子,你可以修改任何故事,从故事上下文中检索 Locale global
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
const getCaptionForLocale = (locale) => {
switch (locale) {
case 'es':
return 'Hola!';
case 'fr':
return 'Bonjour!';
case 'kr':
return '안녕하세요!';
case 'zh':
return '你好!';
default:
return 'Hello!';
}
};
export const StoryWithLocale = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <p>{caption}</p>;
},
};
在插件中消费全局变量
如果你正在开发 Storybook 插件并需要检索全局变量,你可以这样做。storybook/manager-api
模块为此场景提供了一个钩子。你可以使用useGlobals()
钩子检索你想要的任何全局变量。
使用上面的 ThemeProvider 示例,你可以扩展它,以便在面板中显示哪个主题是活动的:
import React from 'react';
import { useGlobals } from 'storybook/manager-api';
import {
AddonPanel,
Placeholder,
Separator,
Source,
Spaced,
Title,
} from 'storybook/internal/components';
import { MyThemes } from '../my-theme-folder/my-theme-file';
// Function to obtain the intended theme
const getTheme = (themeName) => {
return MyThemes[themeName];
};
const ThemePanel = (props) => {
const [{ theme: themeName }] = useGlobals();
const selectedTheme = getTheme(themeName);
return (
<AddonPanel {...props}>
{selectedTheme ? (
<Spaced row={3} outer={1}>
<Title>{selectedTheme.name}</Title>
<p>The full theme object</p>
<Source
code={JSON.stringify(selectedTheme, null, 2)}
language="js"
copyable
padded
showLineNumbers
/>
</Spaced>
) : (
<Placeholder>No theme selected</Placeholder>
)}
</AddonPanel>
);
};
在插件中更新全局变量
如果你正在开发需要更新全局变量并刷新 UI 的 Storybook 插件,你可以这样做。如前所述,storybook/manager-api
模块提供了此场景所需的钩子。你可以使用 updateGlobals
函数更新你需要的任何全局变量值。
例如,如果你正在开发一个工具栏插件,并且想在用户点击按钮后刷新 UI 并更新全局变量:
import React, { useCallback } from 'react';
import { OutlineIcon } from '@storybook/icons';
import { useGlobals } from 'storybook/manager-api';
import { addons } from 'storybook/preview-api';
import { IconButton } from 'storybook/internal/components';
import { FORCE_RE_RENDER } from 'storybook/internal/core-events';
const ExampleToolbar = () => {
const [globals, updateGlobals] = useGlobals();
const isActive = globals['my-param-key'] || false;
// Function that will update the global value and trigger a UI refresh.
const refreshAndUpdateGlobal = () => {
// Updates Storybook global value
updateGlobals({
['my-param-key']: !isActive,
}),
// Invokes Storybook's addon API method (with the FORCE_RE_RENDER) event to trigger a UI refresh
addons.getChannel().emit(FORCE_RE_RENDER);
};
const toggleOutline = useCallback(() => refreshAndUpdateGlobal(), [isActive]);
return (
<IconButton
key="Example"
active={isActive}
title="Show a Storybook toolbar"
onClick={toggleOutline}
>
<OutlineIcon />
</IconButton>
);
};