文件
Storybook 文件

建構器 API

Storybook 的架構旨在支援多個建構器,包括 WebpackViteESBuild。建構器 API 是您可以用来向 Storybook 新增建構器的一組介面。

Storybook builders

建構器如何運作?

在 Storybook 中,建構器負責將您的元件和 stories 編譯成在瀏覽器中執行的 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 呼叫負責初始化開發伺服器,以監控檔案系統的變更(例如,元件和 stories),然後在瀏覽器中執行熱模組重新載入。它還提供一個 bail 函式,以允許透過使用者輸入或錯誤正常結束執行中的程序。

在生產環境中,build API 呼叫負責產生靜態 Storybook 建構,如果未提供其他設定,則預設將其儲存在 storybook-static 目錄中。產生的輸出應包含使用者所需的一切,以透過在瀏覽器中開啟 index.htmliframe.html 來檢視其 Storybook,而無需執行其他程序。

實作

在幕後,建構器負責服務/建構預覽 iframe,它有自己的一組要求。為了完全支援 Storybook,包括 Storybook 隨附的基本外掛,它必須考慮以下事項。

匯入 stories

stories 設定欄位可在 Storybook 中啟用 story 載入。它定義了一個檔案 glob 陣列,其中包含元件 stories 的實體位置。建構器必須能夠載入這些檔案並監控它們的變更,並相應地更新 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 設定檔允許使用者控制 story 在 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 loader 以 MDX 撰寫 stories/文件的功能。建構器 (builder) 也必須知道如何解析 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 會使用與其輸入相關的額外元數據來註解元件和 stories,以自動產生互動式控制項和文件。目前,這是透過 Webpack loaders/plugins 來提供的。建構器必須重新實作此功能以支援這些特性。

產生靜態建置

Storybook 的核心功能之一是能夠產生可發佈到網路託管服務的靜態建置。建構器也必須能夠提供類似的機制。例如

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

在開發模式下執行時,建構器的開發伺服器必須能夠在 story、元件或 helper 函式發生變更時重新載入頁面。

更多資訊

此區域正在快速開發中,相關的文件仍在進行中且可能會變更。如果您有興趣建立建構器,可以透過查看 ViteWebpack 或 Modern Web 的 dev-server-storybook 的原始碼來了解更多關於在 Storybook 中實作建構器的資訊。準備好後,請開啟一份 RFC 來與 Storybook 社群和維護者討論您的提案。

深入了解建構器