哎呀!那些我在做 React 元件庫時,沒注意到的小細節!?

Taiming
19 min readMar 24, 2023

--

刻一套元件庫,到底難不難?

Q: 你覺得刻一套元件庫,到底難不難?為什麼?
Q: 有沒有自己或公司團隊刻過元件庫的經驗?是否能夠分享當時的情境,為什麼公司想要這麼做?希望這個元件庫對團隊帶來什麼好處?

這個問題,從我參加鐵人賽之前,到目前已經出書了,都還一直圍繞著我。
最一開始是我自己問我這個問題,「我選這個題目,到底難不難?能不能完賽?」

可能有些人覺得很簡單,有些人覺得很難。
因為每個人實作的能力不一樣,或是看事情的角度也不一樣。

事實上,「難不難」這個問題,定義得有點模糊,隨著他不同的題目條件,難度也會不同。

在跟一些開發者聊天的時候,不知不覺就會發現,很多開發團隊、公司都有嘗試過想要自己刻一套公司自己的元件庫。所以,刻一套元件庫並不是那麼特別的事情,很多人都有類似的經驗,只是在不同的環境、條件之下,這些經驗會有些不同。舉例:

考量開發時間

給你半年、一年的時間刻一套元件庫,當作你的年度績效,或許不難。
但如果要在「30 天內」,完成一套約有 30 個元件的元件庫呢?難度就會不一樣,時間充裕的話,我們可以:

  • 前期規劃的時候,考慮更多實作、規格的細節
  • 不用考慮時間的急迫性,你要做得多複雜都可以,不需要為了時間而捨棄功能
  • 實作的過程中,有時間可以除錯、優化、重構
  • 可以仔細、慢慢的寫測試,確保每一個細節運作正常、符合規格

但如果你只有 30 天呢?你會想要怎麼取捨這些功能?

當天有聽眾回饋或許會考慮使用一些 headless component,然後自己再基於這些基礎,實現自己想要的樣式或延伸功能

考量開發人數

一個團隊一起合作完成一套元件庫?還是一個人獨自完成一套元件庫?
其實各有其難處

團隊優點:

  • 有許多人可以一起討論,看見彼此疏忽的地方
  • 有機會發揮 1+1 > 2 的效果,開發更快

團隊缺點:

  • 人多嘴雜,難以下決定的時候,或許會拖慢開發時間
  • 需要訂定合作規範、統一 coding style,否則大家寫的 code 都不一樣。
  • 跟你討厭的人/討厭你的人一起開發(處理人的問題,又是另外一件故事了)

個人優點:

  • 自己能夠決定所有一切,自己就是主宰
  • 較能統一風格

個人缺點:

  • 所有事情都要自己來、所有規格、設計、實作都要自己來
  • 只能用自己的角度看事情,沒有人一起討論,自己的盲點或疏失沒有人發現

考量元件庫的目標使用者

過去在跟一些開發者聊天交流的時候,曾經被問過下面這個問題:

每一個 component 開發,你是怎麼去想他的使用者行為?或者,你當初在設計這些 component 的時候,你是怎麼讓他更 general,假設你的 UI Library 要 release 出去的時候,如何符合大部分的人期待的使用場景?

開發這套元件是要給誰用?這確實也是在開發元件時需要好好考慮的一個面向,這套元件庫是要:

  • 使用在自己心愛的 sideproject 上面,當然,那你就是這一切的主宰
  • 公司內部使用,或許不同公司規模也有不同考量,假設只有一兩個團隊,那相對比較單純一點。但如果是有五六個團隊以上,每個團隊又有自己的設計師、工程師,那這樣複雜度又更高了
  • 公開給全世界的開發者使用,可能又要考慮不同國家、各種不同情境,例如適合用在 B2C、B2B 產品?前台產品或後台產品的使用?要隨取隨用?還是要能夠適應各種客製化?考量點也又不一樣了

但我覺得,想辦法做得 general 一點,固然是很有企圖心,不過如果要顧到每一種情境和開發者,一方面難度很高,另一方面會有點失去產品的定位。就像是你要開一家服飾店,你不能設定你的衣服要符合所有年齡層、男女通吃、中式、西式、歐式、美式、各種式都能夠滿足。你要做一個社群平台,你不能讓他同時又是 FB,又是 LinkedIn,又是 IG,又是 Twitter,又是 Youtube,這樣你的產品什麼都不是。

