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 階層中的位置)、decorators 和 parameters。
component
欄位是必要的,addons 會使用它來自動產生屬性表和顯示其他元件 metadata。title
欄位是選填的,且應為唯一 (即,不在檔案之間重複使用)。
// 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 物件。
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 函數轉換為「起始大寫」格式。例如
識別符 | 轉換 |
---|---|
name | Name |
someName | Some Name |
someNAME | Some NAME |
some_custom_NAME | Some Custom NAME |
someName1234 | Some Name 1 2 3 4 |
我們建議所有導出名稱都以大寫字母開頭。
Story 物件可以使用一些不同的欄位進行註解,以定義 story 層級的decorators 和parameters,並定義 story 的 name
。
Storybook 的 name
設定元素在特定情況下很有用。常見的使用案例是具有特殊字元或 Javascript 保留字詞的名稱。如果未指定,Storybook 預設為具名導出。
// 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」範例,它是一個文字按鈕,會記錄其點擊事件
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 重新編寫
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} />,
};
甚至更簡單
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
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 編寫為物件,並且想要指定元件應如何渲染,則可以編寫以下內容
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 的 storyNameFromExport
和 lodash.startCase
函數處理。
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
設定元素
- 您希望名稱以具名導出無法實現的方式顯示在 Storybook UI 中,例如,保留關鍵字 (如 "default")、特殊字元 (如表情符號)、間距/大小寫 (與
storyNameFromExport
提供的不同)。 - 您想要獨立於變更其顯示方式來保留 Story ID。擁有穩定的 Story ID 對於與第三方工具整合很有幫助。
非 story 導出
在某些情況下,您可能想要導出 stories 和非 stories (例如,模擬資料) 的混合。
您可以使用預設導出中的選填設定欄位 includeStories
和 excludeStories
來實現此目的。您可以將它們定義為字串或正則表達式陣列。
考慮以下 story 檔案
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 中渲染時,它將 ComplexStory
和 SimpleStory
視為 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 中,具名導出始終是實例化元件的函數,並且這些函數可以使用配置選項進行註解。例如
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 等效版本
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 實作
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
物件以繼承其所有註解
export const PrimaryOnDark: Story = {
...Primary,
parameters: { background: { default: 'dark' } },
};
進一步了解具名 story 導出。
預設渲染函數
在 CSF 3 中,您通過 render
函數指定 story 的渲染方式。我們可以通過以下步驟將 CSF 2 範例重寫為 CSF 3。
讓我們從一個簡單的 CSF 2 story 函數開始
// Other imports and story implementation
export const Default: ComponentStory<typeof Button> = (args) => <Button {...args} />;
現在,讓我們在 CSF 3 中將其重寫為 story 物件,並使用顯式的 render
函數來告訴 story 如何渲染自身。與 CSF 2 類似,這讓我們可以完全控制元件甚至元件集合的渲染方式。
// Other imports and story implementation
export const Default: Story = {
render: (args) => <Button {...args} />,
};
進一步了解渲染函數。
但在 CSF 2 中,許多 story 函數是相同的:採用預設導出中指定的元件,並將 args 展開到其中。這些 stories 的有趣之處不是函數,而是傳遞到函數中的 args。
CSF 3 為每個渲染器提供預設渲染函數。如果您要做的只是將 args 展開到您的元件中 (這是最常見的情況),則完全不需要指定任何 render
函數
export const Default = {};
如需更多資訊,請參閱關於自訂渲染函數的章節。
自動產生標題
最後,CSF 3 可以自動產生標題。
export default {
title: 'components/Button',
component: Button,
};
export default { component: Button };
您仍然可以像在 CSF 2 中一樣指定標題,但是如果您不指定標題,則可以從 story 在磁碟上的路徑推斷出來。如需更多資訊,請參閱關於設定 story 載入的章節。