Storybook Toolbars & Globals:在 Story 的 render 函数中消费 globaltype(以 locale 国际化为例)
本篇聚焦 Storybook "Toolbars & globals" 功能中最易被忽略的一个使用姿势:在 story 内部(而非 decorator 中)直接读取 context.globals,让单个 story 对某个 globaltype 做出响应。文章以官方 locale 国际化示例为主线,完整覆盖各框架(React、Angular、Vue、Svelte、Solid、Web Components)的写法,并结合核心仓库源码说明 globals 的三级合并机制(initialGlobals → userGlobals → storyGlobals),读完后可直接在项目中实现一个带 toolbar 下拉菜单、并能被任意 story 按需读取的全局状态。
Globals 是什么:与 args 的本质区别
在 Storybook 中,Globals 表示不针对某个具体 story 的全局输入。根据 Toolbars & globals 文档的说明:
- Globals 不会通过
args参数传入 story 函数(尽管它们可以通过context.globals访问); - 它们通常用于 decorators,作用于所有 story;
- 当 globals 值变化时,story 会重新渲染,decorator 会用新值重新执行;
- 最简单改变 globals 的方式,就是为它创建一个 toolbar 菜单项。
这与 args 形成对照:args 是组件属性级别的输入,每个 story 各自独立;而 globals 是"环境级"输入(主题、语言、背景、视口等),默认在整个 story 间导航时保持共享。
前置配置:在 preview 中声明 locale globaltype
"在 story 中消费 globals" 的前提是先在 .storybook/preview.* 中声明该 globaltype 及其 toolbar。官方 Advanced usage 一节给出的完整配置(摘自 storybook-preview-locales-globaltype.md):
// 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;
其中两个关键约束值得强调:
globalTypes与initialGlobals只能配置在.storybook/preview.*中——因为 globals 是"全局"的,不能在 story 或 meta 级别定义菜单结构;- 官方文档提示:示例中的
icon: 'globe'会加载 icons 包 中的图标;配置项right会在用户选中该项时,把对应文本显示在工具栏菜单的右侧。
items 数组中每一项(MenuItem)的完整字段说明如下(直接继承自原文档的选项表):
| MenuItem | Type | Description | Required |
|---|---|---|---|
| value | String | 写入 globals 的字符串值 | Yes |
| title | String | 菜单项的主文本 | Yes |
| right | String | 显示在菜单右侧的字符串 | No |
| icon | String | 该项被选中时在工具栏显示的图标 | No |
核心写法:在 story 的 render 中解构 globals
官方文档推荐"在 decorator 中消费 globals,为所有 story 统一生效",但明确承认有时在 story 级别使用 toolbar 选项更有益——例如某个 story 需要按用户选择的语言展示对应文案。实现方式只有一个:在 render 函数的第二个参数(story context)中解构 globals。
以下示例完整继承自 my-component-story-use-globaltype.md,按框架分组。逻辑部分(getCaptionForLocale)在所有框架中完全一致,仅展示一次:
const getCaptionForLocale = (locale) => {
switch (locale) {
case 'es':
return 'Hola!';
case 'fr':
return 'Bonjour!';
case 'kr':
return '안녕하세요!';
case 'zh':
return '你好!';
default:
return 'Hello!';
}
};
React(TypeScript)
// 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>;
export const StoryWithLocale = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <p>{caption}</p>;
},
};
React(JavaScript)
import { MyComponent } from './MyComponent';
export default {
component: MyComponent,
};
export const StoryWithLocale = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <p>{caption}</p>;
},
};
Angular
Angular 的 render 返回渲染配置对象,template 为内联模板字符串:
import type { Meta, StoryObj } from '@storybook/angular';
import { MyComponent } from './my-component.component';
const meta: Meta<MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<MyComponent>;
export const StoryWithLocale: Story = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return {
template: `<p>${caption}</p>`,
};
},
};
Vue(TypeScript)
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import MyComponent from './MyComponent.vue';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const StoryWithLocale: Story = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return {
template: `<p>${caption}</p>`,
};
},
};
Svelte(CSF 3 语法)
Svelte 的 render 返回 { Component, props } 结构,注意 caption 是通过 locale prop 传给组件的:
// Replace your-framework with svelte-vite or sveltekit
import type { Meta, StoryObj } from '@storybook/your-framework';
import MyComponent from './MyComponent.svelte';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const StoryWithLocale: Story = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return {
Component: MyComponent,
props: {
locale: caption,
},
};
},
};
Svelte(Svelte-CSF 组件语法)
使用 @storybook/addon-svelte-csf 的 .svelte 组件式写法时,消费 globals 的方式是通过 {#snippet template(...)} 块把 globals 传入组件模板:
<script module>
import { defineMeta } from "@storybook/addon-svelte-csf";
import MyComponent from "./MyComponent.svelte";
const { Story } = defineMeta({
component: MyComponent,
});
</script>
<script lang="ts">
const getCaptionForLocale = (locale: string) => {
switch (locale) {
case 'es':
return 'Hola!';
case 'fr':
return 'Bonjour!';
case "kr":
return '안녕하세요!';
case "zh":
return '你好!';
default:
return 'Hello!';
}
};
</script>
<Story name="StoryWithLocale">
{#snippet template(args, { globals: { locale } })}
<MyComponent
{...args}
locale={getCaptionForLocale(locale)}
/>
{/snippet}
</Story>
Web Components(Lit)
Web Components 场景下 component 是标签名字符串,render 返回 Lit 模板:
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
const meta: Meta = {
component: 'my-component',
};
export default meta;
type Story = StoryObj;
export const StoryWithLocale: Story = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return html`<p>${caption}</p>`;
},
};
Solid
Solid 的写法与 React 一致,render 返回 JSX:
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const StoryWithLocale: Story = {
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <p>{caption}</p>;
},
};
实验性 CSF Next 语法
仓库中的 snippet 同时提供了标注为 "CSF Next 🧪" 的实验性变体,风格与 meta.story({...}) 工厂调用一致(适用于 Angular、Vue、React、Web Components)。例如 React 版本:
import preview from '../.storybook/preview';
import { MyComponent } from './MyComponent';
const meta = preview.meta({
component: MyComponent,
});
export const StoryWithLocale = meta.story({
render: (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
return <p>{caption}</p>;
},
});
其消费 globals 的核心写法与稳定版 CSF 完全相同,只是 meta/story 的声明从 export default + StoryObj 泛型换成了 preview.meta() / meta.story() 的链式 API。
源码纵深:globals 的三级合并如何落地
理解"为什么在 render 里解构 globals 拿到的就是 toolbar 上选中的值",需要看核心仓库中 globals 的数据流。从源码结构看,整个链路是:manager(UI 侧)发起 → channel 消息广播 → preview(渲染侧)store 合并 → 注入 story context。
1. 消息载荷:四个 globals 字段的定义
code/core/src/types/modules/channelApi.ts 中定义了跨进程消息的载荷结构:
export interface GlobalsUpdatedPayload {
initialGlobals: Globals; // preview 配置中的 initialGlobals
userGlobals: Globals; // 用户在 toolbar 中改出的值
storyGlobals: Globals; // story 级别 globals 注解固定的值
globals: Globals; // 三者合并后的最终结果
}
这四个字段正是官方文档 "Setting globals on a story" 一节的机制解释:story 级别 globals 注解的值(storyGlobals)拥有最高优先级,会覆盖 toolbar 选择,并让对应菜单在查看这些 story 时禁用(显示"该 global 在 story 级别已固定"的提示)。
2. 合并逻辑:prepareStory 计算 storyGlobals
code/core/src/preview-api/modules/store/csf/prepareStory.ts 在准备每个 story 时完成合并。从该文件的测试 prepareStory.test.ts 可以看到一个专门的用例组 globals => storyGlobals,验证 initialGlobals、userGlobals 与 story 级 globals 注解的合并顺序。也就是说,你在 render: (args, { globals }) 中读到的 globals,不是原始用户选择,而是已经过 story 级覆盖的最终值——这保证了"story 固定值"语义在任意消费点(decorator 或 render)行为一致。
3. Addon 侧的等价 API:useGlobals()
如果你不是写 story 而是写 addon,消费 globals 的官方入口是 useGlobals() 钩子(见 addons-api 文档)。其实现位于 code/core/src/preview-api/modules/addons/hooks.ts:
export function useGlobals(): [Args, (newGlobals: Args) => void] {
const channel = addons.getChannel();
const { globals } = useStoryContext();
const updateGlobals = useCallback(
(newGlobals: Args) => channel.emit(UPDATE_GLOBALS, { globals: newGlobals }),
[channel]
);
return [globals, updateGlobals];
}
两个要点:
- 读取走
useStoryContext(),即与 storyrender函数第二个参数同源——addon 面板看到的 globals 和 story 里读到的完全是同一份合并结果; - 更新通过
channel.emit(UPDATE_GLOBALS, ...)广播,manager 与 preview 两侧 UI 同步刷新,这正是 "Updating globals from within an addon" 一节的能力来源。
使用建议与注意事项
- 优先 decorator,story 级消费是例外。官方原话:"We recommend consuming globals from within a decorator and defining a global setting for all stories"。只有当某个 story 的渲染逻辑必须随 toolbar 选项单独变化时(如本例的 locale 文案),才在 story 的
render中解构。 - story 级固定 globals 要克制。原文档 Callout 明确提醒:未在 story 级别定义的 globals 可以在 UI 中交互选择,用户可以探索所有取值组合(与 args 的笛卡尔积);一旦在 story 级别写死,对应 toolbar 菜单对该 story 失效,探索能力随之消失。
globalTypes/initialGlobals只能出现在.storybook/preview.*,写在 meta 或 story 上不会生效。- 消费点拿到的永远是合并后的值。由于
storyGlobals覆盖发生在prepareStory阶段,你不需要在render中自行判断"toolbar 值 vs story 固定值"的优先级——直接解构globals即可,语义与文档承诺一致。
参考文件
- 主题文档:docs/essentials/toolbars-and-globals.mdx
- 本篇 snippet:docs/_snippets/my-component-story-use-globaltype.md
- locale globaltype 的 preview 配置:docs/_snippets/storybook-preview-locales-globaltype.md
- 消息载荷类型:code/core/src/types/modules/channelApi.ts
- story 准备与 globals 合并:code/core/src/preview-api/modules/store/csf/prepareStory.ts
- useGlobals 钩子实现:code/core/src/preview-api/modules/addons/hooks.ts
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
