全新的 React 組件設(shè)計(jì)理念 Headless UI
點(diǎn)擊上方 前端Q,關(guān)注公眾號(hào)
回復(fù)加群,加入前端Q技術(shù)交流群
前言
其實(shí),最早接觸 Headless UI 是在去年,碰巧看到了一個(gè)非常前沿且優(yōu)秀的組件庫(kù) ---- Chakra UI,這個(gè)組件庫(kù)本身就是 Headless UI 的實(shí)踐者,同時(shí)也是 CSS-IN-JS 的集大成者。
我當(dāng)時(shí)看過(guò)之后,就對(duì)該理念產(chǎn)生了很大的興趣,同時(shí)工作中也正好有機(jī)會(huì)實(shí)踐(著手公司開(kāi)源組件庫(kù)大版本重構(gòu)),因此對(duì)該理念也有一定的實(shí)踐經(jīng)驗(yàn)。
那么今天,也是想和大家分享介紹下這項(xiàng)還算前沿的技術(shù),另一方面是也算是個(gè)人的一份技術(shù)總結(jié),這里也希望感興趣的小伙伴可以在評(píng)論區(qū)探討。
契機(jī):React Hooks 的誕生
React Hooks 可以說(shuō)是 Headless UI 得以實(shí)現(xiàn)的基石,為什么這么說(shuō),這里我們首先聊聊 React Hooks。
React Hooks 是什么
我們都知道,React Hooks 是在 V16.8 版本誕生了,是它讓我們的函數(shù)組件真正擁有了狀態(tài)。如下圖,我們以數(shù)字累加這個(gè)功能舉例,可以看到對(duì)于同樣的功能,React Hooks 的寫(xiě)法相對(duì)于過(guò)去類(lèi)組件的寫(xiě)法從代碼上會(huì)減少一丟丟。

但僅僅是因?yàn)槿绱瞬胖С炙鼏幔?/p>
我們要知道,在 React v16.8 之前,一般情況下,普通的 UI 渲染直接使用函數(shù)組件就好,需要使用 state 或者其他副作用之類(lèi)功能時(shí),才會(huì)使用類(lèi)組件。
兩者分工也算合理,那么 hooks 的誕生又是為何??jī)H僅是為函數(shù)組件賦能嗎?從使用者的角度來(lái)說(shuō),這顯然說(shuō)不過(guò)去,徒增了學(xué)習(xí)成本不說(shuō),還多了一個(gè)糾結(jié)選項(xiàng)(函數(shù)組件 vs 類(lèi)組件)。
React Hooks 的意義
所以,事情并沒(méi)有那么簡(jiǎn)單。我們可以推斷,對(duì)于 hooks 它肯定解決一些“「類(lèi)組件存在的不足或痛點(diǎn)”」 ,這里就不賣(mài)關(guān)子,羅列 2 點(diǎn):
狀態(tài)邏輯在組件之間難以復(fù)用
在過(guò)去,狀態(tài)邏輯的復(fù)用往往會(huì)采用高階組件來(lái)實(shí)現(xiàn)。但劣勢(shì)也非常明顯,需要「在原來(lái)的組件外再包裹一層父容器?!?/strong> 導(dǎo)致層級(jí)冗余,甚至嵌套地獄,引來(lái)了很多吐槽點(diǎn):
增強(qiáng)調(diào)試的難度 拉低運(yùn)行的效率
相信使用 Redux 的同學(xué)都知道,為了快速狀態(tài)管理到組件的注入,會(huì)使用 connect 對(duì)組件進(jìn)行包裹,但是隨著項(xiàng)目迭代,打開(kāi) DevTools 查看時(shí)發(fā)現(xiàn) DOM 往往臃腫不堪。

