效果图
需求
项目中需要实现app中常见的提示效果Toast。这个效果看似简单,实现起来没有那么容易。
首先Toast的使用方法必须十分简单,简单到一行代码搞定:
Toast.info('普通的Toast我普通的摇!!',3000);
随时用随时调用上述方法即可。
再有一点,Toast不用插入到页面中,他不会向其他组件一样一直出现在DOM中。
只有在调用该方法的时候,动态插入到DOM中。
还有,页面可以存在多个提示,多个提示单独存在,互不影响。
所以,实现Toast并不像其他组件那么普通。
使用方法
import React from 'react' import ListTitle from '../../components/DataDisplay/ListTitle' import Button from '../../components/DataEntry/Button' import Toast from '../../components/Feedback/Toast' import Tools from '../../components/Tools/Tools' const ToastPage = () => { const commonInfo = () => { Toast.info('普通的Toast我普通的摇!!',3000); }; const commonSuccess = () => { Toast.success('操作成功',3000,'fa-check'); }; const commonError = () => { Toast.error('有错误!!',undefined,false,()=>{console.log("callback");}); }; const commonToast = () => { Toast.info('欢迎来到本直播间',false); }; const successToast = () => { Toast.success('操作成功!','fa-check',false); }; const errorToast = () => { Toast.error('操作失败!','fa-times',false); }; const warningToast = () => { Toast.warning('警告:手机2s后爆炸','fa-exclamation-triangle',false); }; const loadingToast = () => { Toast.show('加载中...','fa-circle-o-notch fa-spin',false); const timer = setTimeout(()=>{ Toast.hide(); clearTimeout(timer); },3000); }; return ( <div className="page toast"> <h1 className="title"> <i className="fa fa-home" onClick={()=>{Tools.linkTo("/index")}}></i> Toast </h1> <ListTitle title="基本" /> <div className="button-Box"> <Button onClick={commonInfo}>纯文字提示</Button> <Button onClick={commonSuccess}>icon成功提示有蒙版</Button> <Button onClick={commonError}>纯文字报错提示有回调</Button> </div> <ListTitle title="场景使用" /> <div className="button-Box"> <Button type="primary" onClick={commonToast}>普通提示</Button> <Button type="primary" onClick={successToast}>成功提示</Button> <Button type="primary" onClick={errorToast}>失败提示</Button> <Button type="primary" onClick={warningToast}>警告</Button> <Button type="primary" onClick={loadingToast}>加载中</Button> </div> </div> ) }; export default ToastPage
可以看到在ToastPage中,render return出来的DOM中没有<Toast/>。
只是在点击Button的回调中直接调用的Toast。
按理说,组件都应该在render时候return出来,Toast是怎么实现在React中动态添加删除DOM的。
组件分析
首先多个提示可以堆叠,不同提示定制化也不同,很显然是个组件,起名为Notice。
然后Notice外面还有个容器组件,用来装载Notice并且,暴露一些方法给Toast,起名Notification。
最后就是Toast组件,负责直接生成不同的Notice,或者销毁Notification。但是其实Toast只是个对象,而不是真正意义的组件。
所以简单的Toast其实是也是分成三部分来完成。
Toast -> Notification -> Notice * n;
接下来就是逐个开发。
Notification开发
为什么要先开发Notification,因为他特别重要,起到承上启下的作用。
首先,Notification是个容器,他自己有state,state中的notices数组就是存放生成Notice关键的数据notice(每个Notice都是不同的,所以notice中比如有一个属性:key)。
然后render的时候,循环notices生成一段DOM节点,放到自己的div中。
同时,其还提供一个向notices中添加notice的方法(add)和根据key,在notices中删除notice的方法(remove)。
最后关键的地方,定义一个reRwrite方法,该方法接受一些参数,动态的向DOM中插入一个div,然后再向这个div中插入Notification,最后返回一个含有几个操作这个Notification的方法的对象。(这就是动态实现插入DOM的关键)
Notification的代码:
// Notification是Notice父组件,容器 // 是动态插入和删除DOM节点的核心 // 同时也向上暴露给Toast重写改变自己的方法 import React from 'react' import ReactDOM from 'react-dom' import Notice from './Notice' class Notification extends React.Component { constructor (props) { super(props); this.state = { notices: [],// 存储当前有的notices hasMask: true,// 是否显示蒙版 } } add (notice) { // 添加notice // 创造一个不重复的key const {notices} = this.state; const key = notice.key ? notice.key : notice.key = getUuid(); const mask = notice.mask ? notice.mask : false; const temp = notices.filter((item) => item.key === key).length; if(!temp){ // 不存在重复的 添加 notices.push(notice); this.setState({ notices: notices,hasMask: mask }); } } remove (key) { // 根据key删除对应 this.setState(prevIoUsState => { return { notices: prevIoUsState.notices.filter(notice => notice.key !== key),}; }); } getNoticeDOM () { const _this = this; const {notices} = this.state; let result = []; notices.map((notice)=>{ // 每个Notice onClose的时候 删除掉notices中对应key的notice const closeCallback = () => { _this.remove(notice.key); // 如果有用户传入的onClose 执行 if(notice.onClose) notice.onClose(); }; result.push( <Notice key={notice.key} {...notice} onClose={closeCallback} /> ); }); return result; } getMaskDOM () { const {notices,hasMask} = this.state; // notices为空的时候 不显示蒙版 // 始终只有一个蒙版 if(notices.length > 0 && hasMask == true) return <div className="zby-mask"></div>; } render () { const noticesDOM = this.getNoticeDOM(); const maskDOM = this.getMaskDOM(); return ( <div className="zby-notification-Box"> {maskDOM} {noticesDOM} </div> ) } } // 统计notice总数 防止重复 let noticeNumber = 0; // 生成唯一的id const getUuid = () => { return "notification-" + new Date().getTime() + "-" + noticeNumber++; }; // Notification增加一个重写方法 // 该方法方便Notification组件动态添加到页面中和重写 Notification.reWrite = function (properties) { const { ...props } = properties || {}; let div; div = document.createElement('div'); document.body.appendChild(div); const notification = ReactDOM.render(<Notification {...props} />,div); return { notice(noticeProps) { notification.add(noticeProps); },removeNotice(key) { notification.remove(key); },destroy() { ReactDOM.unmountComponentAtNode(div); document.body.removeChild(div); },component: notification } }; export default Notification
看了Notification其实谜团就解开了,Notice其实就是根据notices中的notice渲染出来的组件,Toast其实就是调用Notification.reWrite返回结果的集合。
Notice开发
这时候在写Notice就简单了,其props有几个关键的参数duration就是Notice显示几秒,content就是其显示的具体内容,onClose就是该销毁时候执行的回调函数。
这里面控制Notice显示几秒,实际上是用定时器setTimeout实现的,onClose实际上就是在父组件Notification中将自己对应的notice删除。
Notice代码:
// Notice是Toast最底层组件 // 每个黑色的小框框其实都是一个Notice // Notice核心就是组件初始化的时候 生成一个定时器 // 根据输入的时间 加载一个动画 然后执行输入的回调 // Notice的显示和隐藏收到父组件Notification的绝对控制 import React from 'react' import classNames from 'classnames' class Notice extends React.Component { static propTypes = { duration: React.PropTypes.number,// Notice显示时间 content: React.PropTypes.any,// Notice显示的内容 onClose: React.PropTypes.func // 显示结束回调 }; static defaultProps = { duration: 3000,}; constructor (props) { super(props); this.state = { shouldClose: false,// 是否开启关闭动画 } } componentDidMount () { if(this.props.duration > 0){ this.closeTimer = setTimeout(() => { this.close(); },this.props.duration - 300); // 减掉消失动画300毫秒 } } componentWillUnmount () { // 当有意外关闭的时候 清掉定时器 this.clearCloseTimer(); } clearCloseTimer () { if (this.closeTimer) { clearTimeout(this.closeTimer); this.closeTimer = null; } } close () { // 关闭的时候 应该先清掉倒数定时器 // 然后开启过场动画 // 等待动画结束 执行回调 this.clearCloseTimer(); const _this = this; _this.setState({shouldClose: true}); this.timer = setTimeout(()=>{ if(this.props.onClose){ this.props.onClose(); } clearTimeout(_this.timer); },300); } render () { const {shouldClose} = this.state; return ( <div className={classNames(['zby-notice-Box',{'leave': shouldClose}])}> {this.props.content} </div> ) } } export default Notice
Toast封装
最后看下Toast就比较简单了。
Toast首先就是要利用Notification.reWrite初始化一个newNotification,并且保持这个Notification为单例。
然后封装一个notice方法,动态的改变这个newNotification。
最后封装几个常用notice方法暴露出去。
Toast代码:
import React from 'react' import classNames from 'classnames' import Notification from './Notification' // Toast组件比较特殊 // 因为<Toast />不会被直接渲染在DOM中 // 而是动态插入页面中 // Toast组件核心就是通过Notification暴露的重写方法 动态改变Notification let newNotification; // 获得一个Notification const getNewNotification = () => { // 单例 保持页面始终只有一个Notification if (!newNotification) { newNotification = Notification.reWrite(); } return newNotification; }; // notice方法实际上就是集合参数 完成对Notification的改变 const notice = (content,type,icon,duration = 3000,onClose,mask = true) => { let notificationInstance = getNewNotification(); notificationInstance.notice({ duration,mask: mask,content: !!icon ? ( <div className={ classNames(['zby-toast-Box',{'info': type === 'info'},{'success': type === 'success'},{'warning': type === 'warning'},{'error': type === 'error'} ]) }> <div className="zby-toast-icon"><i className={"fa " + icon}></i></div> <div className="zby-toast-content">{content}</div> </div> ) : ( <div className={ classNames(['zby-toast-Box',{'error': type === 'error'} ]) }> <div className="zby-toast-content">{content}</div> </div> ),onClose: () => { if (onClose) onClose(); },}); }; export default { // 无动画 show(content,duration,mask,onClose) { return notice(content,mask); },// 翻转效果 info(content,'info',// 缩放效果 success(content,'success',// 从下方滑入 warning(content,'warning',// 抖动 error(content,'error',// 销毁 hide() { if (newNotification) { newNotification.destroy(); newNotification = null; } },}
这样Toast,一个在React中动态插入删除DOM的组件完成了。
总结
这里的Toast,Notification和Notice都是参照antd-mobile源码改写的,这种组件暴露方法给别人调用的场景,和动态插入DOM场景平时不多见,借助其源码也是一次学习。