基于 React, 如何实现全局提示?

在 Web 项目当中,一个全局的提示组件多是一个广泛的需求。javascript

当用户作了一些操做,提示组件能够给用户相应的提醒。好比在页面上,用户作了增删操做,须要提示增长/删除成功。css

好比像下面这样: html

需求分析

若须要用 React 实现一个全局提示组件,这里咱们称它为 message,参考 Ant Design , 全局提示组件向外暴露的不是一个组件,而是一个 API, 调用以下:java

import message from './Message'

message.info('提示信息')
复制代码

这样一来,只须要在须要提示的地方引入 message, 直接调用它的方法就能够弹出提示信息。react

如上面的代码所示,全局提示组件并无输出一个组件,这意味着须要在 message.info 方法调用以前,把相关的组件渲染到页面上,为展现的信息提供一个容器。在调用 message.info 方法以后,再把提示的内容渲染到页面上。git

代码编写

如上面的分析,message 须要的组件是一个容器组件 MessageContainer, 以及展现提示信息的组件 Message, 每当调用了提示的方法,就往容器组件里面新增一个 Message 组件,来展现提示内容。github

因此,代码目录结构以下:web

├─Message
│      index.tsx
│      message.tsx
│      _style.scss
复制代码

容器组件

挂载

容器组件须要预先渲染到页面上,这个渲染的动做就在 message 的入口文件 index.ts.api

message 被某个组件引入,这个文件就会执行,将容器组件 MessageContainer 渲染到页面上;而且,就算有多个组件引入了 message, 入口文件的代码也只会执行一次,不会形成冲突。数组

渲染的代码以下:

let el = document.querySelector('#message-wrapper')
if (!el) {
  el = document.createElement('div')
  el.className = 'message-wrapper'
  el.id = 'message-wrapper'
  document.body.append(el)
}

ReactDOM.render(
  <MessageContainer />,
  el
)
复制代码

在上面的代码中,咱们把 MessageContainer 挂载到页面上。

提示信息队列

容器组件负责加载包含提示信息的 Message 组件。

通常来讲,提示信息会有一个时长,好比弹出 3 秒后自动关闭,而且当 3 秒内再次触发提示,页面上会有两条提示信息,以下所示:

而页面内不可能同时放得下一万条提示信息,因此须要对提示信息须要有一个数目上限,咱们在这里暂时把它定为 10 条。

总结一下,就是调用 message.info 方法,MessageContainer 内渲染一个携带提示信息的 Message 组件,而且在 3 秒后把该 Message 组件移除;假如 3 秒内又有一个提示信息,再添加一个 Message 组件,而且 3 秒后移除。当页面内的提示信息大于十条,就删除第一条提示信息,移除第一个 Message 组件。

添加信息

关于 MessageContainer ,添加信息的代码以下:

import Message from './message'

let add: (notice: Notice) => void

export const MessageContainer = () => {
  const [notices, setNotices] = useState<Notice[]>([])

  add = (notice: Notice) => {
    setNotices((prevNotices) => [...prevNotices, notice])
  }

  return (
    <div className="message-container"> { notices.map(({ text, key, type }) => ( <Message key={key} type={type} text={text} /> )) } </div>
  )
}
复制代码

上面的代码就是 MessageContainer 组件的一部分,这部分负责添加信息,这些代码一样放在 message 的入口文件 index.ts.

注意到,上面的 add 函数,并非在 MessageContainer 内部声明的,由于这个函数须要被外部调用,来改变 MessageContainer 的内部状态。

const [notices, setNotices] = useState<Notice[]>([]) 初始化了 notices, 这里的 notices 就是提示信息的队列。

addMessageContainer 内部完成赋值,接受一个 notice 做为参数,并把这个 notice 添加到 notices 队列当中。这里 setNotices的参数是一个匿名函数,而不是一个值,由于须要拿到先前的 notices 队列来更新 notices, 假如用下面这样的写法,并不必定能正确更新 notices:

add = (notice: Notice) => {
	setNotices([...notices, notice])
}
复制代码

