Material UI框架下SnackBar(消息条)的高级用例--notistack

        notistack是React下Material UI框架的SnackBar(消息条)的高级用例,该用例能同时显示多个独立消息条,使用起来也非常简单。在全局初始化之后就可以在网页上任何需要的地方使用它,而不用为每个页面导入一个Material UI原生的消息条。

       该用例是基于Material UI的SnackBar,做了一下功能和界面封装。下面我们从头来开始学习如何使用它。

一、新建React工程

       我们的第一步照样是新建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

       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高级用法

       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.jshandleClick的定义,在显示一个消息条时增加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。
Material UI框架下SnackBar(消息条)的高级用例--notistack_第1张图片
至此,一个简单的关闭时回调函数示例就完成了。

五、修复一个问题

       注意

       notistack这里有一个问题,使用上面的例子很容易看到,当我们点击按钮显示一个消息条时,点击屏幕任何位置,可以看到我们的log会输出close a snackbar,也就是我们的onClose回调函数会被触发一次。然后等消息条结束时,会又触发一次,再次打印出log,这才是onClose回调函数需要真正执行的时候。我们在开发者工具里点击右键,清理console,然后在页面上点击按钮,就能清楚的复现我刚才提到的问题。
Material UI框架下SnackBar(消息条)的高级用例--notistack_第2张图片
       从图中可以看到回调执行了两次。

       这个问题,也许是我没有仔细查看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输出了。好了,我们来看完工之后的页面。
Material UI框架下SnackBar(消息条)的高级用例--notistack_第3张图片

       补充

       原作者回信之后确认这是一个bug或者不完善的地方,承诺在下一版本修复它。当前notistack已经更新了,这个问题已经修复,不存在了。特此声明,感谢原作者。

七、总结

       我本来也写了一个类似notistack这样的一次初始化全局使用的消息条组件(很简单,并且只能显示一个消息条)并准备写在CSDN上。但是当我在Material UI 文档中看到了notistack的用法后我便立即决定抛弃我自己所写的组件而向大家推荐它(已经有更好的轮子了,何必自己再弄一个)。它和Material UI原生的消息条相比最大的优点是封装了多消息条队列,相互之间独立不影响。

       本来最初只打算写一个notistack的最简单应用,后来想想已经写了就多加一点点功能吧,没想到增加的功能使用起来会有那么一点点小问题(目前是关闭时的回调函数调用)。这些问题也是我在写文章的过程中遇到的,因为我之前也没有用过,属于现学现写。示例很简单,虽然主体文件只有三个简单的js(不含修改的库文件),却也花了不少时间(包含自己摸索问题的临时解决方法)。看来,在CSDN上写一篇原创文章也是不容易的。

       本文中涉及到关闭回调(onClose)时出现了两个问题,虽然我都临时处理了,但还是需要原作者回应后再找到相应的解决办法才行,这样风险才最小。所以建议使用notistack做消息条的读者先暂时不要使用回调功能。期望有的读者能深入研究一下,找到更好的解决办法。

       本文中介绍的消息条功能也可以和我的一篇《从零开始构建React下的多语言实现》中介绍的多语言功能集成在一起,也就是在一个工程中将这两者都应用。有兴趣的读者可以试一下。

       修改后的gitee上的代码为:https://gitee.com/TianCaoJiangLin/notistackdemo.git

       欢迎留言指出错误或者提出宝贵改进意见。

你可能感兴趣的:(React,reactjs)