构建器 API
Storybook 支持多种构建器,包括 Webpack、Vite 和 ESBuild。构建器 API 是您可以用来向 Storybook 添加新构建器的接口集。

构建器如何工作?
在 Storybook 中,构建器负责将您的组件和故事编译成在浏览器中运行的 JS 包。构建器还提供一个用于交互式开发的开发服务器和一个用于优化包的生产模式。
要选择一个构建器,用户必须将其添加为依赖项,然后编辑其配置文件(.storybook/main.js)以启用它。例如,使用 Vite 构建器
npm install @storybook/builder-vite --save-dev// Replace your-framework with the framework you are using (e.g., react-vite, vue3-vite, angular, etc.)
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
core: {
builder: '@storybook/builder-vite', // 👈 The builder enabled here.
},
};
export default config;构建器 API
在 Storybook 中,每个构建器都必须实现以下 API,公开以下配置选项和入口点
export interface Builder<Config, Stats> {
start: (args: {
options: Options;
startTime: ReturnType<typeof process.hrtime>;
router: Router;
server: Server;
}) => Promise<void | {
stats?: Stats;
totalTime: ReturnType<typeof process.hrtime>;
bail: (e?: Error) => Promise<void>;
}>;
build: (arg: {
options: Options;
startTime: ReturnType<typeof process.hrtime>;
}) => Promise<void | Stats>;
bail: (e?: Error) => Promise<void>;
getConfig: (options: Options) => Promise<Config>;
corePresets?: string[];
overridePresets?: string[];
}在开发模式下,start API 调用负责初始化开发服务器,监视文件系统中的更改(例如,组件和故事),然后在浏览器中执行热模块重新加载。它还提供一个 bail 函数,允许运行的进程优雅地结束,无论是通过用户输入还是错误。
在生产模式下,build API 调用负责生成静态 Storybook 构建,默认情况下将其存储在 storybook-static 目录中,如果未提供其他配置。生成的输出应包含用户通过在浏览器中打开 index.html 或 iframe.html 来查看其 Storybook 所需的所有内容,而无需运行其他进程。
实现
底层来说,构建器负责提供/构建预览 iframe,这有自己的一系列要求。要完全支持 Storybook,包括随 Storybook 一起提供的 核心功能,它必须考虑以下几点。
导入故事
stories 配置字段启用 Storybook 中的故事加载。它定义了一个包含组件故事物理位置的文件 glob 数组。构建器必须能够加载这些文件并监视它们的变化,并据此更新 UI。
提供配置选项
默认情况下,Storybook 的配置在一个专用文件(storybook/main.js|ts)中处理,让用户可以选择根据自己的需求进行自定义。构建器也应通过附加字段或其他适合构建器的机制来提供自己的配置支持。例如
import { stringifyProcessEnvs } from './envs';
import { getOptimizeDeps } from './optimizeDeps';
import { commonConfig } from './vite-config';
import type { EnvsRaw, ExtendedOptions } from './types';
export async function createViteServer(options: ExtendedOptions, devServer: Server) {
const { port, presets } = options;
// Defines the baseline config.
const baseConfig = await commonConfig(options, 'development');
const defaultConfig = {
...baseConfig,
server: {
middlewareMode: true,
hmr: {
port,
server: devServer,
},
fs: {
strict: true,
},
},
optimizeDeps: await getOptimizeDeps(baseConfig, options),
};
const finalConfig = await presets.apply('viteFinal', defaultConfig, options);
const envsRaw = await presets.apply<Promise<EnvsRaw>>('env');
// Remainder implementation
}处理 preview.js 导出
preview.js 配置文件允许用户控制故事在 UI 中的渲染方式。这通过 decorators 命名导出提供。当 Storybook 启动时,它通过虚拟模块入口将这些命名导出转换为内部 API 调用,例如 addDecorator()。构建器也必须提供类似的实现。例如
import { virtualPreviewFile, virtualStoriesFile } from './virtual-file-names';
import { transformAbsPath } from './utils/transform-abs-path';
import type { ExtendedOptions } from './types';
export async function generateIframeScriptCode(options: ExtendedOptions) {
const { presets, frameworkPath, framework } = options;
const frameworkImportPath = frameworkPath || `@storybook/${framework}`;
const presetEntries = await presets.apply('config', [], options);
const configEntries = [...presetEntries].filter(Boolean);
const absoluteFilesToImport = (files: string[], name: string) =>
files
.map((el, i) => `import ${name ? `* as ${name}_${i} from ` : ''}'${transformAbsPath(el)}'`)
.join('\n');
const importArray = (name: string, length: number) =>
new Array(length).fill(0).map((_, i) => `${name}_${i}`);
const code = `
// Ensure that the client API is initialized by the framework before any other iframe code
// is loaded. That way our client-apis can assume the existence of the API+store
import { configure } from '${frameworkImportPath}';
import {
addDecorator,
addParameters,
addArgTypesEnhancer,
addArgsEnhancer,
setGlobalRender
} from 'storybook/preview-api';
import { logger } from 'storybook/internal/client-logger';
${absoluteFilesToImport(configEntries, 'config')}
import * as preview from '${virtualPreviewFile}';
import { configStories } from '${virtualStoriesFile}';
const configs = [${importArray('config', configEntries.length)
.concat('preview.default')
.join(',')}].filter(Boolean)
configs.forEach(config => {
Object.keys(config).forEach((key) => {
const value = config[key];
switch (key) {
case 'args':
case 'argTypes': {
return logger.warn('Invalid args/argTypes in config, ignoring.', JSON.stringify(value));
}
case 'decorators': {
return value.forEach((decorator) => addDecorator(decorator, false));
}
case 'parameters': {
return addParameters({ ...value }, false);
}
case 'render': {
return setGlobalRender(value)
}
case 'globals':
case 'globalTypes': {
const v = {};
v[key] = value;
return addParameters(v, false);
}
case 'decorateStory':
case 'renderToCanvas': {
return null;
}
default: {
// eslint-disable-next-line prefer-template
return console.log(key + ' was not supported :( !');
}
}
});
})
configStories(configure);
`.trim();
return code;
}MDX 支持
Storybook 的文档 包括使用 Webpack 加载器以 MDX 格式编写故事/文档的能力。构建器也必须知道如何解释 MDX 并调用 Storybook 的特殊扩展。例如
import mdx from 'vite-plugin-mdx';
import { createCompiler } from 'storybook/internal/csf-tools/mdx';
export function mdxPlugin() {
return mdx((filename) => {
const compilers = [];
if (filename.endsWith('stories.mdx') || filename.endsWith('story.mdx')) {
compilers.push(createCompiler({}));
}
return {
compilers,
};
});
}生成源代码片段
Storybook 使用与输入相关的附加元数据来注解组件和故事,以自动生成交互式控件和文档。目前,这是通过 Webpack 加载器/插件提供的。构建器必须重新实现这一点以支持这些功能。
生成静态构建
Storybook 的核心功能之一是能够生成可以 发布 到 Web 托管服务的静态构建。构建器也必须能够提供类似的机制。例如
import { build as viteBuild } from 'vite';
import { stringifyProcessEnvs } from './envs';
import { commonConfig } from './vite-config';
import type { EnvsRaw, ExtendedOptions } from './types';
export async function build(options: ExtendedOptions) {
const { presets } = options;
const baseConfig = await commonConfig(options, 'build');
const config = {
...baseConfig,
build: {
outDir: options.outputDir,
emptyOutDir: false,
sourcemap: true,
},
};
const finalConfig = await presets.apply('viteFinal', config, options);
const envsRaw = await presets.apply<Promise<EnvsRaw>>('env');
// Stringify env variables after getting `envPrefix` from the final config
const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix);
// Update `define`
finalConfig.define = {
...finalConfig.define,
...envs,
};
await viteBuild(finalConfig);
}开发服务器集成
默认情况下,当 Storybook 在开发模式下启动时,它依赖于其内部开发服务器。构建器需要能够与其集成。例如
import { createServer } from 'vite';
export async function createViteServer(options: ExtendedOptions, devServer: Server) {
const { port } = options;
// Remainder server configuration
// Creates the server.
return createServer({
// The server configuration goes here
server: {
middlewareMode: true,
hmr: {
port,
server: devServer,
},
},
});
}关闭开发服务器
构建器必须提供一种在进程终止后停止开发服务器的方法;这可以通过用户输入或错误来实现。例如
import { createViteServer } from './vite-server';
let server: ViteDevServer;
export async function bail(): Promise<void> {
return server?.close();
}
export const start: ViteBuilder['start'] = async ({ options, server: devServer }) => {
// Remainder implementation goes here
server = await createViteServer(options as ExtendedOptions, devServer);
return {
bail,
totalTime: process.hrtime(startTime),
};
};HMR 支持
在开发模式下运行时,构建器的开发服务器必须能够在发生更改(无论是故事、组件还是辅助函数)时重新加载页面。
更多信息
该领域正处于快速发展中,相关的文档仍在进行中,并可能发生更改。如果您有兴趣创建构建器,可以查看 Vite、Webpack 或 Modern Web 的 dev-server-storybook 的源代码,以了解如何在 Storybook 中实现构建器。准备好后,打开一个 RFC 与 Storybook 社区和维护者讨论您的提案。
了解有关构建器的更多信息
- Vite 构建器,用于使用 Vite 进行打包
- Webpack 构建器,用于使用 Webpack 进行打包
- 用于构建 Storybook 构建器的构建器 API
