Storybook `previewBody` 配置指南:在 main 配置中编程式改写预览页面 Body
previewBody 是 Storybook 的 main.js|ts 配置项之一,用于以编程方式调整预览(preview)iframe 的 <body> 内容,例如根据运行环境条件性地注入脚本或样式。本文以官方配置文档为主体,结合本仓库中该选项的类型定义、preset 实现与模板加载源码,完整讲解其 API 语义、静态文件备选方案、底层原理与可验证依据,帮助你在项目或自定义 addon 中安全、精准地改写 Storybook 预览页面的 body。
一、previewBody 是什么,何时需要它
在 Storybook 中,所有 story 都渲染在名为 Canvas 的预览 iframe 内。这个 iframe 本身的 HTML 骨架由 Storybook 生成,其 <head>、<body> 允许你按需扩展,从而配合自定义字体、注入统计脚本、添加自定义 DOM 挂载点等。
previewBody 的作用就是程序化地改写这个预览 <body>,它接收一个代表当前 body HTML 的字符串,并返回修改后的字符串:
Type: (body: string) => string
官方文档特别提示,它最常被 addon 作者使用(见 Writing presets 中 UI configuration 小节);如果普通项目只需要固定注入一段静态 HTML,无需程序化改写,则应该优先使用静态文件 preview-body.html(下文第三节详述)。两种方式的差异可以概括为:
| 方式 | 载体 | 典型场景 |
|---|---|---|
| 静态注入 | .storybook/preview-body.html |
注入固定的 <div> 挂载点、全局基准样式 |
| 程序化注入 | main 配置中的 previewBody(body) |
依赖环境变量、条件注入、addon 动态改写 |
二、在 main.js / main.ts 中启用 previewBody
配置入口在 .storybook/main.js 或 .storybook/main.ts。仓库中的官方示例片段位于 docs/_snippets/main-config-preview-body.md,下面是其最常见的两种写法。
CSF 3:main.js
export default {
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
previewBody: (body) => `
${body}
${
process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
}
`,
};
CSF 3:main.ts
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
previewBody: (body) => `
${body}
${
process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
}
`,
};
export default config;
要点说明:
stories数组决定了 Storybook 扫描 story 与文档文件的范围;实际使用时应替换为你项目的路径。- 模板字符串开头的
${body}必须保留,它是 Storybook 原有的 body HTML,丢弃它会破坏预览页面的正常渲染。 - 示例利用
process.env.ANALYTICS_ID判断当前环境,只有设置了该环境变量时才向 body 末尾追加<script>统计脚本。同理,你也可以条件性注入<style>标签,例如仅在本地开发时引入调试样式。
CSF Next:基于 defineMain 的类型安全写法
对于 React、Vue、Angular、Web Components 等 renderer,仓库还提供了基于 defineMain 的类型安全写法,从对应框架包的 node 入口导入。以下为官方示例中的完整形态:
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { defineMain } from '@storybook/your-framework/node';
export default defineMain({
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
previewBody: (body) => `
${body}
${
process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
}
`,
});
其余 renderer 只需替换导入来源与 framework 字段:
import { defineMain } from '@storybook/vue3-vite/node';
export default defineMain({
framework: '@storybook/vue3-vite',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
previewBody: (body) => `
${body}
${
process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
}
`,
});
import { defineMain } from '@storybook/angular/node';
export default defineMain({
framework: '@storybook/angular',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
previewBody: (body) => `
${body}
${
process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
}
`,
});
import { defineMain } from '@storybook/web-components-vite/node';
export default defineMain({
framework: '@storybook/web-components-vite',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
previewBody: (body) => `
${body}
${
process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
}
`,
});
同理,JavaScript 项目也可以直接从 @storybook/your-framework/node 导入 defineMain,得到与 TS 相同的 previewBody 配置能力。
三、不加代码也能注入:静态 preview-body.html 备选方案
官方文档在 API 参考页 明确建议:如果你不需要程序化改写预览 body,直接在 .storybook/preview-body.html 中写静态 HTML 即可。这一做法的详细说明见 Story rendering 文档中 Adding to <body> 一节。
例如,需要为组件提供一个额外的 DOM 挂载点时,在 .storybook 目录下创建 preview-body.html(示例见 storybook-preview-body-example.md):
<div id="custom-root"></div>
当项目使用 rem、em 等相对单位时,可在同一文件里通过 <style> 调整根字号基准(示例见 storybook-preview-body-font-size.md):
<style>
html {
font-size: 15px;
}
</style>
需要注意,无论静态文件还是 previewBody 函数,Storybook 注入的都是渲染组件所用的 preview iframe,而不是 Storybook 应用自身的 UI(管理端),见 story-rendering.mdx 中的 Callout 说明。
四、类型与运行时约束
在源码层面,previewBody 被声明在两个位置,先看类型定义:code/core/src/types/modules/core-common.ts:
/**
* Programmatically modify the preview head/body HTML. The previewHead and previewBody functions
* accept a string, which is the existing head/body, and return a modified string.
*/
previewHead?: PresetValue<StorybookConfigRaw['previewHead']>;
previewBody?: PresetValue<StorybookConfigRaw['previewBody']>;
其底层原始配置项类型为 previewBody?: string(见同文件 core-common.ts)。由于它被 PresetValue<> 包裹,既可以被解析为函数,也可以作为 preset 机制中的普通值传递。因此在实际使用中需要注意:
- 该回调在 Node 端(构建/启动 Storybook 时)执行,而不是浏览器运行时;
- 函数接收的
body是已经完成的 body 内容,你返回的新字符串会整体替换它,所以务必基于原body做追加或替换,而不是另起炉灶; - 由于执行在 Node 环境,
process.env.*这类读取是有效的——这正是示例中条件注入依赖的机制。
五、底层实现:previewBody preset 与模板如何被加载
previewBody 配置最终会被解析成 core-server 的一个 preset 项。实现位于 code/core/src/core-server/presets/common-preset.ts:
export const previewBody = async (base: any, { configDir, presets }: Options) => {
const interpolations = await presets.apply<Record<string, string>>('env');
return getPreviewBodyTemplate(configDir, interpolations);
};
这里存在一个关键的间接层:该 preset 读取的是静态模板文件(即 preview-body.html),并把所有 preset 汇总后的 env 插值一并传入。也就是说,main 配置中的 previewBody 函数与静态 preview-body.html 是同一注入通道的两级入口,最终都要经过 getPreviewBodyTemplate 生成 body HTML。
实际拼接逻辑位于 code/core/src/common/utils/template.ts:
export function getPreviewBodyTemplate(
configDirPath: string,
interpolations?: Record<string, string>
) {
const base = readFileSync(
join(resolvePackageDir('storybook'), 'assets/server/base-preview-body.html'),
'utf8'
);
const bodyHtmlPath = resolve(configDirPath, 'preview-body.html');
let result = base;
if (existsSync(bodyHtmlPath)) {
result = readFileSync(bodyHtmlPath, 'utf8') + result;
}
return interpolate(result, interpolations);
}
从源码结构可以读出三个实现事实:
- 存在一个内置基准模板:
base-preview-body.html是打包在storybook包中的默认 body 模板; - 静态文件内容被前置:当
.storybook/preview-body.html存在时,其内容会被读取并放在基准模板之前(fileContent + base),而不是像 head 那样追加在基准模板之后(对照同文件 getPreviewHeadTemplate 的实现); - 支持
%VAR%占位符插值:interpolate会将%KEY%形式的占位符替换为 preset 链路中收集到的环境变量值(见 template.ts),这与 common-preset.ts 中envpreset 返回的raw环境对象相衔接。
另外,previewBody 与对称的 previewHead 均由 code/core/src/core-server/index.ts 统一导出(getPreviewHeadTemplate、getPreviewBodyTemplate),addon 作者可通过 preset 机制在预设配置中改写它们。
六、行为验证:模板加载的单元测试
仓库为上述模板逻辑提供了直接的单元测试,见 code/core/src/common/utils/tests/template.test.ts。其中关键断言包括:
- 当目录下不存在
preview-body.html时,getPreviewBodyTemplate返回内置基准模板内容(BASE_BODY_HTML_CONTENTS); - 当存在自定义
preview-body.html时,返回内容会包含自定义部分,验证了「读取文件并拼接」的行为。
这组测试可以当作你验证自身配置的最小参考:如果你修改了 .storybook/preview-body.html 却发现 preview 页面没有变化,可优先检查文件是否真的位于 configDir(默认即 .storybook)下,以及项目使用的构建流程是否正确应用了 previewBody 这个 preset。
七、配置指引小结
| 项目 | 值 |
|---|---|
| 配置位置 | .storybook/main.js / .storybook/main.ts |
| 类型签名 | (body: string) => string |
| 导入方式 | CSF 3:普通对象导出 StorybookConfig;CSF Next:从 @storybook/<framework>/node 导入 defineMain |
| 静态替代方案 | .storybook/preview-body.html(无需程序化改写时) |
| 注入目标 | 渲染组件的 preview iframe,而非 Storybook 管理 UI |
| 执行环境 | Node(构建 / dev server 启动阶段) |
| 底层 preset | common-preset.ts → template.ts |
最后再强调两条避坑建议:不要丢掉 ${body} 原内容,否则会覆盖 Storybook 默认的预览骨架;previewBody 与静态 preview-body.html 是同一注入通道的两个入口,若两者同时使用,需注意静态文件内容会被前置拼接到基准模板之前,理解这一顺序有助于排查自定义样式被覆盖或渲染层级异常的问题。
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 StartedRust0624
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