精通react/vue組件設計之配合React Portals實現一個(Drawer)組件

2020-12-23 酷扯兒

本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫

前言

本文是筆者寫組件設計的第六篇文章,內容依次從易到難,今天會用到react的高級API React Portals,它也是很多複雜組件必用的方法之一. 通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.

之所以會寫組件設計相關的文章,是因為作為一名前端優秀的前端工程師,面對各種繁瑣而重複的工作,我們不應該按部就班的去"辛勤勞動",而是要根據已有前端的開發經驗,總結出一套自己的高效開發的方法.

作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心. 為了讓工程師們有更多的時間去考慮業務和產品迭代,我們不得不掌握高質量組件設計的思路和方法.所以筆者將花時間去總結各種業務場景下的組件的設計思路和方法,並用原生框架的語法去實現各種常用組件的開發,希望等讓前端新手或者有一定工作經驗的朋友能有所收穫.

正文

在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先看看實現後的組件效果:

1. 組件設計思路

按照之前筆者總結的組件設計原則,我們第一步是要確認需求. 一個抽屜(Drawer)組件會有如下需求點:

能控制抽屜是否可見能手動配置抽屜的關閉按鈕能控制抽屜的打開方向關閉抽屜時是否銷毀裡面的子元素(這個問題是工作中頻繁遇到的問題)指定 Drawer 掛載的 HTML 節點, 可以將抽屜掛載在任何元素上點擊蒙層可以控制是否允許關閉抽屜能控制遮罩層的展示能自定義抽屜彈出層樣式可以設置抽屜彈出層寬度能控制彈出層層級能控制抽屜彈出方向(上下左右)點擊關閉按鈕時能提供回調供開發者進行相關操作需求收集好之後,作為一個有追求的程式設計師, 會得出如下線框圖:

對於react選手來說,如果沒用typescript,建議大家都用PropTypes, 它是react內置的類型檢測工具,我們可以直接在項目中導入. vue有自帶的屬性檢測方式,這裡就不一一介紹了.

通過以上需求分析, 是不是覺得一個抽屜組件要實現這麼多功能很複雜呢? 確實有點複雜,但是不要怕,有了上面精確的需求分析,我們只需要一步步按照功能點實現就好了.對於我們常用的table組件, modal組件等其實也需要考慮到很多使用場景和功能點, 比如antd的table組件暴露了幾十個屬性,如果不好好理清具體的需求, 實現這樣的組件是非常麻煩的.接下來我們就來看看具體實現.

2. 基於react實現一個Drawer組件

2.1. Drawer組件框架設計

首先我們先根據需求將組件框架寫好,這樣後面寫業務邏輯會更清晰:

import PropTypes from 'prop-types'

import styles from './index.less'

/**

* Drawer 抽屜組件

* @param {visible} bool 抽屜是否可見

* @param {closable} bool 是否顯示右上角的關閉按鈕

* @param {destroyOnClose} bool 關閉時銷毀裡面的子元素

* @param {getContainer} HTMLElement 指定 Drawer 掛載的 HTML 節點, false 為掛載在當前 dom

* @param {maskClosable} bool 點擊蒙層是否允許關閉抽屜

* @param {mask} bool 是否展示遮罩

* @param {drawerStyle} object 用來設置抽屜彈出層樣式

* @param {width} number|string 彈出層寬度

* @param {zIndex} number 彈出層層級

* @param {placement} string 抽屜方向

* @param {onClose} string 點擊關閉時的回調

*/

function Drawer(props) {

const {

closable = true,

destroyOnClose,

getContainer = document.body,

maskClosable = true,

mask = true,

drawerStyle,

width = '300px',

zIndex = 10,

placement = 'right',

onClose,

children

} = props

const childDom = (

<div className={styles.xDrawerWrap}>

<div className={styles.xDrawerMask} ></div>

<div

className={styles.xDrawerContent}

{

children

}

{

!!closable && <span className={styles.xCloseBtn}>X</span>

}

</div>

</div>

)

return childDom

}

export default Drawer

有了這個框架,我們來一步步往裡面實現內容吧.