所以,到底要做出什麼樣的元件庫?設定好特定一個族群的需求,也或許是一個聰明的選擇,你的目標很明確,也會讓產品很有特色!

刻一套元件庫,到底難不難?

回到先前提到的這個問題,到底刻一套元件庫難不難?我覺得,如果:

「自己獨自一人」+「沒有設計師幫你設計 Design Guideline」+「時間有限(如 30 天內)」+「需要完成的元件很多(如要完整刻一套元件庫)」+「目標使用者設定得很 general」

那我自己個人覺得,這個任務就會變得非常難!

但反之,若想要做一套品質好一點的元件庫,那麼,我們就應該要盡量避免上述的情況,免得讓自己深陷險境

可以的話,組一個好的團隊,有一群好的工程師夥伴互相良性討論,然後一位可以跟工程師溝通良好的設計師、PM,時間不要壓那麼緊,適量的元件數量,並且設定好使用者對象,那麼,我相信這個元件庫將會是非常棒的!

為何自己當初想不開,想獨自刻一套元件庫?

一、面對曾經面試失利
很久以前,曾經有一個面試官問我說,「你有沒有刻過什麼元件呢?」,身為一個前端工程師,當然是回答「有!」,說了一輪之後,問我有沒有做過某個元件可以廣泛被用在產品當中,例如「Button」這種元件,那我的回答當然也是「有呀!」,但下一個問題是,「那你怎麼去設計一個 Button 呢?」,當下真的因為經驗不足,所以面對這個問題有點傻住了,心想「Button 不就 Button 嗎?要怎麼去設計是什麼意思?」,所以這個面試也就在這樣的尷尬氛圍下結束。這成為我面試經驗當中一個很不堪的回憶,因此很希望能夠透過做點什麼來累積經驗,藉此彌補自己的不足。

二、覺得要湊 IT 鐵人賽 30 天,容易湊到 30 篇
如這個標題所說,理由就是這麼單純,世界上元件那麼多,隨隨便便應該就可以湊到 30 個吧?一天一個,剛好 30 篇,完美!但是,開始擬定大綱的時候才發現,參考各大元件庫之後,擠出二十幾個就已經很不容易了,若真的要湊到 30 個的話,真的會需要去挑戰一些自己沒把握的元件。但所幸最後還是順利完成,可說是有驚無險。

三、覺得這個題目夠硬、夠瘋
這個題目真的很硬,有理論、有分析、有實作。所以我覺得,雖然可能大家也有想過這個題目,但是應該沒有人敢衝。所以選這個題目,要跟人家重複的機會應該是很難,因此也更能顯示出我的作品的獨特性,覺得算是有亮點。很高深的技術、知識,我沒有把握,但是做一些瘋狂的挑戰,對我來說很可以,大不了咬著牙,眼睛一閉,撐一下就過去了。畢竟,痛苦會過去,幸福會來臨。

四、對自己誤會太深
事實上我對這個題目難度的評估也是超出我意料之外,我想,這也算是我經驗不足的一個體現吧!選定這個題目之時,我覺得有點難,但應該沒問題。實際上頭洗下去之後,才發現,天啊,這真的有夠硬!早知道就不選這個題目了!不過既然已經深陷泥沼,也是自己選的,終究要對自己負責,還是硬著頭皮完成挑戰。

在自己刻元件之前,那些不曾想過的瑣事

大家在工作的時候,不知道有沒有這樣的經驗?
有些元件、功能,原本覺得應該很簡單。但是,實際上動手去做的時候,才發現跟自己原本想像的不同,很多細節是過去沒有考慮過的,會出現很多意外。

這些事情真的很多,今天我選三個小瑣事來跟大家分享:

  • Infinite Scroll
  • Pagination
  • 進場/離場動畫

Infinite Scroll

Infinite scroll 能在面對多筆資料時,讓捲軸滑動到底部時再載入下一頁面的資料。

Infinite Scroll

相信大家對這個元件都不陌生,大部分的社群軟體,例如臉書、IG 等等,都是不斷往下滑就能夠看見更多貼文。

