首页
/ 在 Storybook 中通过 globals 为 Story 定义并锁定视口(viewport):组件级与单 Story 粒度的响应式渲染配置指南

在 Storybook 中通过 globals 为 Story 定义并锁定视口(viewport):组件级与单 Story 粒度的响应式渲染配置指南

2026-09-06 19:15:17作者:翟江哲Frasier

Viewport 功能通过调整 Story 渲染 iframe 的宽高,让你在不离开 Storybook 的情况下开发与验证响应式 UI。本文以仓库中的代码片段 docs/_snippets/addon-viewport-define-globals.md 为核心主体,系统讲解如何用 globals.viewport 在组件(meta)层级为全部 Story 预设视口、在单个 Story 层级进行覆盖,并结合 Storybook 源码 剖析其"一旦指定即锁定"的底层机制。读完本文,你将掌握 React、Angular、Vue、Svelte、Web Components 等各框架/CSF 方言下统一而精确的视口定义写法。

Storybook 默认 viewport 工具栏外观

该图片展示的即是 Storybook 工具栏中的 viewport 下拉菜单,它是 Viewport 模块在 manager 界面中提供的可视化入口(完整功能介绍见 docs/essentials/viewport.mdx)。

一、先理解 globals.viewportvalueisRotated

Viewport 模块向 Storybook 贡献了一个位于 viewport 命名空间下的全局变量(global),结构定义在 code/core/src/viewport/types.ts 中:

export interface ViewportStyles {
  height: string;
  width: string;
}

export type Viewport = {
  name: string;
  styles: ViewportStyles;
  type?: ViewportType;
};

export type GlobalState = {
  // 一旦设置,该 viewport 即被应用,且无法通过工具栏再更改;
  // 值必须是某个可用 viewport 的 key,或形如 '{width}-{height}' 的自定义尺寸(如 '320-480'),
  // 也可以带单位(如 '100vw' / '100pct')。
  value: string | undefined;
  // 为 true 时,应用的 viewport 会旋转 90°(例如从竖屏旋转为横屏)。
  isRotated?: boolean;
};

模块的初始全局值在 code/core/src/viewport/preview.ts 中声明:

export const initialGlobals: Record<string, GlobalState> = {
  [PARAM_KEY]: { value: undefined, isRotated: false },
};

即未做任何设置时,viewport.valueundefined,Story 以默认的 "Responsive"(100% × 100%)渲染;一旦你在 CSF 文件中通过 globals 指定了视口,Storybook 就会据此改变 iframe 尺寸。

value 字段可接受的值

value 字段需要与当前可用的 viewport 列表中的 key 对应。可用列表来自 parameters.viewport.options;当不配置 options 时,默认使用 MINIMAL_VIEWPORTS 最小集合(定义于 code/core/src/viewport/defaults.ts),共 4 个:

Key 名称(name) 实际样式(按 styles 的 width × height,px)
mobile1 Small mobile 320 × 568
mobile2 Large mobile 414 × 896
tablet Tablet 834 × 1112
desktop Desktop 1280 × 1024

本文示例中使用的 'tablet''mobile1' 都是该默认集合的 key,因此开箱即可运行、无需任何额外配置。如果你需要更全的设备清单,可引入同一文件中导出的 INITIAL_VIEWPORTS(内含从 iPhone 5 到 iPhone 14 Pro Max、Galaxy、Nexus、Pixel、iPad 系列等数十个设备预设),具体配置方式见 addon-viewport-options-in-preview.md

除了预设 viewport 的 key,value 还支持自定义尺寸格式 '{width}-{height}'(例如 '320-480''100pct-200px'),其解析由 code/core/src/viewport/resolveViewport.ts 中的 URL_VALUE_PATTERN 完成。也就是说,即使不想在 options 里注册新设备,也能直接把某个 Story 锁在任意像素/百分比尺寸上。

isRotated 的旋转语义

isRotated: true 时,渲染时的宽高会互换(相当于把 portrait 旋转成 landscape)。这在 resolveViewport.ts 中被显式实现:

