indexers
(⚠️ 實驗性功能)
雖然此功能為實驗性功能,但必須由 StorybookConfig
的 experimental_indexers
屬性指定。
類型:(existingIndexers: Indexer[]) => Promise<Indexer[]>
Indexers 負責建構 Storybook 的 stories 索引—所有 stories 的列表及其元資料的子集,例如 id
、title
、tags
等。 索引可以在 Storybook 的 /index.json
路由中讀取。
indexers API 是一項進階功能,可讓您自訂 Storybook 的 indexers,這些 indexers 指定 Storybook 如何將檔案編入索引並解析為 story 條目。 這為您撰寫 stories 的方式增加了更多彈性,包括定義 stories 的語言或從何處取得 stories。
它們被定義為一個函數,該函數返回完整的 indexers 列表,包括現有的 indexers。 這允許您將自己的 indexer 新增到列表中,或替換現有的 indexer
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
// 👇 Make sure files to index are included in `stories`
'../src/**/*.custom-stories.@(js|jsx|ts|tsx)',
],
experimental_indexers: async (existingIndexers) => {
const customIndexer = {
test: /\.custom-stories\.[tj]sx?$/,
createIndex: async (fileName) => {
// See API and examples below...
},
};
return [...existingIndexers, customIndexer];
},
};
export default config;
除非您的 indexer 正在執行相對簡單的操作(例如使用不同的命名慣例為 stories 編製索引),否則除了為檔案編製索引外,您可能還需要將其轉譯為 CSF,以便 Storybook 可以在瀏覽器中讀取它們。
Indexer
類型
{
test: RegExp;
createIndex: (fileName: string, options: IndexerOptions) => Promise<IndexInput[]>;
}
指定要索引哪些檔案以及如何將它們索引為 stories。
test
(必填)
類型:RegExp
針對包含在 stories
設定中的檔案名稱執行的正則表達式,應符合此 indexer 要處理的所有檔案。
createIndex
(必填)
類型:(fileName: string, options: IndexerOptions) => Promise<IndexInput[]>
接受單個 CSF 檔案並傳回要索引的條目列表的函數。
fileName
類型:string
用於建立索引條目的 CSF 檔案名稱。
IndexerOptions
類型
{
makeTitle: (userTitle?: string) => string;
}
用於索引檔案的選項。
makeTitle
類型:(userTitle?: string) => string
一個函數,它接受使用者提供的標題,並傳回索引條目的格式化標題,該標題用於側邊欄。 如果未提供使用者標題,則會根據檔案名稱和路徑自動產生一個標題。
有關範例用法,請參閱 IndexInput.title
。
IndexInput
類型
{
exportName: string;
importPath: string;
type: 'story';
rawComponentPath?: string;
metaId?: string;
name?: string;
tags?: string[];
title?: string;
__id?: string;
}
代表要新增至 stories 索引的 story 的物件。
exportName
(必填)
類型:string
對於每個 IndexInput
,indexer 都會將此匯出(來自 importPath
中找到的檔案)新增為索引中的條目。
importPath
(必填)
類型:string
要從中導入的檔案,例如 CSF 檔案。
被索引的fileName
很可能不是 CSF,在這種情況下,您需要將其轉譯為 CSF,以便 Storybook 可以在瀏覽器中讀取它。
type
(必填)
類型:'story'
條目的類型。
rawComponentPath
類型:string
提供 meta.component
的檔案的原始路徑/套件(如果存在)。
metaId
類型:string
預設值:從 title
自動產生
定義條目元資料的自訂 ID。
如果指定,CSF 檔案中的 export default (meta) 必須具有對應的 id
屬性,才能正確匹配。
name
類型:string
預設值:從 exportName
自動產生
條目的名稱。
tags
類型:string[]
用於在 Storybook 及其工具中篩選條目的標籤。
title
類型:string
預設值:從 importPath
的預設匯出自動產生
決定條目在側邊欄中的位置。
在大多數情況下,您應該不要指定標題,以便您的 indexer 將使用預設的命名行為。 在指定標題時,您必須使用 IndexerOptions
中提供的 makeTitle
函數,以便也使用此行為。 例如,以下是一個 indexer,它僅將 "Custom" 前綴附加到從檔案名稱派生的標題中
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
import type { Indexer } from '@storybook/types';
const combosIndexer: Indexer = {
test: /\.stories\.[tj]sx?$/,
createIndex: async (fileName, { makeTitle }) => {
// 👇 Grab title from fileName
const title = fileName.match(/\/(.*)\.stories/)[1];
// Read file and generate entries ...
const entries = [];
return entries.map((entry) => ({
type: 'story',
// 👇 Use makeTitle to format the title
title: `${makeTitle(title)} Custom`,
importPath: fileName,
exportName: entry.name,
}));
},
};
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
experimental_indexers: async (existingIndexers) => [...existingIndexers, combosIndexer],
};
export default config;
__id
類型:string
預設值:從 title
/metaId
和 exportName
自動產生
定義條目的 story 的自訂 ID。
如果指定,CSF 檔案中的 story 必須具有對應的 __id
屬性,才能正確匹配。
僅當您需要覆寫自動產生的 ID 時才使用此選項。
轉譯為 CSF
在 IndexInput
中,importPath
的值必須解析為 CSF 檔案。 然而,大多數自訂 indexers 僅在輸入不是 CSF 時才是必要的。 因此,您可能需要將輸入轉譯為 CSF,以便 Storybook 可以在瀏覽器中讀取它並渲染您的 stories。
將自訂來源格式轉譯為 CSF 超出了本文檔的範圍。 這種轉譯通常在建置器層級 (Vite 和/或 Webpack) 完成,我們建議使用 unplugin 為多個建置器建立外掛程式。
一般架構如下所示
- 使用
stories
設定,Storybook 會找到所有符合您的 indexer 的test
屬性的檔案 - Storybook 將每個符合的檔案傳遞給您的 indexer 的
createIndex
函數,該函數使用檔案內容產生並傳回要新增到索引的索引條目(stories)列表 - 索引會填充 Storybook UI 中的側邊欄
- 在 Storybook UI 中,使用者導航到與 story ID 相符的 URL,並且瀏覽器請求索引條目的
importPath
屬性指定的 CSF 檔案 - 回到伺服器上,您的建置器外掛程式將來源檔案轉譯為 CSF,並將其提供給用戶端
- Storybook UI 讀取 CSF 檔案,導入
exportName
指定的 story,並渲染它
讓我們看看一個例子,說明這可能如何運作。
首先,這是一個非 CSF 來源檔案的範例
// Button.variants.js|ts
import { variantsFromComponent, createStoryFromVariant } from '../utils';
import { Button } from './Button';
/**
* Returns raw strings representing stories via component props, eg.
* 'export const PrimaryVariant = {
* args: {
* primary: true
* },
* };'
*/
export const generateStories = () => {
const variants = variantsFromComponent(Button);
return variants.map((variant) => createStoryFromVariant(variant));
};
然後,建置器外掛程式將
- 接收並讀取來源檔案
- 導入匯出的
generateStories
函數 - 執行該函數以產生 stories
- 將 stories 寫入 CSF 檔案
然後,產生的 CSF 檔案將由 Storybook 編製索引。 它看起來會像這樣
// virtual:Button.variants.js|ts
import { Button } from './Button';
export default {
component: Button,
};
export const Primary = {
args: {
primary: true,
},
};
範例
自訂 indexers 的一些範例用法包括
從夾具資料或 API 端點動態產生 stories
此 indexer 根據 JSON 夾具資料為組件產生 stories。 它在專案中尋找 *.stories.json
檔案,將它們新增到索引,並將其內容單獨轉換為 CSF。
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
import type { Indexer } from '@storybook/types';
import fs from 'fs/promises';
const jsonStoriesIndexer: Indexer = {
test: /stories\.json$/,
createIndex: async (fileName) => {
const content = JSON.parse(fs.readFileSync(fileName));
const stories = generateStoryIndexesFromJson(content);
return stories.map((story) => ({
type: 'story',
importPath: `virtual:jsonstories--${fileName}--${story.componentName}`,
exportName: story.name,
}));
},
};
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
// 👇 Make sure files to index are included in `stories`
'../src/**/*.stories.json',
],
experimental_indexers: async (existingIndexers) => [...existingIndexers, jsonStoriesIndexer],
};
export default config;
輸入 JSON 檔案的範例可能如下所示
{
"Button": {
"componentPath": "./button/Button.jsx",
"stories": {
"Primary": {
"args": {
"primary": true
},
"Secondary": {
"args": {
"primary": false
}
}
}
},
"Dialog": {
"componentPath": "./dialog/Dialog.jsx",
"stories": {
"Closed": {},
"Open": {
"args": {
"isOpen": true
}
},
}
}
}
然後,建置器外掛程式需要將 JSON 檔案轉換為常規 CSF 檔案。 此轉換可以使用類似於此的 Vite 外掛程式來完成
// vite-plugin-storybook-json-stories.ts
import type { PluginOption } from 'vite';
import fs from 'fs/promises';
function JsonStoriesPlugin(): PluginOption {
return {
name: 'vite-plugin-storybook-json-stories',
load(id) {
if (!id.startsWith('virtual:jsonstories')) {
return;
}
const [, fileName, componentName] = id.split('--');
const content = JSON.parse(fs.readFileSync(fileName));
const { componentPath, stories } = getComponentStoriesFromJson(content, componentName);
return `
import ${componentName} from '${componentPath}';
export default { component: ${componentName} };
${stories.map((story) => `export const ${story.name} = ${story.config};\n`)}
`;
},
};
}
使用替代 API 產生 stories
您可以使用自訂 indexer 和建置器外掛程式來建立您的 API,以定義擴展 CSF 格式的 stories。 若要瞭解更多資訊,請參閱以下概念驗證,以設定自訂 indexer 以動態產生 stories。 它包含支援此功能所需的一切,包括 indexer、Vite 外掛程式和 Webpack 載入器。
以非 JavaScript 語言定義 stories
自訂 indexers 可用於進階用途:以任何語言(包括範本語言)定義 stories,並將檔案轉換為 CSF。 若要查看實際範例,您可以參考 @storybook/addon-svelte-csf
以取得 Svelte 範本語法,以及 storybook-vue-addon
以取得 Vue 範本語法。
從 URL 集合新增側邊欄連結
indexer API 足夠靈活,可讓您處理任意內容,只要您的框架工具可以將該內容中的匯出轉換為它可以執行的實際 stories 即可。 這個進階範例示範了如何建立自訂 indexer 來處理 URL 集合、從每個頁面中提取標題和 URL,並將它們渲染為 UI 中的側邊欄連結。 它以 Svelte 實作,可以適用於任何框架。
首先建立 URL 集合檔案(即 src/MyLinks.url.js
),其中 URL 列表列為具名匯出。 indexer 將使用匯出名稱作為 story 標題,並使用該值作為唯一識別碼。
export default {};
export const DesignTokens = 'https://www.designtokens.org/';
export const CobaltUI = 'https://cobalt-ui.pages.dev/';
export const MiseEnMode = 'https://mode.place/';
export const IndexerAPI = 'https://github.com/storybookjs/storybook/discussions/23176';
調整您的 Vite 設定檔,以包含補充 indexer 的自訂外掛程式。 這將允許 Storybook 處理 URL 集合檔案並將其作為 stories 導入。
import * as acorn from 'acorn';
import * as walk from 'acorn-walk';
import { defineConfig, type Plugin } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
function StorybookUrlLinksPlugin(): Plugin {
return {
name: 'storybook-url-links',
async transform(code: string, id: string) {
if (id.endsWith('.url.js')) {
const ast = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
});
const namedExports: string[] = [];
let defaultExport = 'export default {};';
walk.simple(ast, {
// Extracts the named exports, those represent our stories, and for each of them, we'll return a valid Svelte component.
ExportNamedDeclaration(node: acorn.ExportNamedDeclaration) {
if (
node.declaration &&
node.declaration.type === 'VariableDeclaration'
) {
node.declaration.declarations.forEach((declaration) => {
if ('name' in declaration.id) {
namedExports.push(declaration.id.name);
}
});
}
},
// Preserve our default export.
ExportDefaultDeclaration(node: acorn.ExportDefaultDeclaration) {
defaultExport = code.slice(node.start, node.end);
},
});
return {
code: `
import RedirectBack from '../../.storybook/components/RedirectBack.svelte';
${namedExports
.map(
(name) =>
`export const ${name} = () => new RedirectBack();`
)
.join('\n')}
${defaultExport}
`,
map: null,
};
}
},
};
}
export default defineConfig({
plugins: [StorybookUrlLinksPlugin(), svelte()],
})
更新您的 Storybook 設定(即 .storybook/main.js|ts
)以包含自訂 indexer。
import type { StorybookConfig } from '@storybook/svelte-vite';
import type { Indexer } from '@storybook/types';
const urlIndexer: Indexer = {
test: /\.url\.js$/,
createIndex: async (fileName, { makeTitle }) => {
const fileData = await import(fileName);
return Object.entries(fileData)
.filter(([key]) => key != 'default')
.map(([name, url]) => {
return {
type: 'docs',
importPath: fileName,
exportName: name,
title: makeTitle(name)
.replace(/([a-z])([A-Z])/g, '$1 $2')
.trim(),
__id: `url--${name}--${encodeURIComponent(url as string)}`,
tags: ['!autodocs', 'url']
};
});
}
};
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|ts|svelte)', '../src/**/*.url.js'],
framework: {
name: '@storybook/svelte-vite',
options: {},
},
experimental_indexers: async (existingIndexers) => [urlIndexer, ...existingIndexers]
};
export default config;
新增 Storybook UI 設定檔(即 .storybook/manager.js|ts
)以將索引的 URL 渲染為 UI 中的側邊欄連結
import { addons } from '@storybook/manager-api';
import SidebarLabelWrapper from './components/SidebarLabelWrapper.tsx';
addons.setConfig({
sidebar: {
renderLabel: (item) => SidebarLabelWrapper({ item }),
},
});
此範例的程式碼和即時示範可在 StackBlitz 上取得。