2.2 實現visible, closable, onClose, mask, maskClosable, width, zIndex, drawerStyle

之所以要先實現這幾個功能,是因為他們實現都比較簡單,不會牽扯到其他複雜邏輯.只需要對外暴露屬性並使用屬性即可. 具體實現如下:

function Drawer(props) {

const {

closable = true,

destroyOnClose,

getContainer = document.body,

maskClosable = true,

mask = true,

drawerStyle,

width = '300px',

zIndex = 10,

placement = 'right',

onClose,

children

} = props

let [visible, setVisible] = useState(props.visible)

const handleClose = () => {

setVisible(false)

onClose && onClose()

}

useEffect(() => {

setVisible(props.visible)

}, [props.visible])

const childDom = (

<div

className={styles.xDrawerWrap}

style={{

width: visible ? '100%' : '0',

zIndex

}}

>

{ !!mask && <div className={styles.xDrawerMask} onClick={maskClosable ? handleClose : null}></div> }

<div

className={styles.xDrawerContent}

style={{

width,

...drawerStyle

}}>

{ children }

{

!!closable && <span className={styles.xCloseBtn} onClick={handleClose}>X</span>

}

</div>

</div>

)

return childDom

}

上述實現過程值得注意的就是我們組件設計採用了react hooks技術, 在這裡用到了useState, useEffect, 如果大家不懂的可以去官網學習, 非常簡單,如果有不懂的可以和筆者交流或者在評論區提問. 抽屜動畫我們通過控制抽屜內容的寬度來實現,配合overflow:hidden, 後面我會單獨附上css代碼供大家參考.

2.3 實現destroyOnClose

destroyOnClose主要是用來清除組件緩存,比較常用的場景就是輸入文本,比如當我是的抽屜的內容是一個表單創建頁面時,我們關閉抽屜希望表單中用戶輸入的內容清空,保證下次進入時用戶能重新創建, 但是實際情況是如果我們不銷毀抽屜裡的子組件, 子組件內容不會清空,用戶下次打開時開始之前的輸入,這明顯不合理. 如下圖所示:

要想清除緩存,首先就要要內部組件重新渲染,所以我們可以通過一個state來控制,如果用戶明確指定了關閉時要銷毀組件,那麼我們就更新這個state,從而這個子元素也就不會有緩存了.具體實現如下:

function Drawer(props) {

// ...

let [isDesChild, setIsDesChild] = useState(false)

const handleClose = () => {

// ...

if(destroyOnClose) {

setIsDesChild(true)

}

}

useEffect(() => {

// ...

setIsDesChild(false)

}, [props.visible])

const childDom = (

<div className={styles.xDrawerWrap}>

<div className={styles.xDrawerContent}

{

isDesChild ? null : children

}

</div>

</div>

)

return childDom

}

上述代碼中我們省略了部分不相關代碼, 主要來關注isDesChild和setIsDesChild, 這個屬性用來根據用戶傳入的destroyOnClose屬性倆判斷是否該更新這個state, 如果destroyOnClose為true,說明要更新,那麼此時當用戶點擊關閉按鈕的時候, 組件將重新渲染, 在用戶再次點開抽屜時, 我們根據props.visible的變化,來重新讓子組件渲染出來,這樣就實現了組件卸載的完整流程.

2.4 實現getContainer

getContainer主要用來控制抽屜組件的渲染位置,默認會渲染到body下, 為了提供更靈活的配置,我們需要讓抽屜可以渲染到任何元素下,這樣又怎麼實現呢? 這塊實現我們可以採用React Portals來實現,具體api介紹如下:

Portal 提供了一種將子節點渲染到存在於父組件以外的 DOM 節點的優秀的方案。第一個參數(child)是任何可渲染的 React 子元素,例如一個元素,字符串或 fragment。第二個參數(container)是一個 DOM 元素。

具體使用如下:

render() {

// `domNode` 是一個可以在任何位置的有效 DOM 節點。

return ReactDOM.createPortal(

this.props.children,

domNode

);

}

