[UI/UX Note] — StoryBook 透過元件說故事

Introduction & 前言

StoryBook Demo

是否還在透過跟 PM客戶 通靈開發呢?還在想怎麼快速跟同事介紹你的網站怎麼設計嗎?

你早該了解怎麼透過元件說故事的,何不現在就開始呢?


Summary & 摘要

那麼什麼是 StoryBook(以下簡稱SB) 呢?

透過將每個獨立的 UI 元件,編譯成靜態檔案或者直接在本地執行 SB,將元件渲染到網頁上,可直接透過操作了解該元件的 UI/UX,甚至可以提供客製化樣式提供選擇。

透過 SB,可以達到幾點需求:

  1. 簡單配置及快速啟用
  2. 使開發人員及非開發人員(如 PM、顧客…等)快速理解 或 確認元件及 UI/UX 是否符合預期
  3. 可獨立打包成 APP,作為 UI 文檔發布

事前準備

其實 SB 和任何前端套件一樣,透過安裝後,將會在 package.json 上留下紀錄,且會直接附帶啟動指令。

首先預設你的環境已經有下列幾項:

  • npx
  • 一個前端專案

空專案的情況下是無法安裝的

可安裝列表

安裝

首先透過 SB 的指令快速安裝

1
$ npx sb init

安裝範例

安裝結束後會看見 package.json 上面多了一些東西,

scripts 上面會多兩行指令:

  • storybook 為啟動 port 6006 去打開一個網頁可以在頁面上操作元件,類似 yarn serve
  • build-storybook 為編譯出靜態的 SB,類似 yarn build

devDependencies 會多出幾行,這些都是 SB 依賴的套件。

Package.json

到現在我們的專案架構會變成下圖:

專案架構

最外層的 .storybook 裡面有兩隻檔案:

  1. main.js 這是 SB 的路徑及插件配置
  2. preview.js 這是全局組建預覽配置

而內層的 stories 資料夾則是安裝 SB 後預設生成的 Demo 元件。

啟動

接著我們使用指令來啟動 SB

1
$ npm run storybook / yarn storybook

啟動結果

跑一陣子後會發現 SB 已經跑在剛剛 package.jsonscript 指令的 port 上了。

使用

透過操作頁面上的元件,可以快速選擇我們想要的樣式,也可以讓使用者知道我們的元件提供什麼的參數。

更改相關參數

選到頁籤 Docs 也可以讓開發者快速了解該元件怎麼使用及提供了什麼 props 可供傳入

更改相關參數

整個安裝到啟動過程不到三分鐘,接下來才是重頭戲,該怎麼去寫我們的故事呢?


撰寫故事

首先我們我們需要先知道配置有分 全局Story的文件配置

關於配置

在全局配置中我們可以設定一些基本的參數,例如背景顏色,這邊可以設置的參數可以參考官方文件 - **Essential addons**。

全局配置

全局配置是會影響全部 Story 的,如果不想要影響到全部的 Story 就請直接對 .stories.js/.stories.tsx 檔案進行配置。

之後我們大多精力都會花在 Story 配置上, 基本上每個 Storydefault function 會寫入該元件的名稱、組建來源、基本配置,而具名的 function 則是你元件的故事,還有參數樣式設定。

相關設定

層級分類配置

default function 裡面的 titile 設定是可以拿來分類的,名稱前方可以加入斜線『 / 』,例如 “example/button“,這樣會使該元件前面再度加一個大分類,而加入『 | 』則會直接用資料夾去分類,像下圖就是官網的範例,舉其中一個例子 “LIBRARY/Charts | LineGraph“。

分類配置

在了解全局及 Story 配置後,我們可以得知一件事情,整個 SB 都是以故事的方向來講述所有的元件,在 Story 配置中,我們可以有條理得整理我們的分類,接下來讓我們建立一個工程師的好夥伴元件 - TodoList

故事開始

首先我們建立 components 及底下的資料夾跟檔案

架構

打開 /components/TodoItem/index.tsx 並且修改內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';

import './TodoItem.css';

interface TodoItemProps {
/**
* TodoItem 的項目名稱
*/
title: string;
/**
* TodoItem 是否完成了
*/
isDone: boolean;
key: number;
onClickHandler: (key: number) => void;
}

export const TodoItem = ({ title, isDone, id, onClickHandler }: TodoItemProps) => (
<div className={isDone ? "todoItem__done todoItem" : "todoItem"} onClick={() => onClickHandler(id)}>
<input className="input" type="checkbox" id={`checkbox_${id}`} checked={isDone} onChange={() => onClickHandler(id)} />
<label className="label" htmlFor="checkbox">{title}</label>
</div>
);

