實現基於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 組件可以被分爲三個部分,分別爲:

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

項目目錄結構如下:

├── toast
│   ├── icons.js
│   ├── index.js
│   ├── notice.js
│   ├── notification.js
│   ├── toast.css
│   ├── toast.js

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

toast.js

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

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)。

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

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

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

(注:關於頁面中同時存在多條提示時的顯示問題,本文中採用的方案時直接將後一條提示替換掉前一條消息,所以代碼中添加 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 方法。原文中的代碼未作修改,修改後的代碼請參見 項目源碼

結語

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章