所以基於這個api我們就能把抽屜渲染到任何元素下了, 具體實現如下:

const childDom = (

<div

className={styles.xDrawerWrap}

style={{

position: getContainer === false ? 'absolute' : 'fixed',

width: visible ? '100%' : '0',

zIndex

}}

>

{ !!mask && <div className={styles.xDrawerMask} onClick={maskClosable ? handleClose : null}></div> }

<div

className={styles.xDrawerContent}

style={{

width,

[placement]: visible ? 0 : '-100%',

...drawerStyle

}}>

{

isDesChild ? null : children

}

{

!!closable && <span className={styles.xCloseBtn} onClick={handleClose}>X</span>

}

</div>

</div>

)

return getContainer === false ? childDom

: ReactDOM.createPortal(childDom, getContainer)

因為這裡getContainer要支持3種情況,一種是用戶不配置屬性,那麼默認就掛載到body下,還有就是用戶傳的值為false, 那麼就為最近的父元素, 他如果傳一個dom元素,那麼將掛載到該元素下,所以以上代碼我們會分情況考慮,還有一點要注意,當抽屜打開時,我們要讓父元素溢出隱藏,不讓其滾動,所以我們在這裡要設置一下:

useEffect(() => {

setVisible(() => {

if(getContainer !== false && props.visible) {

getContainer.style.overflow = 'hidden'

}

return props.visible

})

setIsDesChild(false)

}, [props.visible, getContainer])

當關閉時恢復邏輯父級的overflow, 避免影響外部樣式:

const handleClose = () => {

onClose && onClose()

setVisible((prev) => {

if(getContainer !== false && prev) {

getContainer.style.overflow = 'auto'

}

return false

})

if(destroyOnClose) {

setIsDesChild(true)

}

}

2.5 實現placement

placement主要用來控制抽屜的彈出方向, 可以從左彈出,也可以從右彈出, 實現過程也比較簡單,我們主要要更具屬性動態修改定位屬性即可,這裡我們會用到es新版的新特性,對象的變量屬性. 核心代碼如下:

<div

className={styles.xDrawerContent}

style={{

width,

[placement]: visible ? 0 : '-100%',

...drawerStyle

}}>

</div>

這樣,無論是上下左右,都可以完美實現了.

2.6 健壯性支持, 我們採用react提供的propTypes工具:

import PropTypes from 'prop-types'

// ...

Drawer.propTypes = {

visible: PropTypes.bool,

closable: PropTypes.bool,

destroyOnClose: PropTypes.bool,

getContainer: PropTypes.element,

maskClosable: PropTypes.bool,

mask: PropTypes.bool,

drawerStyle: PropTypes.object,

width: PropTypes.oneOfType([

PropTypes.string,

PropTypes.number

]),

zIndex: PropTypes.number,

placement: PropTypes.string,

onClose: PropTypes.func

}

關於prop-types的使用官網上有很詳細的案例,這裡說一點就是oneOfType的用法, 它用來支持一個組件可能是多種類型中的一個. 組件相關css代碼如下:

.xDrawerWrap {

top: 0;

height: 100vh;

overflow: hidden;

.xDrawerMask {

position: absolute;

left: 0;

right: 0;

top: 0;

bottom: 0;

background-color: rgba(0, 0, 0, .5);

}

.xDrawerContent {

position: absolute;

top: 0;

padding: 16px;

height: 100%;

transition: all .3s;

background-color: #fff;

box-shadow: 0 0 20px rgba(0,0,0, .2);

.xCloseBtn {

position: absolute;

top: 10px;

right: 10px;

color: #ccc;

cursor: pointer;

}

}

}

通過以上步驟, 一個功能強大的的drawer組件就完成了,關於代碼中的css module和classnames的使用大家可以自己去官網學習,非常簡單.如果不懂的可以在評論區提問,筆者看到後會第一時間解答.

最後

後續筆者將會繼續實現

modal(模態窗),alert(警告提示),badge(徽標),table(表格),tooltip(工具提示條),Skeleton(骨架屏),Message(全局提示),form(form表單),switch(開關),日期/日曆,二維碼識別器組件等組件, 來復盤筆者多年的組件化之旅.

