<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          《精通react/vue組件設(shè)計》之配合React Portals實(shí)現(xiàn)一個功能強(qiáng)大的抽屜組件

          共 14738字,需瀏覽 30分鐘

           ·

          2021-04-06 20:16

          前言

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

          之所以會寫組件設(shè)計相關(guān)的文章,是因?yàn)樽鳛橐幻岸藘?yōu)秀的前端工程師,面對各種繁瑣而重復(fù)的工作,我們不應(yīng)該按部就班的去"辛勤勞動",而是要根據(jù)已有前端的開發(fā)經(jīng)驗(yàn),總結(jié)出一套自己的高效開發(fā)的方法.

          作為數(shù)據(jù)驅(qū)動的領(lǐng)導(dǎo)者react/vue等MVVM框架的出現(xiàn),幫我們減少了工作中大量的冗余代碼, 一切皆組件的思想深得人心. 為了讓工程師們有更多的時間去考慮業(yè)務(wù)和產(chǎn)品迭代,我們不得不掌握高質(zhì)量組件設(shè)計的思路和方法.所以筆者將花時間去總結(jié)各種業(yè)務(wù)場景下的組件的設(shè)計思路和方法,并用原生框架的語法去實(shí)現(xiàn)各種常用組件的開發(fā),希望能讓前端新手或者有一定工作經(jīng)驗(yàn)的朋友能有所收獲.

          如果對于react/vue組件設(shè)計原理不熟悉的,可以參考我的之前寫的組件設(shè)計系列文章:

          正文

          在開始組件設(shè)計之前希望大家對css3和js有一定的基礎(chǔ),并了解基本的react/vue語法.我們先看看實(shí)現(xiàn)后的組件效果:

          1. 組件設(shè)計思路

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

          • 能控制抽屜是否可見

          • 能手動配置抽屜的關(guān)閉按鈕

          • 能控制抽屜的打開方向

          • 關(guān)閉抽屜時是否銷毀里面的子元素(這個問題是工作中頻繁遇到的問題)

          • 指定 Drawer 掛載的 HTML 節(jié)點(diǎn), 可以將抽屜掛載在任何元素上

          • 點(diǎn)擊蒙層可以控制是否允許關(guān)閉抽屜

          • 能控制遮罩層的展示

          • 能自定義抽屜彈出層樣式

          • 可以設(shè)置抽屜彈出層寬度

          • 能控制彈出層層級

          • 能控制抽屜彈出方向(上下左右)

          • 點(diǎn)擊關(guān)閉按鈕時能提供回調(diào)供開發(fā)者進(jìn)行相關(guān)操作

          需求收集好之后,作為一個有追求的程序員, 會得出如下線框圖:



          對于react選手來說,如果沒用typescript,建議大家都用PropTypes, 它是react內(nèi)置的類型檢測工具,我們可以直接在項(xiàng)目中導(dǎo)入. vue有自帶的屬性檢測方式,這里就不一一介紹了.

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

          2. 基于react實(shí)現(xiàn)一個Drawer組件

          2.1. Drawer組件框架設(shè)計

          首先我們先根據(jù)需求將組件框架寫好,這樣后面寫業(yè)務(wù)邏輯會更清晰:

          import PropTypes from 'prop-types'
          import styles from './index.less'

          /**
           * Drawer 抽屜組件
           * @param {visible} bool 抽屜是否可見
           * @param {closable} bool 是否顯示右上角的關(guān)閉按鈕
           * @param {destroyOnClose} bool 關(guān)閉時銷毀里面的子元素
           * @param {getContainer} HTMLElement 指定 Drawer 掛載的 HTML 節(jié)點(diǎn), false 為掛載在當(dāng)前 dom
           * @param {maskClosable} bool 點(diǎn)擊蒙層是否允許關(guān)閉抽屜
           * @param {mask} bool 是否展示遮罩
           * @param {drawerStyle} object 用來設(shè)置抽屜彈出層樣式
           * @param {width} number|string 彈出層寬度
           * @param {zIndex} number 彈出層層級
           * @param {placement} string 抽屜方向
           * @param {onClose} string 點(diǎn)擊關(guān)閉時的回調(diào)
           */

          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

          有了這個框架,我們來一步步往里面實(shí)現(xiàn)內(nèi)容吧.

          2.2 實(shí)現(xiàn)visible, closable, onClose, mask, maskClosable, width, zIndex, drawerStyle

          之所以要先實(shí)現(xiàn)這幾個功能,是因?yàn)樗麄儗?shí)現(xiàn)都比較簡單,不會牽扯到其他復(fù)雜邏輯.只需要對外暴露屬性并使用屬性即可. 具體實(shí)現(xiàn)如下:

          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
          }

          上述實(shí)現(xiàn)過程值得注意的就是我們組件設(shè)計采用了react hooks技術(shù), 在這里用到了useState, useEffect, 如果大家不懂的可以去官網(wǎng)學(xué)習(xí), 非常簡單,如果有不懂的可以和筆者交流或者在評論區(qū)提問. 抽屜動畫我們通過控制抽屜內(nèi)容的寬度來實(shí)現(xiàn),配合overflow:hidden, 后面我會單獨(dú)附上css代碼供大家參考.

          2.3 實(shí)現(xiàn)destroyOnClose

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

          要想清除緩存,首先就要要內(nèi)部組件重新渲染,所以我們可以通過一個state來控制,如果用戶明確指定了關(guān)閉時要銷毀組件,那么我們就更新這個state,從而這個子元素也就不會有緩存了.具體實(shí)現(xiàn)如下:
          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
          }

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

          2.4 實(shí)現(xiàn)getContainer

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

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

          具體使用如下:
          render() {
            // `domNode` 是一個可以在任何位置的有效 DOM 節(jié)點(diǎn)。
            return ReactDOM.createPortal(
              this.props.children,
              domNode
            );
          }
          所以基于這個api我們就能把抽屜渲染到任何元素下了, 具體實(shí)現(xiàn)如下:
          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)

          因?yàn)檫@里getContainer要支持3種情況,一種是用戶不配置屬性,那么默認(rèn)就掛載到body下,還有就是用戶傳的值為false, 那么就為最近的父元素, 他如果傳一個dom元素,那么將掛載到該元素下,所以以上代碼我們會分情況考慮,還有一點(diǎn)要注意,當(dāng)抽屜打開時,我們要讓父元素溢出隱藏,不讓其滾動,所以我們在這里要設(shè)置一下:

          useEffect(() => {
              setVisible(() => {
                if(getContainer !== false && props.visible) {
                  getContainer.style.overflow = 'hidden'
                }
                return props.visible
              })
              setIsDesChild(false)
            }, [props.visible, getContainer])
          當(dāng)關(guān)閉時恢復(fù)邏輯父級的overflow, 避免影響外部樣式:
          const handleClose = () => {
              onClose && onClose()
              setVisible((prev) => {
                if(getContainer !== false && prev) {
                  getContainer.style.overflow = 'auto'
                }
                return false
              })
              if(destroyOnClose) {
                setIsDesChild(true)
              }
            }

          2.5 實(shí)現(xiàn)placement

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

          <div 
            className={styles.xDrawerContent} 
            style={{
              width,
              [placement]: visible ? 0 : '-100%',
              ...drawerStyle
              }}>

           </div>

          這樣,無論是上下左右,都可以完美實(shí)現(xiàn)了.

          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
          }

          關(guān)于prop-types的使用官網(wǎng)上有很詳細(xì)的案例,這里說一點(diǎn)就是oneOfType的用法, 它用來支持一個組件可能是多種類型中的一個. 組件相關(guān)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;
          }
          }
          }

          通過以上步驟, 一個功能強(qiáng)大的的drawer組件就完成了,關(guān)于代碼中的css module和classnames的使用大家可以自己去官網(wǎng)學(xué)習(xí),非常簡單.如果不懂的可以在評論區(qū)提問,筆者看到后會第一時間解答.

          擴(kuò)展

          目前筆者已經(jīng)將完成的組件庫發(fā)布到npm上了,大家可以通過npm安裝包的方式使用:

          npm i @alex_xu/xui

          // 使用
          import { Button, Alert } from '@alex_xu/xui'

          在線文檔地址: xui——基于react的輕量級UI組件庫

          npm包地址: @alex_xu/xui

          最后

          后續(xù)筆者已經(jīng)實(shí)現(xiàn)

          • modal(模態(tài)窗),

          • alert(警告提示),

          • badge(徽標(biāo)),

          • table(表格),

          • tooltip(工具提示條),

          • Skeleton(骨架屏),

          • Message(全局提示),

          • form(form表單),

          • switch(開關(guān)),

          • 日期/日歷,

          • 二維碼識別器組件

          等組件, 來復(fù)盤筆者多年的組件化之旅.

          如果想獲取組件設(shè)計系列完整源碼, 或者想學(xué)習(xí)更多H5游戲webpack,node,gulp,css3,javascript,nodeJScanvas數(shù)據(jù)可視化等前端知識和實(shí)戰(zhàn),歡迎在公號《趣談前端》加入我們的技術(shù)群一起學(xué)習(xí)討論,共同探索前端的邊界。

          ??愛心三連擊

          1.看到這里了就點(diǎn)個在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動力。

          2.關(guān)注公眾號趣談前端,進(jìn)程序員優(yōu)質(zhì)學(xué)習(xí)交流群, 字節(jié), 阿里大佬和你一起學(xué)習(xí)成長!

          3.也可添加微信【Mr_xuxiaoxi】獲取大廠內(nèi)推機(jī)會。

          瀏覽 72
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色一级视频免费观看 | 国产日产精品一区二区三区四区 | 日皮视频免费观看 | 欧美丰满大爆乳波霸奶 | 日韩A片网站 |