在 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
就是提示信息的队列。
add
在 MessageContainer
内部完成赋值,接受一个 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
方法中,先取得 notice
的 key
, 这个 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
组件有接受两个属性,分别是 type
和 text
.
涉及到图标渲染,这里利用的图标库是 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
方法,便是根据提示类型来渲染不一样类型,不一样颜色的图标。
当容器组件 MessageContainer
和 Message
组件都准备好,就须要暴露一个 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
后缀, 分别表明组件“开始出现”,”出现过程当中“的状态;exit
和 exit-active
后缀分别对应“开始消失”,“消失过程当中”的状态。这些后缀都是 CSSTransition
所赋予的。
这就是一个 React 全局提示的简单实现,关键之处就是 MessageContainer
的 add
方法,它暴露在外部,让外部方法能够修改内部状态。