由于当频繁触发 add 的时候,颇有可能会跳过其中的几回更新,其中原因,可参考 useState 函数式更新

MessageContainer 返回的便是它的渲染内容,根据提示信息队列来渲染 Message 组件。Message 组件稍后会分析。关于 Notice,由以上MessageContainer 返回内容的代码可见,Notice 实例有 text, type, key 三个属性,其结构以下:

export interface Notice {
  text: string; // 提示消息文本
  key: string; // 该条信息的 uuid
  type: MessageType; // 提示信息的类型
}
复制代码

删除信息

上面说到,当一条信息出现超过 3 秒,或者信息队列的长度超过 10, 都会删除信息。添加了删除逻辑的 MessageContainer 代码以下:

import Message from './message'

let add: (notice: Notice) => void

export const MessageContainer = () => {
  const [notices, setNotices] = useState<Notice[]>([])
  const timeout = 3 * 1000
  const maxCount = 10

  const remove = (notice: Notice) => {
    const { key } = notice

    setNotices((prevNotices) => (
      prevNotices.filter(({ key: itemKey }) => key !== itemKey)
    ))
  }

  add = (notice: Notice) => {
    setNotices((prevNotices) => [...prevNotices, notice])

    setTimeout(() => {
      remove(notice)
    }, timeout)
  }

  useEffect(() => {
    if (notices.length > maxCount) {
      const [firstNotice] = notices
      remove(firstNotice)
    }
  }, [notices])

  return (
    <div className="message-container"> { notices.map(({ text, key, type }) => ( <Message key={key} type={type} text={text} /> )) } </div>
  )
}
复制代码

上面的代码中,变量 timeout 定义了单条信息的时长,maxCount 则是信息数量的上限。

remove 方法中,先取得 noticekey, 这个 key 是单条 notice 的惟一值,能够根据这个值删除信息队列 notices 中的某一条 notice. 删除 notice 与添加 notice 相似,用了函数式更新 state . 这里的删除方法是数组的 filter 方法。

add 方法中,能够看到,多出了一个定时器,在 timeout 时间以后,将删除该条信息的代码放入执行队列。

当信息超过 10 条,将删除第一条信息,这里利用 useEffect 实现。useEffect 的依赖项就是提示信息队列 notices , 当 notices 发生变化,就会执行 uesEffect 中的回调函数。当 notices 的长度大于 10,将会调用 remove 方法移除第一条提示信息。提取第一条信息的代码是const [firstNotice] = notices , 这里利用了数组的解构赋值。

Message 组件

Message 组件只是一个纯展现的组件,负责展现提示信息文本。除了文本,提示信息通常还会有各类类型,这里用图标来表示,以下所示:

不一样类型的提示,对应不一样的图标,在视觉上给出更加直观的表述。

可见 Message 内部有一个表明提示类型的图标,还有提示信息的文本内容,因此 Message 组件有接受两个属性,分别是 typetext .

涉及到图标渲染,这里利用的图标库是 react-fontawesome, 而且在编写 message 以前,已经简单封装了一个 Icon 组件,固然,这两点不是特别重要,只是一个前情提要,方便如下代码的阅读。

Message 的代码以下:

import React, { FC, ReactElement } from 'react'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import Icon from '../Icon/icon'

export type MessageType = 'info' | 'success' | 'danger' | 'warning'

export interface MessageProps {
 text: string;
 type: MessageType
}

const Message: FC<MessageProps> = (props: MessageProps) => {
 const { text, type } = props

 const renderIcon = (messageType: MessageType): ReactElement => {
   let messageIcon: IconProp

   switch (messageType) {
     case 'success':
       messageIcon = 'check-circle'
       break
     case 'danger':
       messageIcon = 'times-circle'
       break
     case 'warning':
       messageIcon = 'exclamation-circle'
       break
     case 'info':
     default:
       messageIcon = 'info-circle'
       break
   }

   return <Icon icon={messageIcon} theme={messageType} />
 }

 return (
   <div className="message"> <div className="message-content"> <div className="icon"> {renderIcon(type)} </div> <div className="text"> {text} </div> </div> </div>
 )
}
复制代码

