Storybook | 用 addon-controls 打造更好的 storybook 體驗
取代 addon-knobs 的官方插件
前言
在 2020 年以前使用 storybook 想要動態地操作 component 的 props 通常都是使用 @storybook/addon-knobs
這個套件,而在 2020 作為它的替代品 @storybook/addon-controls
出現了。從下載數來看,短短一年之間,controls 的下載次數急起直追,到了寫這篇文章的時間點 2021 年 6 月 controls 的下載次數已經高於 knobs。
之所以 controls 的下載會超越 knobs 的原因有很多個,包括官方直接認列該套件於 essential addons 中,而 essential addons 是在初始化 storybook 時 npx sb init
就會被包含在預設安裝的套件底下。
如果你是第一次知道 essential addons,簡單來說就是 VIP 包的概念,一開始就給你很多工具,讓你打怪升級不用煩惱。你可以不用煩惱要裝哪些 addons,還要使用哪些工具才能提升 storybook 的 UX。
筆者認為 controls 的下載次數漸漸追上 knobs 的主要原因是「controls 寫起來就是比較爽」,用過 knobs 的使用方式有點彆扭,需要設定 decorators
,還需要在一開始 import 許多 props 的封裝函式,像是 text
、 boolean
、 number
,使用起來會需要多些工, component 的 story 從原始碼來看也會更不好讀,多加了很多料的感覺。
Controls 的寫法可以讓我們擺脫使用 knobs 的繁瑣感,讓我們可以更專注在撰寫 story 上,其他工程是在閱讀 story 的原始碼時也可以更加容易暸解 component 要怎麼使用。
接下來就讓我們來一瞥 Storybook Controls 的面容吧!
初始化專案與 storybook
用 CRA 初始化專案
在一開始,我們用 CRA (Create React App) 建立一個 typescript 的專案,當專案使用的 typescript 或是 prop-type 時,對於 controls 有很大的益處,因為 controls 可以抽離出 component 的 props type,為我們自動建立可操作的 props 跟類文件自動化,後面會再提到這件事。
npx create-react-app my-app --template typescript
初始化 storybook
專案建立後,我們再用以下指令初始化 storybook 的環境,這個指令其實挺用心的 😃 為什麼這樣說呢?如果是使用 storybook 的新手,由於它除了會幫我們安裝 storybook 的相關套件之外,還會自動安裝幾款 addons,不用再費盡心思搜尋需要的 addons。此外,它甚至還提供了一些 story 的範例,讓我們可以從中學習該如何用最新的語法撰寫 story。
npx sb init
在初始化 storybook 結束後,就可以輸入以下指令進入 storybook 的環境囉!
yarn storybook
以下是 storybook 初始化完畢後提供的範例 story,包含了三個 component 的 story,光是從這幾個範例就可以學習到很多東西,足以了解該怎麼基本的使用 storybook。
除了 *.stories.tsx
之外,另外還提供了 introduction.mdx
讓我們學習很潮的 MDX 格式,可以把 JSX 跟 Markdown 全部寫在一起,看過 MDX 的人都說酷 😆
安裝 addon controls
如果你是近期加入 storybook 的行列,可能在 package.json
中就可以看到 @storybook/addon-essentials
的身影,這表示你的專案中已經包含 controls 了。如果沒有的話可以考慮安裝 essentials,如同上文他就像是 VIP 包,已經什麼都給你了。當然,如果不想在專案中裝很多套件,也可以考慮單獨安裝 controls:
yarn add -D @storybook/addon-controls
別忘了,在安裝完後要記得在 .storybook/main.js
裡面加上以下內容:
module.exports = {
addons: ['@storybook/addon-controls'],
};
但假設各位讀者都使用了 npx sb init
的指令,而這個指令已經幫我們都設定好 addons 了,所以也不用再自己安裝跟設定 controls,接下來我們就直接開始使用囉!
第一次使用 addon controls
為了使用 controls,其中一個重要的關鍵就是 Story.args
,主要就是透過從外部傳入 props 到 story 內部,controls 會擷取 props 的訊息,為我們建立起可操作的 control panel。
現在大致上可以歸類 Story 成兩種寫法,第一種是像我們平常在使用 component 時的寫法,如下面的 Primary story,如上面提到,因為要建立 controls,所已透過 Primary.args
傳入 props。
如果你遇到的情境是可以大量覆用的 presentation component,也可以考慮使用 Template.bind({})
這種寫法,如下面的 Secondary component,與第一種寫法相同,使用 controls 時也是需要用 Secondary.args
傳入 props。
接下來,你可以打開 storybook,從下方看到 Controls 的分頁,裡面寫的是 <Button />
的 props,每個 props 都有對應的操作方式,可以方便我們把玩這個 story。
看到這裡你有沒有發現,下方圖中打開的是 Primary story,而 Primary.args
只有傳入 primary
跟 label
而已,為什麼其他的 props 都一起出現了,是不是很神奇。
這是因為在 export default
裡面有加上 component
的參數,在 storybook 的 Component Story Format 中有提到這個參數的用途,它可以讓 storybook 獲得 component 的 metadata。雖然 component
是一個可以選擇不填的參數,但平常還是推薦必填這個參數變成團隊的 convention,方便在使用像是 Controls 這類的套件時可以幫助我們少寫很多程式碼。
Storybook 是怎麼拿到 props 的資料的
應該有些人會很好奇 storybook 是怎麼知道在 <Button />
可以傳入哪些 props
的,之所以能做到這件事要歸功於 react-docgen-typescript
這個套件,storybook 就是透過它來拿到 component 的 props 型別的。
你可以在 Button.stories.tsx
中試試看在 Primary 中加入一段程式碼:
然後再打開 console 看看 Button.__docgenInfo.props
究竟會顯示什麼,你會發現這個物件裡面包含的正是 Button 這個 component 的 metadata,而 controls 便是用這樣的方式來自動萃取出 component 的 props 並顯示選項在頁面上的。
可以再做一點點嘗試,我們把 export default
中 component
參數拿掉,看看會發生什麼事情。不賣關子,你會發現 controls 剩下兩個參數,其他的 props 像是 backgroundColor
、size
、onClick
都不見了。
所以為了讓 storybook 可以幫我們萃取出 component 的 props
一定要記得在 export default 中加上 component
為 args 選擇 control type
因為 storybook 很聰明,它可以根據 props 的 type 建立不同的操作元素,像是 radio、text、boolean 選擇器等等。可是有時候我們希望可以自定義操作 props 的方式,像是 Button 顯示「Text」跟「Long long text」的寬度應該會不一樣,這時如果想改用 radio 顯示顯示的內容選項該怎麼做呢?
第一種做法是可以在 export default
修改 argTypes
的參數,達到讓所有的 Button story 可以讀取到共同的設定,如此一來 Primary、Secondary、Large、Small 的 label
都變成了 radio。
但有時候我們不想要讓所有的 story 都用同樣的 argTypes
設定 ,所以有第二種作作法是單獨對一個 story 設定 argTypes
。如下方的範例,設定 Primary story 的 label
是 radio 的選項,而其他的 story 中的 label
都還是要輸入文字。
最後,我們整理一下 argTypes
的結構大致上是這個樣子:
在 control
裡面還有一個 [option: string]
,如果不知道要填什麼,可以參考上方的表格的 Options,舉一個例子,當 type
是 range
時,便可以填入 max
、min
、step
的選項。
讓 controls type 自動匹配 props 命名
如果說每次都要為 story 寫 argType
,如果有幾十個、幾百個,甚至幾千個 story 時,我們不會想要逐一地為它們都加上 argType
,所以我們可以在 preview.js
裡面加上 regular express 自動為 props
匹配 control type。
例如下方的範例,只要匹配到 /background|color$/i
就可以自動轉換操作該 props
的方式為 { type: 'color' }
,而 color type 對應的是 color picker。
你可以看到預設 Button 的 story,裡面的 backgroundColor 就自動被轉換成了 color picker。
有趣的是 npx sb init
後產生的 Button.stories.tsx
裡面有一段 code 如下,它另外加上了 argTypes
,但其實前面在 preview.js
裡面定義的 matchers 就已經讓 backgroundColor
是 color picker 了,所以儘管把 argTypes
刪掉,原本的 control 也不會因此而變動。
在 control panel 中描述 props
接著,我們來看一個讓 control panel 變得更像是文件的功能,你可以在 preview.js
加上以下這段設定,就可以讓 props
多了 description 跟 default 的欄位,可以讓其他工程師更清楚這個 story 或是 component 可以帶入什麼樣的 props
。
加入這個設定以後神奇的事情發生了,Description 裡面自動加入了很多描述,但是剛剛根本沒有在 argTypes
裡面加上任何設定啊!為什麼他會知道 props
的描述文字是什麼?
你可以看到 Button.tsx
裡面的型別定義都加上了註解,嘗試修改註解,control panel 裡面的 props
描述也會跟著一起改變,是不是很讚呢!
為了讓 control 有這個功能,我們勢必得在定義 type 時加上一段註解,從另一個角度想 storybook 其實也是在鼓勵我們平時就要做好管理程式碼的工作,不僅可以增加可讀性,在維護時也可以減少通靈一段程式碼時間 XD
說到這邊,如果想在 story 裡面另外修改 props 的描述怎麼辦,因為有時候一個 props
並不會真正傳入到 component 裡面,而是為了展示 story 而定義的 props
,這時候我們可以這樣做:
如此一來,就可以看到 backgroundColor
這個的 description 被改變了。
最後…
看完了 storybook 的 addon controls 的介紹是不是讓你躍躍欲試,如果原本還在用 knobs 的工程團隊應該會想試試看這個工具吧!
這個工具可以幫助團隊在寫 story 時更加解省時間,還可以增加 story 程式碼的可讀性,甚至搭配最後介紹讓 controls panel 變成有點像文件的功能,額外還有讓整個 codebase 變得越來越好的優點,在等什麼,裝起來用用看吧!