But how?

Infinite Scroll 的特點是讓資料滾到底部時自動載入,所以這邊的關鍵是,我們要如何判斷「是否已經滾動到底部」?

Infinite Scroll 的討論在網路上非常多,但假設我們是一個新手,如果以前真的完全沒有實作過這個元件,過去也沒有看過網路上文章的分享,那你會想要怎麼做呢?

最直覺的方法就是,對 scroll 做事件監聽,並且不斷的去計算高度。

「滾動到底部」換句話來說,就是你滾過的距離加上自己元素的高度,大於等於可滾動範圍的高度。

「滾過的距離」+「自己元素的高度」 ≥ 「可滾動範圍的高度」

寫成程式碼大概會是這樣:

用 React 來實現,我們可以考慮在 useEffect 裡面對 scroll 做事件監聽,當滾動到底部的條件達成時,去加載更多的內容:

這個做法的優點是很直覺、很簡單,基本上我們第一時間應該都可以想到這個做法。

但缺點也是很顯而易見,這些程式碼都在 main thread 上運行,需要不斷監聽 scroll 事件,每次滾動時都需要重新計算元素的位置信息,因此可能會影響性能。

即使,你不是往下滑,而是往上滑,因著 scroll 事件,這個計算的 function 還是會不斷的被觸發,顯然這是個沒有效率的做法,因為他一直在做一些無謂的運算。

當你意識到這個問題,開始想要去找一些解決方案的時候,我們就能看到許多開發者會推薦你另一種做法,就是透過「Intersection Observer API」來實現。

Intersection Observer API 的核心精神是「當被觀察者與觀察範圍重疊到某個百分比時,呼叫我的 callback function 做某件事」。

所以重點有三個,「觀察者」、「被觀察者」、「要被呼叫的 callback」。

以上圖來說

  • 觀察者:藍色的框框就是我們觀察的範圍,上述例子是 Browsers viewport。
  • 被觀察者:我們可以看到藍色框框下面有一個深色矩形,這表示被觀察的對象。
  • 要被呼叫的 callback: 當被觀察的深色矩形因著滾動事件進到藍色的框框的可視範圍內,就觸發這個 callback,裡面做的事情就是去加載更多內容進來。

詳細的實作方式我就不在這邊說明了,我想說的是,直覺的做法固然很好,也很有用,但是如果能夠參考別人的做法,就能夠得到意外的收穫和學習!這是我曾經忽略的部分。

參考文章:

Pagination

Pagination 是一個分頁元件。當頁面中一次要載入過多的資料時,載入及渲染將會花費更多的時間,因此,考慮分批載入資料的時候,需要分頁元件來幫助我們在不同頁面之間切換。

Pagination

[情境一]

當我們開開心心的完成了一個 Pagination 元件的時候:

我們是否能夠想到,會不會有一天他會變得太長呢?如下圖:

我個人覺得,能夠想到這一步其實就已經蠻厲害的了!

過往比較沒有經驗的時候,通常會忽略頁數太長的問題,因為自己在測試的時候通常覺得功能沒什麼問題就 ok。剛剛上到正式機,因為資料量也還沒那麼多,所以也不會發現問題。等到使用者新增的資料越來越多,才發現,怎麼頁數會變這麼多!或是使用者發現怎麼破版了!這時候才會突然驚覺糗大了!

所以我才覺得,能夠在這之前就發現,真的是擁有豐富的經驗,或是曾經被這件事情給雷過。

在行動裝置普及的當代,Pagination 太長很容易造成破版,因此需要適當的縮短節點。但是,縮短節點應該要怎麼處理?

在沒有時間考慮太多的情況下,我想了一個規則來縮短節點:

  • 留頭、留尾
  • 留 current — 1, current, current + 1 這幾個 page
  • 其他的都省略

成功縮短之後成果如下圖:

看起來有模有樣的不是嗎?我當初真的覺得自己很聰明…

但事實上,這樣想還是太單純了,怎麼說呢?

想想看,考慮到邊界狀況,例如頭尾剛好就是 current + 1, current — 1 ,我們可能就會這樣處理:

另外,符合上述三個規則的還有這樣:

符合上述三個規則的其實有各種可能性,簡單條列一下如下圖:

有沒有發現一件事?我們的 Pagination 居然在不同的邊界條件之下會有不同的節點數量,會像金箍棒一樣變長變短。

因此,使用者沒有辦法透過你的 Pagination 做「連續點擊」。換句話說,當你的 Pagination 會忽長忽短的時候,使用者在滑鼠游標位置不變的情況下,有可能會點擊到他預期以外的節點。

這真的是非常的糟糕,就像是你在看漫畫網站的時候,想要點下一頁,卻不小心點到突然跳出來的色情網站廣告一樣慘,因為你媽媽可能要叫你去吃飯的時候來到你房間,而你剛好就在那個不小心的時間點出意外。

因此,要如何讓 Pagination 能夠固定長度,確保使用者不會不小心點到他不想要點的按鈕?這真的也是容易被我們忽略的小細節呀!

[情境二]

Pagination 是一個很常見的元件,因此在各個產品上面都很容易發現他的蹤跡。也因此,常常他也需要為了符合各個產品的情境而需要做出對應的調整。

有時候我們需要不同樣式,但同樣邏輯的 Pagination。例如:

  • 比較龐大複雜的系統,為了符合不同頁面功能的需要
  • 一個大公司內部有好多個不同產品的團隊,雖然公司有內部開發的 UI Library,但是不同產品希望有不同的樣式
  • 開放給大眾用的 Pagination,希望盡可能做得 general 一點

那該怎麼做才能夠讓這個 Pagination 能夠 general 一點呢?

其中一個很直覺的想法就是,或許我們可以用 props 來控制各種可以客製化的屬性,例如節點大小、填充模式、顏色、外觀是否有圓角、隱藏節點的時候該保留幾個兄弟節點…等等。

要客製化的話,會有非常多的屬性可以設置,但是,如果這些屬性全部都由 props 來控制的話,那想必你的 Pagination 有可能就會變成下面這樣:

我想,看到這樣千瘡百孔的 props 傳入的時候,必定會眉頭一皺,因為這樣真的很不容易看出這個元件長什麼樣子,密密麻麻的,很難閱讀也很難維護。

而且 props 太多的話,到時候我們的 Pagination 要用在各個頁面的時候,也會很難移植,因為你必須要確保每一個 props 傳入的值都是你所預期的,未來要改也會很難改,因為東西一多,就可能會漏東漏西。

就在這個時候,我發現 Material UI 提供了一個令我醍醐灌頂的想法,「usePagination」。

usePagination
For advanced customization use cases, a headless usePagination() hook is exposed. It accepts almost the same options as the Pagination component minus all the props related to the rendering of JSX. The Pagination component is built on this hook.

一個天外之音打進我的心頭,「誰跟你講元件一定要帶有樣式?」

usePagination 很漂亮的把 Pagination 的邏輯和選染樣式拆開來處理。usePagination 只處理邏輯的部分,而樣式的部分則留給開發者自己客製化。

因為這是一個「React」的元件庫,因此他使用到了 React 的特色,Custom Hooks 來實作,這點我真的覺得很厲害!

在 React 中,Custom Hooks 是一種函數,它們可以讓你在多個組件之間共享邏輯、狀態和行為,從而幫助你更好地組織和重用程式碼。

如果我們把邏輯跟樣式分開,邏輯的部分共用成 usePagination 的話,這樣各團隊就能夠共用程式碼,也能夠擁有自己客製的樣式了。

簡單示範一下 usePagination 的使用:

我們只需要傳入適當的參數,就能夠得到一系列的節點,這些節點包含了每一個節點所需要的資訊還有被點擊時觸發的事件,例如上一頁、下一頁:

「誰跟你講元件一定要帶有樣式?」這句話真的帶我從原本固定習慣的思維當中跳脫出來。

進場/離場動畫

在寫前端應用程式難免會碰到需要動畫的時候,尤其是元件的進場與離場動畫。例如 Modal, Drawer…等等。

動畫在前端介面當中, 真的扮演了畫龍點睛的角色。

想想看,如果我們很生硬的直接把元件塞進畫面,那元件在使用者看來就會很「突然的」出現或消失,像是下面這樣:

這真的是非常的突兀、非常不優雅!

