在 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
方法,它暴露在外部,讓外部方法能夠修改內部狀態。