Angular 與 Storybook:元件開發與展示指南

1. Storybook 介紹

1.1 什麼是 Storybook?

Storybook 是一個開源工具,專為 UI 元件的開發和測試而設計。它提供了一個獨立的環境,讓開發者能夠以隔離的方式開發、展示和測試元件,不受主應用程式的影響。

Storybook 的主要特色:

  • 元件隔離:在與應用程式分離的環境中開發元件

  • 互動展示:以互動方式展示元件的各種狀態

  • 自動文檔:自動生成元件文檔,包括 API 說明

  • 豐富的插件生態:支援各種視覺測試、無障礙性檢查等功能

1.2 為什麼使用 Storybook?

使用 Storybook 開發 UI 元件有許多優勢:

  1. 提高開發效率

    • 專注於單一元件開發,不需要啟動整個應用

    • 快速切換元件的不同狀態進行測試

  2. 提升元件品質

    • 確保元件在各種狀態下的行為一致

    • 輕鬆測試邊界情況

  3. 改善團隊協作

    • 為設計師和開發者提供一個共同的參考點

    • 作為可互動的元件文檔,幫助團隊理解元件用法

  4. 建立設計系統

    • 統一管理和展示 UI 元件庫

    • 確保元件在整個應用中的一致性

2. 安裝與設定

2.1 在 Angular 專案中安裝 Storybook

在現有的 Angular 專案中安裝 Storybook:

# 使用 npx 安裝最新版本的 Storybook
npx storybook@latest init

這個命令會自動:

  • 安裝必要的依賴

  • 創建 .storybook 配置資料夾

  • 添加需要的 npm scripts

  • 生成一些範例 stories

2.2 Storybook 資料夾結構

安裝後,專案中會新增以下關鍵檔案和資料夾:

.storybook/
  ├── main.ts           # 主要配置文件
  ├── preview.ts        # 全局預覽配置
  └── tsconfig.json     # TypeScript 配置
src/
  └── stories/          # 示例 stories
      └── Button.stories.ts  # 範例元件的 story
package.json            # 新增 storybook 相關的 scripts

2.3 執行 Storybook

安裝後,可以使用以下命令啟動 Storybook:

# 使用 npm script
npm run storybook

# 或使用 ng 指令 (Angular CLI)
ng run your-project-name:storybook

啟動後,Storybook 通常會在 http://localhost:6006 運行,打開瀏覽器即可看到元件展示界面。

3. Storybook 核心配置文件

3.1 main.ts

.storybook/main.ts 是 Storybook 的主要配置文件,用於定義全局設定:

import type { StorybookConfig } from "@storybook/angular";

const config: StorybookConfig = {
  // 定義 stories 檔案的位置模式
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  
  // 添加 Storybook 插件
  addons: [
    "@storybook/addon-links",        // 元件間連結
    "@storybook/addon-essentials",   // 核心功能插件集
    "@storybook/addon-interactions", // 互動測試
    "@storybook/addon-a11y",         // 無障礙性測試
  ],
  
  // 框架配置
  framework: {
    name: "@storybook/angular",
    options: {},
  },
  
  // 文檔生成配置
  docs: {
    autodocs: "tag", // 自動為帶有特定標籤的 stories 生成文檔
  },
};

export default config;

3.2 preview.ts

.storybook/preview.ts 文件用於設定 stories 的全局預覽行為:

import { applicationConfig, type Preview } from "@storybook/angular";
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
import { provideHttpClient } from "@angular/common/http";

// 設置 Compodoc 生成的文檔
setCompodocJson(docJson);

// 全局預覽配置
const preview: Preview = {
  parameters: {
    // 控制面板配置
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    // 其他全局參數
    backgrounds: {
      default: 'light',
      values: [
        { name: 'light', value: '#f8f8f8' },
        { name: 'dark', value: '#333333' },
      ],
    },
  },
};

// 全局裝飾器 - 提供應用程式配置
export const decorators = [
  applicationConfig({
    providers: [
      provideHttpClient(), // 全局提供 HttpClient
    ],
  }),
];

export default preview;

4. 創建 Stories

4.1 Story 基本結構

Story 是 Storybook 的基本單位,每個 story 代表元件的一種狀態或用例。以下是一個基本的 story 文件結構:

// button.stories.ts
import { Meta, StoryObj } from '@storybook/angular';
import { ButtonComponent } from './button.component';

// Meta 定義了元件的元數據
const meta: Meta<ButtonComponent> = {
  title: 'Components/Button', // 導航中顯示的標題路徑
  component: ButtonComponent, // 關聯的元件
  tags: ['autodocs'],         // 標籤,用於自動生成文檔
  argTypes: {                 // 參數類型定義
    backgroundColor: { control: 'color' },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large'],
    },
  },
  // 所有 stories 的默認參數
  args: {
    label: 'Button',
  },
};

export default meta;

// Story 定義
type Story = StoryObj<ButtonComponent>;

// 主要按鈕 story
export const Primary: Story = {
  args: {
    primary: true,
  },
};

// 次要按鈕 story
export const Secondary: Story = {
  args: {
    primary: false,
  },
};

// 大型按鈕 story
export const Large: Story = {
  args: {
    size: 'large',
  },
};

4.2 args 與 argTypes

args

args 是傳遞給元件的參數,用於控制元件的狀態:

// 元件層級的默認 args
args: {
  label: 'Click me',
  size: 'medium',
},

