如何撰寫 stories
觀看影片教學
一個 story 捕捉了 UI 元件的渲染狀態。它是一個帶有註釋的物件,描述了在給定一組引數下,元件的行為和外觀。
Storybook 在談論 React 的 props
、Vue 的 props
、Angular 的 @Input
和其他類似概念時,使用通用術語引數(簡稱 args)。
stories 應放置的位置
元件的 stories 定義在 story 檔案中,該檔案與元件檔案位於同一位置。story 檔案僅供開發使用,不會包含在您的生產套件中。在您的檔案系統中,它看起來像這樣
components/
├─ Button/
│ ├─ Button.js | ts | jsx | tsx | vue | svelte
│ ├─ Button.stories.js | ts | jsx | tsx | svelte
元件 Story 格式
我們根據 元件 Story 格式 (CSF) 定義 stories,這是一種基於 ES6 模組的標準,易於編寫且可在工具之間移植。
關鍵要素是描述元件的預設匯出,以及描述 stories 的具名匯出。
預設匯出
預設匯出元數據控制 Storybook 如何列出您的 stories,並提供附加元件使用的資訊。例如,以下是 story 檔案 Button.stories.js|ts
的預設匯出
import type { Meta } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
從 Storybook 版本 7.0 開始,story 標題會在建置過程中靜態分析。預設匯出必須包含可靜態讀取的 title
屬性,或是可以從中計算自動標題的 component
屬性。使用 id
屬性自訂您的 story URL 也必須是可靜態讀取的。
定義 stories
使用 CSF 檔案的具名匯出,來定義元件的 stories。我們建議您為 story 匯出使用 UpperCamelCase 命名方式。以下是如何在 “primary” 狀態下渲染 Button
,並匯出名為 Primary
的 story。
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,
label: 'Button',
},
};
使用 React Hooks
React Hooks 是方便的輔助方法,可以使用更簡化的方法建立元件。如果您需要,可以在建立元件的 stories 時使用它們,但您應該將其視為進階使用案例。我們在撰寫自己的 stories 時,強烈建議盡可能使用args。例如,以下是一個使用 React Hooks 變更按鈕狀態的 story
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>;
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.dev.org.tw/docs/api/csf
* to learn how to use render functions.
*/
export const Primary: Story = {
render: () => <Button primary label="Button" />,
};
重新命名 stories
您可以根據需要重新命名任何特定的 story。例如,為了給它一個更精確的名稱。以下是如何變更 Primary
story 的名稱
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 = {
// 👇 Rename this story
name: 'I am the primary',
args: {
label: 'Button',
primary: true,
},
};
您的 story 現在將以給定的文字顯示在側邊欄中。
如何撰寫 stories
一個 story 是一個物件,描述如何渲染元件。每個元件可以有多個 stories,而這些 stories 可以彼此建立關聯。例如,我們可以根據上面 Primary story 新增 Secondary 和 Tertiary stories。
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: {
backgroundColor: '#ff0',
label: 'Button',
},
};
export const Secondary: Story = {
args: {
...Primary.args,
label: '😄👍😍💯',
},
};
export const Tertiary: Story = {
args: {
...Primary.args,
label: '📚📕📈🤓',
},
};
更重要的是,您可以匯入 args
以在為其他元件撰寫 stories 時重複使用,當您建構複合元件時,這會很有幫助。例如,如果我們建立一個 ButtonGroup
story,我們可能會混用其子元件 Button
的兩個 stories。
import type { Meta, StoryObj } from '@storybook/react';
import { ButtonGroup } from '../ButtonGroup';
//👇 Imports the Button stories
import * as ButtonStories from './Button.stories';
const meta: Meta<typeof ButtonGroup> = {
component: ButtonGroup,
};
export default meta;
type Story = StoryObj<typeof ButtonGroup>;
export const Pair: Story = {
args: {
buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
orientation: 'horizontal',
},
};
當 Button 的簽名變更時,您只需要變更 Button 的 stories 以反映新的架構,ButtonGroup 的 stories 將會自動更新。這種模式允許您在元件階層中重複使用資料定義,使您的 stories 更易於維護。
還不止這些!story 函式中的每個 args 都可以使用 Storybook 的 Controls 面板進行即時編輯。這表示您的團隊可以在 Storybook 中動態變更元件,以進行壓力測試並找出邊緣案例。
您也可以在使用 Controls 面板調整其控制項值後,編輯或儲存新的 story。
附加元件可以增強 args。例如,Actions 會自動偵測哪些 args 是回呼,並在其中附加記錄函式。這樣,互動(例如點擊)就會記錄在 actions 面板中。
使用 Play 函式
Storybook 的 play
函式和 @storybook/addon-interactions
是方便的輔助方法,可用於測試原本需要使用者介入的元件情境。它們是小段程式碼,會在您的 story 渲染後執行一次。例如,假設您想要驗證表單元件,您可以使用 play
函式編寫以下 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();
},
};
如果沒有 play
函式和 @storybook/addon-interactions
的幫助,您必須編寫自己的 stories 並手動與元件互動,以測試每種可能的用例情境。
使用 Parameters(參數)
Parameters(參數)是 Storybook 定義 stories 靜態元數據的方法。story 的參數可用於在 story 或 story 群組層級為各種附加元件提供設定。
例如,假設您想要針對與應用程式中其他元件不同的一組背景測試 Button 元件。您可以新增元件層級的 backgrounds
參數
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { Meta } from '@storybook/your-framework';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
//👇 Creates specific parameters at the component level
parameters: {
backgrounds: {
default: 'dark',
},
},
};
export default meta;
每當選取 Button story 時,此參數都會指示 backgrounds 附加元件重新設定自身。大多數附加元件都透過基於參數的 API 進行設定,並且可以在全域、元件和story層級受到影響。
使用 Decorators(裝飾器)
Decorators(裝飾器)是一種機制,可以在渲染 story 時將元件包裝在任意標記中。元件的建立通常基於對「渲染位置」的假設。您的樣式可能需要主題或版面配置包裝器,或者您的 UI 可能需要特定的上下文或資料提供者。
一個簡單的範例是在元件的 stories 中新增 padding。使用裝飾器來完成此操作,該裝飾器將 stories 包裝在具有 padding 的 div
中,如下所示
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</div>
),
],
};
export default meta;
Decorators(裝飾器)可能更複雜,並且通常由附加元件提供。您也可以在story、元件和全域層級設定裝飾器。
兩個或多個元件的 Stories
有時您可能會有兩個或多個元件一起建立來協同運作。例如,如果您有一個父元件 List
,它可能需要子元件 ListItem
。
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
const meta: Meta<typeof List> = {
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
// Always an empty list, not super interesting
export const Empty: Story = {};
在這種情況下,為每個 story 渲染不同的函式是有意義的
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
const meta: Meta<typeof List> = {
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
export const Empty: Story = {};
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.dev.org.tw/docs/api/csf
* to learn how to use render functions.
*/
export const OneItem: Story = {
render: (args) => (
<List {...args}>
<ListItem />
</List>
),
};
export const ManyItems: Story = {
render: (args) => (
<List {...args}>
<ListItem />
<ListItem />
<ListItem />
</List>
),
};
您也可以在 List
元件中重複使用來自子元件 ListItem
的story 資料。這樣更容易維護,因為您不必在多個位置更新它。
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
//👇 We're importing the necessary stories from ListItem
import { Selected, Unselected } from './ListItem.stories';
const meta: Meta<typeof List> = {
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
export const ManyItems: Story = {
render: (args) => (
<List {...args}>
<ListItem {...Selected.args} />
<ListItem {...Unselected.args} />
<ListItem {...Unselected.args} />
</List>
),
};
請注意,以這種方式編寫 stories 存在缺點,因為您無法充分利用 args 機制,也無法在建構更複雜的複合元件時組合 args。如需更多討論,請參閱多元件 stories 工作流程文件。