首页
/ Storybook 的 `previewHead` 与 `previewBody` 配置指南:以编程方式调整预览 HTML

Storybook 的 `previewHead` 与 `previewBody` 配置指南:以编程方式调整预览 HTML

2026-09-07 11:14:52作者:管翌锬

本篇指南讲解 Storybook 中两个由 Preset 驱动的配置入口 previewHeadpreviewBody。它们在 .storybook/main.jsmain.ts 中通过函数接收 Storybook 渲染 iframe 当前的 <head><body> 内容并返回修改后的字符串,可用于按环境条件注入脚本、样式或字体,也能被 addon 作者用于封装 UI 定制能力。读完本文,你将掌握两种配置的签名、适用场景、静态文件(preview-head.html / preview-body.html)与编程方案的取舍,以及底层模板是如何与 HTML 文件合并的。

配置项速览:类型与文档位置

previewHeadpreviewBody 属于 main.js|ts 顶层配置 家族,官方文档分别定义在:

在类型层面,它们被统一声明在核心类型模块 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']>;

注意这里的 PresetValue 语义:这两个字段本质上是 Preset 入口(preset 机制)的一个组成部分。也就是说,它们并不只是"你项目的某次性配置",而是 Storybook 在加载、合并 presets 时对每个 preset 的 previewHead / previewBody 依次调用后拼接得到最终 HTML 的预设值。正因如此,writing-presets.mdx 明确指出:预设 API 提供对 UI 配置的访问,包括通过 previewHeadpreviewBody 配置的 preview 的 headbody HTML 元素,与使用 preview-head.htmlpreview-body.html 文件的效果类似。

调用链路:common-preset.ts

在实现层面,Storybook 的核心预设 code/core/src/core-server/presets/common-preset.ts 提供了两个核心 preset 函数:

export const previewHead = async (base: any, { configDir, presets }: Options) => {
  const interpolations = await presets.apply<Record<string, string>>('env');
  return getPreviewHeadTemplate(configDir, interpolations);
};

export const previewBody = async (base: any, { configDir, presets }: Options) => {
  const interpolations = await presets.apply<Record<string, string>>('env');
  return getPreviewBodyTemplate(configDir, interpolations);
};

它们做的事是:先通过 presets.apply('env') 收集环境变量插值,再读取模板文件内容(详见下文"模板如何生成"),拿到"已有字符串"后交给你的 previewHead(head => ...) / previewBody(body => ...) 修改。换句话说:你函数收到的字符串参数,是底层模板已经渲染好的 <head>/<body> 内容,你只需在其基础上追加(或改写)并原样返回。

何时用编程式配置、何时用静态 HTML 文件

Storybook 提供了两套等价机制向预览 iframe 注入内容:

需求场景 静态 HTML 文件方案 编程式 preset 方案
注入固定脚本 / 样式,无需条件判断 .storybook/preview-head.htmlpreview-body.html 中添加内容 无需使用 previewHead / previewBody
按环境、按特性开关条件注入 无法实现(HTML 文件是纯静态的) previewHead / previewBody 函数返回拼接结果
开发插件 / addon 需要修改 UI 用户难以复用 addon 的逻辑 addon 在 preset 中导出这两个字段,随 addon 一并生效

官方文档在 main-config-preview-head.mdx 给出的 Callout 很明确:如果你不需要程序化调整 preview 的 <head>,可以直接改用 preview-head.html 添加脚本与样式previewBody 对应 preview-body.html)。反之,需要条件判断或要在 addon 里封装能力时,就使用 previewHead / previewBody 函数。

这个设计在 template.ts 的实现中也得到印证:模板函数会先检查 .storybook/preview-head.html / preview-body.html 是否存在,存在则把静态文件内容与内置基础模板拼接,再交给预设管线。也就是说,HTML 文件方案与函数方案并不会冲突,而是分层处理——文件内容先被合并进基础 HTML,你的函数再拿到合并结果做最后一步加工。

模板如何生成:preview-head.html / preview-body.html 与基础模板合并

previewHead / previewBody 的函数参数到底长什么样?看 code/core/src/common/utils/template.ts 的实现:

export function getPreviewHeadTemplate(configDirPath, interpolations?) {
  const base = readFileSync(
    join(resolvePackageDir('storybook'), 'assets/server/base-preview-head.html'),
    'utf8'
  );
  const headHtmlPath = resolve(configDirPath, 'preview-head.html');

  let result = base;
  if (existsSync(headHtmlPath)) {
    result += readFileSync(headHtmlPath, 'utf8');
  }
  return interpolate(result, interpolations);
}

对应地,getPreviewBodyTemplate 的逻辑是将用户 preview-body.html 的内容前置到内置基础模板 base-preview-body.html 之前再合并。两处都会经过 interpolate() 做环境变量插值——即把 %VAR_NAME% 占位符替换为 presets.apply('env') 得到的环境变量值。

配套单元测试 code/core/src/common/utils/tests/template.test.ts 也验证了两个关键行为:

  • .storybook/preview-body.html 不存在时,返回空内容 / 仅基础模板;
  • 当文件存在时,返回"用户文件内容 + 基础模板"的合并结果。