width: isRotated ? styles.height : styles.width,
height: isRotated ? styles.width : styles.height,

值得一提的是,globals.viewport 也允许直接用字符串简写。类型声明与解析逻辑中的 normalizeGlobal 都兼容两种形态,例如下面两种写法等价:

// 完整对象形态
globals: { viewport: { value: 'mobile1', isRotated: false } }

// 字符串简写形态(isRotated 缺省为 false)
globals: { viewport: 'mobile1' }

二、组件级统一设置 + 单 Story 覆盖:通用 CSF 3 写法

在 CSF 文件中,globals 既可以放在 meta(组件)层,也可以放在单个导出 Story 上。meta 层的 globals 会作用于该组件下的所有 Story,而 Story 层自己的 globals 会覆盖 meta 层的设置。通用 TypeScript CSF 3 写法如下:

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';

import { Button } from './Button';

const meta = {
  component: Button,
  globals: {
    // 👇 为该组件下所有 Story 设置 viewport
    viewport: { value: 'tablet', isRotated: false },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const OnPhone: Story = {
  globals: {
    // 👇 仅覆盖当前这个 Story 的 viewport
    viewport: { value: 'mobile1', isRotated: false },
  },
};

对应的通用 JavaScript CSF 3 写法完全一致,只是不需要类型注解:

import { Button } from './Button';

export default {
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
};

export const OnPhone = {
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
};

这样,Button 组件的其余 Story 默认都在 834 × 1112 的平板(tablet)视口下渲染,而 OnPhone 这一个 Story 会以 320 × 568 的小屏手机(mobile1)视口呈现,从而精准复现移动端断点下的真实渲染。

三、逐框架/CSF 方言落地示例

代码片段 addon-viewport-define-globals.md 为每种渲染器都准备了对应的写法。下面按框架组织,覆盖 CSF 3、CSF Next 🧪 与 Svelte CSF 三种形态。

3.1 React(CSF 3 与 CSF Next 🧪)

React 的 CSF 3 写法可沿用上文"通用 CSF 3"代码(将 your-framework 替换为 react-vite 等)。CSF Next 🧪 的写法通过 .storybook/preview 中导出的 preview 实例进行链式声明:

// Button.stories.tsx
import preview from '../.storybook/preview';

import { Button } from './Button';

const meta = preview.meta({
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
});
// Button.stories.jsx
import preview from '../.storybook/preview';

import { Button } from './Button';

const meta = preview.meta({
  component: Button,
  globals: {
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    viewport: { value: 'mobile1', isRotated: false },
  },
});

3.2 Angular(CSF 3 与 CSF Next 🧪)

// Button.stories.ts(CSF 3)
import type { Meta, StoryObj } from '@storybook/angular';

import { Button } from './button.component';

const meta: Meta<Button> = {
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
};

export default meta;
type Story = StoryObj<Button>;

export const OnPhone: Story = {
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
};
// Button.stories.ts(CSF Next 🧪)
import preview from '../.storybook/preview';

import { Button } from './button.component';

const meta = preview.meta({
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
});

3.3 Vue 3(CSF Next 🧪)

// Button.stories.ts
import preview from '../.storybook/preview';

import Button from './Button.vue';

const meta = preview.meta({
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
});
// Button.stories.js
import preview from '../.storybook/preview';

import Button from './Button.vue';

const meta = preview.meta({
  component: Button,
  globals: {
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    viewport: { value: 'mobile1', isRotated: false },
  },
});

3.4 Svelte(CSF 3 与 Svelte CSF)

Svelte 组件的 Story 既可以写在传统 Button.stories.js 中,也可以写在 .stories.svelte 单文件组件中:

// Button.stories.ts(CSF 3;Replace your-framework with svelte-vite or sveltekit)
import type { Meta, StoryObj } from '@storybook/your-framework';

import Button from './Button.svelte';

const meta = {
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const OnPhone: Story = {
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
};
// Button.stories.js(Svelte CSF 3)
import Button from './Button.svelte';

export default {
  component: Button,
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
};

export const OnPhone = {
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
};
<!-- Button.stories.svelte(Svelte CSF,通过 defineMeta 声明组件级 globals) -->
<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';

  import Button from './Button.svelte';

  const { Story } = defineMeta({
    component: Button,
    globals: {
      // 👇 Set viewport for all component stories
      viewport: { value: "tablet", isRotated: false },
    },
  });
</script>

<Story
  name="OnPhone"
  globals={{
    // 👇 Override viewport for this story
    viewport: { value: "mobile1", isRotated: false },
  }}
/>

Svelte CSF 在 <script module> 中用 defineMetaglobals 声明组件级视口,再用 <Story globals={...}> 对单个 Story 做覆盖,JS/TS 两种语言下的写法一致。

3.5 Web Components(CSF 3 与 CSF Next 🧪)

Web Components 渲染器里,component 字段填写的是自定义元素名(如 'demo-button'):

// Button.stories.js(CSF 3)
export default {
  component: 'demo-button',
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
};

export const OnPhone = {
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
};
// Button.stories.ts(CSF 3)
import type { Meta, StoryObj } from '@storybook/web-components-vite';

const meta: Meta = {
  component: 'demo-button',
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
};

export default meta;
type Story = StoryObj;

export const OnPhone: Story = {
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
};
// Button.stories.js(CSF Next 🧪)
import preview from '../.storybook/preview';

const meta = preview.meta({
  component: 'demo-button',
  globals: {
    // 👇 Set viewport for all component stories
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    // 👇 Override viewport for this story
    viewport: { value: 'mobile1', isRotated: false },
  },
});
// Button.stories.ts(CSF Next 🧪)
import preview from '../.storybook/preview';

const meta = preview.meta({
  component: 'demo-button',
  globals: {
    viewport: { value: 'tablet', isRotated: false },
  },
});

export const OnPhone = meta.story({
  globals: {
    viewport: { value: 'mobile1', isRotated: false },
  },
});

3.6 各框架写法速查

框架/渲染器 支持的文件形态 globals 声明位置
React(react-vite / nextjs 等) CSF 3:*.stories.tsx;CSF Next:preview.meta() / meta.story() meta 层 + Story 层
Angular CSF 3:Meta<Button>;CSF Next:preview.meta() meta 层 + Story 层
Vue 3 CSF Next:preview.meta() / meta.story() meta 层 + Story 层
Svelte CSF 3:*.stories.js;Svelte CSF:*.stories.sveltedefineMeta / <Story> defineMeta + <Story globals>
Web Components CSF 3:*.stories.js/ts;CSF Next:preview.meta() meta 层 + Story 层

四、底层原理:为什么一旦指定就无法用工具栏更改

viewport.mdx 中有一条关键提示:当你在 Story(或其组件)上通过 globals 指定了 viewport,该视口会被应用且工具栏中无法再更改,以此保证某个 Story 始终在特定视口下渲染。从源码可以确认这一"锁定(locked)"行为的实现位置。

resolveViewport.ts 中,解析逻辑会先区分三层 globals 的来源:

const storyHasViewport = PARAM_KEY in storyGlobals;
// Story 层的 viewport globals 会覆盖用户全局设置(userGlobals)
const primaryGlobal = storyHasViewport ? storyGlobal : userGlobal;
const secondaryGlobal = storyHasViewport ? userGlobal : storyGlobal;

// 锁定条件:功能被禁用、Story 声明了 viewport、或没有任何可用视口
const isLocked = disable || PARAM_KEY in storyGlobals || !keys.length;

也就是说:只要 viewport 出现在某个 Story 的 globals 中(无论写在 CSF 的 meta 还是具体 Story 上),Storybook 就会把该 Story 的视口解析为"锁定"状态。此时工具栏下拉菜单对该 Story 不可用,这与文档中"一旦指定即锁定"的表述完全吻合——这正是用它来固化某个 Story 的渲染尺寸(例如作为截图/视觉回归基线)时最想得到的行为。

在 manager 侧,useViewport.ts 通过 useGlobals() 读取并更新全局值:

const { options = MINIMAL_VIEWPORTS, disable = false } = parameter || {};
const { name, type, width, height, value, isLocked, isRotated, ... } =
  resolveViewport({
    globals,
    storyGlobals,
    userGlobals,
    options,
    ...
  });

几个值得注意的实现细节:

  • 非 story 视图下的行为resolveViewportviewMode !== 'story' 的情况会直接返回 Responsive(100% × 100%)并标记 isLocked: true。因此用 globals 定义视口只作用于 story 渲染视图,docs/文档模式不受影响。
  • 无效值回退:若 value 指向不存在的 key(例如误填 'phone'),而 Story 本身没有声明 viewport,useViewport 会重置为默认值;若 Story 声明了 viewport,则不会被意外清除。
  • 旋转快捷指令:工具栏旋转按钮与键盘快捷键(下一篇 API 会展开)本质都是对该全局值的更新,例如 rotate: () => update({ value, isRotated: !isRotated }),这解释了为什么 isRotated 能在保持 value 不变的前提下独立切换。
  • 旧版参数的迁移useViewport.ts 中还内置了对已废弃的 viewport.defaultViewport 参数的检测——该参数在 Storybook 10 中已被移除,会提示改用 globals: { viewport: ... } 或运行 npx storybook automigrate 自动迁移。这也说明"用 globals 定义视口"是当前版本推荐的标准姿势。

五、仓库内置的可运行验证示例

Storybook 仓库自身在 code/core/template/stories/viewport/globals.stories.ts 中内置了一套覆盖各种形态的验证 Story,可以作为本文写法的活教材。它演示了本文所述机制的多种变体:

// code/core/template/stories/viewport/globals.stories.ts
export const Selected = {
  globals: {
    viewport: { value: first, isRotated: false }, // 完整对象形态(first = 'mobile1')
  },
};

export const Orientation = {
  globals: {
    viewport: { value: first, isRotated: true },  // 竖屏旋转为横屏
  },
};

export const Invalid = {
  globals: {
    viewport: { value: 'phone', isRotated: false }, // 无效 key,用于验证回退逻辑
  },
};

export const Shorthand = {
  globals: {
    viewport: first, // 字符串简写形态
  },
};

export const NoRatioDefined = {
  globals: {
    viewport: { value: first }, // 省略 isRotated,缺省为 false
  },
};

这个模板文件中的 Unset(不设置)、Selected(对象形态)、Shorthand(字符串简写)、Orientation(旋转)、Invalid(无效值)、NoRatioDefined(省略 isRotated)与 Disabled(通过 parameters.viewport.disable 禁用)等用例,恰好从测试角度验证了本文第三节描述的各类边界情况。

六、实用建议

  • 优先组件级而非全局:如果某个组件的 Story 都适合在平板上查看,就把 viewport globals 放在 meta 层,仅对特例(如移动端断点)在单个 Story 上覆盖,避免在每个 Story 里重复声明。
  • "锁定"是特性而非限制:需要 Story 以固定尺寸稳定渲染(例如配合视觉回归快照、Chromatic 快照)时,用 globals 锁死视口可以避免测试结果随默认浏览器窗口大小波动。
  • isRotated 只在需要横屏布局时才声明:省略该字段与显式写 false 等价(参见上文的 normalizeGlobal 缺省逻辑)。
  • 自定义尺寸无需注册新设备value 支持 '320-480''100pct-200px' 这类 {width}-{height} 格式,适合那些临时想精确到像素的检查,不需要为此改动 parameters.viewport.options
  • 相关配套文档:若要进一步定制可用设备集合(options)、添加自定义设备或在 preview 中指定初始视口,可继续阅读 addon-viewport-options-in-preview.mdaddon-viewport-add-viewport-in-preview.mdaddon-viewport-configuration-in-meta.md
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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