上面代码,一开始定义了提示信息的类型 MessageType, 以及 Message 组件的 props 类型 MessageProps .

Message 内部的 renderIcon 方法,便是根据提示类型来渲染不一样类型,不一样颜色的图标。

message API

当容器组件 MessageContainerMessage 组件都准备好,就须要暴露一个 API 给外部调用,来渲染提示信息。

已知 MessageContainer 已经预先渲染到页面中,一开始,它的内部信息队列 notices 是空的。而且, MessageContainer 中添加信息的方法 add 所在做用域并非在 MessageContainer 内部,咱们能够在外部调用这个方法来给 MessageContainer 添加信息。

index.ts 内的代码大体以下:

export interface MessageApi {
  info: (text: string) => void;
  success: (text: string) => void;
  warning: (text: string) => void;
  error: (text: string) => void;
}

export interface Notice {
  text: string; // 提示消息文本
  key: string; // 该条信息 uuid
  type: MessageType; // 提示信息的类型
}

let seed = 0
const now = Date.now()
const getUuid = (): string => {
  const id = seed
  seed += 1
  return `MESSAGE_${now}_${id}`
}

let add: (notice: Notice) => void
export const MessageContainer = () => {
  // 省略
}

const api: MessageApi = {
  info: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'info'
    })
  },
  success: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'success'
    })
  },
  warning: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'warning'
    })
  },
  error: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'danger'
    })
  }
}

export default api
复制代码

MessageApi 接口规定了 message API 的形状,info, success, warning, error 四个字段表明四个类型不一样的方法,调用方式如 message.success('成功信息'), message.info('提示信息') 等。

Notice 接口规定了单条提示信息 notice 的字段。

getUuid 则是获取单条提示信息的 uuid 的方法,在 MessageContainer 中,须要依据这个值,来删除某条提示信息。

接下来就是 add 方法的声明,以及 MessageContainer 组件,add 方法声明在外部,赋值在 MessageContainer 内部,便可实如今 MessageContainer 外部改变其状态。

最后是变量 api, 实现了 MessageApi 接口的各个方法,在 api 实现的方法中,调用 add 来添加信息,改变 MessageContainer 的状态,使得提示信息渲染到页面上。

动画

到此,message 就实现了基本的功能。在提示信息中,若是有动画的过渡,那么信息就不会忽然弹出或忽然关闭,显得很突兀,并且,添加了动画,也更加美观。

添加了动画以后,调用 message.info('默认提示'), 效果以下:

可见提示信息在出现的时候,有个从上到下的过渡,以及透明度的变化;消失的时候则反之。

这里使用的动画库是 React Transition Group,这个库能够在组件加载卸载过程当中,为组件添加相应的 className, 这样一来,就能够对应的 className 编写样式,实现动画的过渡效果。

动画的样式以下:

.slide-in-top-enter {
  opacity: 0;
  transform: translateY(-100%);
}

.slide-in-top-enter-active {
  opacity: 1;
  transform: translate(0);
  transition: transform 200ms ease-out, opacity 200ms ease-in-out;
}

.slide-in-top-exit {
  opacity: 1;
}

.slide-in-top-exit-active {
  opacity: 0;
  transform: translateY(-100%);
  transition: transform 300ms linear 100ms, opacity 300ms ease-in-out;
}
复制代码

这里结合 React Transition Group 的 CSSTransition 实现了一个“从上往下出现,从下往上消失”的动画。

以上代码中,类名里的 enter, enter-active 后缀, 分别表明组件“开始出现”,”出现过程当中“的状态;exitexit-active 后缀分别对应“开始消失”,“消失过程当中”的状态。这些后缀都是 CSSTransition 所赋予的。

总结

这就是一个 React 全局提示的简单实现,关键之处就是 MessageContaineradd 方法,它暴露在外部,让外部方法能够修改内部状态。

文中的代码只是大体呈现,完整的代码可参考这里,查看 message 的演示可点击这里

相关文章
相关标签/搜索