然後再修改 /components/TodoItem/TodoItem.stories.tsx 的內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { TodoItem } from './index';

export default {
title: 'TodoList/TodoItem',
component: TodoItem,
argTypes: {
test: {
name: "testName",
type: {
require: false,
},
description: "這是測試的 Props"
}
},
} as ComponentMeta<typeof TodoItem>;

const Template: ComponentStory<typeof TodoItem> = (args) => <TodoItem {...args} />;

export const DefaultTodoItem = Template.bind({});
DefaultTodoItem.args = {
title: '測試',
isDone: false,
key: 1
};

export const UndoneTodoItem = Template.bind({});
UndoneTodoItem.args = {
...DefaultTodoItem.args
};

export const DoneTodoItem = Template.bind({});
DoneTodoItem.args = {
...DefaultTodoItem.args,
isDone: true,
};

最後修改 /components/TodoItem/TodoItem.css 的內容

1
2
3
4
5
6
7
8
.todoItem {
padding: 5px;
border: 2px solid #000;
}

.todoItem__done .label{
text-decoration: line-through;
}

接著儲存後下指令啟動 SB

1
$ npm run storybook / yarn storybook 

接下來在瀏覽器上就可以看到(如果 package.json 和上面設定一樣會在 http://localhost:6006/)成果。

項目選單

基本上從 index.tsx 開始我們的寫法和一般我們開發時差不多,但這邊要注意如果使用 TS,在 Props 的註解是會預設出現在 SBDocsDescription

如果是在 stories.tsx/stories.js 裡面的 argTypes 寫的設定,也會出現在 SBDocsDescription。

設定檔教學

請注意:層級順序大小為 stories.tsx/stories.js > index.tsx,如果在 stories.tsx/stories.js 裡面設定和 index.tsx 一模一樣的名稱會把 index.tsxProp 蓋掉。

接著新增 TodoList 資料夾及相關檔案

資料夾架構

打開 /components/TodoList/index.tsx 並且修改內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { useState } from 'react';

import './TodoList.css';

import { TodoItem } from '../TodoItem';

const fakeApi = [
{
title: "測試項目1",
isDone: false,
},
{
title: "測試項目2",
isDone: false,
},
{
title: "測試項目3",
isDone: true,
},
{
title: "測試項目4",
isDone: false,
},
]

export const TodoList = () => {
const [todo, setTodo] = useState(fakeApi);

const onClickHandler = (key: number) => {
setTodo((preVal) => {
preVal[key].isDone = !preVal[key].isDone;
return [...preVal];
})
};

return (
<div>
{
todo.map((item, index) => (
<TodoItem {...item} id={index} key={index} onClickHandler={onClickHandler} />
))
}
</div>
)
};

然後再修改 /components/TodoItem/TodoList.stories.tsx 的內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { TodoList } from './index';
import * as TodoItemStories from '../TodoItem/TodoItem.stories';

export default {
title: 'TodoList/TodoList',
component: TodoList,
} as ComponentMeta<typeof TodoList>;

const Template: ComponentStory<typeof TodoList> = () => <TodoList />;

export const TodoListItem = Template.bind({});

TodoListItem.args = {
...TodoItemStories.DefaultTodoItem.args
};

最後修改 /components/TodoItem/TodoList.css 的內容

1
2
3
.todoList .todoItem:not(:last-child) {
margin-bottom: 5px;
}

接下來一樣在瀏覽器上就可以看到(如果 package.json 和上面設定一樣會在 http://localhost:6006/)成果。

最終成果

這邊要注意一下,在 SB 上如果有使用 useState 之類的 React API,可能會有所延遲或者會有預期外的事情發生,這邊還是建議 SB 上面還是用來切版設計為主,如果有牽扯到資料的更動或者任何行爲預測,還請直接在專案上執行。


Conclusion & 結論

最近在研究前人專案時發現一直就很想研究的 StoryBook,剛好趁這次研究架構時一併學習這個套件。透過這個套件可以有效的減少多次的無效來回溝通,私心強烈建議公司都應該導入這個套件(當然前提是專案時程不趕的情況下),因為這個套件也可以產出靜態檔案,方便直接給其他相關人員查看。

關於 SB 的部署方式之後有機會再寫一篇文章介紹,但網路上應該已經有許多的文章了,希望該篇文章能幫助到你,也能幫助到金魚腦的我之後喚醒相關的記憶。


參考網站