復(fù)雜組件變得難以理解和維護(hù)
復(fù)雜組件本身就很復(fù)雜,但是類(lèi)組件讓其變得更賤難以理解和維護(hù)。比如:在一個(gè)生命周期函數(shù)中往往存在「不相干的邏輯混雜」在一起,或者「一組相干的邏輯分散」在不同的生命周期函數(shù)中,這里分別舉個(gè)例子:
在 componentWillReceiveProps**中往往寫(xiě)入不相干 props 更新渲染的判斷邏輯),對(duì)于一次更新,往往會(huì)有一些無(wú)效的執(zhí)行,拉低執(zhí)行效率在 componentDidMount中注冊(cè)事件,在componentWillUnmount中卸載該事件),往往容易寫(xiě)出 bug 甚至忘記。
長(zhǎng)期以往我們的代碼只會(huì)變得糟糕難懂。

React Hooks 對(duì)組件開(kāi)發(fā)的影響
通過(guò) React Hooks,我們可以把組件的狀態(tài)邏輯抽離成自定義 hooks,相干的邏輯放在一個(gè) Hook 里,不相干的拆分成不同的 hook,最終在組件需要時(shí)引入,實(shí)現(xiàn)狀態(tài)邏輯在不同組件之間復(fù)用。

正是因?yàn)?React Hooks 的誕生,使 Headless UI 組件在技術(shù)上成為可能,這也是它為什么最近才開(kāi)始流行的原因。
什么是 HeadLess UI
Headless UI 的定義
Headless UI 目前社區(qū)還在探索實(shí)踐階段,這里我對(duì)它做了個(gè)簡(jiǎn)單定義:Headless UI 「一套基于 React Hooks 的組件開(kāi)發(fā)設(shè)計(jì)理念,強(qiáng)調(diào)只負(fù)責(zé)組件的狀態(tài)及交互邏輯,而不管標(biāo)簽和樣式?!?/strong> 其本質(zhì)思想其實(shí)就是關(guān)注點(diǎn)分離:將組件的“狀態(tài)及交互邏輯”和“UI 展示層”實(shí)現(xiàn)解耦。
Headless UI 組件
從實(shí)體上看,Headless UI 組件就是一個(gè) React Hook。

從表象上來(lái)看,Headless UI 組件其實(shí)就是一個(gè)什么也不渲染的組件。

為什么會(huì)有 Headless UI
那么我們?yōu)槭裁葱枰粋€(gè)啥也不渲染的組件呢?
這里我們還是以數(shù)字加減這個(gè)功能舉例,先思考設(shè)計(jì)實(shí)現(xiàn)一個(gè)數(shù)字加減器 Counter 組件。
傳統(tǒng)版組件的設(shè)計(jì)痛點(diǎn)
按照傳統(tǒng)的模式,我們可能會(huì)直接去編寫(xiě)導(dǎo)出一個(gè)名字叫 Counter 組件,然后使用上直接渲染它即可,對(duì)于組件的功能通過(guò) props 設(shè)置,比如非受控初始數(shù)字值。

那么這么做有什么滿(mǎn)足不了的痛點(diǎn)呢?我們這里隨便舉個(gè)場(chǎng)景,然后分別來(lái)從「組件的使用者、維護(hù)者以及服務(wù)的產(chǎn)品」三個(gè)角度來(lái)分析下。
使用者 - 高定制業(yè)務(wù)場(chǎng)景如何實(shí)現(xiàn)滿(mǎn)足?
現(xiàn)在我們業(yè)務(wù)有這樣的訴求:左右兩個(gè)加減按鈕要求支持長(zhǎng)按后懸浮展示 Tooltip 提示。

其實(shí)從產(chǎn)品角度這個(gè)需求很樸實(shí),提升交互體驗(yàn)嘛。但是如果按照之前傳統(tǒng)的組件設(shè)計(jì),那就頭疼了。它一整個(gè)都是組件庫(kù)里面暴露出來(lái)的(假設(shè)哈),怎么去侵入到里面給加減按鈕加 Tooltip 呢?
其實(shí),對(duì)于組件這樣定制業(yè)務(wù)場(chǎng)景的訴求,我們一般解決思路可能是這樣:

隨著方案越往后選擇,我們的代價(jià)是越來(lái)越高的,臉上的痛苦面具也越來(lái)越明顯。
維護(hù)者 - 「組件」 「API」 「日趨復(fù)雜,功能擴(kuò)展 & 向下兼容的苦惱?」
對(duì)于維護(hù)者而言,如果要去滿(mǎn)足這樣的訴求,那么他可能會(huì)這么做。
一開(kāi)始,需求比較簡(jiǎn)單,我們可以通過(guò)新增 API 動(dòng)態(tài)注入要實(shí)現(xiàn)的功能,對(duì)于上面的訴求,我們可能會(huì)新增 xxxButtonTooltipText 之類(lèi)的 API 來(lái)實(shí)現(xiàn) Tooltip 文案的配置;
一周后,又需要加減按鈕支持 Icon 自定義,我們可能會(huì)添加 xxxButtonText 之類(lèi)的 API 來(lái)滿(mǎn)足;
又過(guò)了2周,我們又想支持 Tooltip 展示方位配置,避免遮擋核心內(nèi)容展示,我們可能會(huì)添加 xxxButtonTooltipPlacement 。。。
日復(fù)一日,組件 API 數(shù)快速擴(kuò)展,最后,維護(hù)者發(fā)現(xiàn)實(shí)在忍受不了了,決定嘗試使用 Render Props 設(shè)計(jì),以此一勞永逸,于是新增了 xxxButtonRender 支持加減按鈕自定義函數(shù)渲染。

我們發(fā)現(xiàn),通過(guò)這么做,一個(gè)簡(jiǎn)單的組件變得日趨復(fù)雜,不僅僅存在功能冗余實(shí)現(xiàn),而且后面還要考慮功能擴(kuò)展以及向下兼容,臉上的痛苦面具也逐漸明顯。
另外,對(duì)于使用者,當(dāng)想使用一個(gè)組件發(fā)現(xiàn)有幾頁(yè)的 API 數(shù)量時(shí),也會(huì)淺嘆一聲,功能難以檢索到,而且大部分可能都不需要,面對(duì)性能優(yōu)化也難以入手。
「產(chǎn)品:如何快速打造好用定制的品牌」 「UI」 「?」
對(duì)于一個(gè)產(chǎn)品,最重要的一點(diǎn)就是塑造產(chǎn)品本身的品牌形象和產(chǎn)品特色。對(duì)于用戶(hù)最直接接觸的 UI 交互,那更是至關(guān)重要。那么「如何快速打造好用定制的品牌 UI 呢?」
還是以數(shù)字加減器舉例,那么,它的好用可能體現(xiàn)在它具備較為完善且好用的能力。
點(diǎn)擊加減按鈕:數(shù)字加減步長(zhǎng) Accessibility 可訪問(wèn)性 數(shù)字值最大最小值控制 ... ...
對(duì)于它的定制,可能體現(xiàn)在它 UI 視圖層上的差異化。如下圖,僅僅是 Counter 這種小組件,就有五花八門(mén)的 UI 形態(tài)。

Headless UI 的解法
從上面的分析我們可以看到,「UI」 「是一個(gè)自由度非常高的玩意,而構(gòu)建 UI 是一種非常品牌化和定制化的體驗(yàn)?!?/strong>
那么,我們能不能「只需復(fù)用組件的交互邏輯,布局和樣式完全自定義」呢?顯然,Headless UI 就是干這件事情的。

對(duì)于 Headless UI 組件,我們要做到第一件事,就是分析和抽離組件的狀態(tài)以及交互邏輯。對(duì)于 Counter 組件,它的狀態(tài)邏輯大致如下:

我們把這些狀態(tài)邏輯收斂到一個(gè)叫 useCounter 的 React Hook 中。它接收用戶(hù)傳入的功能 API 設(shè)置,然后返回一套已處理過(guò)的全新 API。
對(duì)于用戶(hù)而言,我們只需把返回的 API 賦予到想賦予的標(biāo)簽上,那么就得到了一個(gè)「只帶交互能力的無(wú)頭組件?!?/strong>

