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