文件
Storybook 文件

元件故事格式 (CSF)

觀看影片教學

元件故事格式 (CSF) 是建議的 撰寫故事方式。它是一個基於 ES6 模組的 開放標準,可在 Storybook 以外使用。

如果您有使用較舊的 storiesOf() 語法撰寫的故事,則已在 Storybook 8.0 中移除且不再維護。我們建議您將故事移轉至 CSF。如需更多資訊,請參閱移轉指南

在 CSF 中,故事和元件中繼資料定義為 ES 模組。每個元件故事檔案都包含必要的預設匯出和一個或多個具名匯出

預設匯出

預設匯出定義元件的中繼資料,包括 component 本身、其 title (它將顯示在導覽 UI 故事階層中)、裝飾器參數

component 欄位是必要欄位,外掛程式會使用它來自動產生屬性表格並顯示其他元件中繼資料。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;

如需更多範例,請參閱撰寫故事

具名故事匯出

使用 CSF 時,檔案中的每個具名匯出預設都會代表一個故事物件。

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

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

故事物件可以使用一些不同的欄位來註解,以定義故事層級的裝飾器參數,也可以定義故事的 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 = {
  decorators: [],
  name: 'So simple!',
  parameters: {},
};

Args 故事輸入

從 SB 6.0 開始,故事接受名為 Args 的具名輸入。Args 是由 Storybook 及其外掛程式提供 (且可能更新) 的動態資料。

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

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 對應版本更簡短且更容易撰寫,而且它們也更具可攜性,因為程式碼不特定依賴動作外掛程式。

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

Play 函式

Storybook 的 play 函式是在 UI 中轉譯故事時執行的小段程式碼。它們是方便的輔助方法,可協助您測試原本不可能或需要使用者介入的使用案例。

play 函式的良好使用案例是表單元件。在先前的 Storybook 版本中,您會撰寫您的故事集,並且必須與元件互動以驗證它。使用 Storybook 的 play 函式,您可以撰寫下列故事

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();
  },
};

當故事在 UI 中轉譯時,Storybook 會執行 play 函式中定義的每個步驟,並執行判斷提示,而無需使用者互動。

自訂渲染函式

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

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 載入此故事時,它會偵測到 render 函式的存在,並根據定義調整元件的渲染方式。

Storybook 匯出 vs. 名稱處理

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

Storybook 將始終使用具名匯出,來確定故事 ID 和 URL。

如果您指定 name 選項,它將用作 UI 中顯示的故事名稱。否則,它會預設為具名匯出,並透過 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);
  });
});

當您想要變更故事的名稱時,請重新命名 CSF 匯出。它將變更故事的名稱,也會變更故事的 ID 和 URL。

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

  1. 您希望名稱以命名匯出無法實現的方式顯示在 Storybook UI 中,例如,保留關鍵字(如「default」)、特殊字元(如 emoji)、間距/大小寫(與 storyNameFromExport 提供的不同)。
  2. 您希望在不變更顯示方式的情況下,保留故事 ID。擁有穩定的故事 ID 對於與第三方工具整合很有幫助。

非故事匯出

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

您可以在預設匯出中使用選用的設定欄位 includeStoriesexcludeStories,使其成為可能。您可以將它們定義為字串或正規表示式的陣列。

請考慮以下故事檔案

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 視為故事,並忽略 data 具名匯出。

對於這個特定的範例,您可以根據方便性,透過不同的方式達成相同的結果

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

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

從 CSF 2 升級到 CSF 3

在 CSF 2 中,具名匯出始終是實例化元件的函式,這些函式可以使用設定選項進行註釋。例如

CSF 2
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 故事,透過將 { primary: true } 展開到元件中來渲染自己。default.title 中繼資料會說明在導覽階層中的故事放置位置。

以下是 CSF 3 的等效程式碼

CSF 3
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 } };

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

可展開的故事物件

在 CSF 3 中,具名匯出是物件,而不是函式。這讓我們可以透過 JS 展開運算子更有效率地重複使用故事。

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

以下是 CSF 2 的實作

CSF 2
export const PrimaryOnDark = Primary.bind({});
PrimaryOnDark.args = Primary.args;
PrimaryOnDark.parameters = { background: { default: 'dark' } };

Primary.bind({}) 會複製故事函式,但不會複製掛在函式上的註釋,因此我們必須新增 PrimaryOnDark.args = Primary.args 來繼承 args。

在 CSF 3 中,我們可以展開 Primary 物件來攜帶其所有註釋

CSF 3
export const PrimaryOnDark: Story = {
  ...Primary,
  parameters: { background: { default: 'dark' } },
};

深入了解具名故事匯出

預設渲染函式

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

讓我們先從一個簡單的 CSF 2 故事函式開始

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

現在,讓我們將它重寫為 CSF 3 中的故事物件,其中包含一個明確的 render 函式,告訴故事如何渲染自己。如同 CSF 2,這讓我們完全控制如何渲染元件,甚至是元件的集合。

CSF 3 - 明確的渲染函式
// Other imports and story implementation
export const Default: Story = {
  render: (args) => <Button {...args} />,
};

深入了解渲染函式

但在 CSF 2 中,許多故事函式都相同:取得預設匯出中指定的元件,並將 args 展開到其中。這些故事的有趣之處不在於函式,而在於傳遞到函式中的 args。

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

CSF 3 - 預設渲染函式
export const Default = {};

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

自動產生標題

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

CSF 2
export default {
  title: 'components/Button',
  component: Button,
};
CSF 3
export default { component: Button };

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