在業務開發中,特別是移動端的業務,Toast 使用很是頻繁,幾乎涵蓋全部操做結果反饋的交互:如提交表單成功、驗證表單失敗提示、loading 態提醒...,這種輕量且使用較爲頻繁的組件,咱們要求它使用足夠簡單,不侵入業務代碼,即用即丟,基於這些要求,Toast 組件的實現方式也與其餘組件有着不同的關鍵點,這也是本篇博客的存在乎義。html
由於使用很是頻繁,且要求其隨地可用,所以,咱們但願只用一行代碼:react
Toast.info('this is a toast', 1000);
複製代碼
咱們使用其餘諸如 antd 組件時,大部分的組件須要注入到業務 Dom 中,例如:git
render() {
return (
<div>other components...</div>
<Dropdown overlay={menu}>
<a className="ant-dropdown-link" href="#">
Hover me <Icon type="down" />
</a>
</Dropdown>,
)
}
複製代碼
然而由於 Toast 組件無需常駐頁面當中,即用即丟,且使用的位置變幻無窮,假如須要每次都在須要 Toast 的頁面當中手動注入組件的話,會很是影響效率和業務代碼的可維護性。github
業務中每每會存在多個提示同時發出的場景,好比兩個接口同時請求失敗須要同事提示錯誤緣由,那麼 Toast 就要求不能產生衝突。api
按照咱們書寫組件的慣性思惟,咱們實現一版最簡單的 toast,只須要知足其基本使用,實現後,咱們再分析其存在的問題。bash
再不考慮上述關鍵點的狀況下,咱們書寫以下代碼來實現最簡單粗暴的 Toast:antd
class App extends React.Component {
state = {
isToastShow: false, // 是否展現 Toast
toastText: '', // Toast 文字內容
}
// 設置 Toast 屬性
handleToastShow = (toastText, showTime) => {
this.setState({
isToastShow: true,
toastText
});
// 定時銷燬 Toast Dom
setTimeout(() => {
this.setState({
isToastShow: false
})
}, showTime)
}
// 顯示 Toast
handleShowToast = () => {
this.handleToastShow('this is a toast', 3000)
}
render() {
const { isToastShow, toastText } = this.state;
return (
<div>
<button onClick={this.handleShowToast}>show toast</button>
{isToastShow && <div className="toast-wrap">
<div className="toast-mask" />
<div className="toast-text">{toastText}</div>
</div>}
</div>
)
}
}
複製代碼
這裏咱們發現了幾個問題:app
針對這些問題,接下來咱們逐步實現一個使用簡單方便的 Toast。dom
項目源碼地址: Vincedream/easy-toastide
在講解組件實現前,咱們簡單地閱覽實現後的調用方法:
import React from 'react';
import Toast from './Toast';
function App () {
const handleClick1 = () => {
Toast.info('test111', 2000);
}
const handleClick2 = () => {
Toast.info('test222', 1000, true);
}
const handleClick3 = () => {
Toast.info('test333', 1000, true);
Toast.info('test long duration', 4000, true);
}
const handleHideAllToast = () => {
Toast.hide();
}
return(
<div>
<button onClick={handleClick1}>no mask Toast</button><br/>
<button onClick={handleClick2}>with mask Toast</button><br/>
<button onClick={handleClick3}>long duration</button><br/>
<button onClick={handleHideAllToast}>hideAllToast</button>
</div>
)
}
export default App;
複製代碼
效果:
這裏,咱們調用了 Toast.info()
後,動態地注入組件到 Dom 中,並無將 Toast 任何邏輯在業務容器中的 Dom 或者 Style 中注入。
咱們如何在不侵入容器的條件下,動態地注入 Dom,難道是像十年前 jQuery 時代去手動操做 Dom 嗎?確定不是的。這裏有個關鍵的方法:ReactDom.render(<組件/>, 真實 Dom)
,下面咱們看一個例子:
class App extends React.Component {
handleAddDom = () => {
// 在真實 dom 上建立一個真的的 div 宿主節點,並將其加入到頁面根節點 body 當中
const containerDiv = document.createElement('div');
document.body.appendChild(containerDiv);
// 這裏返回的是對該組件的引用
const TestCompInstance = ReactDom.render(<TestComp />, containerDiv);
console.log(TestCompInstance);
// 這裏能夠調用任何 TestCompInstance 上的方法,而且可以訪問到其 this
TestCompInstance.sayName();
}
render() {
return (
<div>
<button onClick={this.handleAddDom}>add Dom</button>
</div>
)
}
}
複製代碼
執行結果:
從上面的例子咱們能夠看出,咱們能夠在 js 邏輯代碼中直接建立注入一個 React 組件到真實的 dom 中,而且能夠任意操控該組件,理解這點後,咱們便獲得了編寫 Toast 組件最核心的方法。
首先,咱們建立一個 Toast 容器組件:
// ToastContainer.js
class ToastContainer extends Component {
state = {
isShowMask: false, // 當前 mask 是否顯示
toastList: [] // 當前 Toast item 列表
}
// 將新的 toast push 到 toastContainer 中
pushToast = (toastProps) => {
const { type, text, duration, isShowMask = false } = toastProps;
const { toastList } = this.state;
toastList.push({
id: getUuid(),
type,
text,
duration,
isShowMask
});
this.setState({
toastList,
isShowMask
});
}
render() {
const { toastList, isShowMask } = this.state;
return (
<div className="toast-container">
{isShowMask && <div className="mask"/>}
<div className="toast-wrap">
{toastList.reverse().map((item) => (
<Toast {...item} key={item.id} />
))}
</div>
</div>
);
}
}
複製代碼
這個容器用來存放多個 Toast Item,用來控制 Toast 的顯示個數和是否展現 mask,而且將其渲染到容器當中,這裏面邏輯很是簡單。
接着咱們建立真正用來展現的 Toast Item 組件:
// ToastItem.js
class ToastItem extends Component {
render() {
const { text } = this.props;
return (
<div className="toast-item">
{text}
</div>
);
}
}
複製代碼
兩個關鍵組件已經建立完成,咱們須要「動態注入」將其渲染到 dom 中,使用上面講解的 ReactDom.render()
方法,爲此,咱們在建立一個 Toast 統一入口文件:
// index.js
import React from 'react';
import ReactDom from 'react-dom';
import ToastContainer from './ToastContainer';
// 在真實 dom 中建立一個 div 節點,而且注入到 body 根結點中,該節點用來存放下面的 React 組件
const toastContainerDiv = document.createElement('div');
document.body.appendChild(toastContainerDiv);
// 這裏返回的是 ToastContainer 組件引用
const getToastContainerRef = () => {
// 將 <ToastContainer /> React 組件,渲染到 toastContainerDiv 中,而且返回了 <ToastContainer /> 的引用
return ReactDom.render(<ToastContainer />, toastContainerDiv);
}
// 這裏是 <ToastContainer /> 的引用
let toastContainer = getToastContainerRef();
export default {
info: (text, duration, isShowMask) => (toastContainer.pushToast({type: 'info', text, duration, isShowMask})),
};
複製代碼
這裏,咱們按照上面講解的 ReactDom.render()
方法, 將 <ToastContainer />
渲染到 dom 中,而且得到了其引用,咱們只須要在這裏調用 <ToastContainer />
中的 pushToast
方法,便能展現出 Toast 提示。
到這裏,咱們便完成了一個最簡化版的 動態注入版 Toast 組件,接下來的一節中,咱們將爲其添加如下兩個功能:
首先咱們改造 ToastContainer 容器組件,添加一個隱藏 mask 方法,並將其傳入到 <ToastItem />
中:
class ToastContainer extends Component {
...
// 將被銷燬的 toast 剔除
popToast = (id, isShowMask) => {
const { toastList } = this.state;
const newList = toastList.filter(item => item.id !== id);
this.setState({
toastList: newList,
});
// 該 toast item 是否爲 toastList 中 duration 最長的 item
let isTheMaxDuration = true;
// 該 toast item 的 duration
const targetDuration = toastList.find(item => item.id === id).duration;
// 遍歷 toastList 檢查是否爲最長 duration
toastList.forEach(item => {
if (item.isShowMask && item.duration > targetDuration) {
isTheMaxDuration = false
}
return null;
});
// 隱藏 mask
if (isShowMask && isTheMaxDuration) {
this.setState({
isShowMask: false
})
}
}
render() {
...
<ToastItem onClose={this.popToast} {...item} key={item.id} />
...
}
}
複製代碼
接着,咱們改造 <ToastItem />
,在起 componentDidMount
中設置一個定時器,根據傳入的 duration 參數,設置隱藏 Toast 的定時器,而且在組件銷燬前,將定時器清除。
// ToastItem.js
class ToastItem extends Component {
componentDidMount() {
const { id, duration, onClose, isShowMask } = this.props;
this.timer = setTimeout(() => {
if (onClose) {
onClose(id, isShowMask);
}
}, duration)
}
// 卸載組件後,清除定時器
componentWillUnmount() {
clearTimeout(this.timer)
}
render() {
...
}
}
複製代碼
這裏咱們便完成了隱藏 Toast 的功能,其細節在代碼中有詳細的解釋,這裏再也不作贅述。
如何強制的隱藏已經出現的 Toast 呢?這裏咱們依舊使用到 ReactDom
的 api:ReactDom.unmountComponentAtNode(container)
,這個方法的做用是從 Dom 中卸載組件,會將其事件處理器(event handlers)和 state 一併清除:
// index.js
...
// 這裏返回的是 ToastContainer 組件引用
const getToastContainerRef = () => {
// 將 <ToastContainer /> React 組件,渲染到 toastContainerDiv 中,而且返回了 <ToastContainer /> 的引用
return ReactDom.render(<ToastContainer />, toastContainerDiv);
}
// 這裏是 <ToastContainer /> 的引用
let toastContainer = getToastContainerRef();
const destroy = () => {
// 將 <ToastContainer /> 組件 unMount,卸載組件
ReactDom.unmountComponentAtNode(toastContainerDiv);
// 再次建立新的 <ToastContainer /> 引用,以便再次觸發 Toast
toastContainer = getToastContainerRef();
}
export default {
...
hide: destroy
};
複製代碼
須要注意的是,卸載 <ToastContainer />
後,須要再次建立一個新的、空的 <ToastContainer />
組件,以便後續再次調用 Toast。
本篇文章咱們用了新的一種方法來建立一個特殊的 React 組件,實踐了一些你或許沒有使用過的 ReactDom 方法,除了 Toast 組件,咱們還能用一樣的思路編寫其餘的組件,如 Modal、Notification 等組件。
參考:
項目源碼地址: Vincedream/easy-toast