文件
Storybook 文件

Component Story Format (CSF)

觀看影片教學

元件 Story Format (CSF) 是編寫 stories的建議方式。它是一個基於 ES6 模組的開放標準,可跨 Storybook 平台使用。

如果您有使用較舊的 storiesOf() 語法編寫的 stories,它已在 Storybook 8.0 中移除且不再維護。我們建議將您的 stories 遷移至 CSF。請參閱遷移指南以取得更多資訊。

在 CSF 中,stories 和元件 metadata 定義為 ES 模組。每個元件 story 檔案都包含一個必要的預設導出和一個或多個具名導出

預設導出

預設導出定義了關於您的元件的 metadata,包括 component 本身、其 title (它將顯示在導航 UI story 階層中的位置)、decoratorsparameters

component 欄位是必要的,addons 會使用它來自動產生屬性表和顯示其他元件 metadata。title 欄位是選填的,且應為唯一 (即,不在檔案之間重複使用)。

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta } from '@storybook/your-framework';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  /* 👇 The title prop is optional.
   * See https://storybook.dev.org.tw/docs/configure/#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'Path/To/MyComponent',
  component: MyComponent,
  decorators: [/* ... */],
  parameters: {/* ... */},
};
 
export default meta;

如需更多範例,請參閱編寫 stories

具名 story 導出

使用 CSF,檔案中的每個具名導出預設都代表一個 story 物件。

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const Basic: Story = {};
 
export const WithProp: Story = {
  render: () => <MyComponent prop="value" />,
};

導出的識別符將使用 Lodash 的 startCase 函數轉換為「起始大寫」格式。例如

識別符轉換
nameName
someNameSome Name
someNAMESome NAME
some_custom_NAMESome Custom NAME
someName1234Some Name 1 2 3 4

我們建議所有導出名稱都以大寫字母開頭。

Story 物件可以使用一些不同的欄位進行註解,以定義 story 層級的decoratorsparameters,並定義 story 的 name

Storybook 的 name 設定元素在特定情況下很有用。常見的使用案例是具有特殊字元或 Javascript 保留字詞的名稱。如果未指定,Storybook 預設為具名導出。

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const Simple: Story = {
  name: 'So simple!',
  // ...
};

Args story 輸入

從 SB 6.0 開始,stories 接受名為 Args 的具名輸入。Args 是動態資料,由 Storybook 及其 addons 提供 (並可能更新)。

考慮 Storybook 的「Button」範例,它是一個文字按鈕,會記錄其點擊事件

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { action } from '@storybook/addon-actions';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Basic: Story = {
  render: () => <Button label="Hello" onClick={action('clicked')} />,
};

現在考慮相同的範例,使用 args 重新編寫

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { action } from '@storybook/addon-actions';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Text = {
  args: {
    label: 'Hello',
    onClick: action('clicked'),
  },
  render: ({ label, onClick }) => <Button label={label} onClick={onClick} />,
};

甚至更簡單

Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = {
  component: Button,
};
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Text: Story = {
  args: {},
};

這些版本不僅比非 args 版本更簡短且更易於編寫,而且也更具可攜性,因為程式碼不特定依賴 actions addon。

如需設定文件Actions的更多資訊,請參閱其各自的文件。

Play function

Storybook 的 play functions 是在 story 於 UI 中渲染時執行的小程式碼片段。它們是方便的輔助方法,可協助您測試原本不可能或需要使用者介入的使用案例。

表單元件是 play function 的一個很好的使用案例。在先前的 Storybook 版本中,您會編寫您的 stories 集合,並且必須與元件互動以驗證它。使用 Storybook 的 play functions,您可以編寫以下 story

LoginForm.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { userEvent, within, expect } from '@storybook/test';
 
import { LoginForm } from './LoginForm';
 
const meta: Meta<typeof LoginForm> = {
  component: LoginForm,
};
 
export default meta;
type Story = StoryObj<typeof LoginForm>;
 
export const EmptyForm: Story = {};
 
