《精通React/Vue组件设计》之手把手实现一个轻量级可扩展的模态框(Modal)组件 环球焦点
本文是笔者写组件设计的第九篇文章, 今天带大家实现一个轻量级且可灵活配置组合的模态框(Modal)组件, 该组件在诸如Antd或者elementUI等第三方组件库中都会出现。
前言
本文是笔者写组件设计的第九篇文章, 今天带大家实现一个轻量级且可灵活配置组合的模态框(Modal)组件, 该组件在诸如Antd或者elementUI等第三方组件库中都会出现,主要用来提供系统的用户反馈。
(资料图)
之所以会写组件设计相关的文章,是因为作为一名前端优秀的前端工程师,面对各种繁琐而重复的工作,我们不应该按部就班的去”辛勤劳动”,而是要根据已有前端的开发经验,总结出一套自己的高效开发的方法。
[笔记]前端组件的一般分类:
通用型组件: 比如Button, Icon等。布局型组件: 比如Grid, Layout布局等。导航型组件: 比如面包屑Breadcrumb, 下拉菜单Dropdown, 菜单Menu等。数据录入型组件: 比如form表单, Switch开关, Upload文件上传等。数据展示型组件: 比如Avator头像, Table表格, List列表等。反馈型组件: 比如Progress进度条, Drawer抽屉, Modal对话框等。其他业务类型所以我们在设计组件系统的时候可以参考如上分类去设计,该分类也是antd, element, zend等主流UI库的分类方式。
正文
在开始组件设计之前希望大家对css3和js有一定的基础,并了解基本的react/vue语法.我们先来解构一下Modal组件, 一个Modal分为以下几个部分:
每一个区块都可以自定义配置, 也可以组合其他组件.实现后的组件效果:
1、组件设计思路
按照之前笔者总结的组件设计原则,我们第一步是要确认需求. 模态框(Modal)组件一般会有如下需求点:
能控制Modal主体的样式提供Modal完全关闭后的回调能控制取消按钮文字和样式能控制确认按钮文字和样式控制modal展示的位置控制是否显示右上角的关闭按钮可以配置自定义关闭图标配置关闭时是否销毁Modal里的子元素自定义模态框底部内容控制是否支持键盘esc关闭控制是否展示遮罩控制点击蒙层是否允许关闭自定义遮罩样式自定义标题控制对话框是否可见自定义对话框宽度暴露点击遮罩层或右上角叉或取消按钮的回调提供点击确定回调需求收集好之后,作为一个有追求的程序员, 会得出如下线框图:
对于react选手来说,如果没用typescript,建议大家都用PropTypes, 它是react内置的类型检测工具,我们可以直接在项目中导入. vue有自带的属性检测方式,这里就不一一介绍了。
2、基于react实现一个Modal组件
(1)Modal组件框架设计
首先我们先根据需求将组件框架写好,这样后面写业务逻辑会更清晰:
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 }export default Modal
有了这个框架,我们来一步步往里面实现内容吧。
(2)实现基础配置功能
基础配置功能往往和业务逻辑无关, 仅仅用来控制元素的显示隐藏等,由于其非常容易实现,所以我们先来实现以下这些属性的功能:
bodyStylecancelTextclosablecloseIconfootermaskmaskStyleokTexttitlewidth这几个功能在框架搭建好之后已经部分实现了,是因为他们都比较简单,不会牵扯到其他复杂逻辑。只需要对外暴露属性并使用属性即可。具体实现如下:
// ...function Modal(props) { // ... return { title } { closable && { closeIcon || } } { children } { footer === null ? null : { footer ? footer : } } { mask && } }
通过以上实现,我们很容易控制一个modal组件具体显示那些元素,以及那些元素是可关闭modal的,具体案例如下:
去除footer(通过设置footer为null)。去除右上角的关闭按钮。去除mask遮罩。(3)实现visible(带有弹窗出来和隐藏的动画animation)
熟悉antd或者element的朋友都知道,visible用来控制modal的显示和隐藏,我们这里也来实现同样的功能,关于隐藏和显示的动画,我们这里用transform:scale来实现。先来看看实现效果吧:
这里笔者使用了react hooks的useState这个API,来设置弹窗可见性的state,modal默认不可见。具体逻辑如下:
let [isHidden, setHidden] = useState(!props.visible)const handleClose = () => { setHidden(false)}
html结构如下:
由以上代码我们知道模态框的显示隐藏是通过设置display:none/block来控制的,但是我们都知道display:none是不能执行动画效果的,为了实现内容弹窗的动画,我们这里采用了@keyframe动画,对于低版本浏览器也采用了很好的向下兼容。具体css代码如下:
@keyframes xSpread { 0% { opacity: 0; transform: scale(0); } 100% { opacity: 1; transform: scale(1); }}
(4)实现centered
centered属性的作用就是来控制弹窗内容距离整个遮罩或者可视区域的位置的,值为true则居与遮罩或者可视区域的正中心。因为我们默认设置的modal内容区域的位置是左右居中,顶部距离可视区域顶部100px,所以这里我们实现如下:
&.xCentered { top: 50%; transform: translateY(-50%);}
这个实现也非常简单,就是通过属性centered来动态的设置类名即可。
(5)实现destroyOnClose
这个功能意思是在弹窗关闭时是否清除子元素,我在:《精通react/vue组件设计》之配合React Portals实现一个功能强大的抽屉(Drawer)组件这篇文章中有详细的介绍,大家感兴趣可以研究以下,这里我指介绍实现过程。当destroyOnClose为true时,我们销毁子元素即可,通过维护一个state来实现组件的重新渲染。要想实现该功能,我们需要处理如下几个事件:
当点击关闭按钮时,根据destroyOnClose销毁子组件。当点击确认按钮时,根据destroyOnClose销毁子组件。当visible为true,根据destroyOnClose将子组件重新渲染出来。具体实现代码如下:// 关闭事件(关闭和确认事件逻辑基本一致,这里就不单独写了)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])
这样我们就实现了弹窗关闭时销毁组件的功能。
(6)实现键盘按键ESC时关闭模态框(Modal)
为了更好的用户体检,笔者的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) } }, [])
通过这种方式,代码和功能实现上是不是会更优雅呢?
(7)实现afterClose
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组件的函数的影响。
(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}
关于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组件算是组件库中中等复杂的组件,如果不懂的可以在评论区提问,笔者看到后会第一时间解答。
(9)使用Modal组件
我们可以通过如下方式使用它:
我是弹窗内容
我是弹窗内容
我是弹窗内容
我是弹窗内容
笔者已经将实现过的组件发布到npm上了,大家如果感兴趣可以直接用npm安装后使用,方式如下:
npm i @alex_xu/xui// 导入xuiimport { 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库截图如下:
最后
后续笔者将会继续实现
badge(徽标)table(表格)tooltip(工具提示条)Skeleton(骨架屏)Message(全局提示)form(form表单)switch(开关)日期/日历二维码识别器组件等组件, 来复盘笔者多年的组件化之旅。
标签:
为您推荐
广告
随机阅读
- 《精通React/Vue组件设计》之手把手实现一个轻量级可扩展的模态框(Modal)组件 环球焦点
- 全球首个5G异网漫游试商用正式启动|当前速读
- 均普智能携多样产品亮相第十五届深圳国际电池技术交流会_焦点播报
- 5·15打击和防范经济犯罪宣传日 | 与民同心 荷塘公安为您守护
- excel怎么大写金额自动变换 excel自动转换大写金额的角分_世界热议
- 全球快资讯:上海:支持中小企业购买人工智能算力等服务
- 世界热资讯!电脑上显示打印机暂停了怎么恢复打印
- 柔美的拼音_柔美的拼音及解释-当前速读
- 金星航宇公司开发“观星者”高超声速公务机 热议
- 奥雅股份与建瓯市政府签订战略合作协议
- 马斯克最新发声:特斯拉将尝试投放广告 今年Model Y地球销量第一 全球要闻
- 马斯克称微软控制了OpenAI,纳德拉予以否认
- 高考前的失眠怎么办?高情商家长常挂在嘴边的是这8句话!
- 亿百润的钱还能拿出来吗(亿百润为何出现兑付问题)
- 硫化锰
- 【世界播资讯】广西警方通报持刀抢劫事件:2死2伤,嫌疑人已被控制
- 焦点滚动:北横通道新建工程Ⅷ标段有新进展!“纵横号”创三项全国纪录
- 瑞金八一起义军转战壬田遗址_关于瑞金八一起义军转战壬田遗址介绍-观天下
- 今日观点!2023上海国际电影节排片表
- 【环球热闻】万物皆有灵性_万物皆有灵
- 1当日快讯:2日1板祥明智能:2日累涨超30%,公司经营情况及内外部经营环境未发生重大变化
- 2格林美(002340.SZ):三元前驱体材料出货量居全球市场前二_世界热点
- 3坦克300 PHEV动力参数曝光:2.0T插混 纯电续航105km
- 4湖南省辰溪县发布暴雨黄色预警
- 5体坛:泰山队新帅初选共4人,薪水预算确实有限崔康熙不太爱财
- 6【地评线】新华时论|用乡情拓展“人才朋友圈” 每日看点
- 7当前报道:历史上的今天:第一张彩色照片诞生
- 8电报解读|需求端弱势,库存充足,稀土价格仍需下游复苏
- 9董事长轮流坐庄!三一集团管理制度创新了! 天天热门
- 10重庆高新区7.28亿元挂牌10.78万平宅地 采取“拍卖+摇号”方式出让
- 1天天微动态丨海底光缆龙头股有哪些(三只值得关注的海底光缆龙头股)
- 2四方新材(605122)5月16日主力资金净买入1186.00元
- 3微头条丨格力员工称孟羽童是被公司开除 被曝经常旷工接私活
- 4东京居民血检有机氟化合物超标 疑美军基地污染水源所致
- 5重点聚焦!三祥科技(831195)2023年一季报简析:营收净利润双双增长,三费占比上升明显
- 6天天微头条丨室外给水设计标准gb50013-2018_室外给水规范
- 7全国首座“行走的高铁博物馆”亮相_视讯
- 8世界热头条丨A股午评:三大指数涨跌不一,存储芯片板块领涨
- 9开封市禹王台区:“火眼金睛”识骗术 快速处置保财产
- 10把“吉祥如意”穿在身上
广告
财经
- 4 个男孩结伴出游,被困 10 米高悬崖
- 消息称疯狂小杨哥和辛巴团队将入淘-速讯
- 大反攻!新能源现涨停潮,所为何因?这些基金仍大亏,何时能回本?|每日视讯
- 速看:他宁死不降李自成,自杀后留下六个字给崇祯,讲透了明亡的原因
- “ 沐浴书香 润泽吕梁 ” 经典诗文朗诵活动举办
- 当前观点:八仙过海主题曲歌曲85版_八仙过海主题曲
- 今日正式生效!内地与澳门驾驶证实现免试互认换领
- 央行报告:当前我国经济没有出现通缩
- 北京国际光影艺术季今晚开启-世界速读
- 绝对值是什么意思数学_绝对值是什么意思
- METROPOLIS CAP(08621)发布一季度业绩,股东应占亏损1402.26万元 同比盈转亏|天天新动态
- 云南最差的二本大学名单有哪些_云南最差的二本大学名单 世界热议
- 聚焦失业青年 中央十部门启动实施百万就业见习岗位募集计划
- 新华全媒+|西安:千年古都尽显光影魅力 全球热议
- 三生石下_三石-世界热文
- 公积金每月交540能贷多少钱?贷款额度有多少?
- 郑州颐和医院黄遂柱教授当选河南省医学会骨科学分会关节镜学组组长_天天速递
- 天蝎座女生喜欢一个人的表现暗恋还是暗恋_天蝎座女生喜欢一个人的表现暗恋_天天滚动
- 世界最资讯丨供需错配 锂价半个月涨超40% 今日锂矿板块全线飘红
- 龙虎榜丨晶澳科技涨停低位反弹后机构现分歧 世界报道