摘要:引言組件中有很多彈出式組件,常見的如,以及等。這樣一種層次結(jié)構(gòu)在實踐中大大降低了各類彈層組件的實現(xiàn)和維護(hù)成本。但是的組件實現(xiàn)了一個大多數(shù)組件庫都沒有實現(xiàn)的功能彈層的嵌套處理。
引言
UI 組件中有很多彈出式組件,常見的如 Dialog,Tooltip 以及 Select 等。這些組件都有一個特點,它們的彈出層通常不是渲染在當(dāng)前的 DOM 樹中,而是直接插入在 body (或者其它類似的地方)上的。這么做的主要目的是方便控制這些彈出層的 z-index ,確保它們能夠處于合適的層級上,不至于被遮擋。
我們都知道 React App 的頂層某個地方肯定有這么一行代碼:ReactDOM.render(
在 React 的這種管理模式下,會發(fā)現(xiàn)使用彈層似乎不太方便,因為組件樹是逐層往下生長的,但React 的 API 中并沒有直接提供跳出這棵組件樹的方法[注1]。
所以,為了實現(xiàn)彈層組件,我們需要先實現(xiàn)一個 Portal 組件(玩游戲的都知道,這是傳送門的意思),這個組件只做一件事:將組件樹中某些節(jié)點移出當(dāng)前的DOM 樹,并且渲染到指定的 DOM 節(jié)點中。
Portal 組件Portal 組件的要做的事情很簡單,render 函數(shù)因為不需要在當(dāng)前位置輸出任何東西,所以直接返回 null 就可以了,剩下的就是在組件的生命周期中去手動管理要渲染到指定位置的那些組件。
// 簡化的 Portal 實現(xiàn) class Portal extends Component { static propTypes = { children: PropTypes.node.isRequired, container: PropTypes.object.isRequired }; render() { return null; } componentDidMount() { const { children, container } = this.props; mountChildrenAtNode(children, container); } componentWillUnmount() { const { container } = this.props; unmountChildrenAtNode(container); } }
剩下唯一的問題是 mountChildrenAtNode 這個函數(shù)怎么實現(xiàn)?仔細(xì)的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了,這個函數(shù)和 ReactDOM.render 非常像,仔細(xì)一想,其實它們做的事情就是一樣的。所以我們直接用 ReactDOM.render 去替換 mountChildrenAtNode 就可以了。
那么真的這么簡單嗎?
是,但也不是。
說是,是因為邏輯上這代碼并沒有什么問題,而且大部分場景下是確實可以完美工作。
說不是,是因為剩下的小部分場景下這段代碼確實存在很嚴(yán)重的問題。
那么問題是什么呢?別急,我們先聊點別的。
相信大部分 React 開發(fā)者都用過 redux(至少聽過吧),react-redux 這個 binding 庫提供了連接 React 和 redux 的一個橋梁。react-redux 的實現(xiàn)依賴 React 很有用的一個功能Context,簡單來說 context 就是提供了一個方便的跨越層級往下傳遞數(shù)據(jù)的方式。
ReactDOM.render 的問題正是在于這個 context 的功能,它無法連接兩棵 React 組件樹的 context。
ReactDOM.render 的函數(shù)原型中并沒有當(dāng)前組件樹的信息,而 context 是跟組件樹有關(guān)的。
ReactDOM.render( element, container, [callback] )
解決這個問題的方法也很簡單,這里也不賣關(guān)子了,React 提供了另一個非公開 API:ReactDOM.unstable_renderSubtreeIntoContainer。這個 API 多了一個參數(shù),這個參數(shù)就是用來指定新的 React 組件樹根節(jié)點的父組件的,有了這個參數(shù),兩棵本來互不相干的 React 組件樹就被聯(lián)系起來了,同時它們的 context 也連接了起來。
ReactDOM.unstable_renderSubtreeIntoContainer( parentComponent, element, container, [callback] )
想更好的了解 Context 的同學(xué)可以自己 Google,這不是本文重點,這里不做展開了。
Portal 組件的可擴(kuò)展性不同的 UI 組件對彈層可能會有不同的功能需求,舉個例子, Dialog 組件需要在彈出的時候禁止頁面滾動,同時有些場景下需要支持點擊背景部分關(guān)閉,或者按 ESC 鍵關(guān)閉。
這些很細(xì)節(jié)的功能點往往會出現(xiàn)需要不同組合的使用場景,例如只需要禁止?jié)L動,或者同時需要禁止?jié)L動和 ESC 鍵關(guān)閉。
一個很自然的想法是在 Portal 組件上加幾個可配置的 props 來控制這些功能。這么做有個問題,不管用戶需不需要,代碼都在那里。
更好的方式是通過高階組件(HOC)的方式讓使用者自己去組合這些功能,這樣子沒有用到的功能并不會出現(xiàn)在最終的代碼中。
說了這么多關(guān)于 Portal 組件的實現(xiàn)細(xì)節(jié),有興趣的同學(xué)可以去看看有贊的組件庫 Zent 里面的 Portal 是如何實現(xiàn)的,大體上就是按上面說的那些方案做的。
彈層組件有了 Portal 組件之后,基本上所有彈層組件都可以基于 Portal 去實現(xiàn)。例如 Dialog 無非就是在 Portal 組件的基礎(chǔ)上加了一些 CSS 樣式。復(fù)雜一點的組件例如 Select,需要實現(xiàn)一些觸發(fā)邏輯來控制彈層的打開和關(guān)閉,比如 click 打開或者 hover 打開。我們接下來要討論的彈層組件正是特指類似 Select 中的這些彈層。
在 Zent 里面有一個叫 Popover 的組件來處理這些復(fù)雜的彈層場景,Popover 封裝了常用的觸發(fā)邏輯,例如 click, hover, focus,同時 Popover 的觸發(fā)機(jī)制是可擴(kuò)展的,使用者可以實現(xiàn)自己的觸發(fā)邏輯。
Popover 組件提供的另外一個重要功能是彈層的定位能力,也就是相對于 Trigger 的一個定位功能。除了內(nèi)置的十幾種定位算法,使用者可以實現(xiàn)自己的定位算法來實現(xiàn)特殊場景下的需求。
有了 Popover 組件提供的觸發(fā)邏輯以及彈層定位這兩個功能之后,類似 Tooltip , Select 這樣的組件在實現(xiàn)時就完全不需要關(guān)心彈層的事了,只需要實現(xiàn)彈層內(nèi)的組件邏輯就行了。
這里已經(jīng)能夠看出一個層次化的彈層組件設(shè)計了:Portal 負(fù)責(zé)脫離組件樹,Popover 在 Portal 的基礎(chǔ)上提供了更豐富的功能邏輯,其它組件又在 Popover 的基礎(chǔ)上去做封裝。這樣一種層次結(jié)構(gòu)在實踐中大大降低了各類彈層組件的實現(xiàn)和維護(hù)成本。
在組件庫的設(shè)計中,這種對能力的抽象封裝是很重要的,在提高開發(fā)效率的同時也保證了各個組件行為的一致性。
干貨:彈層組件的嵌套處理上面介紹的彈層組件實現(xiàn)細(xì)節(jié)上并沒有特別之處,成熟的組件庫基本都是用類似方式實現(xiàn)的。但是 Zent 的 Popover 組件實現(xiàn)了一個大多數(shù) React 組件庫都沒有實現(xiàn)的功能:彈層的嵌套處理。
如果你還沒有明白這里的彈層嵌套是什么意思,沒關(guān)系,給你舉個例子就明白了。
如下圖,點擊按鈕之后會彈出一個氣泡,這個氣泡中又有一個時間選擇器,所謂的彈層嵌套指的就是這種彈層之中又嵌了彈層的場景。正常的操作邏輯是鼠標(biāo)點擊位置1的時候氣泡和時間選擇器同時關(guān)閉,但是點擊位置2的時候應(yīng)該只有時間選擇器關(guān)閉。
上面提到的點擊兩個不同位置的不同行為其實就是彈層嵌套最主要的問題:上級的彈層組件應(yīng)該知道哪個區(qū)域是屬于下級彈層組件的。
由于彈層組件的特殊性,它們在 DOM 樹中的位置跟它們實際的層次以及包含關(guān)系是沒有必然聯(lián)系的,上圖中的兩個彈層是body 下面的兩個兄弟節(jié)點,但從彈層的角度看它們是有層次關(guān)系的,并不是并列的。
通常來說,彈層的層次結(jié)構(gòu)也是一個樹狀結(jié)構(gòu),那么處理嵌套問題最直接的想法就是每個彈層組件都各自維護(hù)一個子彈層的列表。當(dāng)需要判斷點擊是否在彈層外面時,不光要考慮當(dāng)前彈層對應(yīng)的 DOM 節(jié)點,還要考慮它的下級彈層對應(yīng)的 DOM 節(jié)點。
這種方式處理的話需要手動維護(hù)這棵彈層的層級關(guān)系樹,包括樹中節(jié)點的插入/刪除,這些操作都不是很難。這個方法最大的問題在于,在 React 的體系內(nèi)一個彈層組件很難跟不是它直接孩子(direct child)的子彈層交互。
Zent 的 Popover 組件并沒有直接去維護(hù)這棵層級關(guān)系樹,而是利用了 React 中 context 的層級關(guān)系來避免自己去維護(hù)這棵樹。使用 context 的另一個附帶好處是,和非直接孩子的交互也不再是問題,因為 context 本身就是可以跨層級傳遞信息的。Popover 的層級管理結(jié)構(gòu)示意圖如下:
* context context * ------> ------> * Popover Root Popover child Popover grand-child ...... * <------ <------ * isOutsideQuery isOutsideQuery
就是這么一個很簡單的設(shè)計解決了 Zent 中彈層組件的層級嵌套問題,想了解實現(xiàn)細(xì)節(jié)的同學(xué)可以看 Popover 的源碼。
總結(jié)彈層組件是 UI 組件庫中很重要的部分,一個逐層抽象的結(jié)構(gòu)可以極大簡化這些組件的開發(fā)和維護(hù)成本。
合理利用 React 的 context 功能可以很方便地解決一些像嵌套彈層一樣看似很麻煩的問題。
如果覺得有所收獲,請給 Zent 點個 star 吧。
*注1: React Fiber 中提供了一個新的 API:ReactDOM. unstable_createPortal ,這個 API 可以將一個組件渲染到指定的 DOM 節(jié)點內(nèi)。
本文由 李晨 首發(fā)于 有贊技術(shù)博客。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/88434.html
摘要:前端日報精選低成本將你的網(wǎng)站切換為漫談組件庫開發(fā)一多層嵌套彈層組件可作的備胎深入理解進(jìn)階系列如何設(shè)計中文刷題系列前端筆試面試題知乎專欄個拯救前端開發(fā)者的工具庫和資源眾成翻譯前端技術(shù)大會震撼登陸,明星團(tuán)隊講師傾城而出前端組件庫我們做 2017-09-08 前端日報 精選 低成本將你的網(wǎng)站切換為 HTTPS漫談 React 組件庫開發(fā)(一):多層嵌套彈層組件Preact: 可作React的...
摘要:前端日報精選譯中一些超級好用的內(nèi)置方法漫談組件庫開發(fā)一多層嵌套彈層組件高階組件淺析的工廠函數(shù)打包優(yōu)化之速度篇中文教程用純實現(xiàn)跳跳球動畫眾成翻譯個幫助你學(xué)習(xí)的快速且久經(jīng)考驗的技巧眾成翻譯自定義屬性使用進(jìn)行動態(tài)更改眾成翻譯真假值知多 2017-08-26 前端日報 精選 【譯】ES6中一些超!級!好!用!的內(nèi)置方法漫談 React 組件庫開發(fā)(一):多層嵌套彈層組件React 高階組件淺析...
摘要:又一篇來自日常開發(fā)的匯總各位客官請對號入席,店小二逐一上菜。解決方案有很多種,例如把字符串?dāng)?shù)組等重組對象數(shù)組,每個元素設(shè)置一個唯一等。另外有個方式推薦使用生成唯一的數(shù)組,和數(shù)據(jù)數(shù)組一起使用,省去提交數(shù)據(jù)時再重組數(shù)組。 又一篇來自日常開發(fā)的匯總:各位客官請對號入席,店小二逐一上菜。 第一道菜:回鍋肉 react數(shù)組循環(huán),基本都會設(shè)置一個唯一的key,表格的對象數(shù)組循環(huán)一般沒什么問題,數(shù)據(jù)...
摘要:但是,最后一步,事件怎么綁定呢這塊沒有深入研究了,不過我想,應(yīng)該這樣去實現(xiàn)也是沒有問題的。的具體做法是,把方法放到了一個叫做的組件上去實現(xiàn)這個功能,然后再把內(nèi)容放進(jìn)這個組件。其他的邏輯比如顯示隱藏之類,全部都放到組件自身上去實現(xiàn)。 1、Dialog組件提供什么功能,解決什么問題? zent的Dialog組件,使用姿勢是這樣的(代碼摘自zent官方文檔:https://www.youza...
摘要:父組件向子組件之間非常常見,通過機(jī)制傳遞即可。我們應(yīng)該聽說過高階函數(shù),這種函數(shù)接受函數(shù)作為輸入,或者是輸出一個函數(shù),比如以及等函數(shù)。在傳遞數(shù)據(jù)的時候,我們可以用進(jìn)一步提高性能。 本文主要談自己在react學(xué)習(xí)的過程中總結(jié)出來的一些經(jīng)驗和資源,內(nèi)容邏輯參考了深入react技術(shù)棧一書以及網(wǎng)上的諸多資源,但也并非完全照抄,代碼基本都是自己實踐,主要為平時個人學(xué)習(xí)做一個總結(jié)和參考。 本文的關(guān)鍵...
閱讀 2975·2021-11-16 11:51
閱讀 2608·2021-09-22 15:02
閱讀 3723·2021-08-04 10:21
閱讀 3605·2019-08-30 15:43
閱讀 1947·2019-08-30 11:04
閱讀 3599·2019-08-29 17:14
閱讀 490·2019-08-29 12:16
閱讀 2933·2019-08-28 18:31