因此,為了比較好的使用者體驗,我們會讓元件「優雅的」進場或離場,例如滑入 (Slide In)/滑出 (Slide Out) ,所以我們加上一些 transition:

我們可以看到上述程式碼當中,透過 styled-components 的 props 傳入,來控制元件的樣式,因此,我們就會得到了一個擁有優雅動畫的 Drawer 元件了:

看起來真的是令人通體舒暢不是嗎?

然而,當你沉浸在這個優雅的動畫中時,突然開啟了檢視原始碼,就會赫然想到一件令人介意的事,那就是,當 Drawer 離場之後,他的節點在 DOM Tree 裡面還是沒有消失,只是使用者看到的畫面消失而已。

因此,為了讓節點可以消失,所以除了動畫的 open 以外,我們再把 open 拿來控制節點是否渲染:

這樣,我們同時有了可以控制動畫的 props,也會在 open 變成 false 的時候把 Drawer 元件拿掉。

但事實上真是如此嗎?我們來看一下成果:

你會發現你又回到了不優雅的樣子……

雖然這個方法解決了節點不渲染在 DOM Tree 上的問題,可是原本寫的 transition 動畫卻消失了!

原因是若直接把元件拔掉,他沒有時間可以做 Transition。

我的天啊!原來這件事情比我想像的還要複雜許多!

可是我們觀察那些常用的元件庫,例如 MUI,卻可以發現 Drawer 在離場的時候,DOM Tree 裡面的節點會被移除,並且擁有非常優雅的動畫:

這表示,我們想要保持優雅動畫的同時,要控制 Drawer 退場後在 DOM Tree 裡面的節點會被移除這件事情是可行的,只是我們的方法或想法錯了。

仔細想想,只透過一個 boolean 來控制的話,有點難同時做到這個效果。
因為滑入、滑出,跟是否在 DOM Tree 當中渲染,是兩件獨立不同的事情。沒有一開始想的那麼單純。

那我們有可能的解決方法如下:

  • 使用 setTimeout 搭配 open/visible 兩個參數分別控制這兩件事
  • 使用 react-transition-group 等處理動畫的套件幫忙
  • 其他厲害的方法

犯了這個蠢,確實讓我思考了一些事。有時候我們乍看之下很自然、很簡單的東西,仔細觀察之後會發現其實有很多巧思在其中,瞭解他的巧思之後,
不禁會對這個元件設計的用心敬畏三分。

總結

在做元件庫的過程當中,除了今天小聚提到的幾個主題之外,還有許多我沒注意到的小細節,例如 Controlled vs Uncontrolled 的問題、props 參數命名的問題、元件庫整體性一致性的問題…等等,有許多小細節在實作的過程當中值得拿出來討論。

另外,在忽略這些小細節的過程當中,越來越覺得自己關起來蠻幹是一個對工程師而言自殺式的做法,所謂「獨自做一套元件庫」並不意味著你的環境沒有人可以跟你合作。不願意傾聽別人的意見和想法,覺得別人的想法都不如自己、都有疏漏,其實也是一種閉門造車。然而,如果你的團隊裡面只有你一個人,也不代表沒辦法跟別人交流,現今網路這麼發達,懂得尋找資源,也能夠避免讓自己成為井底之蛙。

最後,有時候我們乍看之下很自然、很簡單的東西,仔細觀察之後會發現其實有很多巧思在其中,有他厲害的地方、值得學習的地方。事實上,不只是對自己手上的專案是如此,對於身邊的人、合作的同事也是一樣,在這部分我也是有深刻的感觸。

參考:

React.tw 小聚

Reactjs.tw 社群小聚 16(2023/03/23),這是我第一次參加小聚,沒想到第一次參加就要上台分享,真是讓人感到非常緊張,而且上半場是帥哥大神 Kyle Mo,分享得非常得精彩!

不過也感謝有這次的意外,才能夠在職涯過程當中有這樣難得的經驗。如果不是意外的話,我自己可能都沒有勇氣站上台吧!

真的感謝在場的聽眾給予熱情的回饋,從觀眾的問答當中我也收穫了很多,希望大家能夠繼續彼此交流,彼此成長!

(附上這次分享的投影片連結

活動記錄

--

--