Storybook Vue Decorator 响应式 Globals 使用指南:让 Toolbar 全局状态安全地驱动组件渲染
本篇指南聚焦 Storybook Vue3 渲染器中的一个实战细节:如何在全局 Decorator 中读取并响应 globals(例如由 Toolbar 菜单控制的 locale、theme 等全局状态),并通过 setup() 与 Vue 的 computed 保证其完全响应式。读完本文,你将掌握 Vue 装饰器访问全局状态的正确姿势、背后的响应式实现原理,以及如何把 Toolbar 下拉菜单与组件包装逻辑无缝衔接。
背景:什么是 Globals,装饰器为何要读取它
Storybook 中的 globals 是"全局"(而非某个 story 专属)的渲染输入。它们不会随 story 变化而重置,也不是通过 args 传入 story,而是作为 context.globals 暴露给渲染过程。相关文档明确说明(见 Decorators 文档):
- 全局状态变化时,当前 story 会重新渲染,Decorator 会携带新值重新执行;
- 最便捷的改变全局状态的方式,就是在 Toolbar & Globals 文档 中通过
globalTypes的toolbar注解为其创建工具栏菜单。
globals 之所以通常落在 Decorator 里消费,是因为它不属于某个 story,而 Decorator 天然作用于多个甚至全部 story。比如国际化场景:一个 locale 全局值可能需要给所有 story 的外层包裹 <div :lang>、提供对应语言的 greeting 文案,这正是本文 snippet 演示的典型用例。
Decorator 函数的第二个参数是 story context,其中除 globals 外还包含:
args:story 参数;argTypes:Storybook 用于定制、微调args的类型信息;hooks:Storybook 的 API 钩子(如useArgs、useGlobals),在 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;
逐段拆解这段代码:
- Decorator 外层签名
(story, { globals }):从 story context 中解构出globals。这是官方推荐消费全局状态的位置——全局 Decorator 作用于所有 story。 - 返回组件对象而非直接返回 VNode:Vue 渲染器下的 Decorator 约定是返回一个"组件选项对象",其中
components: { story }把被包装的 story 注册为模板子组件,模板里的<story />即原始 story 的挂载点。 setup()中读取并派生状态:const greeting = computed(...)依据当前globals?.locale计算文案,返回给模板使用;同时把globals本身也 return 出来,供模板中的:lang表达式直接读取。- 模板绑定:外层
<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 | 该项被选中时工具栏展示的图标 | 否 |
两点约束需要牢记:
globalTypes与initialGlobals只能在.storybook/preview.*中声明,因为 globals 是全局的(文档 Toolbar & Globals 中有明确 Callout 提示);- 若在 story/component 级用
globals注解覆盖某全局值,则该全局在对应 story 的 Toolbar 菜单会被禁用,需谨慎使用以免牺牲交互探索能力。
响应式链路的源码级佐证
从更底层的 store 看,globals 的生命周期如下(对应 code/core/src/preview-api/modules/store/StoryStore.ts):
- StoryStore 以
initialGlobals与globalTypes构造GlobalsStore(this.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(useArgs、useGlobals 等),以避免重渲染时出错(依据 Decorators 文档 对 hooks 的说明)。
小结
Vue 渲染器下让 Decorator 正确感知全局状态的关键有三步:从 story context 解构 globals → 在返回组件的 setup() 内读取/return → 派生值一律走 computed。仓库中 Vue3 渲染器 对 globals 做 reactive() 包裹、StoryStore 负责全局值合并,这两处实现共同保证了"Toolbar 切 locale → decorator 重新渲染 → 画面即时更新"的完整响应式闭环。本模式除国际化外壳外,同样适用于主题 Provider、语言包注入、布局包裹等一切需要随全局状态变化的场景。
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