相關焦點

  • 《精通react/vue組件設計》之快速實現一個可定製的進度條組件
    今天要來實現一個高可定製的進度條組件,在介紹組件設計之前,我們先牢記以下幾個原則.每一個組件只負責某一特定的表現或者功能)正文在開始組件設計之前希望大家對css3和js有一定的基礎.我們先看看實現後的組件效果:上圖可以知道封裝後的進度條組件通過對外暴露的接口(react/vue裡面可以看做
  • 精通react/vue組件設計之實現一個輕量級可擴展的模態框組件
    正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先來解構一下Modal組件, 一個Modal分為以下幾個部分:編輯搜圖每一個區塊都可以自定義配置, 也可以組合其他組件.實現後的組件效果:編輯搜圖1.
  • 精通react/vue組件設計之實現一個Tag(標籤)和Empty(空狀態)組件
    "辛勤勞動",而是要根據已有前端的開發經驗,總結出一套自己的高效開發的方法.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.所以, 為了讓工程師們有更多的時間去考慮業務和產品迭代,我們不得不掌握高質量組件設計的思路和方法.所以筆者將花時間去總結各種業務場景下的組件的設計思路和方法,並用原生框架的語法去實現各種常用組件的開發,希望等讓前端新手或者有一定工作經驗的朋友能有所收穫
  • 《精通react/vue組件設計》之實現一個健壯的警告提示(Alert)組件
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言本文是筆者寫組件設計的第七篇文章, 今天帶大家實現一個自帶主題且可關閉的Alert組件, 該組件在諸如Antd或者elementUI等第三方組件庫中都會出現,主要用來提供系統的用戶反饋
  • 精通react/vue組件設計教你實現一個極具創意的加載(Loading)組件
    正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先看看實現後的組件效果:因為動圖體積太大,就不給大家傳gif了,接下來我們具體分析一下該組件的特點.1.
  • 精通React/Vue系列之帶你實現一個功能強大的通知提醒框
    本文將會使用React來開發該組件,也會使用到Javascript中常用的一些設計模式,比如單例模式,但是不管你使用什麼框架來實現,原理都是通用的,如果感興趣的朋友可以用vue也實現以一下。正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先來解構一下Notification組件, 一個Notification分為以下幾個部分:每一個區塊都可以自定義配置, 也可以組合其他組件.並且我們可以配置提醒框出現的位置,就像antd
  • 基於jsoneditor二次封裝一個可實時預覽的json編輯器組件react版
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言做為一名前端開發人員,掌握vue/react/angular等框架已經是必不可少的技能了,我們都知道,vue或react等MVVM框架提倡組件化開發
  • 如何用純css打造類materialUI的按鈕點擊動畫並封裝成react組件
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言作為一個前端框架的重度使用者,在技術選型上也會非常注意其生態和完整性.筆者先後開發過基於vue,react,angular等框架的項目,碧如vue生態的elementUI, ant-design-vue, iView等成熟的UI框架
  • React源碼之組件的實現與首次渲染
    react: v15.0.0 本文講 組件如何編譯 以及 ReactDOM.render 的渲染過程。 babel 的編譯 babel 將 React JSX 編譯成 JavaScript.
  • 快速在你的vue/react應用中實現ssr(服務端渲染)
    默認情況下,可以在瀏覽器中輸出自定義組件,進行生成 DOM和操作 DOM, 也就是我們常說的客戶端渲染, 並且我們大部分主流的場景都是SPA(單頁面)應用, 而隨著 SPA尤其是 React、Vue、Angular 為代表的前端框架的流行
  • 手把手教你用JS/Vue/React實現幸運水果機(80後情懷之作)
    項目體驗地址免費視頻教程分別使用原生JS,Vue和React,手把手教你開發一個H5小遊戲,快速上手Vue和React框架的使用。項目截圖在線體驗在線體驗遊戲介紹幸運水果機是一款街機遊戲,遊戲界面由24個方格拼接成一個正方形,每個方格中都有一個不同的水果圖形,方格下都有一個小燈。
  • React系列(一) -邂逅React開發
    聲明式編程組件化開發:組件化開發頁面目前前端的流行趨勢,我們會講複雜的界面拆分成一個個小的組件;如何合理的進行組件的劃分和設計也是後面我會講到的一個重點;React的優勢React由Facebook來更新和維護,它是大量優秀程式設計師的思想結晶:React的流行不僅僅局限於普通開發工程師對它的認可,大量流行的其他框架借鑑React的思想;Vue.js框架設計之初,有很多的靈感來自Angular和React。
  • React是什麼?React特點及框架整理(React一)-開課吧
    學習是個吃苦的過程,而學編程那就是個更吃苦的事情,一個技術點需要上萬的字來說明,一個實現頁面需要上萬的代碼數據來湊,既然走上了這條路,願大家無怨無悔,今天為大家帶來了關於前端開發項目中javascript庫中recact詳解,你看或者不看,它就在這裡。
  • Vue、React 和 Angular:該選擇哪個框架?
    框架組件框架的性能是由最有價值的部分——它的組件決定的。它們的工作流與接收輸入數據的方式以及對數據的響應方式有關。AngularAngular 的組件命名為 directive(指令) , 它們是由 Angular 跟蹤的 DOM 元素上的標記。
  • React SSR 同構入門與原理
    它有一個非常大的優勢就是,只是首次訪問會請求後臺服務加載相應文件,之後的訪問都是前端自己判斷 URL 展示相關組件,因此除了首次訪問速度慢些之外,之後的訪問速度都很快。執行命令: create-react-app react-csr 創建一個 React SPA 單頁面應用項目 。執行命令: npm run start 啟動項目。
  • react中關於hook介紹及其使用
    簡單來說就是可以使用函數組件去使用react中的一些特性所要解決的問題:解決組件之間復用狀態邏輯很難得問題,hook能解決的就是在你無需修改之前組件結構的情況下復用狀態邏輯,在不使用hook的情況下,需要使用到一些高級的用法如高級組件、provider、customer等,這種方式對於新手來說不太友好
  • 尚矽谷前端視頻周 | React新版教程發布,聽著上頭,學到痴迷!
    教程配備了豐富的圖示、案例、筆記,即便零基礎的小夥伴也可以愉快地學會React技術,僅需一套課程讓你精通React技術棧,讓開發如絲滑般流暢!-一個簡單的Hello組件053.React-樣式的模塊化054.React-vscode中react插件的安裝055.React-組件化編碼流程056.React-TodoList案例_靜態組件057.React-TodoList案例_動態初始化列表058.React-TodoList案例_添加todo
  • 值得選用的十三種優秀React JS框架
    2.Material Kit React源於Google Material Design系統的Material Kit React是構建React UI組件的絕佳選擇。該框架庫提供了多種組件的組合,以及超過1000個完全編碼的組件。每一個組件都有著單獨的一層。
  • React、Angular和Vue三種最流行的前端框架哪一個最好?
    因此,在深入比較之前,我們首先需要確定哪一個是必需的 - 一個庫或一個框架?實際上,庫被設計來執行一些特定的任務,而且通常並不複雜。因此,如果我們使用庫來構建我們的應用程式,那麼我們需要為每個任務選擇一個庫,以及設置任務運行者。庫的主要優點是我們可以完全控制應用程式。但問題是建立該項目需要更多的時間。另一方面,框架被設計用於執行更複雜的事情。
  • 如何設計 React 代碼結構?
    怎樣設計一個項目的文件和組件的結構、甚至是某個組件的內部結構?這個問題永遠沒有正確答案。我編寫的每個功能、每個函數、每個組件都是一副拼圖中的一部分。我很喜歡將我做的東西可視化。我喜歡用畫圖的方式解釋問題時,我希望能夠將用戶真正使用的UI功能反映到代碼中。對於我而言,還有一件重要的事情,那就是能迅速看到整體樣貌,並儘可能簡單地理解組件的功能。這兩個目的都可以通過選擇正確的命名方式以及可視化結構來實現。