// Story 層級的 args,會覆蓋元件層級的默認值
export const Small: Story = {
  args: {
    size: 'small',
    label: 'Small button',
  },
};

argTypes

argTypes 定義了參數的元數據,如控制器類型、描述等:

argTypes: {
  // 使用顏色選擇器
  backgroundColor: {
    control: 'color',
    description: '按鈕背景顏色',
    table: {
      category: '樣式',
      defaultValue: { summary: 'transparent' },
    },
  },
  
  // 使用下拉選單
  size: {
    control: { type: 'select' },
    options: ['small', 'medium', 'large'],
    description: '按鈕大小',
    table: {
      category: '尺寸',
      defaultValue: { summary: 'medium' },
    },
  },
  
  // 使用開關
  disabled: {
    control: 'boolean',
    description: '是否禁用',
    defaultValue: false,
    table: {
      category: '狀態',
    },
  },
}

4.3 Actions

Actions 用於監聽和記錄元件觸發的事件:

// 使用 action 函數
import { action } from '@storybook/addon-actions';

const meta: Meta<ButtonComponent> = {
  // ...
  args: {
    onClick: action('button clicked')
  }
};

// 或使用 Storybook 7 提供的 fn 函數
import { fn } from '@storybook/test';

const meta: Meta<ButtonComponent> = {
  // ...
  args: {
    onClick: fn()
  }
};

5. 高級功能

5.1 元件文檔

Storybook 可以自動生成元件文檔,只需添加 tags: ['autodocs'] 到 meta 配置:

const meta: Meta<MyComponent> = {
  title: 'Components/MyComponent',
  component: MyComponent,
  tags: ['autodocs'], // 啟用自動文檔
};

也可以使用 MDX 格式編寫更豐富的文檔:

// MyComponent.mdx
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { MyComponent } from './MyComponent';
import * as Stories from './MyComponent.stories';

<Meta of={Stories} />

# MyComponent

這是一個自定義元件的說明。

<Canvas>
  <Story of={Stories.Default} />
</Canvas>

## 屬性說明

<ArgsTable of={MyComponent} />

## 使用示例

以下是一個使用示例...

5.2 交互測試

使用 play 函數可以自動化測試元件的交互行為:

import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';

export const ClickTest: Story = {
  args: {
    label: 'Click me',
  },
  play: async ({ canvasElement }) => {
    // 獲取 canvas 內的元素
    const canvas = within(canvasElement);
    
    // 找到按鈕元素
    const button = canvas.getByRole('button', { name: /Click me/i });
    
    // 模擬點擊
    await userEvent.click(button);
    
    // 檢查結果
    await expect(button).toHaveClass('clicked');
  },
};

6. 實用技巧

6.1 全局樣式

.storybook/preview.ts 中引入全局樣式:

// 引入應用的全局樣式
import '../src/styles.scss';

const preview: Preview = {
  // ...
};

6.2 自定義裝飾器

使用裝飾器包裝 stories 來添加上下文或樣式:

// 全局裝飾器
export const decorators = [
  (Story) => ({
    template: `
      <div style="margin: 2em;">
        <story />
      </div>
    `,
  }),
];

// 單個 story 裝飾器
export const WithCustomWrapper: Story = {
  decorators: [
    (Story) => ({
      template: `
        <div style="border: 2px dashed red; padding: 1em;">
          <story />
        </div>
      `,
    }),
  ],
};

6.3 動態載入數據

使用 loaders 在 story 渲染前載入數據:

export const WithLoadedData: Story = {
  loaders: [
    async () => ({
      users: await fetch('/api/users').then(r => r.json())
    }),
  ],
  render: (args, { loaded: { users } }) => ({
    props: {
      ...args,
      users,
    },
  }),
};

7. 最佳實踐

7.1 組織 Stories

  • 使用一致的命名和分類結構

  • 按功能或模塊組織 stories

  • 考慮使用子目錄來組織複雜元件

// 使用斜線來建立分層結構
const meta: Meta = {
  title: 'Design System/Atoms/Button',
};

7.2 測試覆蓋

  • 為每個元件的主要用例和邊界情況創建 stories

  • 使用 play 函數測試互動行為

  • 結合視覺回歸測試工具如 Chromatic

7.3 文檔完善

  • 為每個元件添加清晰的描述

  • 使用 argTypes 提供參數的詳細說明

  • 添加使用示例和最佳實踐說明

8. 常見問題與解決方案

8.1 故障排除

問題:Stories 不顯示在側邊欄

  • 檢查 main.ts 中的 stories 路徑是否正確

  • 確認 story 文件名稱符合匹配模式

  • 檢查 story 文件中 title 屬性是否設置正確

問題:元件無法渲染

  • 檢查元件導入路徑是否正確

  • 確保所有依賴都已正確提供

  • 檢查控制台錯誤信息

8.2 性能優化

  • 使用動態導入大型元件

  • 優化元件渲染性能

  • 考慮使用 Storybook 的構建優化選項

9. 結語

Storybook 是開發 Angular 元件的強大工具,它不僅提高了開發效率,還能確保元件質量和一致性。通過隔離環境展示和測試元件,團隊可以更快地迭代、溝通和協作。

掌握 Storybook 的基本概念和高級功能,將幫助你建立更強大、更易維護的元件庫,為你的 Angular 應用提供可靠的 UI 基礎。

Last updated