notistack是React下Material UI框架的SnackBar(消息条)的高级用例,该用例能同时显示多个独立消息条,使用起来也非常简单。在全局初始化之后就可以在网页上任何需要的地方使用它,而不用为每个页面导入一个Material UI原生的消息条。
该用例是基于Material UI的SnackBar,做了一下功能和界面封装。下面我们从头来开始学习如何使用它。
我们的第一步照样是新建React工程,在工作目录下执行:
npx create-react-app notistackdemo
耐心等待安装完毕。因为notistack是基于Matrial UI,所以需要再安装相应的库:
cd notistackdemo
npm install @material-ui/core --save
npm install @material-ui/icons --save
npm install notistack --save
好了,我们的准备工作完成了。
notistack提供了一个provider,在使用之前必须先初始化这个provider,首先我们将src/index.js
稍微增加一点内容改写成这样:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { SnackbarProvider } from 'notistack'
ReactDOM.render(
<SnackbarProvider>
<App />
</SnackbarProvider>
,
document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
接下来,我们在主页面上增加一个按钮,用来点击显示消息条。修改src/App.js
,在导入语句中增加下面两行:
import Button from '@material-ui/core/Button';
import { useSnackbar } from 'notistack';
然后在函数组件App里使用如下hook:
const { enqueueSnackbar } = useSnackbar()
最后再增加一个按钮和对应的处理程序,最后修改完成的App.js完整代码如下:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Button from '@material-ui/core/Button';
import { useSnackbar } from 'notistack';
function App() {
const { enqueueSnackbar } = useSnackbar();
const handleClick = event => {
event.preventDefault();
enqueueSnackbar("This is a message",{variant:"success"});
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<Button style={{fontSize:"25px",color:"white",margin:"10px"}} onClick={handleClick}>
Click Me
</Button>
</header>
</div>
);
}
export default App;
运行npm start
,在主页面上点击按钮,就可以看到左下角会显示一个绿色的消息条了。好了,最基本的使用我们已经学习完成了,接下来我们学习一些稍微高级点的用法。
notistack的文档地址为:https://iamhosseindhv.com/notistack/api
从它的文档中我们看到可以自己设置很多属性,比如消息条的显示时间,关闭消息条后的回调函数等等。因为把全部属性在src/index.js
中设置并不是很整洁,于是我们新建一个NotistackWrapper
来进行集中设置。我们先ctrl + c
来关闭开发服务器的运行。
在src/
目录下新建NotistackWrapper.js
,里面代码如下:
//本JS进行一些notistack的常用设置
import React from 'react';
import { SnackbarProvider } from 'notistack';
import { isMobile } from 'react-device-detect'
/**
* 显示的消息条的最大数量,如果超过,会关掉最先打开的然后再显示新的,是一个队列
* 如果只想显示1个,设置为1,3是默认值
*/
const MAX_SNACKBAR = 3
//设置自动隐藏时间,默认值是5秒,也就是5000毫秒
const AUTO_HIDE_DURATION = 3000
//设置消息条位置,默认值为底部左边
const POSITION = {
vertical: 'bottom',
horizontal: 'left'
}
export default function NotistackWrapper({children}) {
return (
<SnackbarProvider
maxSnack={MAX_SNACKBAR}
autoHideDuration={AUTO_HIDE_DURATION}
anchorOrigin={POSITION}
dense={isMobile}
>
{children}
</SnackbarProvider>
)
}
然后再改写src/index.js
,使用刚才自定义包装器来代替SnackbarProvider
,将第6行到第13行代码改为:
import NotistackWrapper from './NotistackWrapper.js'
ReactDOM.render(
<NotistackWrapper>
<App />
</NotistackWrapper>
,
document.getElementById('root'));
这里我们做了移动端适配,移动端显示时让消息条底部靠边,用react-device-detect
库来判断是否移动端,需要进行安装。
npm install react-device-detect --save
让我们再次运行npm start
,大家可以自己对照一下文档更改一下参数,来查看一下效果。
有时,我们需要在消息条关闭时进行一些操作,比如提示完成之后的页面跳转等,这时就需要增加一个回调函数。
让我们改写一下src/App.js
中handleClick
的定义,在显示一个消息条时增加onClose
属性。
const handleClick = event => {
event.preventDefault();
// variant could be success, error, warning, info, or default
let options = {
variant:"success",
onClose:() => console.log("close a snackbar")
};
enqueueSnackbar("This is a message", options);
};
注意variant
属性只能是上面列举的几种值。
打开Chrome浏览器的开发者工具,在console那一栏就能看到我们打印的log。
至此,一个简单的关闭时回调函数示例就完成了。
注意
notistack这里有一个问题,使用上面的例子很容易看到,当我们点击按钮显示一个消息条时,点击屏幕任何位置,可以看到我们的log会输出close a snackbar
,也就是我们的onClose
回调函数会被触发一次。然后等消息条结束时,会又触发一次,再次打印出log,这才是onClose
回调函数需要真正执行的时候。我们在开发者工具里点击右键,清理console,然后在页面上点击按钮,就能清楚的复现我刚才提到的问题。
从图中可以看到回调执行了两次。
这个问题,也许是我没有仔细查看notistack的文档导致用法不对,也许是它本身的问题。我已经向作者发了邮件进行请教,在未得到作者的回应之前,让我们先采取一个临时措施来处理它。
打开node_modules\notistack\build\SnackbarItem
目录下的SnackbarItem.js
,找到第77行,也就是_this.handleClose
这个函数。修改它的定义,在第79行var snack = _this.props.snack;
上面增加以下代码片断:
if(reason === _constants.REASONS.CLICKAWAY) {
return;
}
保存之后我们ctrl + c
,然后再次运行npm start
。这时可以看到,消息条出现时无论你点击屏幕多少下,都没有log输出了。
如果是我操作上存在的问题,请大家指正后我再更新文章。
补充
这个问题我和作者进行了几次邮件沟通,作者回信中指出这不是一个问题或者bug,而就是这样设计的,点击网页上任意地方(‘clickaway’)都会触发回调执行,因为有些人可能需要在此做一些处理。回调函数有两个参数event和reason,可以使用第二个参数来做判断和过滤。虽然我觉得任意点击执行关闭回调怪怪的,可以增加一个任意点击回调函数嘛,但是问题还是得到了解决。将上面的回调函数改写如下:
const handleClick = event => {
event.preventDefault();
// variant could be success, error, warning, info, or default
let options = {
variant:"success",
onClose:(_blank,reason) => {
if(reason === 'clickaway') {
return;
}
console.log("close a snackbar")
}
};
enqueueSnackbar("This is a message", options);
};
这样,再次点击屏幕任意位置,代表回调执行的log就不再输出了。感谢作者的耐心回复。
有时用户可能不想等待消息条显示特定时间(比如3秒)而是想读完之后立刻关掉它。这时我们可以在消息条上增加一个关闭按钮。首先让我们修改src/NotistackWrapper.js
,在导入语句中增加关闭按钮导入:
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
然后我们再增加点击关闭功能,修改完成的最终代码如下:
//本JS进行一些notistack的常用设置
import React from 'react';
import { SnackbarProvider } from 'notistack';
import { isMobile } from 'react-device-detect';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
/**
* 显示的消息条的最大数量,如果超过,会关掉最先打开的然后再显示新的,是一个队列
* 如果只想显示1个,设置为1,3是默认值
*/
const MAX_SNACKBAR = 3
//设置自动隐藏时间,默认值是5秒
const AUTO_HIDE_DURATION = 10000
//设置消息条位置,默认值为底部左边
const POSITION = {
vertical: 'bottom',
horizontal: 'left'
}
export default function NotistackWrapper({children}) {
const notistackRef = React.createRef();
const onClickDismiss = key => () => {
notistackRef.current.closeSnackbar(key);
}
return (
<SnackbarProvider
maxSnack={MAX_SNACKBAR}
autoHideDuration={AUTO_HIDE_DURATION}
anchorOrigin={POSITION}
dense={isMobile}
ref={notistackRef}
action={(key) => (
<IconButton key="close" aria-label="Close" color="inherit" onClick={onClickDismiss(key)}>
<CloseIcon style={{fontSize:"20px"}}/>
</IconButton>
)}
>
{children}
</SnackbarProvider>
)
}
保存后页面会自动刷新,此时再点击按钮,出现的消息条就有一个关闭按钮了,点击这个按钮可以关掉它。
再次注意
等等,有问题?如果读者边读边操作可能会发现,手动点击关闭按钮关掉消息条时,没有打印log,也就是没有触发onClose
回调。这里我看了一下源码,点击这个按钮时并不是通过具体的SnackbarItem
本身来处理的,而是在它的上一层处理,所以就没有执行回调。这个问题我同样发邮件向作者请教了,也可能是我用法的问题。不过目前我们仍然采取一个临时措施来处理它。
我们需要修改node_modules\notistack\build\SnackbarProvider.js
这个文件,找到_this.handleCloseSnack
这个方法定义,在第202行左右,将它里面的_this.setState
那一段代码加上一点内容(大概217行下面),整个代码片断如下:
_this.setState(function (_ref3) {
var snacks = _ref3.snacks,
queue = _ref3.queue;
return {
snacks: snacks.map(function (item) {
if (!shouldCloseAll && item.key !== key) {
return _extends({}, item);
}
if(item.key === key) {
if(reason === null) {
if(item.onClose) {
item.onClose()
}
}
}
return item.entered ? _extends({}, item, { open: false }) : _extends({}, item, { requestClose: true });
}),
queue: queue.filter(function (item) {
return item.key !== key;
}) // eslint-disable-line react/no-unused-state
};
});
然后保存,ctrl + c
之后再npm start
,这时你再点击消息条上的关闭按钮,就会有正确的log输出了。好了,我们来看完工之后的页面。
补充
原作者回信之后确认这是一个bug或者不完善的地方,承诺在下一版本修复它。当前notistack已经更新了,这个问题已经修复,不存在了。特此声明,感谢原作者。
我本来也写了一个类似notistack这样的一次初始化全局使用的消息条组件(很简单,并且只能显示一个消息条)并准备写在CSDN上。但是当我在Material UI 文档中看到了notistack的用法后我便立即决定抛弃我自己所写的组件而向大家推荐它(已经有更好的轮子了,何必自己再弄一个)。它和Material UI原生的消息条相比最大的优点是封装了多消息条队列,相互之间独立不影响。
本来最初只打算写一个notistack的最简单应用,后来想想已经写了就多加一点点功能吧,没想到增加的功能使用起来会有那么一点点小问题(目前是关闭时的回调函数调用)。这些问题也是我在写文章的过程中遇到的,因为我之前也没有用过,属于现学现写。示例很简单,虽然主体文件只有三个简单的js(不含修改的库文件),却也花了不少时间(包含自己摸索问题的临时解决方法)。看来,在CSDN上写一篇原创文章也是不容易的。
本文中涉及到关闭回调(onClose
)时出现了两个问题,虽然我都临时处理了,但还是需要原作者回应后再找到相应的解决办法才行,这样风险才最小。所以建议使用notistack做消息条的读者先暂时不要使用回调功能。期望有的读者能深入研究一下,找到更好的解决办法。
本文中介绍的消息条功能也可以和我的一篇《从零开始构建React下的多语言实现》中介绍的多语言功能集成在一起,也就是在一个工程中将这两者都应用。有兴趣的读者可以试一下。
修改后的gitee上的代码为:https://gitee.com/TianCaoJiangLin/notistackdemo.git
欢迎留言指出错误或者提出宝贵改进意见。