如何撰寫故事
觀看影片教學
故事會捕捉 UI 元件的渲染狀態。它是一個帶有註釋的物件,描述元件在給定一組引數的情況下的行為和外觀。
在談論 React 的 props
、Vue 的 props
、Angular 的 @Input
和其他類似概念時,Storybook 使用通用術語引數 (args)。
故事的放置位置
元件的故事定義在與元件檔案並存的故事檔案中。故事檔案僅用於開發,不會包含在您的生產套件中。在您的檔案系統中,它看起來像這樣
components/
├─ Button/
│ ├─ Button.js | ts | jsx | tsx | vue | svelte
│ ├─ Button.stories.js | ts | jsx | tsx
元件故事格式
我們根據元件故事格式 (CSF) 來定義故事,這是一個基於 ES6 模組的標準,易於撰寫且可在工具之間移植。
預設匯出
預設匯出中繼資料控制 Storybook 如何列出您的故事,並提供外掛程式使用的資訊。例如,以下是故事檔案 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 開始,故事標題會在建構過程中以靜態方式分析。預設匯出必須包含可以靜態讀取的 title
屬性,或可以從中計算自動標題的 component
屬性。使用 id
屬性來自訂您的故事 URL 也必須是靜態可讀取的。
定義故事
使用 CSF 檔案的具名匯出來定義元件的故事。我們建議您對故事匯出使用 UpperCamelCase。以下說明如何在「主要」狀態下渲染 Button
,並匯出一個名為 Primary
的故事。
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 是方便的輔助方法,可以使用更精簡的方式建立元件。您可以在建立元件的故事時使用它們,如果需要的話,儘管您應該將它們視為進階用法。我們建議在撰寫自己的故事時,盡可能使用 args。舉例來說,這是一個使用 React Hooks 來變更按鈕狀態的故事:
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" />,
};
重新命名故事
您可以重新命名任何特定的故事,例如,給它一個更精確的名稱。以下是如何更改 Primary
故事的名稱:
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,
},
};
您的故事現在將在側邊欄中顯示指定的文字。
如何撰寫故事
故事是一個描述如何呈現元件的物件。每個元件可以有多個故事,而這些故事可以彼此建構。例如,我們可以根據上面的 Primary 故事新增 Secondary 和 Tertiary 故事。
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
來重複使用,這在您建立複合元件時很有幫助。例如,如果我們建立一個 ButtonGroup
故事,我們可能會將其子元件 Button
的兩個故事混合。
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 的故事以反映新的結構描述,而 ButtonGroup 的故事將會自動更新。這種模式可讓您在元件階層中重複使用您的資料定義,使您的故事更易於維護。
不僅如此!故事函數中的每個 args 都可以使用 Storybook 的 Controls 面板進行即時編輯。這表示您的團隊可以在 Storybook 中動態變更元件,以進行壓力測試並找出邊緣案例。
您也可以在使用 Controls 面板調整其控制值之後,編輯或儲存新的故事。
附加元件可以增強 args。例如,Actions 會自動偵測哪些 args 是回呼函數,並將記錄函數附加到它們。這樣,互動(例如點擊)就會記錄在 Actions 面板中。
使用 play 函數
Storybook 的 play
函數和 @storybook/addon-interactions
是方便的輔助方法,可測試元件情境,否則需要使用者介入。它們是小型程式碼片段,會在您的故事呈現後執行一次。例如,假設您想要驗證表單元件,您可以使用 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();
},
};
如果沒有 play
函數和 @storybook/addon-interactions
的協助,您必須撰寫自己的故事並手動與元件互動,才能測試每個可能的使用案例情境。
使用參數
參數是 Storybook 用於定義故事靜態中繼資料的方法。故事的參數可用於為各種附加元件提供組態,無論是在故事或故事群組的層級。
例如,假設您想要針對與應用程式中其他元件不同的背景集測試您的 Button 元件。您可能會新增元件層級的 backgrounds
參數:
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
// 👇 Meta-level parameters
parameters: {
backgrounds: {
default: 'dark',
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Basic: Story = {};
每當選取 Button 故事時,此參數會指示 backgrounds 附加元件重新設定自身。大多數附加元件都是透過基於參數的 API 來設定,並且可以在全域、元件和故事層級進行影響。
使用裝飾器
裝飾器是一種機制,可以在呈現故事時將元件包裝在任意標記中。元件通常是在對「呈現位置」的假設下建立的。您的樣式可能需要主題或版面配置包裝函式,或者您的 UI 可能需要特定的內容或資料提供者。
一個簡單的範例是在元件的故事中新增內邊距。使用裝飾器來完成此操作,該裝飾器會將故事包裝在具有內邊距的 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;
裝飾器可能更複雜,而且通常由附加元件提供。您也可以在故事、元件和全域層級設定裝飾器。
兩個或多個元件的故事
有時您可能會有兩個或多個協同工作的元件。例如,如果您有一個父 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 = {};
在這種情況下,為每個故事呈現不同的函式是有意義的。
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
的故事資料。這樣更容易維護,因為您不必在多個位置更新它。
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>
),
};
請注意,像這樣撰寫故事存在缺點,因為您無法充分利用 args 機制,以及在您建立更複雜的複合元件時組合 args。如需更多討論,請參閱多元件故事工作流程文件。