自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

《精通React/Vue組件設(shè)計(jì)》之手把手實(shí)現(xiàn)一個(gè)輕量級可擴(kuò)展的模態(tài)框(Modal)組件

開發(fā) 前端
本文是筆者寫組件設(shè)計(jì)的第九篇文章, 今天帶大家實(shí)現(xiàn)一個(gè)輕量級且可靈活配置組合的模態(tài)框(Modal)組件, 該組件在諸如Antd或者elementUI等第三方組件庫中都會(huì)出現(xiàn)。

前言

本文是筆者寫組件設(shè)計(jì)的第九篇文章, 今天帶大家實(shí)現(xiàn)一個(gè)輕量級且可靈活配置組合的模態(tài)框(Modal)組件, 該組件在諸如Antd或者elementUI等第三方組件庫中都會(huì)出現(xiàn),主要用來提供系統(tǒng)的用戶反饋。

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


[筆記]前端組件的一般分類:

  • 通用型組件: 比如Button, Icon等。
  • 布局型組件: 比如Grid, Layout布局等。
  • 導(dǎo)航型組件: 比如面包屑Breadcrumb, 下拉菜單Dropdown, 菜單Menu等。
  • 數(shù)據(jù)錄入型組件: 比如form表單, Switch開關(guān), Upload文件上傳等。
  • 數(shù)據(jù)展示型組件: 比如Avator頭像, Table表格, List列表等。
  • 反饋型組件: 比如Progress進(jìn)度條, Drawer抽屜, Modal對話框等。
  • 其他業(yè)務(wù)類型

所以我們在設(shè)計(jì)組件系統(tǒng)的時(shí)候可以參考如上分類去設(shè)計(jì),該分類也是antd, element, zend等主流UI庫的分類方式。

正文

在開始組件設(shè)計(jì)之前希望大家對css3和js有一定的基礎(chǔ),并了解基本的react/vue語法.我們先來解構(gòu)一下Modal組件, 一個(gè)Modal分為以下幾個(gè)部分:

圖片

每一個(gè)區(qū)塊都可以自定義配置, 也可以組合其他組件.實(shí)現(xiàn)后的組件效果:

圖片

1、組件設(shè)計(jì)思路

按照之前筆者總結(jié)的組件設(shè)計(jì)原則,我們第一步是要確認(rèn)需求. 模態(tài)框(Modal)組件一般會(huì)有如下需求點(diǎn):

  • 能控制Modal主體的樣式
  • 提供Modal完全關(guān)閉后的回調(diào)
  • 能控制取消按鈕文字和樣式
  • 能控制確認(rèn)按鈕文字和樣式
  • 控制modal展示的位置
  • 控制是否顯示右上角的關(guān)閉按鈕
  • 可以配置自定義關(guān)閉圖標(biāo)
  • 配置關(guān)閉時(shí)是否銷毀Modal里的子元素
  • 自定義模態(tài)框底部內(nèi)容
  • 控制是否支持鍵盤esc關(guān)閉
  • 控制是否展示遮罩
  • 控制點(diǎn)擊蒙層是否允許關(guān)閉
  • 自定義遮罩樣式
  • 自定義標(biāo)題
  • 控制對話框是否可見
  • 自定義對話框?qū)挾?/strong>
  • 暴露點(diǎn)擊遮罩層或右上角叉或取消按鈕的回調(diào)
  • 提供點(diǎn)擊確定回調(diào)

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

圖片

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

2、基于react實(shí)現(xiàn)一個(gè)Modal組件

(1)Modal組件框架設(shè)計(jì)

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

