首页
/ Storybook Vue Decorator 响应式 Globals 使用指南:让 Toolbar 全局状态安全地驱动组件渲染

Storybook Vue Decorator 响应式 Globals 使用指南:让 Toolbar 全局状态安全地驱动组件渲染

2026-09-07 22:01:02作者:凤尚柏Louis

本篇指南聚焦 Storybook Vue3 渲染器中的一个实战细节:如何在全局 Decorator 中读取并响应 globals(例如由 Toolbar 菜单控制的 localetheme 等全局状态),并通过 setup() 与 Vue 的 computed 保证其完全响应式。读完本文,你将掌握 Vue 装饰器访问全局状态的正确姿势、背后的响应式实现原理,以及如何把 Toolbar 下拉菜单与组件包装逻辑无缝衔接。

背景:什么是 Globals,装饰器为何要读取它

Storybook 中的 globals 是"全局"(而非某个 story 专属)的渲染输入。它们不会随 story 变化而重置,也不是通过 args 传入 story,而是作为 context.globals 暴露给渲染过程。相关文档明确说明(见 Decorators 文档):

  • 全局状态变化时,当前 story 会重新渲染,Decorator 会携带新值重新执行;
  • 最便捷的改变全局状态的方式,就是在 Toolbar & Globals 文档 中通过 globalTypestoolbar 注解为其创建工具栏菜单。

globals 之所以通常落在 Decorator 里消费,是因为它不属于某个 story,而 Decorator 天然作用于多个甚至全部 story。比如国际化场景:一个 locale 全局值可能需要给所有 story 的外层包裹 <div :lang>、提供对应语言的 greeting 文案,这正是本文 snippet 演示的典型用例。

Decorator 函数的第二个参数是 story context,其中除 globals 外还包含:

  • args:story 参数;
  • argTypes:Storybook 用于定制、微调 args 的类型信息;
  • hooks:Storybook 的 API 钩子(如 useArgsuseGlobals),在 Decorator 与 story 渲染函数中均可用;
  • parameters:story 的静态元数据;
  • viewMode:Storybook 当前的活动窗口(canvas / docs)。

核心片段逐行解读:Vue Decorator 中的响应式 globals

对于 Vue 渲染器globals 必须在 Decorator 返回对象的 setup() 中经手一遍,才能确保完全响应式;派生值则应交给 Vue 的 computed 计算(对应 Decorators 文档 Reactive globals 一节)。官方给出的配置如下(与关联片段 decorator-with-reactive-globals.md 一致):

.storybook/preview.js

import { computed } from 'vue';

export default {
  decorators: [
    (story, { globals }) => {
      return {
        components: { story },
        setup() {
          const greeting = computed(() => (globals?.locale === 'en' ? 'Hello!' : '¡Hola!'));

          return { greeting, globals };
        },
        template: `
          <div :lang={{globals?.locale || 'en'}}>
            <p>Greeting: {{greeting}}</p>
            <story />
          </div>
        `,
      };
    },
  ],
};

.storybook/preview.ts

import { computed } from 'vue';
import type { Preview } from '@storybook/vue3-vite';

const preview: Preview = {
  decorators: [
    (story, { globals }) => {
      return {
        components: { story },
        setup() {
          const greeting = computed(() => (globals?.locale === 'en' ? 'Hello!' : '¡Hola!'));

          return { greeting, globals };
        },
        template: `
          <div :lang={{globals?.locale || 'en'}}>
            <p>Greeting: {{greeting}}</p>
            <story />
          </div>
        `,
      };
    },
  ],
};

export default preview;

逐段拆解这段代码:

  1. Decorator 外层签名 (story, { globals }):从 story context 中解构出 globals。这是官方推荐消费全局状态的位置——全局 Decorator 作用于所有 story。
  2. 返回组件对象而非直接返回 VNode:Vue 渲染器下的 Decorator 约定是返回一个"组件选项对象",其中 components: { story } 把被包装的 story 注册为模板子组件,模板里的 <story /> 即原始 story 的挂载点。
  3. setup() 中读取并派生状态const greeting = computed(...) 依据当前 globals?.locale 计算文案,返回给模板使用;同时把 globals 本身也 return 出来,供模板中的 :lang 表达式直接读取。
  4. 模板绑定:外层 <div :lang> 将当前 locale(缺省回退为 'en')写到 DOM 语言属性上,{{greeting}} 插值派生文案——整个 Decorator 为所有 story 提供了一层"国际化外壳"。

注意 globals?.locale === 'en' 中的可选链:即使全局值尚未初始化也能安全求值,模板里再配合 || 'en' 做默认回退。

为什么必须经过 setup() 传递 globals

这不是约定俗成的风格,而是 Vue3 渲染器的实现使然。查看仓库中 Vue3 渲染器实现 code/renderers/vue3/src/render.ts,可以看到根 Vue 应用创建时:

setup() {
  storyContext.args = reactive(storyContext.args);
  storyContext.globals = reactive(storyContext.globals);
  const rootElement = storyFn(); // 在 setup 中调用 story 函数,得到带全部装饰器的根节点
  ...
}