最后,我們結(jié)合設(shè)計(jì)稿進(jìn)行 UI 還原,對(duì)編寫(xiě)自定義樣式,最終就能實(shí)現(xiàn)一個(gè)全新數(shù)字加減器組件了;
另外,我們還可以將標(biāo)簽重新排版,然后樣式改吧改吧,將按鈕絕對(duì)定位一下,最終就能實(shí)現(xiàn)一個(gè)數(shù)字輸入框組件;
除此之外,我們還可以基于它封裝,比如原本的最大值表示總頁(yè)數(shù),插入到標(biāo)簽中間,樣式再改吧改吧,就能實(shí)現(xiàn)了一個(gè)迷你版的分頁(yè)器組件了。

可以看到,通過(guò) Headless UI 的設(shè)計(jì)思路,我們最終產(chǎn)出了一個(gè)叫 useCounter 的 React Hook,「通過(guò)它,我們不用關(guān)心組件最為復(fù)雜且最通用的部分----交互邏輯,而是把它交給組件維護(hù)者管理;而對(duì)于經(jīng)常變化需要定制的 UI 部分完全由我們自由發(fā)揮,從而實(shí)現(xiàn)最大化地滿(mǎn)足業(yè)務(wù)高定制擴(kuò)展的訴求,同時(shí),也盡可能實(shí)現(xiàn)代碼的充分復(fù)用。」
Headless UI 的優(yōu)與劣
這里我們簡(jiǎn)單梳理下 Headless UI 的優(yōu)勢(shì)和劣勢(shì),以及目前建議的適用場(chǎng)景,方便大家做技術(shù)選型和學(xué)習(xí)。
優(yōu)勢(shì)
「有極強(qiáng)大的」 「UI」 「自定義發(fā)揮空間,支持高定制擴(kuò)展」
可以看到 headless 的優(yōu)勢(shì)也非常明顯,因?yàn)樗橄螅运鼡碛蟹浅?qiáng)大的「定制擴(kuò)展能力:支持標(biāo)簽排版、元素組合,內(nèi)容插入、樣式定義等等都能滿(mǎn)足?!?/strong>
「最大化代碼復(fù)用,減小包體積」
從上面可以看到,組件的狀態(tài)邏輯可以盡可能達(dá)到最大化復(fù)用,幫助我們減小包體積,增強(qiáng)整體可維護(hù)性。
「對(duì)單測(cè)編寫(xiě)友好」
因?yàn)榛径际沁壿?,?duì)于事件回調(diào)、React 運(yùn)行管理等都可以快速模擬實(shí)現(xiàn)單測(cè)編寫(xiě)和回歸;而 UI 部分,一般容易變化,且不容易出 bug,可以避免測(cè)試。
劣勢(shì)
「對(duì)開(kāi)發(fā)者能力要求高,需要較強(qiáng)的組件抽象設(shè)計(jì)能力」
抽象層次越高,編寫(xiě)難度越大。對(duì)于這樣 headless 組件,我們關(guān)注的組件 API 設(shè)計(jì)和交互邏輯抽離,這非??简?yàn)開(kāi)發(fā)者的組件設(shè)計(jì)能力。
「使用成本大,不建議簡(jiǎn)單業(yè)務(wù)場(chǎng)景下鋪開(kāi)使用」
UI 層完全自定義,存在一定開(kāi)發(fā)成本,因此需要評(píng)估好投入產(chǎn)出,對(duì)于沒(méi)有特別高要求的 2b 業(yè)務(wù)的話(huà),還是建議使用 Ant Design 這樣自帶 UI 規(guī)范的組件庫(kù)進(jìn)行開(kāi)發(fā)。
Headless UI 的生態(tài)與展望
社區(qū)生態(tài)
關(guān)于組件,目前在國(guó)外已經(jīng)有些探索和實(shí)踐的案例,比如 React-Popper、React-Hook-Form、TanStack-Table,三個(gè)是組件庫(kù)“三大難”,它們 stars (均上萬(wàn))和活躍度都非常高,未來(lái)基于 headless UI 設(shè)計(jì)實(shí)踐的組件只會(huì)越來(lái)越多。

