- 移动端的 modal 弹窗 在项目开发中 非常的常见,各种组件库也有此功能。但是它究竟是怎样的方式实现的呢,今天让我们一起来探究探究
先看看实现效果
要造个轮子之前,我们需要先分析步骤,而后才开始动手写代码,实现具体细节。
- 弹窗 是 一个组件 ,暂且取名叫 BaseModal 。在页面引入 BaseModal 组件。
- 点击按钮,弹出弹窗( 命令式 调用 ), BaseModal.show({ option })
- option 是要配置的各种参数,比如 弹窗标题、弹窗内容、弹窗按钮文案等等
- 点击弹窗上的确定、取消按钮,关闭弹窗。
- 以上就是 实现弹窗的 逻辑分析,接下来我们按照上面的步骤来写出代码逻辑
一、BaseModal 组件
// 最初始的 一个 ui 组件,这里的css 样式在后面
const BaseModal = () => {
return (
<div className={`${styles.container}`}>
<div className={`${styles.inner}`}>
<div className={styles.top}>
<div className={styles.title}>我是标题</div>
<div className={styles.content}>我是内容</div>
</div>
<div className={`${styles.footer} ${styles.footerNormal}`}>
<div className={`${styles.public} ${styles.cancel}`}>
取消
</div>
<div className={`${styles.public} ${styles.sure}`}>
确定
</div>
</div>
</div>
</div>
)
}
export default BaseModal
效果图
二、命令式调用 BaseModal.show() 弹出弹窗
import reactDom from 'react-dom/client'
const BaseModal = () => {
// ... 同上
}
// 新加show 方法,动态的向body 里面插入标签
const show = () => {
const div = document.createElement('div')
const root = reactDom.createRoot(div)
document.body.append(div)
root.render(<BaseModal />)
}
BaseModal.show = show
export default BaseModal
三、配置 option 参数
export interface OptionsType {
/** 提示标题 */
title?: string
/** 提示的内容 */
content?: React.ReactNode
/** 是否显示取消按钮 @default true */
showCancel?: boolean
/** 取消按钮文字 @default "取消" */
cancelText?: string
/** 确定按钮文字 @default "确认" */
confirmText?: string
/** 点击取消按钮 */
onCancel?: () => void
/** 点击确定按钮 */
onConfirm?: (_?: any) => void
}
const BaseModal = (props: OptionsType) => {
const { title, content } = props
return (
<div className={`${styles.container}`}>
<div className={`${styles.inner}`}>
<div className={styles.top}>
// 更改成动态传入
{ title && <div className={styles.title}>{ title }</div> }
{ content && <div className={styles.content}>{ content }</div> }
</div>
<div className={`${styles.footer} ${styles.footerNormal}`}>
<div className={`${styles.public} ${styles.cancel}`}>
取消
</div>
<div className={`${styles.public} ${styles.sure}`}>
确定
</div>
</div>
</div>
</div>
)
}
// 新增入参 option
const show = (option: OptionsType) => {
const div = document.createElement('div')
const root = reactDom.createRoot(div)
document.body.append(div)
// 把参数 option 传给 BaseModal 组件
root.render(<BaseModal {...option} />)
}
BaseModal.show = show
export default BaseModal
/**
* 调用 BaseModal1.show({ title: '我是传入标题', content: '我是传入内容' })
*/
四、点击 确认 、取消 按钮,关闭弹窗
- 借助发布订阅 直接复制 发布订阅的实现,代码到一个单独的文件
import reactDom from 'react-dom/client'
import events from './events'
export interface OptionsType {
/** 提示标题 */
title?: string
/** 提示的内容 */
content?: React.ReactNode
/** 是否显示取消按钮 @default true */
showCancel?: boolean
/** 取消按钮文字 @default "取消" */
cancelText?: string
/** 确定按钮文字 @default "确认" */
confirmText?: string
/** 点击取消按钮 */
onCancel?: () => void
/** 点击确定按钮 */
onConfirm?: (_?: any) => void
}
/**
* @author czj
* @description modal弹窗
*/
const BaseModal = (props: OptionsType) => {
const { title, content,} = props
const clickCancel = () => {
events.emit('closeModal', 'cancel')
}
const clickSure = () => {
events.emit('closeModal', 'confirm')
}
return (
<div className={`${styles.container}`}>
<div className={`${styles.inner}`}>
<div className={styles.top}>
{ title && <div className={styles.title}>{ title }</div> }
{ content && <div className={styles.content}>{ content }</div> }
</div>
<div className={`${styles.footer} ${styles.footerNormal}`}>
<div className={`${styles.public} ${styles.cancel}`} onClick={clickCancel}>
取消
</div>
<div className={`${styles.public} ${styles.sure}`} onClick={clickSure}>
确定
</div>
</div>
</div>
</div>
)
}
const show = (option: OptionsType) => {
const div = document.createElement('div')
const root = reactDom.createRoot(div)
document.body.append(div)
root.render(<BaseModal {...option} />)
// 订阅 一个 关闭弹窗事件
events.on('closeModal', (type: string) => {
// type 点击的事件源,取消按钮还是 确认按钮
root.unmount()
document.body.removeChild(div)
// 解绑事件
events.off('closeModal')
})
}
BaseModal.show = show
export default BaseModal
/**
* 调用 BaseModal1.show({ title: '我是传入标题', content: '我是传入内容' })
*/
最终效果:
效果优化
import reactDom from 'react-dom/client'
import styles from './index.module.scss'
import events from './events'
import { useEffect, useState } from 'react'
export interface OptionsType {
/** 提示标题 */
title?: string
/** 提示的内容 */
content?: React.ReactNode
/** 是否显示取消按钮 @default true */
showCancel?: boolean
/** 取消按钮文字 @default "取消" */
cancelText?: string
/** 确定按钮文字 @default "确认" */
confirmText?: string
/** 点击取消按钮 */
onCancel?: () => void
/** 点击确定按钮 */
onConfirm?: (_?: any) => void
}
/**
* @description modal弹窗
*/
const BaseModal = (props: OptionsType) => {
const { title, content, showCancel, confirmText, cancelText, onCancel, onConfirm } = props
// 做动画
const [show, setShow] = useState(false)
/** 弹窗关闭时的动画 */
const hiddenModal = (cb: () => void) => {
return () => {
setShow(false)
// 延迟300 ms 执行后面的操作
setTimeout(() => cb(), 400)
}
}
/** 点击取消按钮 */
const cancelClick = hiddenModal(() => {
events.emit('closeModal', 'cancel')
onCancel?.()
})
/** 点击确认按钮 */
const confirmClick = hiddenModal(() => {
events.emit('closeModal', 'confirm')
onConfirm?.()
})
useEffect(() => {
setTimeout(() => {
setShow(true)
}, 0)
}, [])
return (
<div className={`${styles.container} ${show && styles.containerShow}`}>
<div className={`${styles.inner} ${show && styles.innerShow}`}>
<div className={styles.top}>
{
title && <div className={styles.title}>{title}</div>
}
<div className={styles.content}>{content}</div>
</div>
<div className={`${styles.footer} ${styles.footerNormal}`}>
{ showCancel
&& <div onClick={cancelClick} className={`${styles.public} ${styles.cancel}`}>
{cancelText || '取消'}
</div>
}
<div onClick={confirmClick} className={`${styles.public} ${styles.sure}`}>
{confirmText || '确定'}
</div>
</div>
</div>
</div>
)
}
export default BaseModal
/**
* @description 显示出弹窗
*/
const show = (options: OptionsType) => {
// 返回promise
return new Promise((resolve, reject) => {
const div = document.createElement('div')
const root = reactDom.createRoot(div)
document.body.append(div)
root.render(<BaseModal {...options} />)
events.on('closeModal', (type) => {
type === 'confirm' ? resolve({}) : reject()
root.unmount()
document.body.removeChild(div)
// 解绑事件
events.off('closeModal')
})
})
}
BaseModal.show = show
/**
* await BaseModal1.show({ title: '我是传入标题', content: '我是传入内容' })
* console.log('----')
*/
css
.container {
width: 100vw;
height: 100vh;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: .4s;
}
.containerShow {
opacity: 1;
}
.inner {
width: 640px;
border-radius: 16px;
position: absolute;
left: 50%;
top: 50%;
background-color: #fff;
text-align: center;
transform: translate(-50%,-50%) scale(0.7);
transition: .4s;
overflow: hidden;
}
.innerShow {
transform:translate(-50%,-50%) scale(1);
}
.top {
padding: 32px 24px;
}
.title {
font-size: 18px;
color: #323232;
font-size: 36px;
font-weight: bold;
}
.content {
margin-top: 20px;
color: rgba(0, 0, 0, 0.85);
font-size: 32px;
}
.footer {
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 32px;
padding-bottom: 20px;
border-radius: 8px;
}
.footerNormal {
border-top: 1px solid rgba(230, 230, 230, 1);
height: 96px;
padding: 0;
}
.public {
width: 200px;
height: 96px;
color: #323232;
font-size: 32px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
background-color: #fff !important;
}
.cancel {
background-color: #fff;
color: rgba(50, 50, 51, 1);
border-right: 1px solid rgba(230, 230, 230, 1);
}
.sure {
background-color: #5290fd;
color: rgba(82, 144, 253, 1);
}
.buttonCancel {
margin-right: 32px;
}