首页
/ Storybook `previewHead` 配置指南:在 `.storybook/main.js|ts` 中动态注入预览页 `<head>` 内容

Storybook `previewHead` 配置指南:在 `.storybook/main.js|ts` 中动态注入预览页 `<head>` 内容

2026-09-07 09:46:42作者:毕习沙Eudora

导读

previewHead 是 Storybook 主配置(main.js|ts)中用于编程式调整 preview 预览页 <head> 的核心配置项,类型为 (head: string) => string。它适用于需要根据运行环境(如 NODE_ENV、自定义环境变量)或运行时状态条件注入第三方脚本、外链字体与全局样式,以及编写 Addon 时向预览页统一注入脚本的场景。读完本篇,你将掌握 previewHead 的完整签名、JS/TS 与各框架写法、与静态文件 preview-head.html 的取舍,以及它在源码中从主配置到 iframe 构建产物的完整执行链路。

为什么需要 previewHead

Storybook 的界面由两个互相隔离的 HTML 文档构成:

  • Manager(管理界面):包含左侧导航栏、工具栏、面板等 UI 外壳;
  • Preview(预览页):真正渲染组件/story 的 iframe 页面。

previewHead 操作的是** Preview iframe 的 <head>**,即渲染组件的那一层文档头部。因此适合它的内容,是对“被渲染的组件自身运行环境”产生影响的脚本与样式(例如组件主题所需的全局 CSS 变量、字体、外部 SDK),而不是对 Storybook 管理界面产生作用的脚本。

从类型定义看,公共配置接口中该字段被定义为:

/**
 * 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']>;

其中 PresetValue<T> = T | ((config: T, options: Options) => T | Promise<T>)(见 core-common.ts)。也就是说 previewHead 既可以是一个直接返回的字符串,也可以是一个接收“当前已有 head 内容”并返回新内容的函数——官方推荐并广泛使用的正是函数形式,因为它允许你保留 Storybook 默认注入的头部内容。

基础用法:函数形式注入自定义内容

previewHead 的签名非常直白:它接收一个字符串参数 head,该参数是 Storybook 处理好的现有 head 内容(包含基础模板与 .storybook/preview-head.html 静态文件内容),你负责在其基础上拼接新内容并整体返回。

以下示例演示了根据环境变量条件注入统计脚本的经典写法,同时覆盖 main.js(Common 渲染器,CSF 3)、main.ts(含 CSF Next)以及 React / Vue 3 / Angular / Web Components 等框架的 defineMain 写法。

经典 CSF 3 写法(.storybook/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)'],
  previewHead: (head) => `
    ${head}
    ${
      process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
    }
  `,
};

类型安全的 .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 = {
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  previewHead: (head) => `
    ${head}
    ${
      process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
    }
  `,
};

export default config;

CSF Next 写法(.storybook/main.ts + defineMain

在框架的 node 端入口(如 @storybook/react-vite/node)提供了 defineMain 工厂(见 react-vite 入口vue3-vite 入口angular 入口web-components-vite 入口)时,推荐使用:

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { defineMain } from '@storybook/react-vite/node';

export default defineMain({
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  previewHead: (head) => `
    ${head}
    ${
      process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
    }
  `,
});

Vue 3、Angular、Web Components 等框架只需把 defineMain 的导入路径与 framework 字段换成对应框架包即可,例如 Vue 3 使用 @storybook/vue3-vite/node + framework: '@storybook/vue3-vite',Angular 使用 @storybook/angular/node + framework: '@storybook/angular',Web Components 使用 @storybook/web-components-vite/node + framework: '@storybook/web-components-vite'

CSF Next 写法(.storybook/main.js

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { defineMain } from '@storybook/react-vite/node';

export default defineMain({
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  previewHead: (head) => `
    ${head}
    ${
      process.env.ANALYTICS_ID ? '<script src="https://cdn.example.com/analytics.js"></script>' : ''
    }
  `,
});

三处值得注意的要点

  1. 必须拼接 ${head}:传入的 head 参数是 Storybook 已经生成的头部基线。若忘记在模板字符串中输出 ${head},会直接丢掉基础头部内容,可能导致预览页运行异常。上例把所有新内容都放在 ${head} 之后追加,安全且顺序可控。
  2. 环境变量按需注入:示例通过 process.env.ANALYTICS_ID 判断是否注入统计脚本。这展示了 previewHead 相对静态 preview-head.html 的核心优势——同一份配置在不同环境(开发/CI/本地)下可以产出不同的 head 内容
  3. 字符串拼接自由度高:返回值是普通字符串,因此既可以做简单的字符串拼接,也可以使用数组 join、条件分支乃至调用其他工具函数,只要是最终产出一个合法的 HTML 片段字符串即可。

何时使用 previewHead,何时使用 preview-head.html

如果不需要编程式地调整,Storybook 提供了更简单的静态文件方式:在配置目录 .storybook 下创建 preview-head.html,其内容会被原样追加到预览页 head(详见 story-rendering 配置文档)。

两者的取舍可归纳为:

场景 推荐方式 理由
无条件注入固定的脚本、<link>、字体或全局样式 .storybook/preview-head.html 声明式、简单直接、无需 JS
需要依据 process.env.*、构建模式或自定义逻辑动态决定注入内容 `main.js tspreviewHead` 函数
Addon / Preset 作者希望向所有用户预览页统一注入脚本或样式 previewHead(在 Preset 中导出) 通过 preset 机制与用户配置无缝合并

从源码看,previewHead 函数最终产出的字符串是在静态文件之上继续叠加的。在 template.ts 中,getPreviewHeadTemplate() 会读取 Storybook 自带的 base-preview-head.html 作为基底,若 .storybook/preview-head.html 存在则将其追加到基底之后,最后再做 %KEY% 占位符插值。换言之,即使你同时使用了 preview-head.htmlpreviewHead 函数,函数中收到的 head 参数也已经包含前者,两种方式可以共存而不会互相覆盖。

这一行为也直接反映在单测 template.test.ts 中:当 preview-head.html 不存在时返回纯基础模板内容;当存在时返回 BASE_HTML_CONTENTS + HEAD_HTML_CONTENTS(基础内容在前、静态文件内容追加在后)。

执行链路:从 main.js 到 iframe 的 <head>

理解了用法后,了解它背后的 preset 合并与模板注入机制,有助于在排查“head 内容没有生效 / 被覆盖”问题时快速定位。

第一步:preset 的 reduce 式合并

Storybook 会把主配置与所有 Addon/Preset 一起加载,并以 preset 为单位做链式合并。presets.ts 中的 applyPresets 对所有 preset 依次执行 reduce:当某个 preset 导出的同名配置项是函数时,就以前一次的输出作为入参继续调用,形如:

configA(head) => configB(head) => ... => 最终 head 字符串

因此你的 main.jspreviewHead 返回的结果会成为链路的最终输出(前提是你的主配置在加载链末尾),而传入函数的第一参数正是上游 preset(含 Addon 注入)已经处理好的 head。

第二步:核心 preset 生成默认 head

common-preset.ts 中定义了 previewHead 默认 preset,它先通过 presets.apply('env') 拿到环境变量插值表,再调用前文提到的 getPreviewHeadTemplate(configDir, interpolations) 组装出基础 + preview-head.html 的内容。这也是为什么你在自定义函数里能拿到一份“已含默认内容”的 head 字符串。

第三步:builder 将 head 注入 iframe 模板

以 Vite builder 为例,transform-iframe-html.tstransformIframeHtml() 中通过 presets.apply('previewHead')presets.apply('previewBody') 取得最终片段,并将其替换进 iframe HTML 模板的 <!-- [HEAD HTML SNIPPET HERE] --><!-- [BODY HTML SNIPPET HERE] --> 占位符。Webpack5 builder 也有对应的 iframe HTML 处理(见 iframe-webpack.config.ts)。由此可以确认:previewHead 的输出在 dev server 与静态构建时都会被消费,无论你通过 storybook dev 还是 storybook build 启动都会生效。

模板插值:%VAR% 占位符

template.ts 中的 interpolate() 会用 %KEY% 占位符与传入的数据做全局替换(template.ts)。这解释了官方模板文件中常见的 %BASE_URL% 一类变量从何而来:它们来源于 env preset(见 common-preset.tsloadEnvs)。这意味着你在 preview-head.html 静态文件里也可以使用 %YOUR_ENV_VAR% 这类占位符,由 Storybook 在读取时统一插值。

与相关配置项的对比

previewBody:注入 <body> 起始位置

previewHead 对称的 previewBody 接收 (body: string) => string,用于向预览页 <body> 开头注入内容(例如全屏背景容器、加载遮罩、Provider 外包层等),详见 previewBody 配置文档。它同样支持 .storybook/preview-body.html 静态文件方式,且从 template.test.ts 看,body 的拼接顺序与 head 相反——静态文件内容在前、基础模板在后。两者共享同一套模板插值与 preset 合并机制。

managerHead:管理界面(而非预览 iframe)

容易混淆的是 managerHead。它编程式调整的是 Storybook Manager(外壳 UI)的 <head>,与渲染组件的 iframe 完全隔离,仅处理 manager-head.html 静态文件(见 common-preset.tsmanagerHead preset)。注入自定义 UI 主题、Manager 侧字体时应使用它,详见 managerHead 配置文档

面向 Addon / Preset 作者的补充说明

previewHead 最常见的生产场景其实是 Addon 作者向用户的预览页统一注入脚本与样式(参考 writing-presets 文档 中的 UI 配置一节)。在编写自定义 Addon 的 preset 时,你可以这样组合:

export const previewHead = (head: string) => `
  ${head}
  <script src="your-addon-runtime.js"></script>
`;

得益于第一步介绍过的 reduce 合并链,多个 Addon 的 previewHead 会依次叠加、互不覆盖;只要每个 preset 都保留 ${head},用户的最终 head 就能同时包含所有参与方的贡献。这也是把预览页内容“插桩”做得幂等、可组合的推荐方式。

调试与验证

  • 开发模式下打开 Storybook 页面,用浏览器开发者工具检查 预览 iframe 内部<head>(注意不要误看 Manager 外壳的 <head>);确认自定义 <script><link> 是否出现在 </head> 闭合标签之前、且位于默认内容之后。
  • 若注入的脚本没有生效,优先检查 main.jsframeworkstories 之外是否真的导出了 previewHead,以及是否误用了 module.exports(主配置文件必须是 ESM,需使用 export default,详见 主配置总览)。
  • 若内容“莫名被覆盖”,检查是否有其他 Addon/Preset 也导出了 previewHead丢弃了入参 head——reduce 链中任一环丢弃参数都会造成后续内容整体丢失。
  • 环境变量相关逻辑建议通过 process.env.XXX 直接读取,并留意在调试运行与 CI 构建时的取值差异。

小结

previewHead 是把“环境感知的 HTML 注入能力”交还给开发者与 Addon 作者的官方入口:它以 (head: string) => string 的函数签名挂载在主配置 main.js|ts 上,经由 preset reduce 链完成多来源合并,最终由 builder 注入预览 iframe 的 <head>。与静态 preview-head.html 各司其职、与 previewBody/managerHead 分工明确,掌握它即可系统性地管理 Storybook 预览环境的脚本、样式与集成代码。

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

项目优选

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