/*
 * See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const FilledForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // 👇 Simulate interactions with the component
    await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
 
    await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
 
    // See https://storybook.dev.org.tw/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await userEvent.click(canvas.getByRole('button'));
 
    // 👇 Assert DOM structure
    await expect(
      canvas.getByText(
        'Everything is perfect. Your account is ready and we should probably get you started!',
      ),
    ).toBeInTheDocument();
  },
};

當 story 在 UI 中渲染時,Storybook 會執行在 play function 中定義的每個步驟,並在不需要使用者互動的情況下執行斷言。

自訂渲染函數

從 Storybook 6.4 開始,您可以將您的 stories 編寫為 JavaScript 物件,從而減少產生程式碼來測試元件所需的樣板程式碼,進而提高功能性和可用性。Render 函數是有用的方法,可讓您額外控制 story 的渲染方式。例如,如果您要將 story 編寫為物件,並且想要指定元件應如何渲染,則可以編寫以下內容

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Layout } from './Layout';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
// This story uses a render function to fully control how the component renders.
export const Example: Story = {
  render: () => (
    <Layout>
      <header>
        <h1>Example</h1>
      </header>
      <article>
        <MyComponent />
      </article>
    </Layout>
  ),
};

當 Storybook 載入此 story 時,它將偵測到 render 函數的存在,並根據定義調整元件渲染方式。

Storybook 導出與名稱處理

Storybook 處理具名導出和 name 選項的方式略有不同。何時應該使用其中一種?

Storybook 將始終使用具名導出來確定 story ID 和 URL。

如果您指定 name 選項,它將在 UI 中用作 story 顯示名稱。否則,它預設為具名導出,並通過 Storybook 的 storyNameFromExportlodash.startCase 函數處理。

MyComponent-test.js
it('should format CSF exports with sensible defaults', () => {
  const testCases = {
    name: 'Name',
    someName: 'Some Name',
    someNAME: 'Some NAME',
    some_custom_NAME: 'Some Custom NAME',
    someName1234: 'Some Name 1234',
    someName1_2_3_4: 'Some Name 1 2 3 4',
  };
  Object.entries(testCases).forEach(([key, val]) => {
    expect(storyNameFromExport(key)).toBe(val);
  });
});

當您想要變更 story 的名稱時,請重新命名 CSF 導出。它將變更 story 的名稱,並變更 story 的 ID 和 URL。

在以下情況下,最好使用 name 設定元素

  1. 您希望名稱以具名導出無法實現的方式顯示在 Storybook UI 中,例如,保留關鍵字 (如 "default")、特殊字元 (如表情符號)、間距/大小寫 (與 storyNameFromExport 提供的不同)。
  2. 您想要獨立於變更其顯示方式來保留 Story ID。擁有穩定的 Story ID 對於與第三方工具整合很有幫助。

非 story 導出

在某些情況下,您可能想要導出 stories 和非 stories (例如,模擬資料) 的混合。

您可以使用預設導出中的選填設定欄位 includeStoriesexcludeStories 來實現此目的。您可以將它們定義為字串或正則表達式陣列。

考慮以下 story 檔案

MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { MyComponent } from './MyComponent';
 
import someData from './data.json';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
  includeStories: ['SimpleStory', 'ComplexStory'], // 👈 Storybook loads these stories
  excludeStories: /.*Data$/, // 👈 Storybook ignores anything that contains Data
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const simpleData = { foo: 1, bar: 'baz' };
export const complexData = { foo: 1, foobar: { bar: 'baz', baz: someData } };
 
export const SimpleStory: Story = {
  args: {
    data: simpleData,
  },
};
 
export const ComplexStory: Story = {
  args: {
    data: complexData,
  },
};

當此檔案在 Storybook 中渲染時,它將 ComplexStorySimpleStory 視為 stories,並忽略 data 具名導出。

對於這個特定的範例,您可以通過不同的方式實現相同的結果,具體取決於哪種方式方便

  • includeStories: /^[A-Z]/
  • includeStories: /.*Story$/
  • includeStories: ['SimpleStory', 'ComplexStory']
  • excludeStories: /^[a-z]/
  • excludeStories: /.*Data$/
  • excludeStories: ['simpleData', 'complexData']

如果您遵循以大寫字母開始 story 導出的最佳實務 (即,使用 UpperCamelCase),則第一個選項是建議的解決方案。

從 CSF 2 升級到 CSF 3

在 CSF 2 中,具名導出始終是實例化元件的函數,並且這些函數可以使用配置選項進行註解。例如

CSF 2 - Button.stories.ts|tsx
import { ComponentStory, ComponentMeta } from '@storybook/react';
 
import { Button } from './Button';
 
export default {
  title: 'Button',
  component: Button,
} as ComponentMeta<typeof Button>;
 
export const Primary: ComponentStory<typeof Button> = (args) => <Button {...args} />;
Primary.args = { primary: true };

這為 Button 宣告了一個 Primary story,該 story 通過將 { primary: true } 展開到元件中來渲染自身。default.title metadata 說明了將 story 放置在導航階層中的哪個位置。

以下是 CSF 3 等效版本

CSF 3 - Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
 
import { Button } from './Button';
 
const meta: Meta<typeof Button> = { component: Button };
 
export default meta;
type Story = StoryObj<typeof Button>;
 
export const Primary: Story = { args: { primary: true } };

讓我們逐個了解變更,以了解正在發生的情況。

可展開的 story 物件

在 CSF 3 中,具名導出是物件,而不是函數。這讓我們可以使用 JS 展開運算符更有效率地重複使用 stories。

考慮向簡介範例新增以下內容,這會建立一個 PrimaryOnDark story,該 story 會針對深色背景進行渲染

以下是 CSF 2 實作

CSF 2 - Button.stories.js|jsx|ts|tsx
export const PrimaryOnDark = Primary.bind({});
PrimaryOnDark.args = Primary.args;
PrimaryOnDark.parameters = { background: { default: 'dark' } };

Primary.bind({}) 複製 story 函數,但它不會複製掛在函數上的註解,因此我們必須新增 PrimaryOnDark.args = Primary.args 以繼承 args。

在 CSF 3 中,我們可以展開 Primary 物件以繼承其所有註解

CSF 3 - Button.stories.ts|tsx
export const PrimaryOnDark: Story = {
  ...Primary,
  parameters: { background: { default: 'dark' } },
};

進一步了解具名 story 導出

預設渲染函數

在 CSF 3 中,您通過 render 函數指定 story 的渲染方式。我們可以通過以下步驟將 CSF 2 範例重寫為 CSF 3。

讓我們從一個簡單的 CSF 2 story 函數開始

CSF 2 - Button.stories.ts|tsx
// Other imports and story implementation
export const Default: ComponentStory<typeof Button> = (args) => <Button {...args} />;

現在,讓我們在 CSF 3 中將其重寫為 story 物件,並使用顯式的 render 函數來告訴 story 如何渲染自身。與 CSF 2 類似,這讓我們可以完全控制元件甚至元件集合的渲染方式。

CSF 3 - Button.stories.ts|tsx
// Other imports and story implementation
export const Default: Story = {
  render: (args) => <Button {...args} />,
};

進一步了解渲染函數

但在 CSF 2 中,許多 story 函數是相同的:採用預設導出中指定的元件,並將 args 展開到其中。這些 stories 的有趣之處不是函數,而是傳遞到函數中的 args。

CSF 3 為每個渲染器提供預設渲染函數。如果您要做的只是將 args 展開到您的元件中 (這是最常見的情況),則完全不需要指定任何 render 函數

CSF 3 - Button.stories.js|jsx|ts|tsx
export const Default = {};

如需更多資訊,請參閱關於自訂渲染函數的章節。

自動產生標題

最後,CSF 3 可以自動產生標題。

CSF 2 - Button.stories.js|jsx|ts|tsx
export default {
  title: 'components/Button',
  component: Button,
};
CSF 3 - Button.stories.js|jsx|ts|tsx
export default { component: Button };

您仍然可以像在 CSF 2 中一樣指定標題,但是如果您不指定標題,則可以從 story 在磁碟上的路徑推斷出來。如需更多資訊,請參閱關於設定 story 載入的章節。