![9e882e8fbaac366fd800472ce0806a5f.png](https://i-blog.csdnimg.cn/blog_migrate/94b570d96e22e1b6767d1fba310962a8.jpeg)
在 Web 项目当中,一个全局的提示组件可能是一个普遍的需求。
当用户做了一些操作,提示组件可以给用户相应的提醒。比如在页面上,用户做了增删操作,需要提示增加/删除成功。
比如像下面这样:
![c78a58b49ca8b57785d8d520c7c0ba48.png](https://i-blog.csdnimg.cn/blog_migrate/40e05b7a400dae69876110cb16d95ea8.png)
需求分析
若需要用 React 实现一个全局提示组件,这里我们称它为 message
,参考 Ant Design , 全局提示组件向外暴露的不是一个组件,而是一个 API, 调用如下:
import message from './Message'
message.info('提示信息')
这样一来,只需要在需要提示的地方引入 message
, 直接调用它的方法就可以弹出提示信息。
如上面的代码所示,全局提示组件并没有输出一个组件,这意味着需要在 message.info
方法调用之前,把相关的组件渲染到页面上,为展示的信息提供一个容器。在调用 message.info
方法之后,再把提示的内容渲染到页面上。
代码编写
如上面的分析,message
需要的组件是一个容器组件 MessageContainer
, 以及展示提示信息的组件 Message
, 每当调用了提示的方法,就往容器组件里面新增一个 Message
组件,来展示提示内容。
所以,代码目录结构如下:
├─Message
│ index.tsx
│ message.tsx
│ _style.scss
容器组件
挂载
容器组件需要预先渲染到页面上,这个渲染的动作就在 message
的入口文件 index.ts
.
当 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 秒内再次触发提示,页面上会有两条提示信息,如下所示:
![e29da6484e8c48c2a718a57b4a1920fc.png](https://i-blog.csdnimg.cn/blog_migrate/119588d70a778c233e2772295cd05d00.png)
而页面内不可能同时放得下一万条提示信息,所以需要对提示信息需要有一个数目上限,我们在这里暂时把它定为 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
组件只是一个纯展示的组件,负责展示提示信息文本。除了文本,提示信息一般还会有各种类型,这里用图标来表示,如下所示:
![8abba922312b4a3e170bb85fe2834edd.png](https://i-blog.csdnimg.cn/blog_migrate/99f04fb1ef688292f481ae3ff59e3b97.png)
不同类型的提示,对应不同的图标,在视觉上给出更加直观的表述。
可见 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
方法,即是根据提示类型来渲染不同类型,不同颜色的图标。
message API
当容器组件 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('默认提示')
, 效果如下:
![8f2443f20088e22f7a5d46401c9f8ec2.gif](https://i-blog.csdnimg.cn/blog_migrate/3786f32c8ecc8d9bef31fea8549d7605.gif)
可见提示信息在出现的时候,有个从上到下的过渡,以及透明度的变化;消失的时候则反之。
这里使用的动画库是 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
方法,它暴露在外部,让外部方法可以修改内部状态。
文中的代码只是大致呈现,完整的代码可参考这里,查看 message
的演示可点击这里 。