Docs
Storybook Docs

indexers

(⚠️实验性)

¥(⚠️ Experimental)

虽然此功能是实验性的,但必须由 StorybookConfigexperimental_indexers 属性指定。

¥While this feature is experimental, it must be specified by the experimental_indexers property of StorybookConfig.

父级:main.js|ts 配置

¥Parent: main.js|ts configuration

类型:(existingIndexers: Indexer[]) => Promise<Indexer[]>

¥Type: (existingIndexers: Indexer[]) => Promise<Indexer[]>

索引器负责构建 Storybook 的故事索引 - 所有故事的列表及其元数据的子集,如 idtitletags 等。可以在 Storybook 的 /index.json 路由中读取索引。

¥Indexers are responsible for building Storybook's index of stories—the list of all stories and a subset of their metadata like id, title, tags, and more. The index can be read at the /index.json route of your Storybook.

索引器 API 是一项高级功能,允许你自定义 Storybook 的索引器,它决定了 Storybook 如何索引文件并将其解析为故事条目。这为你编写故事的方式增加了更多灵活性,包括故事以哪种语言定义或从哪里获取故事。

¥The indexers API is an advanced feature that allows you to customize Storybook's indexers, which dictate how Storybook indexes and parses files into story entries. This adds more flexibility to how you can write stories, including which language stories are defined in or where to get stories from.

它们被定义为返回索引器完整列表(包括现有索引器)的函数。这允许你将自己的索引器添加到列表中,或替换现有的索引器:

¥They are defined as a function that returns the full list of indexers, including the existing ones. This allows you to add your own indexer to the list, or to replace an existing one:

.storybook/main.ts
// 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: // See API and examples below...
    };
    return [...existingIndexers, customIndexer];
  },
};
 
export default config

除非你的索引器正在做一些相对琐碎的事情(例如 使用不同的命名约定索引故事),否则除了索引文件之外,你可能还需要 将其转换为 CSF,以便 Storybook 可以在浏览器中读取它们。

¥Unless your indexer is doing something relatively trivial (e.g. indexing stories with a different naming convention), in addition to indexing the file, you will likely need to transpile it to CSF so that Storybook can read them in the browser.

Indexer

类型:

¥Type:

{
  test: RegExp;
  createIndex: (fileName: string, options: IndexerOptions) => Promise<IndexInput[]>;
}

指定要索引哪些文件以及如何将它们索引为故事。

¥Specifies which files to index and how to index them as stories.

test

(必需)

¥(Required)

类型:RegExp

¥Type: RegExp

针对 stories 配置中包含的文件名运行的正则表达式应与此索引器要处理的所有文件匹配。

¥A regular expression run against file names included in the stories configuration that should match all files to be handled by this indexer.

createIndex

(必需)

¥(Required)

类型:(fileName: string, options: IndexerOptions) => Promise<IndexInput[]>

¥Type: (fileName: string, options: IndexerOptions) => Promise<IndexInput[]>

接受单个 CSF 文件并返回要索引的条目列表的函数。

¥Function that accepts a single CSF file and returns a list of entries to index.

fileName

类型:string

¥Type: string

用于创建要索引的条目的 CSF 文件的名称。

¥The name of the CSF file used to create entries to index.

IndexerOptions

类型:

¥Type:

{
  makeTitle: (userTitle?: string) => string;
}

用于索引文件的选项。

¥Options for indexing the file.

makeTitle

类型:(userTitle?: string) => string

¥Type: (userTitle?: string) => string

一个函数,它接受用户提供的标题并返回索引条目的格式化标题,该标题用于侧边栏。如果没有提供用户标题,则根据文件名和路径自动生成一个。

¥A function that takes a user-provided title and returns a formatted title for the index entry, which is used in the sidebar. If no user title is provided, one is automatically generated based on the file name and path.

有关示例用法,请参阅 IndexInput.title

¥See IndexInput.title for example usage.

IndexInput

类型:

¥Type:

{
  exportName: string;
  importPath: string;
  type: 'story';
  rawComponentPath?: string;
  metaId?: string;
  name?: string;
  tags?: string[];
  title?: string;
  __id?: string;
}