import PropTypes from 'prop-types'
import './index.less'
/**
 * Modal Modal組件
 * @param {afterClose} func Modal完全關(guān)閉后的回調(diào)
 * @param {bodyStyle} object Modal body的樣式
 * @param {cancelText} string|ReactNode 取消按鈕文字
 * @param {centered} bool 居中展示Modal
 * @param {closable} bool 是否展示右上角的關(guān)閉按鈕
 * @param {closeIcon} ReactNode 自定義關(guān)閉圖標(biāo)
 * @param {destroyOnClose} bool 關(guān)閉時(shí)銷毀Modal里的子元素
 * @param {footer} null|ReactNode 底部內(nèi)容,當(dāng)不需要底部默認(rèn)按鈕時(shí),可以設(shè)置為footer={null}
 * @param {keyboard} bool 是否支持鍵盤的esc鍵退出
 * @param {mask} bool 是否展示遮罩
 * @param {maskclosable} bool 點(diǎn)擊蒙層是否允許關(guān)閉
 * @param {maskStyle} object 遮罩樣式
 * @param {okText} string|ReactNode 確認(rèn)按鈕的文本
 * @param {title} string|ReactNode 標(biāo)題內(nèi)容
 * @param {visible} bool Modal是否可見
 * @param {width} string Modal寬度
 * @param {onCancel} func 點(diǎn)擊遮罩或者取消按鈕,或者鍵盤esc按鍵時(shí)的回調(diào)
 * @param {onOk} func 點(diǎn)擊確定的回調(diào)
 */
function Modal(props) {
  const {
    afterClose,
    bodyStyle,
    cancelText,
    centered,
    closable,
    closeIcon,
    destroyOnClose,
    footer,
    keyboard,
    mask,
    maskclosable,
    maskStyle,
    okText,
    title,
    visible,
    width,
    onCancel,
    onOk
  } = props
  return <div className="xModalWrap">
    <div className="xModalContent">
      <div className="xModalHeader">
      </div>
      <div className="xModalBody">
      </div>
      <div className="xModalFooter">
      </div>
    </div>
    <div className="xModalMask"></div>
  </div>
}
export default Modal

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

(2)實(shí)現(xiàn)基礎(chǔ)配置功能

基礎(chǔ)配置功能往往和業(yè)務(wù)邏輯無關(guān), 僅僅用來控制元素的顯示隱藏等,由于其非常容易實(shí)現(xiàn),所以我們先來實(shí)現(xiàn)以下這些屬性的功能:

  • bodyStyle
  • cancelText
  • closable
  • closeIcon
  • footer
  • mask
  • maskStyle
  • okText
  • title
  • width

這幾個(gè)功能在框架搭建好之后已經(jīng)部分實(shí)現(xiàn)了,是因?yàn)樗麄兌急容^簡單,不會(huì)牽扯到其他復(fù)雜邏輯。只需要對外暴露屬性并使用屬性即可。具體實(shí)現(xiàn)如下:

// ...
function Modal(props) {
  // ...
  return <div className="xModalWrap">
    <div 
      className="xModalContent"
      style={{
        width
      }}
    >
      <div className="xModalHeader">
        <div className="xModalTitle">
          { title }
        </div>
      </div>
      {
        closable &&
        <span className="xModalCloseBtn">
          { closeIcon || <Icon type="FaTimes" /> }
        </span>
      }
      <div className="xModalBody" style={bodyStyle}>
        { children }
      </div>
      {
        footer === null ? null :
          <div className="xModalFooter">
            {
              footer ? footer :
                <div className="xFooterBtn">
                  <Button className="xFooterBtnCancel" type="pure">{ cancelText }</Button>
                  <Button className="xFooterBtnOk">{ okText }</Button>
                </div>
            }
          </div>
      }
    </div>
    {
      mask && <div className="xModalMask" style={maskStyle}></div>
    }
  </div>
}

通過以上實(shí)現(xiàn),我們很容易控制一個(gè)modal組件具體顯示那些元素,以及那些元素是可關(guān)閉modal的,具體案例如下:

  1. 去除footer(通過設(shè)置footer為null)。

圖片

  1. 去除右上角的關(guān)閉按鈕。

圖片

  1. 去除mask遮罩。

圖片

(3)實(shí)現(xiàn)visible(帶有彈窗出來和隱藏的動(dòng)畫animation)

熟悉antd或者element的朋友都知道,visible用來控制modal的顯示和隱藏,我們這里也來實(shí)現(xiàn)同樣的功能,關(guān)于隱藏和顯示的動(dòng)畫,我們這里用transform:scale來實(shí)現(xiàn)。先來看看實(shí)現(xiàn)效果吧:

圖片

這里筆者使用了react hooks的useState這個(gè)API,來設(shè)置彈窗可見性的state,modal默認(rèn)不可見。具體邏輯如下:

let [isHidden, setHidden] = useState(!props.visible)
const handleClose = () => {
    setHidden(false)
}

html結(jié)構(gòu)如下:

<div className="xModalWrap" style={{display: isHidden ? 'none' : 'block'}}>

