Builder API
Storybook 的架構設計旨在支援多個 builder,包括 Webpack、Vite 和 ESBuild。Builder API 是您可以使用的介面集合,用於將新的 builder 新增至 Storybook。
Builder 如何運作?
在 Storybook 中,builder 負責將您的元件和 stories 編譯成可在瀏覽器中執行的 JS 套件。Builder 也提供用於互動式開發的開發伺服器,以及用於最佳化套件的生產模式。
若要選擇使用 builder,使用者必須將其新增為依賴項,然後編輯其設定檔 (.storybook/main.js
) 以啟用它。例如,使用 Vite builder
npm install @storybook/builder-vite --save-dev
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.
},
};
Builder API
在 Storybook 中,每個 builder 都必須實作以下 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.html
或 iframe.html
即可檢視其 Storybook。
實作
在底層,builder 負責服務/建置預覽 iframe
,這有其自身的要求。為了完整支援 Storybook,包括與 Storybook 一起提供的必要附加元件,它必須考量以下事項。
匯入 stories
stories
設定欄位啟用 Storybook 中的 story 載入。它定義一個檔案 glob 陣列,其中包含元件 stories 的實體位置。Builder 必須能夠載入這些檔案並監控它們的變更,並相應地更新 UI。
提供設定選項
預設情況下,Storybook 的設定是在專用檔案 (storybook/main.js|ts
) 中處理,讓使用者可以選擇自訂設定以符合其需求。Builder 也應透過額外的欄位或其他適合 builder 的機制來提供其自身的設定支援。例如
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 中的渲染方式。這是透過 decorators 具名匯出來提供的。當 Storybook 啟動時,它會透過虛擬模組條目將這些具名匯出轉換為內部 API 呼叫,例如 addDecorator()
。Builder 也必須提供類似的實作。例如
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 的特殊擴充功能。例如
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 提供的。Builder 必須重新實作此功能以支援這些功能。
產生靜態建置
Storybook 的核心功能之一是能夠產生靜態建置,該建置可以發佈到網路託管服務。Builder 也必須能夠提供類似的機制。例如
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 在開發模式下啟動時,它依賴其內部的開發伺服器。Builder 需要能夠與其整合。例如
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,
},
},
});
}
關閉開發伺服器
Builder 必須提供一種在程序終止時停止開發伺服器的方法;這可以透過使用者輸入或錯誤來實現。例如
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 支援
在開發模式下執行時,builder 的開發伺服器必須能夠在故事、元件或輔助函式發生變更時重新載入頁面。
更多資訊
此區域正在快速開發中,相關文件仍在製作中,並可能隨時變更。如果您有興趣建立 builder,您可以查看 Vite、Webpack 或 Modern Web 的 dev-server-storybook 的原始碼,以深入瞭解如何在 Storybook 中實作 builder。準備就緒後,開啟 RFC 以與 Storybook 社群和維護者討論您的提案。
深入瞭解 builders
- Vite builder,用於與 Vite 捆綁
- Webpack builder,用於與 Webpack 捆綁
- Builder API,用於建置 Storybook builder