實現基於React的全域性提示元件Toast

clancysong發表於2019-03-04

前戲

正文

需求分析

  • 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 方法。原文中的程式碼未作修改,修改後的程式碼請參見 專案原始碼

結語

相關文章