也就是说,storyContext.globals 是在 Vue 应用的根 setup() 中被替换为 reactive(...) 代理后才传给 Decorator 链的。Vue 的响应式依赖收集依赖"在 effect/render/computed 作用域内访问属性",因此:

  • 只有在 Decorator 返回对象的 setup()(或 computed 中)读取 globals.xxx,Vue 才能建立依赖并订阅其变化;
  • 如果把派生逻辑写在 Decorator 的外层函数体里(它在根 setup() 中同步执行、且不在任何响应式 effect 内),派生结果只会计算一次,globals 变化后无法更新。

而通过 setup() 返回 computed 派生值与 globals 本身后,模板的 :lang{{greeting}} 都会成为响应式依赖的一部分。

再看热更新/状态变更路径。当 story 已渲染且未强制重挂载时,render.ts 会重新执行 storyFn()(从而让 Decorator 链以新 context 再次运行),并通过 updateArgs<Globals>(existingApp.reactiveGlobals, storyContext.globals) 把最新 globals 写入既有的响应式代理。结合 Toolbar & Globals 文档 所述"globals 变化时 story 重渲染、Decorator 携带新值重跑",前端切换语言下拉、story 画面即时切换问候语的链路由此闭环。

让 locale 真正"可变":定义 globalTypes 与 Toolbar

上面的 Decorator 只是"读取方",真正让 locale 在 Toolbar 上可选,需要在 .storybook/preview.* 中声明 globalTypes(含 toolbar 注解)与 initialGlobals。官方 locale 示例参见 storybook-preview-locales-globaltype.md,其核心为:

const preview = {
  globalTypes: {
    locale: {
      description: 'Internationalization locale',
      toolbar: {
        icon: 'globe',
        items: [
          { value: 'en', right: '🇺🇸', title: 'English' },
          { value: 'es', right: '🇪🇸', title: 'Español' },
          // ...
        ],
      },
    },
  },
  initialGlobals: {
    locale: 'en',
  },
};

工具条 MenuItem 可用的配置项(来源同上):

MenuItem 类型 说明 是否必填
value String 选中后写入 globals 的字符串值
title String 菜单项主标题文本
right String 显示在菜单项右侧的字符串(如国旗/语言缩写)
icon String 该项被选中时工具栏展示的图标

两点约束需要牢记:

  • globalTypesinitialGlobals 只能在 .storybook/preview.* 中声明,因为 globals 是全局的(文档 Toolbar & Globals 中有明确 Callout 提示);
  • 若在 story/component 级用 globals 注解覆盖某全局值,则该全局在对应 story 的 Toolbar 菜单会被禁用,需谨慎使用以免牺牲交互探索能力。

响应式链路的源码级佐证

从更底层的 store 看,globals 的生命周期如下(对应 code/core/src/preview-api/modules/store/StoryStore.ts):

  • StoryStore 以 initialGlobalsglobalTypes 构造 GlobalsStorethis.userGlobals),项目注解更新时同步 set({ globals: initialGlobals, globalTypes })
  • getStoryContext 中,最终呈现给渲染层的 globals{ ...userGlobals, ...story.storyGlobals } 合并而来(StoryStore.ts 相关实现),即全局值优先、story 级覆盖次之;
  • Decorator 本身在 story 准备阶段被组合:prepareStory 按 story → component → project(全局)顺序收集 Decorator,再经 applyHooks(applyDecorators)(...) 执行(见 code/core/src/preview-api/modules/store/csf/prepareStory.ts)。

渲染时的执行顺序为:全局 Decorator(按定义顺序)→ 组件级 Decorator(按定义顺序)→ story 级 Decorator,由内向外包裹(对应 Decorators 文档 Decorator inheritance 一节)。因此本片段写在全局限定符下,会作为最外层包装包裹每个 story,天然适用于 locale/theme 这类全站性外壳。

延伸:Decorator 中调用 Hooks 同理

同样的"响应式"约束也适用于在 Vue Decorator 中调用 Storybook Hooks。姊妹片段 decorator-with-updateArgs.md 展示了通过 useArgs 实现计数器递增的 Decorator:

import { useArgs } from 'storybook/preview-api';
// ...
decorators: [
  (story, { args }) => {
    const [, updateArgs] = useArgs();
    return {
      components: { story },
      setup() {
        return { args, updateArgs };
      },
      template: `
        <div>
          <button @click="() => updateArgs({ counter: args.counter + 1 })">
            Increment
          </button>
          <story />
        </div>
      `,
    };
  },
],

要点同样是把 useArgs 的结果通过 setup() 暴露给模板。若在渲染函数中同时使用框架 Hooks(如 React 的 useState)与 Storybook Hooks,应优先使用 storybook/preview-api 提供的等价 Hooks(useArgsuseGlobals 等),以避免重渲染时出错(依据 Decorators 文档hooks 的说明)。

小结

Vue 渲染器下让 Decorator 正确感知全局状态的关键有三步:从 story context 解构 globals → 在返回组件的 setup() 内读取/return → 派生值一律走 computed。仓库中 Vue3 渲染器globalsreactive() 包裹、StoryStore 负责全局值合并,这两处实现共同保证了"Toolbar 切 locale → decorator 重新渲染 → 画面即时更新"的完整响应式闭环。本模式除国际化外壳外,同样适用于主题 Provider、语言包注入、布局包裹等一切需要随全局状态变化的场景。

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

项目优选

收起
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