首页
/ Storybook Toolbars & Globals:在 Story 的 render 函数中消费 globaltype(以 locale 国际化为例)

Storybook Toolbars & Globals:在 Story 的 render 函数中消费 globaltype(以 locale 国际化为例)

2026-09-07 15:10:29作者:毕习沙Eudora

本篇聚焦 Storybook "Toolbars & globals" 功能中最易被忽略的一个使用姿势:在 story 内部(而非 decorator 中)直接读取 context.globals,让单个 story 对某个 globaltype 做出响应。文章以官方 locale 国际化示例为主线,完整覆盖各框架(React、Angular、Vue、Svelte、Solid、Web Components)的写法,并结合核心仓库源码说明 globals 的三级合并机制(initialGlobals → userGlobals → storyGlobals),读完后可直接在项目中实现一个带 toolbar 下拉菜单、并能被任意 story 按需读取的全局状态。

Storybook 工具栏与全局变量示例

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;

其中两个关键约束值得强调:

  1. globalTypesinitialGlobals 只能配置在 .storybook/preview.*——因为 globals 是"全局"的,不能在 story 或 meta 级别定义菜单结构;
  2. 官方文档提示:示例中的 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,验证 initialGlobalsuserGlobals 与 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(),即与 story render 函数第二个参数同源——addon 面板看到的 globals 和 story 里读到的完全是同一份合并结果;
  • 更新通过 channel.emit(UPDATE_GLOBALS, ...) 广播,manager 与 preview 两侧 UI 同步刷新,这正是 "Updating globals from within an addon" 一节的能力来源。

使用建议与注意事项

  1. 优先 decorator,story 级消费是例外。官方原话:"We recommend consuming globals from within a decorator and defining a global setting for all stories"。只有当某个 story 的渲染逻辑必须随 toolbar 选项单独变化时(如本例的 locale 文案),才在 story 的 render 中解构。
  2. story 级固定 globals 要克制。原文档 Callout 明确提醒:未在 story 级别定义的 globals 可以在 UI 中交互选择,用户可以探索所有取值组合(与 args 的笛卡尔积);一旦在 story 级别写死,对应 toolbar 菜单对该 story 失效,探索能力随之消失。
  3. globalTypes/initialGlobals 只能出现在 .storybook/preview.*,写在 meta 或 story 上不会生效。
  4. 消费点拿到的永远是合并后的值。由于 storyGlobals 覆盖发生在 prepareStory 阶段,你不需要在 render 中自行判断"toolbar 值 vs story 固定值"的优先级——直接解构 globals 即可,语义与文档承诺一致。

参考文件

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388