实现基于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>
        )
    }
}
复制代码

结语

猜你喜欢

转载自juejin.im/post/5b63fdd46fb9a04fa7757081