元件故事格式 (CSF)
觀看影片教學
元件故事格式 (CSF) 是建議的 撰寫故事方式。它是一個基於 ES6 模組的 開放標準,可在 Storybook 以外使用。
如果您有使用較舊的 storiesOf()
語法撰寫的故事,則已在 Storybook 8.0 中移除且不再維護。我們建議您將故事移轉至 CSF。如需更多資訊,請參閱移轉指南。
在 CSF 中,故事和元件中繼資料定義為 ES 模組。每個元件故事檔案都包含必要的預設匯出和一個或多個具名匯出。
預設匯出
預設匯出定義元件的中繼資料,包括 component
本身、其 title
(它將顯示在導覽 UI 故事階層中)、裝飾器和參數。
component
欄位是必要欄位,外掛程式會使用它來自動產生屬性表格並顯示其他元件中繼資料。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;
如需更多範例,請參閱撰寫故事。
具名故事匯出
使用 CSF 時,檔案中的每個具名匯出預設都會代表一個故事物件。
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 |
我們建議所有匯出名稱都以大寫字母開頭。
故事物件可以使用一些不同的欄位來註解,以定義故事層級的裝飾器和參數,也可以定義故事的 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 = {
decorators: [],
name: 'So simple!',
parameters: {},
};
Args 故事輸入
從 SB 6.0 開始,故事接受名為 Args 的具名輸入。Args 是由 Storybook 及其外掛程式提供 (且可能更新) 的動態資料。
請考慮 Storybook 的「按鈕」範例,它是一個記錄點擊事件的文字按鈕
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 對應版本更簡短且更容易撰寫,而且它們也更具可攜性,因為程式碼不特定依賴動作外掛程式。
Play 函式
Storybook 的 play
函式是在 UI 中轉譯故事時執行的小段程式碼。它們是方便的輔助方法,可協助您測試原本不可能或需要使用者介入的使用案例。
play
函式的良好使用案例是表單元件。在先前的 Storybook 版本中,您會撰寫您的故事集,並且必須與元件互動以驗證它。使用 Storybook 的 play 函式,您可以撰寫下列故事
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
函式是實用的方法,讓您可以額外控制故事的渲染方式。例如,如果您要將故事撰寫為物件,並且想要指定元件的渲染方式,您可以撰寫如下程式碼
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 的 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);
});
});
當您想要變更故事的名稱時,請重新命名 CSF 匯出。它將變更故事的名稱,也會變更故事的 ID 和 URL。
在以下情況下,最好使用 name
設定元素
- 您希望名稱以命名匯出無法實現的方式顯示在 Storybook UI 中,例如,保留關鍵字(如「default」)、特殊字元(如 emoji)、間距/大小寫(與
storyNameFromExport
提供的不同)。 - 您希望在不變更顯示方式的情況下,保留故事 ID。擁有穩定的故事 ID 對於與第三方工具整合很有幫助。
非故事匯出
在某些情況下,您可能想要匯出故事和非故事的混合(例如,模擬資料)。
您可以在預設匯出中使用選用的設定欄位 includeStories
和 excludeStories
,使其成為可能。您可以將它們定義為字串或正規表示式的陣列。
請考慮以下故事檔案
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
視為故事,並忽略 data
具名匯出。
對於這個特定的範例,您可以根據方便性,透過不同的方式達成相同的結果
includeStories: /^[A-Z]/
includeStories: /.*Story$/
includeStories: ['SimpleStory', 'ComplexStory']
excludeStories: /^[a-z]/
excludeStories: /.*Data$/
excludeStories: ['simpleData', 'complexData']
如果您遵循以大寫字母開頭的故事匯出(即,使用 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 故事,透過將 { primary: true }
展開到元件中來渲染自己。default.title
中繼資料會說明在導覽階層中的故事放置位置。
以下是 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 的實作
export const PrimaryOnDark = Primary.bind({});
PrimaryOnDark.args = Primary.args;
PrimaryOnDark.parameters = { background: { default: 'dark' } };
Primary.bind({})
會複製故事函式,但不會複製掛在函式上的註釋,因此我們必須新增 PrimaryOnDark.args = Primary.args
來繼承 args。
在 CSF 3 中,我們可以展開 Primary
物件來攜帶其所有註釋
export const PrimaryOnDark: Story = {
...Primary,
parameters: { background: { default: 'dark' } },
};
深入了解具名故事匯出。
預設渲染函式
在 CSF 3 中,您透過 render
函式指定故事的渲染方式。我們可以透過以下步驟將 CSF 2 範例重寫為 CSF 3。
讓我們先從一個簡單的 CSF 2 故事函式開始
// Other imports and story implementation
export const Default: ComponentStory<typeof Button> = (args) => <Button {...args} />;
現在,讓我們將它重寫為 CSF 3 中的故事物件,其中包含一個明確的 render
函式,告訴故事如何渲染自己。如同 CSF 2,這讓我們完全控制如何渲染元件,甚至是元件的集合。
// Other imports and story implementation
export const Default: Story = {
render: (args) => <Button {...args} />,
};
深入了解渲染函式。
但在 CSF 2 中,許多故事函式都相同:取得預設匯出中指定的元件,並將 args 展開到其中。這些故事的有趣之處不在於函式,而在於傳遞到函式中的 args。
CSF 3 為每個渲染器提供預設渲染函式。如果您所做的只是將 args 展開到元件中(這是最常見的情況),則完全不需要指定任何 render
函式
export const Default = {};
如需更多資訊,請參閱關於自訂渲染函式的章節。
自動產生標題
最後,CSF 3 可以自動產生標題。
export default {
title: 'components/Button',
component: Button,
};
export default { component: Button };
您仍然可以像在 CSF 2 中一樣指定標題,但如果您未指定標題,則可以從磁碟上的故事路徑推斷出來。如需更多資訊,請參閱關於設定故事載入的章節。