實現基於React的全局提示組件Toast

前戲

正文

需求分析

  • Toast 不須要同頁面一塊兒被渲染,而是根據須要被隨時調用。
  • Toast 是一個輕量級的提示組件,它的提示不會打斷用戶操做,而且會在提示的一段時間後自動關閉。
  • Toast 須要提供幾種不一樣的消息類型以適應不一樣的使用場景。
  • Toast 的方法必須足夠簡潔,以免沒必要要的代碼冗餘。

最終效果:

調用示例

Toast.info('普通提示')
Toast.success('成功提示', 3000)
Toast.warning('警告提示', 1000)
Toast.error('錯誤提示', 2000, () => {
    Toast.info('哈哈')
})
const hideLoading = Toast.loading('加載中...', 0, () => {
    Toast.success('加載完成')
})
setTimeout(hideLoading, 2000)
複製代碼

組件實現

Toast 組件能夠被分爲三個部分,分別爲:css

  • notice.js:Notice。無狀態組件,只負責根據父組件傳遞的參數渲染爲對應提示信息的組件,也就是用戶最終看到的提示框。
  • notification.js:Notification。Notice 組件的容器,用於保存頁面中存在的 Notice 組件,並提供 Notice 組件的添加和移除方法。
  • toast.js:控制最終對外暴露的接口,根據外界傳遞的信息調用 Notification 組件的添加方法以向頁面中添加提示信息組件。

項目目錄結構以下:react

├── toast
│   ├── icons.js
│   ├── index.js
│   ├── notice.js
│   ├── notification.js
│   ├── toast.css
│   ├── toast.js
複製代碼

爲了便於理解,這裏從外部的 toast 部分開始實現。git

toast.js

由於頁面中沒有 Toast 組件相關的元素,爲了在頁面中插入提示信息,即 Notice 組件,須要首先將 Notice 組件的容器 Notification 組件插入到頁面中。這裏定義一個 createNotification 函數,用於在頁面中渲染 Notification 組件,並保留 addNotice 與 destroy 函數。github

function createNotification() {
    const div = document.createElement('div')
    document.body.appendChild(div)
    const notification = ReactDOM.render(<Notification />, div) return { addNotice(notice) { return notification.addNotice(notice) }, destroy() { ReactDOM.unmountComponentAtNode(div) document.body.removeChild(div) } } } 複製代碼

接着定義一個全局的 notification 變量用於保存 createNotification 返回的對象。並定義對外暴露的函數,這些函數被調用時就會將參數傳遞迴 Notification 組件。由於一個頁面中只須要存在一個 Notification 組件,因此每次調用函數時只須要判斷當前 notification 對象是否存在便可,無需重複建立。數組

let notification
const notice = (type, content, duration = 2000, onClose) => {
    if (!notification) notification = createNotification()
    return notification.addNotice({ type, content, duration, onClose })
}

export default {
    info(content, duration, onClose) {
        return notice('info', content, duration, onClose)
    },
    success(content, duration, onClose) {
        return notice('success', content, duration, onClose)
    },
    warning(content, duration, onClose) {
        return notice('warning', content, duration, onClose)
    },
    error(content, duration, onClose) {
        return notice('error', content, duration, onClose)
    },
    loading(content, duration = 0, onClose) {
        return notice('loading', content, duration, onClose)
    }
}
複製代碼

notification.js

這樣外部工做就已經完成,接着須要完成 Notification 組件內部的實現。首先 Notification 組件的 state 屬性中有一個 notices 屬性,用於保存當前頁面中存在的 Notice 的信息。而且 Notification 組件擁有 addNotice 和 removeNotice 兩個方法,用於向 notices 中添加和移除 Notice 的信息(下文簡寫爲 notice)。bash

添加 notice 時,須要使用 getNoticeKey 方法爲這個 notice 添加惟一的key值,再將其添加到 notices 中。並根據參數提供的 duration,設置定時器以在到達時間後將其自動關閉,這裏規定若 duration 的值小於等於0則消息不會自動關閉,而是一直顯示。最後方法返回移除自身 notice 的方法給調用者,以便其根據須要當即關閉這條提示。app

調用 removeNotice 方法時,會根據傳遞的key的值遍歷 notices,若是找到結果,就觸發其回調函數並從 notices 中移除。ide

最後就是遍歷 notices 數組並將 notice 屬性傳遞給 Notice 組件以完成渲染,這裏使用 react-transition-group 實現組件的進場與出場動畫。svg

(注:關於頁面中同時存在多條提示時的顯示問題,本文中採用的方案時直接將後一條提示替換掉前一條消息,因此代碼中添加 notice 直接寫成了 notices[0] = notice 而非 notices.push(notice), 若是想要頁面中多條提示共存的效果能夠自行修改。)函數

class Notification extends Component {
    constructor() {
        super()
        this.transitionTime = 300
        this.state = { notices: [] }
        this.removeNotice = this.removeNotice.bind(this)
    }

    getNoticeKey() {
        const { notices } = this.state
        return `notice-${new Date().getTime()}-${notices.length}`
    }

    addNotice(notice) {
        const { notices } = this.state
        notice.key = this.getNoticeKey()
        if (notices.every(item => item.key !== notice.key)) {
            notices[0] = notice
            this.setState({ notices })
            if (notice.duration > 0) {
                setTimeout(() => {
                    this.removeNotice(notice.key)
                }, notice.duration)
            }
        }
        return () => { this.removeNotice(notice.key) }
    }

    removeNotice(key) {
        this.setState(previousState => ({
            notices: previousState.notices.filter((notice) => {
                if (notice.key === key) {
                    if (notice.onClose) notice.onClose()
                    return false
                }
                return true
            })
        }))
    }

    render() {
        const { notices } = this.state
        return (
            <TransitionGroup className="toast-notification"> { notices.map(notice => ( <CSSTransition key={notice.key} classNames="toast-notice-wrapper notice" timeout={this.transitionTime} > <Notice {...notice} /> </CSSTransition> )) } </TransitionGroup> ) } } 複製代碼

notice.js

最後剩下的 Notice 組件就很簡單了,只須要根據 Notification 組件傳遞的信息輸出最終的內容便可。能夠自行發揮設計樣式。

class Notice extends Component {
    render() {
        const icons = {
            info: 'icon-info-circle-fill',
            success: 'icon-check-circle-fill',
            warning: 'icon-warning-circle-fill',
            error: 'icon-close-circle-fill',
            loading: 'icon-loading'
        }
        const { type, content } = this.props
        return (
            <div className={`toast-notice ${type}`}> <svg className="icon" aria-hidden="true"> <use xlinkHref={`#${icons[type]}`} /> </svg> <span>{content}</span> </div> ) } } 複製代碼

18-08-05 更新

  • 調整頁面中多條提示的顯示方案爲:容許頁面中同時存在多條提示;
  • 修復添加提示時返回的移除提示方法實際不生效的問題;
  • 優化組件樣式與過渡效果。

注:主要改動爲 notification.js 文件中的 addNotice 和 removeNotice 方法。原文中的代碼未做修改,修改後的代碼請參見 項目源碼

結語

相關文章
相關標籤/搜索