關(guān)于組件庫(kù),我目前看到的比較不錯(cuò)的實(shí)踐就是 Chakra-UI 組件庫(kù),整個(gè)組件庫(kù)采用分層架構(gòu)(這里以數(shù)字輸入框組件為例):

「底層」使用 Headless UI 那一套模式,對(duì)外暴露相關(guān)的 React Hook,「保證整個(gè)組件的高定制擴(kuò)展的訴求能得到最大化滿(mǎn)足?!?/strong>

而「上層」則提供了類(lèi)似于 Ant Design 這樣的組件,自帶默認(rèn)的 UI,但不同的是每個(gè)組件都是由顆粒度更小且必要的原子組件構(gòu)成,可以直接引入它們使用,這樣又「保證大部分簡(jiǎn)單或普通的場(chǎng)景可以快速實(shí)現(xiàn)并滿(mǎn)足?!?/strong>
注意:其實(shí)一個(gè)組件拆分成多個(gè)必要的原子組件構(gòu)成,其實(shí)也算是 Headless UI 的一種實(shí)踐形態(tài),把交互邏輯生效的 API 直接綁定在必要的元素標(biāo)簽上,然后以原子組件暴露出來(lái),標(biāo)簽的排版和樣式修改也完全可以由用戶(hù)自定義。

另外,在 React Next 2022 大會(huì)上,也有嘉賓分享介紹 Headless UI 相關(guān)的理念,整個(gè)社區(qū)目前都處在持續(xù)發(fā)酵的階段。

未來(lái)展望
「?jìng)€(gè)人認(rèn)為 Headless」 「UI」 「是未來(lái) React 組件庫(kù)底層的最佳實(shí)踐。」
對(duì)于組件庫(kù)而言,可能大家都不需要讀書(shū)借鑒了,而是都使用同一套組件的底層狀態(tài)以及交互邏輯,在 UI 層以及細(xì)節(jié)上再進(jìn)行品牌、場(chǎng)景定制化擴(kuò)展。

總結(jié)
那么,以上就是關(guān)于 headless 設(shè)計(jì)理念的全部?jī)?nèi)容。「通過(guò) Headless」 「UI」 「,我們可以快速?gòu)?fù)用組件的狀態(tài)以及交互邏輯,對(duì)于布局和樣式實(shí)現(xiàn)完全自定義」。
另外,「Headless」 「UI」 「是一個(gè)組件庫(kù)設(shè)計(jì)的新思路,也是未來(lái)組件庫(kù)必然的趨勢(shì)」。對(duì)于前端同學(xué)而言,學(xué)習(xí)了解它也顯得尤為重要。
值得一提的是,在日常開(kāi)發(fā)中,我們也可以嘗試借鑒這樣的思路,「將通用狀態(tài)邏輯抽離出去,方便復(fù)用,幫助我們?cè)谌粘i_(kāi)發(fā)提效」。比如:常見(jiàn)的篩選過(guò)濾、分頁(yè)請(qǐng)求列表數(shù)據(jù)的邏輯等;甚至,我們還可以將業(yè)務(wù)邏輯同 UI 交互進(jìn)行抽離,比如:在「多端場(chǎng)景(Web」 「PC」 「端、小程序端、RN 端)復(fù)用同」一套業(yè)務(wù)邏輯代碼,實(shí)現(xiàn)業(yè)務(wù)邏輯復(fù)用和統(tǒng)一,以此大大提高我們的生產(chǎn)力。
參考
https://reactjs.org/docs/hooks-intro.html https://medium.com/@nirbenyair/headless-components-in-react-and-why-i-stopped-using-ui-libraries-a8208197c268
完結(jié)。
作者:不敗花丶
Github:https://github.com/Flcwl

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專(zhuān)業(yè)的技術(shù)人...


