項目中須要實現app中常見的提示效果Toast。這個效果看似簡單,實現起來沒有那麼容易。
首先Toast的使用方法必須十分簡單,簡單到一行代碼搞定:react
Toast.info('普通的Toast我普通的搖!!', 3000);
隨時用隨時調用上述方法便可。
再有一點,Toast不用插入到頁面中,他不會向其餘組件同樣一直出如今DOM中。
只有在調用該方法的時候,動態插入到DOM中。
還有,頁面能夠存在多個提示,多個提示單獨存在,互不影響。
因此,實現Toast並不像其餘組件那麼普通。git
此次先來看看效果圖中的使用代碼:github
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('有錯誤!!', 3000, undefined, false, ()=>{console.log("callback");}); }; const commonToast = () => { Toast.info('歡迎來到本直播間', 3000, undefined, false); }; const successToast = () => { Toast.success('操做成功!', 3000, 'fa-check', false); }; const errorToast = () => { Toast.error('操做失敗!', 3000, 'fa-times', false); }; const warningToast = () => { Toast.warning('警告:手機2s後爆炸', 3000, 'fa-exclamation-triangle', false); }; const loadingToast = () => { Toast.show('加載中...', 0, '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;
接下來就是逐個開發。antd
爲何要先開發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的代碼:app
// 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返回結果的集合。dom
這時候在寫Notice就簡單了,其props有幾個關鍵的參數duration就是Notice顯示幾秒,content就是其顯示的具體內容,onClose就是該銷燬時候執行的回調函數。
這裏面控制Notice顯示幾秒,其實是用定時器setTimeout實現的,onClose實際上就是在父組件Notification中將本身對應的notice刪除。
Notice代碼:ide
// 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首先就是要利用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', {'info': type === 'info'}, {'success': type === 'success'}, {'warning': type === 'warning'}, {'error': type === 'error'} ]) }> <div className="zby-toast-content">{content}</div> </div> ), onClose: () => { if (onClose) onClose(); }, }); }; export default { // 無動畫 show(content, duration, icon, mask, onClose) { return notice(content, undefined, icon, duration, onClose, mask); }, // 翻轉效果 info(content, duration, icon, mask, onClose) { return notice(content, 'info', icon, duration, onClose, mask); }, // 縮放效果 success(content, duration, icon, mask, onClose) { return notice(content, 'success', icon, duration, onClose, mask); }, // 從下方滑入 warning(content, duration, icon, mask, onClose) { return notice(content, 'warning', icon, duration, onClose, mask); }, // 抖動 error(content, duration, icon, mask, onClose) { return notice(content, 'error', icon, duration, onClose, mask); }, // 銷燬 hide() { if (newNotification) { newNotification.destroy(); newNotification = null; } }, }
這樣Toast,一個在React中動態插入刪除DOM的組件完成了。學習
這裏的Toast,Notification和Notice都是參照antd-mobile源碼改寫的,這種組件暴露方法給別人調用的場景,和動態插入DOM場景平時很少見,藉助其源碼也是一次學習。