全新的 React 元件設計理念 Headless UI
作者:彭瑞(皮爾)
其實,最早接觸 Headless UI 是在去年,碰巧看到了一個非常前沿且優秀的元件庫 ---- Chakra UI,這個元件庫本身就是 Headless UI 的實踐者,同時也是 CSS-IN-JS 的集大成者。
我當時看過之後,就對該理念產生了很大的興趣,同時工作中也正好有機會實踐(著手公司開源元件庫大版本重構),因此對該理念也有一定的實踐經驗。
那麼今天,也是想和大家分享介紹下這項還算前沿的技術,另一方面是也算是個人的一份技術總結,這裡也希望感興趣的小夥伴可以在評論區探討。
契機:React Hooks 的誕生
React Hooks 可以說是 Headless UI 得以實現的基石,為什麼這麼說,這裡我們首先聊聊 React Hooks。
React Hooks 是什麼
我們都知道,React Hooks 是在 V16.8 版本誕生了,是它讓我們的函式元件真正擁有了狀態。如下圖,我們以數字累加這個功能舉例,可以看到對於同樣的功能,React Hooks 的寫法相對於過去類元件的寫法從程式碼上會減少一丟丟。
但僅僅是因為如此才支援它嗎?
我們要知道,在 React v16.8 之前,一般情況下,普通的 UI 渲染直接使用函式元件就好,需要使用 state 或者其他副作用之類功能時,才會使用類元件。
兩者分工也算合理,那麼 hooks 的誕生又是為何?僅僅是為函式元件賦能嗎?從使用者的角度來說,這顯然說不過去,徒增了學習成本不說,還多了一個糾結選項(函式元件 vs 類元件)。
React Hooks 的意義
所以,事情並沒有那麼簡單。我們可以推斷,對於 hooks 它肯定解決一些“類元件存在的不足或痛點”,這裡就不賣關子,羅列 2 點:
1、狀態邏輯在元件之間難以複用
在過去,狀態邏輯的複用往往會採用高階元件來實現。但劣勢也非常明顯,需要在原來的元件外再包裹一層父容器。 導致層級冗餘,甚至巢狀地獄,引來了很多吐槽點:
- 增強除錯的難度
- 拉低執行的效率
相信使用 Redux 的同學都知道,為了快速狀態管理到元件的注入,會使用 connect
對元件進行包裹,但是隨著專案迭代,開啟 DevTools 檢視時發現 DOM 往往臃腫不堪。
2、複雜元件變得難以理解和維護
複雜元件本身就很複雜,但是類元件讓其變得更加難以理解和維護。比如:在一個生命週期函式中往往存在不相干的邏輯混雜在一起,或者一組相干的邏輯分散在不同的生命週期函式中,這裡分別舉個例子:
- 在
componentWillReceiveProps
中往往寫入不相干 props 更新渲染的判斷邏輯,對於一次更新,往往會多出一些無效的執行,拉低執行效率 - 在
componentDidMount
中註冊事件,在componentWillUnmount
中解除安裝該事件,往往容易忘記甚至寫出 bug。
長此以往我們的程式碼只會變得糟糕難懂。
React Hooks 對元件開發的影響
通過 React Hooks,我們可以把元件的狀態邏輯抽離成自定義 hooks,相干的邏輯放在一個 Hook 裡,不相干的拆分成不同的 hook,最終在元件需要時引入,從而實現狀態邏輯在不同元件之間複用。
正是因為 React Hooks 的誕生,使 Headless UI 元件在技術上成為可能,這也是它為什麼最近才開始流行的原因。
所以,接下來我們介紹下什麼是 Headless UI。
什麼是 HeadLess UI
Headless UI 的定義
Headless UI 目前社群還在探索實踐階段,這裡我對它做了個簡單定義:Headless UI 一套基於 React Hooks 的元件開發設計理念,強調只負責元件的狀態及互動邏輯,而不管標籤和樣式。 其本質思想其實就是關注點分離:將元件的“狀態及互動邏輯”和“UI 展示層”實現解耦。
Headless UI 元件
從實體上看,Headless UI 元件就是一個 React Hook。
從表象上來看,Headless UI 元件其實就是一個什麼也不渲染的元件。
為什麼會有 Headless UI
那麼我們為什麼會需要一個啥也不渲染的元件呢?
這裡我們還是以數字加減這個功能舉例,先思考設計實現一個數字加減器 Counter
元件。
傳統版元件的設計痛點
按照傳統的模式,我們可能會直接去編寫匯出一個名字叫 Counter
元件,然後使用上直接渲染它即可,對於元件的功能通過 props
設定,比如非受控初始數字值。
那麼這麼做有什麼滿足不了的痛點呢?我們這裡隨便舉個場景,然後分別來從元件的使用者、維護者以及服務的產品三個角度來分析下。
1. 使用者 - 高定製業務場景如何實現滿足?
現在我們業務有這樣的訴求:左右兩個加減按鈕要求支援長按後懸浮展示 Tooltip
提示。
其實從產品角度這個需求很樸實,提升互動體驗嘛。但是如果按照之前傳統的元件設計,那就頭疼了。它一整個都是元件庫裡面暴露出來的(假設哈),怎麼去侵入到裡面給加減按鈕加 Tooltip
呢?
其實,對於元件這樣定製業務場景的訴求,我們一般解決思路可能是這樣:
但是隨著方案越往後選擇,我們的代價是越來越高的,臉上的痛苦面具也越來越明顯。
2. 維護者 - 元件 API 日趨複雜,功能擴充套件 & 向下相容的苦惱?
對於維護者而言,如果要去滿足這樣的訴求,那麼他可能會這麼做。
一開始,需求比較簡單,我們可以通過新增 API 動態注入要實現的功能,對於上面的訴求,我們可能會新增 xxxButtonTooltipText
之類的 API 來實現 Tooltip
文案的配置;
一週後,又需要加減按鈕支援 Icon
自定義,我們可能會新增 xxxButtonText
之類的 API 來滿足;
又過了 2 周,我們又想支援 Tooltip
展示方位配置,避免遮擋核心內容展示,我們可能會新增 xxxButtonTooltipPlacement
。。。
日復一日,元件 API 數快速擴充套件,最後,維護者發現實在忍受不了了,決定嘗試使用 Render Props
設計,以此一勞永逸,於是新增了 xxxButtonRender
支援加減按鈕自定義函式渲染。
我們發現,通過這麼做,一個簡單的元件變得日趨複雜,不僅僅存在功能冗餘實現,而且後面還要考慮功能擴充套件以及向下相容,臉上的痛苦面具也逐漸明顯。
另外,對於使用者,當想使用一個元件發現有幾頁的 API 數量時,也會淺嘆一聲,功能難以檢索到,而且大部分可能都不需要,面對效能優化也難以入手。
3. 產品:如何快速打造好用定製的品牌 UI ?
對於一個產品,最重要的一點就是塑造產品本身的品牌形象和產品特色。對於使用者最直接接觸的 UI 互動,那更是尤為重要。那麼如何快速打造好用定製的品牌 UI 呢?
還是以數字加減器舉例,那麼,它的好用可能體現在它具備較為完善且好用的能力。
- 點選加減按鈕:數字加減步長
- Accessibility 可訪問性
- 數字值最大最小值控制
- ... ...
對於它的定製,可能體現在它 UI 檢視層上的差異化。如下圖,僅僅是 Counter
這種小元件,就有五花八門的 UI 形態,更別說其它更復雜的元件了。
Headless UI 的解法
從上面的分析我們可以看到,UI 是一個自由度非常高的玩意,而構建 UI 又是一種非常品牌化和定製化的體驗。
那麼,我們能不能只需複用元件的互動邏輯,佈局和樣式完全自定義呢?顯然,Headless UI 就是幹這件事情的。
對於 Headless UI 元件,我們要做到第一件事,就是分析和抽離元件的狀態以及互動邏輯。對於 Counter
元件,它的狀態邏輯大致如下:
我們把這些狀態邏輯收斂到一個叫 useCounter
的 React Hook 中。它接收使用者傳入的功能 API 設定,然後返回一套已處理過的全新 API。
對於使用者而言,我們只需把返回的 API 賦予到想賦予的標籤上,那麼就得到了一個只帶互動能力的無頭元件。
最後,我們結合設計稿進行 UI 還原,對編寫自定義樣式,最終就能實現一個全新數字加減器元件了;
另外,我們還可以將標籤重新排版,然後樣式改吧改吧,將按鈕絕對定位一下,最終就能實現一個數字輸入框元件;
除此之外,我們還可以基於它封裝,比如原本的最大值表示總頁數,插入到標籤中間,樣式再改吧改吧,就能實現了一個迷你版的分頁器元件了。
可以看到,通過 Headless UI 的設計思路,我們最終產出了一個叫 useCounter
的 React Hook,通過它,我們不用關心元件最為複雜且最通用的部分----互動邏輯,而是把它交給元件維護者管理;而對於經常變化需要定製的 UI 部分完全由我們自由發揮,從而實現最大化地滿足業務高定製擴充套件的訴求,同時,也儘可能實現程式碼的充分複用。
Headless UI 的優與劣
這裡我們簡單梳理下 Headless UI 的優勢和劣勢,以及目前建議的適用場景,方便大家做技術選型和學習。
優勢
- 有極強大的 UI 自定義發揮空間,支援高定製擴充套件
可以看到 headless 的優勢也非常明顯,因為它更抽象,所以它擁有非常強大的定製擴充套件能力:支援標籤排版、元素組合,內容插入、樣式定義等等都能滿足。
- 最大化程式碼複用,減小包體積
從上面可以看到,元件的狀態邏輯可以儘可能達到最大化複用,幫助我們減小包體積,增強整體可維護性。
- 對單測編寫友好
因為基本都是邏輯,對於事件回撥、React 執行管理等都可以快速模擬實現單測編寫和迴歸;而 UI 部分,一般容易變化,且不容易出 bug,可以避免測試。
劣勢
- 對開發者能力要求高,需要較強的元件抽象設計能力
抽象層次越高,編寫難度越大。對於這樣 headless 元件,我們關注的元件 API 設計和互動邏輯抽離,這非常考驗開發者的元件設計能力。
- 使用成本大,不建議簡單業務場景下鋪開使用
UI 層完全自定義,存在一定開發成本,因此需要評估好投入產出,對於沒有特別高要求的 2b 業務的話,還是建議使用 Ant Design 這樣自帶 UI 規範的元件庫進行開發。
Headless UI 的生態與展望
社群生態
關於元件,目前在國外已經有些探索和實踐的案例,比如 React-Popper、React-Hook-Form、TanStack-Table,三個是元件庫“三大難”,它們 stars (均上萬)和活躍度都非常高,未來基於 headless UI 設計實踐的元件只會越來越多。
關於元件庫,我目前看到的比較不錯的實踐就是 Chakra-UI 元件庫,整個元件庫採用分層架構(這裡以數字輸入框元件為例):
- 底層使用 Headless UI 那一套模式,對外暴露相關的 React Hook,保證整個元件的高定製擴充套件的訴求能得到最大化滿足。
- 而上層則提供了類似於 Ant Design 這樣的元件,自帶預設的 UI,但不同的是每個元件都是由顆粒度更小且必要的原子元件構成,可以直接引入它們使用,這樣又保證大部分簡單或普通的場景可以快速實現並滿足。
注意:其實一個元件拆分成多個必要的原子元件構成,其實也算是 Headless UI 的一種實踐形態,把互動邏輯生效的 API 直接繫結在必要的元素標籤上,然後以原子元件暴露出來,標籤的排版和樣式修改也完全可以由使用者自定義。
另外,在 React Next 2022 大會上,也有嘉賓分享介紹 Headless UI 相關的理念,整個社群目前都處在持續發酵的階段。
未來展望
個人認為 Headless UI 是未來 React 元件庫底層的最佳實踐。
對於元件庫而言,可能大家都不需要“讀書借鑑”了,而是都使用同一套元件底層的狀態以及互動邏輯,而在 UI 層以及細節上再進行品牌、場景定製化擴充套件。
總結
那麼,以上就是關於 headless 設計理念的全部內容。通過 Headless UI,我們可以快速複用元件的狀態以及互動邏輯,對於佈局和樣式實現完全自定義。
另外,Headless UI 是一個元件庫設計的新思路,也是未來元件庫必然的趨勢。對於前端同學而言,學習瞭解它也顯得尤為重要。
值得一提的是,在日常開發中,我們也可以嘗試借鑑這樣的思路,將通用狀態邏輯抽離出去,方便複用,幫助我們在日常開發提效。比如:常見的篩選過濾、分頁請求列表資料的邏輯等;甚至,我們還可以將業務邏輯同 UI 互動進行抽離,比如:在多端場景(Web PC 端、小程式端、RN 端)下複用同一套業務邏輯程式碼,實現業務邏輯複用和統一,以此大大提高我們的生產力。
- 為iframe正名,你可能並不需要微前端
- SLS:基於 OTel 的移動端全鏈路 Trace 建設思考和實踐
- 為iframe正名,你可能並不需要微前端
- 釘釘 ANR 治理最佳實踐 | 定位 ANR 不再霧裡看花
- 釘釘 ANR 治理最佳實踐 | 定位 ANR 不再霧裡看花
- 低程式碼多分支協同開發的建設與實踐
- Flutter for Web 首次首屏優化——JS 分片優化
- 全新的 React 元件設計理念 Headless UI
- 我們是如何追逐元宇宙、XR等“概念股”浪潮的?
- 盒馬 iOS Live Activity &“靈動島”配送場景實踐
- ECMAScript 雙月報告:Hashbang Grammer 提案成功進入到 Stage 4
- 如何根治 Script Error.
- 語雀桌面端技術架構實踐
- 語雀桌面端技術架構實踐
- Clang Module 內部實現原理及原始碼分析
- 基於 LowCodeEngine 的除錯能力建設與實踐
- 基於 LowCodeEngine 的除錯能力建設與實踐
- Android Target 31 升級全攻略 —— 記阿里首個超級 App 的坎坷升級之路