值得注意的实现细节<head> 合并时静态内容被追加到基础模板之后result += fileContent),而 <body> 合并时静态内容被插入到基础模板之前fileContent + result)。这意味着你自定义的 <head> 内容会出现在 Storybook 内置 head 内容之后,通常更适合做覆盖;而自定义 body 内容会出现在基础 body 之前。

.storybook/main.ts 中使用 previewHead

文档示例的主用法是按环境条件注入样式或脚本。以 CSF 3 与标准配置文件为例,修改 代码示例片段Head (CSF 3) 对应的 TS 写法(previewHead 版本;其他渲染器与格式仅导入路径不同):

// .storybook/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 = {
  previewHead: (head) => `
    ${head}
    <style>
      html, body {
        background: #827979;
      }
    </style>
 `,
};

export default config;

核心要点:

  1. 必须保留 ${head}:收到的参数是当前已有 <head> 内容的完整字符串,忘记拼接会直接破坏 Storybook 预览所需的既有脚本与样式。
  2. 返回完整的新字符串:你的函数签名是 (head: string) => string,返回什么,Storybook 就使用什么。
  3. JS(CommonJS/ESM)写法与之完全一致,只是把类型注解去掉、直接导出对象:
// .storybook/main.js
export default {
  previewHead: (head) => `
    ${head}
    <style>
      html, body {
        background: #827979;
      }
    </style>
 `,
};

.storybook/main.ts 中使用 previewBody

previewBody 的签名是 (body: string) => string,典型用法是在页面底部按条件追加第三方脚本(例如分析脚本)。文档中的 Body 示例(CSF 3 的 TS 写法)为:

// .storybook/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 = {
  previewBody: (body) => `
    ${body}
    ${
      process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
    }
  `,
};

export default config;

这里展示了与 previewHead 不同的典型用途:

  • 把注入逻辑建立在环境变量判断之上(process.env.ANALYTICS_ID 存在时才输出 <script> 标签);
  • 因为返回的是普通字符串,你可以做任意字符串处理,如条件拼接、压缩空白或插值;
  • 使用该功能时,开发与生产(构建)环境下 process.env 的可用变量以 main-config-env 描述的加载机制为准。

各框架 / 格式的代码形态(CSF 3 与 CSF Next)

针对不同渲染器和新的配置写法,官方代码片段 main-config-preview.md 覆盖了多个变体,完整包含:

写法形态 导入来源 配置对象
CSF 3 .storybook/main.js 直接导出 { previewHead, previewBody } 普通对象字面量
CSF 3 .storybook/main.ts import type { StorybookConfig } from '@storybook/your-framework' 类型为 StorybookConfig
CSF Next 🧪(实验性)main.ts import { defineMain } from '@storybook/your-framework/node' defineMain({ ... }) 包裹

CSF Next 的实验形态示例(以 Vue 渲染器为例):

// .storybook/main.ts
import { defineMain } from '@storybook/vue3-vite/node';

export default defineMain({
  previewHead: (head) => `
    ${head}
    <style>
      html, body {
        background: #827979;
      }
    </style>
 `,
});

而 React 系列的 CSF Next 写法把类型导入从主包换到 Node 入口(如 @storybook/react-vite/node),Angular 为 @storybook/angular/node,Web Components 为 @storybook/web-components-vite/node。JS 文件同样支持 defineMain,只需去掉类型注解。

需要区分:文档中的 previewHead/previewBody 配置与另一组 head/body 静态文件方案并不冲突,同时配置时按前面"模板如何生成"一节的顺序合并。此外,向**管理界面(manager)**注入内容应使用 main-config-manager-head 对应的机制,不要与这里的预览区配置混用。

典型场景与最佳实践小结

综合官方文档与源码,可以总结以下使用建议:

  1. addon 作者的首选封装:官方在 main-config-preview-head.mdxmain-config-preview-body.mdx 中明确写"Most often used by addon authors"(最常被插件作者使用)。addon 在自身 preset 中导出 previewHead/previewBody,用户安装后即可自动获得注入效果,无需手动编辑 HTML 文件。
  2. 条件注入优先用函数:从仓库的 common-preset.ts 看,函数形式在整个 preset 管线中最后执行,拿到的是合并后的完整内容,因此可以基于 process.env 或自身逻辑决定保留、替换或追加。
  3. 永远保留入参前缀:两个函数都是"接收完整字符串 → 返回完整字符串"的纯函数式约定(见 core-common.ts 的注释),漏掉 ${head} / ${body} 会导致 Storybook 预览页面缺失运行时所需的基础 HTML。
  4. 静态内容优先用文件:若注入的是固定不变的脚本/样式,直接用 preview-head.html / preview-body.html 更直观、无需经过函数层;仓库模板函数(template.ts)会自动检测并合并这些文件。
  5. 头部与 body 的合并顺序不同:自定义 head 内容追加在基础 head 之后、自定义 body 内容前置在基础 body 之前,涉及样式覆盖或首个脚本执行时机时可利用这一顺序差异。

通过 previewHeadpreviewBody,你可以在不修改 Storybook 内部渲染模板的前提下,对组件预览 iframe 的文档头部与正文做精确、可复用、可条件化的定制——无论是为项目引入全局主题样式、按环境加载分析脚本,还是以 addon 形式向用户交付开箱即用的 UI 增强能力。

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

项目优选

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