Storybook 环境变量实战:在故事中使用 .env 值的跨框架写法与注入机制源码解析
在 Storybook 中,环境变量是同一套组件在开发、预览、CI 或不同主题下呈现差异化行为的核心手段。本文围绕官方文档 environment-variables.mdx 中「Using .env files」章节所使用的示例片段 my-component-with-env-variables.md 展开,完整覆盖 STORYBOOK_ 前缀变量从命令行、.env 文件到故事 args 的完整链路,给出 Angular、Svelte、React、Vue、Web Components 等多渲染器下的可复制示例,并结合 builder-vite 的源码实现解释这些变量为何能在浏览器端被读取。
核心机制:STORYBOOK_ 前缀决定变量是否暴露到预览端
Storybook 的环境变量遵循一个简单而关键的规则:只有带 STORYBOOK_ 前缀的变量才会被注入到 Storybook 的运行环境中。使用 Webpack 时通过 process.env 访问,使用 Vite builder 时通过 import.meta.env 访问。
最基本的注入方式是直接在命令行前缀变量:
STORYBOOK_THEME=red STORYBOOK_DATA_KEY=12345 npm run storybook
此后在预览端的任意 JavaScript 代码中都可以读取这些值:
// Webpack 构建下
console.log(process.env.STORYBOOK_THEME);
console.log(process.env.STORYBOOK_DATA_KEY);
// Vite 构建下
console.log(import.meta.env.STORYBOOK_THEME);
console.log(import.meta.env.STORYBOOK_DATA_KEY);
安全警告(原文档 Callout):不要在 Storybook 中存放任何机密(如私有 API Key)或敏感信息。环境变量会被内嵌进构建产物,任何人都可以通过检查构建文件查看到它们。
主场景:通过 .env 文件向故事注入数据
在项目中添加一个 .env 文件并写入:
STORYBOOK_DATA_KEY=12345
那么即使在故事(story)内部,也能随时访问该变量。这正是本文核心文档 my-component-with-env-variables.md 的主题:将环境变量作为故事 args 的一部分,例如把一个 propertyA 属性设置为 process.env.STORYBOOK_DATA_KEY。以下按渲染器完整给出原文档中的全部示例。
通用写法(React / Preact / Qwik / Solid / Vue 等常见框架,CSF 3)
JavaScript 版本:
// MyComponent.stories.js
import { MyComponent } from './MyComponent';
export default {
component: MyComponent,
};
export const ExampleStory = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
TypeScript 版本(将 your-framework 替换为实际框架,如 react-vite、nextjs、vue3-vite):
// MyComponent.stories.ts
// 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 { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const ExampleStory: Story = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
Angular 渲染器
CSF 3 写法:
// MyComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';
import { MyComponent } from './my-component.component';
const meta: Meta<MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<MyComponent>;
export const ExampleStory: Story = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
Svelte 渲染器
Svelte CSF(@storybook/addon-svelte-csf)写法,注意 args 在 Svelte 模板中需要用双花括号包裹对象字面量:
<!-- MyComponent.stories.svelte -->
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import MyComponent from './MyComponent.svelte';
const { Story } = defineMeta({
component: MyComponent,
});
</script>
<Story
name="ExampleStory"
args={{
propertyA: process.env.STORYBOOK_DATA_KEY
}}
/>
CSF 3 的 JS / TS 写法(TS 版中 your-framework 应替换为 svelte-vite 或 sveltekit):
// MyComponent.stories.js
import MyComponent from './MyComponent.svelte';
export default {
component: MyComponent,
};
export const ExampleStory = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
// MyComponent.stories.ts
// Replace your-framework with svelte-vite or sveltekit
import type { Meta, StoryObj } from '@storybook/your-framework';
import MyComponent from './MyComponent.svelte';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const ExampleStory: Story = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
Web Components 渲染器
Web Components 的 component 字段是标签名字符串而非组件类:
// MyComponent.stories.js
export default {
component: 'my-component',
};
export const ExampleStory = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
// MyComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/web-components-vite';
const meta: Meta = {
component: 'my-component',
};
export default meta;
type Story = StoryObj;
export const ExampleStory: Story = {
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
};
实验性 CSF Next 写法
原文档同时提供了标注为实验性(CSF Next 🧪)的变体,通过 .storybook/preview 导出的 preview 对象构建 meta 与 story。以 React 为例:
// MyComponent.stories.ts
import preview from '../.storybook/preview';
import { MyComponent } from './MyComponent';
const meta = preview.meta({
component: MyComponent,
});
export const ExampleStory = meta.story({
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
},
);
Vue、Angular、Web Components 的 CSF Next 变体遵循同一模式,仅导入的组件与 component 字段不同(Vue 使用 import MyComponent from './MyComponent.vue';Web Components 使用 component: 'my-component'):
// MyComponent.stories.ts (web-components, CSF Next)
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'my-component',
});
export const ExampleStory = meta.story({
args: {
propertyA: process.env.STORYBOOK_DATA_KEY,
},
});
适用前提:CSF Next 写法是实验性 API(原文档中以 🧪 标记区分于 CSF 3 标签页),生产项目应优先使用 CSF 3 的稳定写法。
Vite 构建下的差异:import.meta.env 与 VITE_ 前缀
使用 Vite builder 的项目不输出 process.env 这类 Node.js 全局,需要改用 import.meta.env 访问 STORYBOOK_、VITE_ 前缀的变量。以通用 TS 写法为例(引自同章节的 my-component-vite-env-variables.md):
// MyComponent.stories.ts
export const ExampleStory: Story = {
args: {
propertyA: import.meta.env.STORYBOOK_DATA_KEY,
propertyB: import.meta.env.VITE_CUSTOM_VAR,
},
};
可以看到 Vite 场景下 VITE_ 前缀的自定义变量(VITE_CUSTOM_VAR)同样可用——这一点可以从 builder-vite 的源码中得到确证。
源码佐证一:STORYBOOK_ 前缀如何进入 Vite 的 envPrefix
从源码结构看,builder-vite 内置了一个 storybook:config-plugin,其职责之一就是合并环境变量前缀。在 storybook-config-plugin.ts 中:
const existingEnvPrefix = config.envPrefix;
// If an envPrefix is specified in the user's vite config, add STORYBOOK_ to it.
// Otherwise, add both VITE_ and STORYBOOK_ so that Vite doesn't lose its default.
const mergedEnvPrefix = existingEnvPrefix
? Array.from(
new Set([
...(Array.isArray(existingEnvPrefix) ? existingEnvPrefix : [existingEnvPrefix]),
'STORYBOOK_',
])
)
: ['VITE_', 'STORYBOOK_'];
return {
resolve: { ... },
envPrefix: mergedEnvPrefix,
};
这段逻辑解释了官方文档的行为边界:如果用户的 Vite 配置已显式指定 envPrefix,Storybook 会把 STORYBOOK_ 追加进去(去重后合并);否则默认注入 ['VITE_', 'STORYBOOK_'],既保留 Vite 自身的 VITE_ 默认前缀,又让 STORYBOOK_ 变量可用。这也是排查问题章节里「框架专属前缀(如 VUE_APP_)不生效时需要自行扩展 envPrefix」的根因。
源码佐证二:import.meta.env 的字符串替换实现
Vite 通过 define 替换的方式暴露环境变量。builder-vite 中的 envs.ts 实现了定制化的 stringifyProcessEnvs:
// Allowed env variables on the client
const allowedEnvVariables = [
'STORYBOOK',
// Vite `import.meta.env` default variables
'BASE_URL',
'MODE',
'DEV',
'PROD',
'SSR',
];
export function stringifyProcessEnvs(raw: Builder_EnvsRaw, envPrefix: ViteConfig['envPrefix']) {
const updatedRaw: Builder_EnvsRaw = {};
const envs = Object.entries(raw).reduce((acc: Builder_EnvsRaw, [key, value]) => {
// Only add allowed values OR values from array OR string started with allowed prefixes
if (
allowedEnvVariables.includes(key) ||
(Array.isArray(envPrefix) && !!envPrefix.find((prefix) => key.startsWith(prefix))) ||
(typeof envPrefix === 'string' && key.startsWith(envPrefix))
) {
acc[`import.meta.env.${key}`] = JSON.stringify(value);
updatedRaw[key] = value;
}
return acc;
}, {});
// support destructuring like
// const { foo } = import.meta.env;
envs['import.meta.env'] = JSON.stringify(stringifyEnvs(updatedRaw));
return envs;
}
这段实现有两个值得注意的细节:
- 白名单 + 前缀双重过滤:只有命中内置白名单(
STORYBOOK、BASE_URL、MODE、DEV、PROD、SSR)或满足envPrefix前缀(即上文合并出的VITE_/STORYBOOK_)的变量才会生成import.meta.env.KEY的替换规则。这就是「只有带前缀的变量才暴露到浏览器端」的底层执行点; - 支持解构写法:函数最后额外输出了完整的
import.meta.env对象替换(envs['import.meta.env'] = JSON.stringify(...)),使得const { foo } = import.meta.env这类解构语法同样可用。
源码佐证三:main.js 的 env 字段
除 .env 文件外,还可以扩展 Storybook 配置(.storybook/main.js|.ts)新增 env 字段来定义变量。该字段在类型定义 core-common.ts 中声明为 env?: PresetValue<StorybookConfigRaw['env']>(注释为 "Modify or return env config."),因此支持函数形式的预设合并。典型写法(引自 main-config-env.md):
// .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)'],
/*
* 👇 The `config` argument contains all the other existing environment variables.
* Either configured in an `.env` file or configured on the command line.
*/
env: (config) => ({
...config,
EXAMPLE_VAR: 'An environment variable configured in Storybook',
}),
};
这里的关键点是 env 接收的 config 参数已经包含了 .env 文件与命令行配置的全部既有环境变量,函数在展开 ...config 的基础上追加或覆盖新变量。CSF Next 体系下则使用框架 node 导出(如 @storybook/react-vite/node)的 defineMain,env 写法完全一致。配置生效后,故事中以与 .env 相同的方式访问即可:
export const Basic: Story = {
args: {
exampleProp: process.env.EXAMPLE_VAR,
},
};
其他注入渠道与衍生用法
.env 文件的模式区分
除了通用的 .env,还可以为不同模式提供专属文件:添加 .env.development 或 .env.production 来为环境变量应用不同的值。
构建时硬编码
运行 build-storybook 时同样可以传入这些环境变量(例如 STORYBOOK_DATA_KEY=12345 npx build-storybook),它们会被硬编码进静态产物,随静态版 Storybook 一起分发,构建后不再可变。
自定义 head/body 模板中的占位替换
在自定义 <head>/<body> 中可使用 %STORYBOOK_X% 占位符,例如 %STORYBOOK_THEME% 会被替换为 red。注意:如果占位符用在 JavaScript 的属性或值位置,由于值是原样插入的,可能需要自行补加引号,例如 <link rel="stylesheet" href="%STORYBOOK_STYLE_URL%" />。
用环境变量选择预览浏览器
| Browser | Example |
|---|---|
| Safari | BROWSER="safari" |
| Firefox | BROWSER="firefox" |
| Chromium | BROWSER="chromium" |
默认情况下 Storybook 启动时会打开一个新的 Chrome 窗口;若本机没有 Chrome,需显式指定上述选项或调整系统默认浏览器。此外可通过 BROWSER_ARGS 向浏览器传参,例如 BROWSER_ARGS="--incognito" 以无痕模式打开 Chrome——该选项仅在同时显式设置了 BROWSER 时才生效。
故障排查
Vite 场景下框架专属前缀不生效:如果你试图使用框架专属的环境变量(例如 VUE_APP_),可能因为 Storybook 与框架各自的配置无法互相识别而失效。此时需要扩展框架配置让其识别目标前缀——对 Vite 系框架,即在上文 storybook-config-plugin.ts 所合并的配置基础上显式设置 envPrefix 选项;其他框架可能需要类似的调整。
Webpack 场景下 Can't find variable: process:若使用基于 Webpack 的框架(如 Angular with Webpack),引用 STORYBOOK_ 前缀变量时出现该运行时错误,通常意味着一个或多个环境变量缺失或未正确配置。修复方式是确保它们通过 .env 文件、命令行参数配置,并按「main.js 的 env 字段」一节提供默认值。
能力速查
| 能力 | 写法 | 适用前提 |
|---|---|---|
| 命令行注入 | STORYBOOK_DATA_KEY=12345 npm run storybook |
任意构建 |
.env 文件 |
STORYBOOK_DATA_KEY=12345 写入项目根 .env |
任意构建 |
| 模式区分 | .env.development / .env.production |
任意构建 |
| 配置注入 | main.js 中 env: (config) => ({ ...config, EXAMPLE_VAR }) |
任意构建 |
| 读取(Webpack) | process.env.STORYBOOK_DATA_KEY |
webpack5 / Angular 等 |
| 读取(Vite) | import.meta.env.STORYBOOK_DATA_KEY、import.meta.env.VITE_CUSTOM_VAR |
Vite 系 builder |
| head/body 占位 | %STORYBOOK_X% |
自定义模板 |
| 静态构建硬编码 | build-storybook 时传入 STORYBOOK_ 变量 |
静态发布 |
| 浏览器选择 | BROWSER="firefox",可选 BROWSER_ARGS |
开发预览 |
整体来看,STORYBOOK_ 前缀是 Storybook 环境变量体系的单一入口:命令行、.env、main.js 的 env 三条注入路径最终汇聚到同一套机制,再由 Webpack 的 process.env 或 Vite builder 中 envs.ts 的前缀过滤与 import.meta.env 替换落地到浏览器端,故事 args 因此成为承载环境差异的最自然落点。
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