基於 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 的演示可點擊這裏

相關文章
相關標籤/搜索