本文是筆者寫組件設計的第九篇文章, 今天帶你們實現一個輕量級且可靈活配置組合的模態框(Modal)組件, 該組件在諸如Antd或者elementUI等第三方組件庫中都會出現,主要用來提供系統的用戶反饋.javascript
之因此會寫組件設計相關的文章,是由於做爲一名前端優秀的前端工程師,面對各類繁瑣而重複的工做,咱們不該該循序漸進的去"辛勤勞動",而是要根據已有前端的開發經驗,總結出一套本身的高效開發的方法.css
因此咱們在設計組件系統的時候能夠參考如上分類去設計,該分類也是antd, element, zend等主流UI庫的分類方式.html
在開始組件設計以前但願你們對css3和js有必定的基礎,並瞭解基本的react/vue語法.咱們先來解構一下Modal組件, 一個Modal分爲如下幾個部分: 前端
按照以前筆者總結的組件設計原則,咱們第一步是要確認需求. 模態框(Modal)組件通常會有以下需求點:vue
需求收集好以後,做爲一個有追求的程序員, 會得出以下線框圖: java
首先咱們先根據需求將組件框架寫好,這樣後面寫業務邏輯會更清晰:node
import PropTypes from 'prop-types'
import './index.less'
/** * Modal Modal組件 * @param {afterClose} func Modal徹底關閉後的回調 * @param {bodyStyle} object Modal body的樣式 * @param {cancelText} string|ReactNode 取消按鈕文字 * @param {centered} bool 居中展現Modal * @param {closable} bool 是否展現右上角的關閉按鈕 * @param {closeIcon} ReactNode 自定義關閉圖標 * @param {destroyOnClose} bool 關閉時銷燬Modal裏的子元素 * @param {footer} null|ReactNode 底部內容,當不須要底部默認按鈕時,能夠設置爲footer={null} * @param {keyboard} bool 是否支持鍵盤的esc鍵退出 * @param {mask} bool 是否展現遮罩 * @param {maskclosable} bool 點擊蒙層是否容許關閉 * @param {maskStyle} object 遮罩樣式 * @param {okText} string|ReactNode 確認按鈕的文本 * @param {title} string|ReactNode 標題內容 * @param {visible} bool Modal是否可見 * @param {width} string Modal寬度 * @param {onCancel} func 點擊遮罩或者取消按鈕,或者鍵盤esc按鍵時的回調 * @param {onOk} func 點擊肯定的回調 */
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
複製代碼
有了這個框架,咱們來一步步往裏面實現內容吧.react
基礎配置功能每每和業務邏輯無關, 僅僅用來控制元素的顯示隱藏等,因爲其很是容易實現,因此咱們先來實現如下這些屬性的功能:webpack
這幾個功能在框架搭建好以後已經部分實現了,是由於他們都比較簡單,不會牽扯到其餘複雜邏輯.只須要對外暴露屬性並使用屬性便可. 具體實現以下:css3
// ...
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> } 複製代碼
經過以上實現,咱們很容易控制一個modal組件具體顯示那些元素,以及那些元素是可關閉modal的,具體案例以下:
熟悉antd或者element的朋友都知道,visible用來控制modal的顯示和隱藏,咱們這裏也來實現一樣的功能,關於隱藏和顯示的動畫,咱們這裏用transform:scale來實現。先來看看實現效果吧:
let [isHidden, setHidden] = useState(!props.visible)
const handleClose = () => {
setHidden(false)
}
複製代碼
html結構以下:
<div className="xModalWrap" style={{display: isHidden ? 'none' : 'block'}}>
複製代碼
由以上代碼咱們知道模態框的顯示隱藏是經過設置display:none/block來控制的,可是咱們都知道display:none是不能執行動畫效果的,爲了實現內容彈窗的動畫,咱們這裏採用了@keyframe動畫,對於低版本瀏覽器也採用了很好的向下兼容。具體css代碼以下:
@keyframes xSpread {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
複製代碼
centered屬性的做用就是來控制彈窗內容距離整個遮罩或者可視區域的位置的,值爲true則居與遮罩或者可視區域的正中心。由於咱們默認設置的modal內容區域的位置是左右居中,頂部距離可視區域頂部100px,因此這裏咱們實現以下:
<div className={`xModalContent${centered ? ' xCentered' : ''}`}>
複製代碼
css代碼以下:
&.xCentered {
top: 50%;
transform: translateY(-50%);
}
複製代碼
這個實現也很是簡單,就是經過屬性centered來動態的設置類名便可。
這個功能意思是在彈窗關閉時是否清除子元素,我在:《精通react/vue組件設計》之配合React Portals實現一個功能強大的抽屜(Drawer)組件這篇文章中有詳細的介紹,你們感興趣能夠研究如下,這裏我指介紹實現過程。 當destroyOnClose爲true時,咱們銷燬子元素便可,經過維護一個state來實現組件的從新渲染。要想實現該功能,咱們須要處理以下幾個事件:
// 關閉事件(關閉和確認事件邏輯基本一致,這裏就不單獨寫了)
const handleClose = () => {
setHidden(true)
if(destroyOnClose) {
setDestroyChild(true)
}
document.body.style.overflow = 'auto'
onCancel && onCancel()
}
// visivle/destroyOnClose更新時,從新渲染子組件
useEffect(() => {
if(visible) {
if(destroyOnClose) {
setDestroyChild(true)
}
}
}, [visible, destroyOnClose])
複製代碼
這樣咱們就實現了彈窗關閉時銷燬組件的功能。
爲了更好的用戶體檢,筆者的Modal組件支持鍵盤事件,咱們都知道鍵盤的ESC對應的事件碼爲27,那麼咱們就能根據這個原理來實現鍵盤按鍵ESC時關閉模態框:
useEffect(() => {
document.onkeydown = function (event) {
let e = event || window.event || arguments.callee.caller.arguments[0]
if (e && e.keyCode === 27) {
handleClose()
}
}
}, [])
複製代碼
由於事件監聽只須要執行一次,因此useEffect的依賴設置爲空數組便可。雖然這樣已經基本實現了鍵盤關閉的功能,可是這樣的代碼明顯不夠優雅,因此咱們來完善如下,咱們能夠將鍵盤關閉的方法抽離出來,而後在useEffect的第一個回調函數中返回另外一個函數(該函數裏是組件卸載前的鉤子),當組件卸載時咱們將事件監聽移除,這樣能夠提升一些性能,對內存優化也有幫助:
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)
}
}, [])
複製代碼
經過這種方式,代碼和功能實現上是否是會更優雅呢?
afterClose的做用主要是在模態框關閉以後執行某個回調函數。咱們使用class組件很好實現這個功能,由於setState能夠傳兩個參數,一個是更新state的回調,另外一個是state更新以後的回調,咱們只須要把afterClose放到更新後的回調便可,也就是第二個參數回調裏。可是咱們modal組件目前是用react hooks和函數式組件寫的,那麼怎麼實現狀態更新後的回調呢?筆者這裏提供一個實現思路,利用閉包來實現,核心代碼以下:
// 函數組件外部
let hiddenCount = 0;
// 函數組件內部
useEffect(() => {
if(isHidden && hiddenCount) {
hiddenCount = 0
afterClose && afterClose()
}
hiddenCount = 1
}, [isHidden])
複製代碼
咱們知道useEffect不只僅能夠實現監聽掛載組件的鉤子,也一樣能監聽state更新,咱們利用這一點來實現該功能,值得注意的是咱們要在執行afterClose前重置hiddenCount,避免其餘使用modal組件的函數的影響。
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
}
複製代碼
關於prop-types的使用官網上有很詳細的案例,這裏說一點就是oneOfType的用法, 它用來支持一個組件多是多種類型中的一個. 組件完整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%),是爲了防止動畫抖動
transform: translateY(-50%) scale(0);
}
100% {
opacity: 1;
transform: translateY(-50%) scale(1);
}
}
複製代碼
經過以上步驟, 一個健壯的的Modal組件就完成了.Modal組件算是組件庫中中等複雜的組件,若是不懂的能夠在評論區提問,筆者看到後會第一時間解答.
咱們能夠經過以下方式使用它:
<Modal title="xui基礎彈窗" centered mask={false} visible={false}>
<p>我是彈窗內容</p>
<p>我是彈窗內容</p>
<p>我是彈窗內容</p>
<p>我是彈窗內容</p>
</Modal>
複製代碼
筆者已經將實現過的組件發佈到npm上了,你們若是感興趣能夠直接用npm安裝後使用,方式以下:
npm i @alex_xu/xui
// 導入xui
import {
Button,
Skeleton,
Empty,
Progress,
Tag,
Switch,
Drawer,
Badge,
Alert
} from '@alex_xu/xui'
複製代碼
該組件庫支持按需導入,咱們只須要在項目裏配置babel-plugin-import便可,具體配置以下:
// .babelrc
"plugins": [
["import", { "libraryName": "@alex_xu/xui", "style": true }]
]
複製代碼
npm庫截圖以下:
後續筆者將會繼續實現
等組件, 來複盤筆者多年的組件化之旅.
若是對於react/vue組件設計原理不熟悉的,能夠參考個人以前寫的組件設計系列文章:
筆者已經將組件庫發佈到npm上了, 你們能夠經過npm安裝的方式體驗組件.
若是想獲取組件設計系列完整源碼, 或者想學習更多H5遊戲, webpack,node,gulp,css3,javascript,nodeJS,canvas數據可視化等前端知識和實戰,歡迎在公號《趣談前端》加入咱們的技術羣一塊兒學習討論,共同探索前端的邊界。