文档
Storybook 文档

构建器 API

Storybook 的架构是为了支持多个构建器,包括 WebpackViteESBuild。构建器 API 是您用来将新的构建器添加到 Storybook 的一组接口。

Storybook builders

构建器是如何工作的?

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

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

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-essentials'],
  core: {
    builder: '@storybook/builder-vite', // 👈 The builder enabled here.
  },
};

构建器 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.htmliframe.html 来查看其 Storybook 所需要的一切,而无需运行其他进程。

实现

在幕后,构建器负责提供/构建预览 iframe,它有一组自己的需求。为了完全支持 Storybook,包括与 Storybook 捆绑在一起的 基本附加组件,它必须考虑以下几点。

导入故事

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

提供配置选项

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

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 导出

preview.js 配置文件允许用户控制故事在 UI 中的渲染方式。这是通过 装饰器 命名导出提供的。当 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/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 的特殊扩展。例如

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

生成源代码片段

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

生成静态构建

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

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);
}

开发服务器集成

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

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

关闭开发服务器

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

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

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

更多信息

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

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