Docs
Storybook Docs

构建器 API

Storybook 的架构支持多个构建器,包括 WebpackViteESBuild。构建器 API 是你可以用来向 Storybook 添加新构建器的一组接口。

¥Storybook is architected to support multiple builders, including Webpack, Vite, and ESBuild. The builder API is the set of interfaces you can use to add a new builder to Storybook.

Storybook builders

构建器如何工作?

¥How do builders work?

在 Storybook 中,构建器负责将你的组件和故事编译成在浏览器中运行的 JS 包。构建器还提供了用于交互式开发的开发服务器和用于优化打包包的生产模式。

¥In Storybook, a builder is responsible for compiling your components and stories into JS bundles that run in the browser. A builder also provides a development server for interactive development and a production mode for optimized bundles.

要选择加入构建器,用户必须将其添加为依赖,然后编辑其配置文件(.storybook/main.js)以启用它。例如,使用 Vite 构建器:

¥To opt into a builder, the user must add it as a dependency and then edit their configuration file (.storybook/main.js) to enable it. For example, with the Vite builder:

npm install @storybook/builder-vite --save-dev
.storybook/main.js|ts
export default {
  stories: ['../src/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
  core: {
    builder: '@storybook/builder-vite', // 👈 The builder enabled here.
  },
};

构建器 API

¥Builder API

在 Storybook 中,每个构建器都必须实现以下 API,并公开以下配置选项和入口点:

¥In Storybook, every builder must implement the following API, exposing the following configuration options and entry points:

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 函数,允许正在运行的进程正常结束,无论是通过用户输入还是错误。

¥In development mode, the start API call is responsible for initializing the development server to monitor the file system for changes (for example, components and stories) then execute a hot module reload in the browser. It also provides a bail function to allow the running process to end gracefully, either via user input or error.

在生产中,build API 调用负责生成静态 Storybook 构建,如果未提供其他配置,则默认将其存储在 storybook-static 目录中。生成的输出应包含用户在浏览器中打开 index.htmliframe.html 且没有其他进程运行的情况下查看其 Storybook 所需的一切。

¥In production, the build API call is responsible for generating a static Storybook build, storing it by default in the storybook-static directory if no additional configuration is provided. The generated output should contain everything the user needs to view its Storybook by opening either the index.html or iframe.html in a browser with no other processes running.

实现

¥Implementation

在底层,构建器负责提供/构建预览 iframe,它有自己的一组要求。要完全支持 Storybook,包括随 Storybook 附带的 基本插件,它必须考虑以下几点。

¥Under the hood, a builder is responsible for serving/building the preview iframe, which has its own set of requirements. To fully support Storybook, including the Essential addons that ship with Storybook, it must consider the following.

导入故事

¥Import stories

stories 配置字段可在 Storybook 中启用故事加载。它定义了一个文件 glob 数组,其中包含组件故事的物理位置。构建器必须能够加载这些文件并监视它们的更改并相应地更新 UI。

¥The stories configuration field enables story loading in Storybook. It defines an array of file globs containing the physical location of the component's stories. The builder must be able to load those files and monitor them for changes and update the UI accordingly.

提供配置选项

¥Provide configuration options

默认情况下,Storybook 的配置在专用文件 (storybook/main.js|ts) 中处理,让用户可以根据需要对其进行自定义。构建器还应通过附加字段或其他构建器适当机制提供自己的配置支持。例如:

¥By default, Storybook's configuration is handled in a dedicated file (storybook/main.js|ts), giving the user the option to customize it to suit its needs. The builder should also provide its own configuration support through additional fields or some other builder-appropriate mechanism. For example:

vite-server.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 导出

¥Handle preview.js exports

preview.js 配置文件允许用户控制故事在 UI 中的渲染方式。这是通过 decorators 命名导出提供的。Storybook 启动时,会通过虚拟模块条目将这些命名的导出转换为内部 API 调用,例如 addDecorator()。构建器还必须提供类似的实现。例如:

¥The preview.js configuration file allows users to control how the story renders in the UI. This is provided via the decorators named export. When Storybook starts, it converts these named exports into internal API calls via virtual module entry, for example, addDecorator(). The builder must also provide a similar implementation. For example:

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/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 支持

¥MDX support

Storybook 的文档 包括使用 Webpack 加载器在 MDX 中编写故事/文档的能力。构建器还必须知道如何解释 MDX 并调用 Storybook 的特殊扩展。例如:

¥Storybook's Docs includes the ability to author stories/documentation in MDX using a Webpack loader. The builder must also know how to interpret MDX and invoke Storybook's special extensions. For example:

mdx-plugin.ts
import mdx from 'vite-plugin-mdx';
 
import { createCompiler } from '@storybook/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,
    };
  });
}

生成源代码片段

¥Generate source code snippets

Storybook 使用与其输入相关的其他元数据注释组件和故事,以自动生成交互式控件和文档。目前,这是通过 Webpack 加载器/插件提供的。构建器必须重新实现它以支持这些功能。

¥Storybook annotates components and stories with additional metadata related to their inputs to automatically generate interactive controls and documentation. Currently, this is provided via Webpack loaders/plugins. The builder must re-implement this to support those features.

生成静态构建

¥Generate a static build

Storybook 的核心功能之一是能够生成可以 published 到 Web 托管服务的静态构建。构建器还必须能够提供类似的机制。例如:

¥One of Storybook's core features it's the ability to generate a static build that can be published to a web hosting service. The builder must also be able to provide a similar mechanism. For example:

build.ts
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);
}

开发服务器集成

¥Development server integration

默认情况下,当 Storybook 以开发模式启动时,它依赖于其内部开发服务器。构建器需要能够与其集成。例如:

¥By default, when Storybook starts in development mode, it relies on its internal development server. The builder needs to be able to integrate with it. For example:

server.ts
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,
      },
    },
  });
}

关闭开发服务器

¥Shutdown the development server

构建器必须提供一种在进程终止后停止开发服务器的方法;这可以通过用户输入或错误来实现。例如:

¥The builder must provide a way to stop the development server once the process terminates; this can be via user input or error. For example:

index.ts
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 支持

¥HMR support

在开发模式下运行时,构建器的开发服务器必须能够在故事、组件或辅助函数发生变化时重新加载页面。

¥While running in development mode, the builder's development server must be able to reload the page once a change happens, either in a story, component, or helper function.

更多信息

¥More information

该字段正在快速发展,相关文档仍在进行中,可能会发生变化。如果你有兴趣创建构建器,你可以通过查看 ViteWebpack 或 Modern Web 的 dev-server-storybook 的源代码来了解有关在 Storybook 中实现构建器的更多信息。当你准备好后,打开 RFC 与 Storybook 社区和维护者讨论你的提案。

¥This area is under rapid development, and the associated documentation is still in progress and subject to change. If you are interested in creating a builder, you can learn more about implementing a builder in Storybook by checking the source code for Vite, Webpack, or Modern Web's dev-server-storybook. When you're ready, open an RFC to discuss your proposal with the Storybook community and maintainers.

了解更多有关构建器的信息

¥Learn more about builders