由以上代碼我們知道模態(tài)框的顯示隱藏是通過設(shè)置display:none/block來控制的,但是我們都知道display:none是不能執(zhí)行動(dòng)畫效果的,為了實(shí)現(xiàn)內(nèi)容彈窗的動(dòng)畫,我們這里采用了@keyframe動(dòng)畫,對于低版本瀏覽器也采用了很好的向下兼容。具體css代碼如下:

@keyframes xSpread {
    0% {
        opacity: 0;
        transform: scale(0);
    }
    100% {
        opacity: 1;
        transform: scale(1);
    }
}

(4)實(shí)現(xiàn)centered

centered屬性的作用就是來控制彈窗內(nèi)容距離整個(gè)遮罩或者可視區(qū)域的位置的,值為true則居與遮罩或者可視區(qū)域的正中心。因?yàn)槲覀兡J(rèn)設(shè)置的modal內(nèi)容區(qū)域的位置是左右居中,頂部距離可視區(qū)域頂部100px,所以這里我們實(shí)現(xiàn)如下:

<div className={`xModalContent${centered ? ' xCentered' : ''}`}>
&.xCentered {
    top: 50%;
    transform: translateY(-50%);
}

這個(gè)實(shí)現(xiàn)也非常簡單,就是通過屬性centered來動(dòng)態(tài)的設(shè)置類名即可。

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

這個(gè)功能意思是在彈窗關(guān)閉時(shí)是否清除子元素,我在:《精通react/vue組件設(shè)計(jì)》之配合React Portals實(shí)現(xiàn)一個(gè)功能強(qiáng)大的抽屜(Drawer)組件這篇文章中有詳細(xì)的介紹,大家感興趣可以研究以下,這里我指介紹實(shí)現(xiàn)過程。
當(dāng)destroyOnClose為true時(shí),我們銷毀子元素即可,通過維護(hù)一個(gè)state來實(shí)現(xiàn)組件的重新渲染。要想實(shí)現(xiàn)該功能,我們需要處理如下幾個(gè)事件:

  • 當(dāng)點(diǎn)擊關(guān)閉按鈕時(shí),根據(jù)destroyOnClose銷毀子組件。
  • 當(dāng)點(diǎn)擊確認(rèn)按鈕時(shí),根據(jù)destroyOnClose銷毀子組件。
  • 當(dāng)visible為true,根據(jù)destroyOnClose將子組件重新渲染出來。
    具體實(shí)現(xiàn)代碼如下:
// 關(guān)閉事件(關(guān)閉和確認(rèn)事件邏輯基本一致,這里就不單獨(dú)寫了)
const handleClose = () => {
    setHidden(true)
    if(destroyOnClose) {
      setDestroyChild(true)
    }
    document.body.style.overflow = 'auto'
    onCancel && onCancel()
}


// visivle/destroyOnClose更新時(shí),重新渲染子組件
useEffect(() => {
    if(visible) {
      if(destroyOnClose) {
        setDestroyChild(true)
      }
    }
  }, [visible, destroyOnClose])

這樣我們就實(shí)現(xiàn)了彈窗關(guān)閉時(shí)銷毀組件的功能。

(6)實(shí)現(xiàn)鍵盤按鍵ESC時(shí)關(guān)閉模態(tài)框(Modal)

為了更好的用戶體檢,筆者的Modal組件支持鍵盤事件,我們都知道鍵盤的ESC對應(yīng)的事件碼為27,那么我們就能根據(jù)這個(gè)原理來實(shí)現(xiàn)鍵盤按鍵ESC時(shí)關(guān)閉模態(tài)框:

useEffect(() => {
    document.onkeydown = function (event) {
      let e = event || window.event || arguments.callee.caller.arguments[0]
      if (e && e.keyCode === 27) {
        handleClose()
      }
    }
  }, [])

因?yàn)槭录O(jiān)聽只需要執(zhí)行一次,所以useEffect的依賴設(shè)置為空數(shù)組即可。雖然這樣已經(jīng)基本實(shí)現(xiàn)了鍵盤關(guān)閉的功能,但是這樣的代碼明顯不夠優(yōu)雅,所以我們來完善以下,我們可以將鍵盤關(guān)閉的方法抽離出來,然后在useEffect的第一個(gè)回調(diào)函數(shù)中返回另一個(gè)函數(shù)(該函數(shù)里是組件卸載前的鉤子),當(dāng)組件卸載時(shí)我們將事件監(jiān)聽移除,這樣可以提高一些性能,對內(nèi)存優(yōu)化也有幫助:

const closeModal = function (event) {
    let e = event || window.event || arguments.callee.caller.arguments[0]
    if (e && e.keyCode === 27) {
      handleClose()
    }
  }


 useEffect(() => {
    document.addEventListener('keydown', closeModal, false)
    return () => {
      document.removeEventListener('keydown', closeModal, false)
    }
  }, [])

通過這種方式,代碼和功能實(shí)現(xiàn)上是不是會(huì)更優(yōu)雅呢?

(7)實(shí)現(xiàn)afterClose

afterClose的作用主要是在模態(tài)框關(guān)閉之后執(zhí)行某個(gè)回調(diào)函數(shù)。我們使用class組件很好實(shí)現(xiàn)這個(gè)功能,因?yàn)閟etState可以傳兩個(gè)參數(shù),一個(gè)是更新state的回調(diào),另一個(gè)是state更新之后的回調(diào),我們只需要把a(bǔ)fterClose放到更新后的回調(diào)即可,也就是第二個(gè)參數(shù)回調(diào)里。但是我們modal組件目前是用react hooks和函數(shù)式組件寫的,那么怎么實(shí)現(xiàn)狀態(tài)更新后的回調(diào)呢?筆者這里提供一個(gè)實(shí)現(xiàn)思路,利用閉包來實(shí)現(xiàn),核心代碼如下:

// 函數(shù)組件外部
let hiddenCount = 0;
//  函數(shù)組件內(nèi)部
useEffect(() => {
    if(isHidden && hiddenCount) {
      hiddenCount = 0
      afterClose && afterClose()
    }
    hiddenCount = 1
  }, [isHidden])

我們知道useEffect不僅僅可以實(shí)現(xiàn)監(jiān)聽掛載組件的鉤子,也同樣能監(jiān)聽state更新,我們利用這一點(diǎn)來實(shí)現(xiàn)該功能,值得注意的是我們要在執(zhí)行afterClose前重置hiddenCount,避免其他使用modal組件的函數(shù)的影響。

(8)健壯性支持, 我們采用react提供的propTypes工具:

import PropTypes from 'prop-types'
// ...
Modal.propTypes = {
  afterClose: PropTypes.func,
  bodyStyle: PropTypes.object,
  cancelText: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.element
  ]),
  centered: PropTypes.bool,
  closable: PropTypes.bool,
  closeIcon: PropTypes.element,
  destroyOnClose: PropTypes.bool,
  footer: PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.object
  ]),
  keyboard: PropTypes.bool,
  mask: PropTypes.bool,
  maskclosable: PropTypes.bool,
  maskStyle: PropTypes.object,
  okText: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.element
  ]),
  title: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.element
  ]),
  visible: PropTypes.bool,
  width: PropTypes.string,
  onCancel: PropTypes.func,
  onOk: PropTypes.func
}

關(guān)于prop-types的使用官網(wǎng)上有很詳細(xì)的案例,這里說一點(diǎn)就是oneOfType的用法, 它用來支持一個(gè)組件可能是多種類型中的一個(gè)。組件完整css代碼如下:

.xModalWrap {
    position: fixed;
    z-index: 999;
    top: 0;
    left: 0;
    width: 100%;
    bottom: 0;
    overflow: hidden;
    .xModalContent {
        position: relative;
        z-index: 1000;
        margin-left: auto;
        margin-right: auto;
        position: relative;
        top: 100px;
        background-color: #fff;
        background-clip: padding-box;
        border-radius: 4px;
        -webkit-box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        pointer-events: auto;
        animation: xSpread .3s;
        &.xCentered {
            top: 50%;
            transform: translateY(-50%);
        }
        .xModalHeader {
            padding: 16px 24px;
            color: rgba(0, 0, 0, 0.65);
            background: #fff;
            border-bottom: 1px solid #e8e8e8;
            border-radius: 4px 4px 0 0;
            .xModalTitle {
                margin: 0;
                color: rgba(0, 0, 0, 0.85);
                font-weight: 500;
                font-size: 16px;
                line-height: 22px;
                word-wrap: break-word;
            }
        }
        .xModalCloseBtn {
            position: absolute;
            top: 0;
            right: 0;
            z-index: 10;
            padding: 0;
            width: 56px;
            height: 56px;
            color: rgba(0, 0, 0, 0.45);
            font-size: 16px;
            line-height: 56px;
            text-align: center;
            text-decoration: none;
            background: transparent;
            border: 0;
            outline: 0;
            cursor: pointer;
        }
        .xModalBody {
            padding: 16px 24px;
        }
        .xModalFooter {
            padding: 10px 16px;
            text-align: right;
            background: transparent;
            border-top: 1px solid #e8e8e8;
            border-radius: 0 0 4px 4px;
            .xFooterBtn {
                .xFooterBtnCancel, .xFooterBtnOk {
                    margin-left: 6px;
                    margin-right: 6px;
                }
            }
        }
    }
    .xModalMask {
        position: fixed;
        z-index: 999;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        overflow: hidden;
        background-color: rgba(0,0,0, .5);
    }
}


@keyframes xSpread {
    0% {
        opacity: 0;
        // 之所以要再加translateY(-50%),是為了防止動(dòng)畫抖動(dòng)
        transform: translateY(-50%) scale(0);
    }
    100% {
        opacity: 1;
        transform: translateY(-50%) scale(1);
    }
}

通過以上步驟, 一個(gè)健壯的的Modal組件就完成了.Modal組件算是組件庫中中等復(fù)雜的組件,如果不懂的可以在評論區(qū)提問,筆者看到后會(huì)第一時(shí)間解答。

(9)使用Modal組件

我們可以通過如下方式使用它:

<Modal title="xui基礎(chǔ)彈窗" centered mask={false} visible={false}>
    <p>我是彈窗內(nèi)容</p>
    <p>我是彈窗內(nèi)容</p>
    <p>我是彈窗內(nèi)容</p>
    <p>我是彈窗內(nèi)容</p>
</Modal>

筆者已經(jīng)將實(shí)現(xiàn)過的組件發(fā)布到npm上了,大家如果感興趣可以直接用npm安裝后使用,方式如下:

npm i @alex_xu/xui


// 導(dǎo)入xui
import {
  Button,
  Skeleton,
  Empty,
  Progress,
  Tag,
  Switch,
  Drawer,
  Badge,
  Alert
} from '@alex_xu/xui'

該組件庫支持按需導(dǎo)入,我們只需要在項(xiàng)目里配置babel-plugin-import即可,具體配置如下:

// .babelrc
"plugins": [
  ["import", { "libraryName": "@alex_xu/xui", "style": true }]
]

npm庫截圖如下:

圖片

最后

后續(xù)筆者將會(huì)繼續(xù)實(shí)現(xiàn)

  • badge(徽標(biāo))
  • table(表格)
  • tooltip(工具提示條)
  • Skeleton(骨架屏)
  • Message(全局提示)
  • form(form表單)
  • switch(開關(guān))
  • 日期/日歷
  • 二維碼識別器組件

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

責(zé)任編輯:姜華 來源: 趣談前端
相關(guān)推薦

2022-03-10 09:00:37

提醒框ReactVue

2023-09-05 20:17:18

typescriptPropTypesreact

2023-04-28 09:30:40

vuereact

2022-09-22 12:38:46

antd form組件代碼

2020-12-15 08:58:07

Vue編輯器vue-cli

2021-03-31 08:01:24

React Portareactcss3

2016-09-26 15:14:28

Javascript前端vue

2020-12-02 12:29:24

Vue無限級聯(lián)樹形

2009-07-14 18:05:28

輕量級Swing組件

2009-07-17 14:38:51

輕量級Swing組件

2022-06-28 15:29:56

Python編程語言計(jì)時(shí)器

2018-11-22 09:17:21

消息推送系統(tǒng)

2022-01-24 11:02:27

PySimpleGUPython計(jì)算器

2021-06-21 15:49:39

React動(dòng)效組件

2021-06-22 10:43:03

Webpack loader plugin

2022-11-07 18:36:03

組件RPC框架

2021-11-10 11:40:42

數(shù)據(jù)加解密算法

2024-11-20 13:18:21

2019-08-26 09:25:23

RedisJavaLinux

2022-02-16 16:24:05

HarmonyOS鴻蒙操作系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號