表示要添加到故事索引的故事的对象。

¥An object representing a story to be added to the stories index.

exportName

(必需)

¥(Required)

类型:string

¥Type: string

对于每个 IndexInput,索引器将添加此导出(来自 importPath 处的文件)作为索引中的条目。

¥For each IndexInput, the indexer will add this export (from the file found at importPath) as an entry in the index.

importPath

(必需)

¥(Required)

类型:string

¥Type: string

要从中导入的文件,例如 CSF 文件。

¥The file to import from, e.g. the CSF file.

被索引的 fileName 可能不是 CSF,你需要 将其转换为 CSF 以便 Storybook 可以在浏览器中读取它。

¥It is likely that the fileName being indexed is not CSF, in which you will need to transpile it to CSF so that Storybook can read it in the browser.

type

(必需)

¥(Required)

类型:'story'

¥Type: 'story'

条目类型。

¥The type of entry.

rawComponentPath

类型:string

¥Type: string

提供 meta.component 的文件的原始路径/包(如果存在)。

¥The raw path/package of the file that provides meta.component, if one exists.

metaId

类型:string

¥Type: string

默认:从 title 自动生成

¥Default: Auto-generated from title

定义条目元数据的自定义 ID。

¥Define the custom id for meta of the entry.

如果指定,CSF 文件中的导出默认值(元)必须具有相应的 id 属性,才能正确匹配。

¥If specified, the export default (meta) in the CSF file must have a corresponding id property, to be correctly matched.

name

类型:string

¥Type: string

默认:从 exportName 自动生成

¥Default: Auto-generated from exportName

条目的名称。

¥The name of the entry.

tags

类型:string[]

¥Type: string[]

用于过滤 Storybook 及其工具中的条目的标签。

¥Tags for filtering entries in Storybook and its tools.

title

类型:string

¥Type: string

默认:从 importPath 的默认导出自动生成

¥Default: Auto-generated from default export of importPath

确定侧边栏中条目的位置。

¥Determines the location of the entry in the sidebar.

大多数情况下,你不应指定标题,这样你的索引器将使用默认命名行为。指定标题时,你必须使用 IndexerOptions 中提供的 makeTitle 函数才能使用此行为。例如,这是一个索引器,它仅将 "自定义" 前缀附加到从文件名派生的标题:

¥Most of the time, you should not specify a title, so that your indexer will use the default naming behavior. When specifying a title, you must use the makeTitle function provided in IndexerOptions to also use this behavior. For example, here's an indexer that merely appends a "Custom" prefix to the title derived from the file name:

.storybook/main.ts
// 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 ...
 
    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

¥Type: string

默认:从 title/metaIdexportName 自动生成

¥Default: Auto-generated from title/metaId and exportName

定义条目故事的自定义 ID。

¥Define the custom id for the story of the entry.

如果指定,CSF 文件中的故事必须具有相应的 __id 属性,才能正确匹配。

¥If specified, the story in the CSF file must have a corresponding __id property, to be correctly matched.

仅在你需要覆盖自动生成的 ID 时才使用此功能。

¥Only use this if you need to override the auto-generated id.

转换为 CSF

¥Transpiling to CSF

IndexInput 中的 importPath 值必须解析为 CSF 文件。但是,大多数自定义索引器只是因为输入不是 CSF 才有必要。因此,你可能需要将输入转换为 CSF,以便 Storybook 可以在浏览器中读取它并渲染你的故事。

¥The value of importPath in an IndexInput must resolve to a CSF file. Most custom indexers, however, are only necessary because the input is not CSF. Therefore, you will likely need to transpile the input to CSF, so that Storybook can read it in the browser and render your stories.

将自定义源格式转换为 CSF 超出了本文档的范围。这种转译通常在构建器级别(Vite 和/或 Webpack)完成,我们建议使用 unplugin 为多个构建器创建插件。

¥Transpiling the custom source format to CSF is beyond the scope of this documentation. This transpilation is often done at the builder level (Vite and/or Webpack), and we recommend using unplugin to create plugins for multiple builders.

一般架构如下所示:

¥The general architecture looks something like this:

Architecture diagram showing how a custom indexer indexes stories from a source file

  1. 使用 stories 配置,Storybook 会查找与索引器的 test 属性匹配的所有文件

    ¥Using the stories configuration, Storybook finds all files that match the test property of your indexer

  2. Storybook 最初引入此功能是为了为故事生成 createIndex 函数

    ¥Storybook passes each matching file to your indexer's createIndex function, which uses the file contents to generate and return a list of index entries (stories) to add to the index

  3. 索引填充了 Storybook UI 中的侧边栏

    ¥The index populates the sidebar in the Storybook UI

Architecture diagram showing how a build plugin transforms a source file into CSF

  1. 在 Storybook UI 中,用户导航到与故事 ID 匹配的 URL,浏览器请求索引条目的 importPath 属性指定的 CSF 文件

    ¥In the Storybook UI, the user navigates to a URL matching the story id and the browser requests the CSF file specified by the importPath property of the index entry

  2. 回到服务器上,你的构建器插件将源文件转换为 CSF,并将其提供给客户端

    ¥Back on the server, your builder plugin transpiles the source file to CSF, and serves it to the client

  3. Storybook UI 读取 CSF 文件,导入 exportName 指定的故事并渲染它

    ¥The Storybook UI reads the CSF file, imports the story specified by exportName, and renders it

让我们看一个例子来了解它是如何工作的。

¥Let's look at an example of how this might work.

首先,这是一个非 CSF 源文件的示例:

¥First, here's an example of a non-CSF source file:

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

构建器插件将:

¥The builder plugin would then:

  1. 接收并阅读源文件

    ¥Receive and read the source file

  2. 导入导出的 generateStories 函数

    ¥Import the exported generateStories function

  3. 运行函数生成故事

    ¥Run the function to generate the stories

  4. 将故事写入 CSF 文件

    ¥Write the stories to a CSF file

然后,Storybook 将对生成的 CSF 文件进行索引。它看起来像这样:

¥That resulting CSF file would then be indexed by Storybook. It would look something like this:

// virtual:Button.variants.js|ts
 
import { Button } from './Button';
 
export default {
  component: Button,
};
 
export const Primary = {
  args: {
    primary: true,
  },
};

示例

¥Examples

自定义索引器的一些示例用法包括:

¥Some example usages of custom indexers include:

Generating stories dynamically from fixture data or API endpoints

此索引器根据 JSON 夹具数据为组件生成故事。它在项目中查找 *.stories.json 文件,将它们添加到索引中,并分别将其内容转换为 CSF。

¥This indexer generates stories for components based on JSON fixture data. It looks for *.stories.json files in the project, adds them to the index and separately converts their content to CSF.

.storybook/main.ts
// 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 文件可能如下所示:

¥An example input JSON file could look like this:

{
  "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 插件完成:

¥A builder plugin will then need to transform the JSON file into a regular CSF file. This transformation could be done with a Vite plugin similar to this:

// 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`)}
      `;
    },
  };
}
Generating stories with an alternative API

你可以使用自定义索引器和构建器插件来创建 API 以定义扩展 CSF 格式的故事。要了解更多信息,请参阅以下 概念验证 以设置自定义索引器以动态生成故事。它包含支持此功能所需的一切,包括索引器、Vite 插件和 Webpack 加载器。

¥You can use a custom indexer and builder plugin to create your API to define stories extending the CSF format. To learn more, see the following proof of concept to set up a custom indexer to generate stories dynamically. It contains everything needed to support such a feature, including the indexer, a Vite plugin, and a Webpack loader.

Defining stories in non-JavaScript language

自定义索引器可用于高级目的:用任何语言定义故事,包括模板语言,并将文件转换为 CSF。要查看实际示例,你可以参考 @storybook/addon-svelte-csf 了解 Svelte 模板语法和 storybook-vue-addon 了解 Vue 模板语法。

¥Custom indexers can be used for an advanced purpose: defining stories in any language, including template languages, and converting the files to CSF. To see examples of this in action, you can refer to @storybook/addon-svelte-csf for Svelte template syntax and storybook-vue-addon for Vue template syntax.