React15.6.0实现Modal弹层组件

比眉伴天荒 2022-05-28 02:41 193阅读 0赞

代码地址如下:
http://www.demodashi.com/demo/12315.html

注:本文Demo环境使用的是我平时开发用的配置:这里是地址。

本文适合对象

  1. 了解React。
  2. 使用过webpack3。
  3. 熟悉es6语法。

项目说明

项目结构截图

oGbRigKFD7BPYetNQap.jpg

项目运行说明

  1. npm install
  2. npm run start
  3. npm run startfe
  4. 登录localhost:8088查看demo

Modal组件分析

Modal组件是属于一个网站中比较常用的基础组件,但是在实现方面上稍微复杂一些,对场景支持的需求度较高。

这里是Antd中Modal组件的演示Demo。

oQgJ4WdHDC347TNWnZR.png

首先分析这个组件的组成结构:

  1. title Modal弹层的标题部分。
  2. content Modal弹层的主体部分。
  3. footer Modal弹层最后的button部分。
  4. background 整个黑色背景

其次,这个弹层不能生硬的出现,所以一定要有动画效果。

最后,弹层是在合适的地方通过用户交互的形式出现的,所以又一个控制器来控制Modal弹层的出现和关闭。

Modal组件的实现

静态组件

首先来思考如何实现静态组件部分的代码。

先在components下面创建我们的modal组件结构。
- -components/
- -modal/
- -modal.js
- -modal.scss

这里样式文件使用scss,如果不熟悉的同学可以使用css代替或者先学习一下scss语法规则。

modal.js中创建出组件的基础部分。

  1. import React, { Component } from 'react';
  2. import PropTypes from 'prop-types';
  3. import './modal.scss';
  4. export default class Modal extends Component {
  5. constructor(props) {
  6. super(props);
  7. }
  8. render() {
  9. return (
  10. <div>Modal</div> ); } } Modal.propTypes = {}; Modal.defaultProps = {};

接下来分析我们的组件都需要预留哪些接口:

  1. 开关状态isOpen
  2. Modal标题title
  3. Modal主体内容children
  4. Modal类名className
  5. 点击黑色区域是否可以关闭maskClosable
  6. 关闭按钮文案 cancelText
  7. 确认按钮文案 okText
  8. 关闭按钮回调函数 onCancel
  9. 确认按钮回调函数 onOk

目前能想到的接口有这些,接下来我们可以补充一下我们的代码。

  1. // 刚才的代码部分
  2. Modal.propTypes = {
  3. isOpen: PropTypes.bool.isRequired,
  4. title: PropTypes.string.isRequired,
  5. children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired,
  6. className: PropTypes.string,
  7. maskClosable: PropTypes.bool,
  8. onCancel: PropTypes.func,
  9. onOk: PropTypes.func,
  10. okText: PropTypes.string,
  11. cancelText: PropTypes.string
  12. };
  13. Modal.defaultProps = {
  14. className: '',
  15. maskClosable: true,
  16. onCancel: () => {},
  17. onOk: () => {},
  18. okText: 'OK',
  19. cancelText: 'Cancel'
  20. };

定义好接口之后,我们可以根据我们的接口来完善一下Modal组件。

  1. export default class Modal extends Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. isOpen: props.isOpen || false
  6. };
  7. }
  8. componentWillReceiveProps(nextProps) {
  9. if('isOpen' in nextProps) {
  10. this.setState({
  11. isOpen: nextProps.isOpen
  12. });
  13. }
  14. }
  15. render() {
  16. const {
  17. title,
  18. children,
  19. className,
  20. okText,
  21. cancelText,
  22. onOk,
  23. onCancel,
  24. maskClosable
  25. } = this.props;
  26. return (
  27. <div className={`mocal-container ${ className}`}> <div className="modal-body"> <div className={`modal-title ${ type}`}>{title}</div> <div className="modal-content">{children}</div> <div className="modal-footer"> <button className="ok-btn" onClick={onOk}>{okText}</button> <button className="cancel-btn" onClick={onCancel}>{cancelText}</button> </div> </div> </div> ); } }

接下来是Modal组件的样式:

  1. .modal-container {
  2. background-color: rgba(33, 33, 33, .4);
  3. position: fixed;
  4. top: 0;
  5. left: 0;
  6. right: 0;
  7. bottom: 0;
  8. opacity: 1;
  9. .modal-body {
  10. background-color: #fff;
  11. border-radius: 5px;
  12. padding: 30px;
  13. width: 400px;
  14. position: absolute;
  15. left: 50%;
  16. top: 40%;
  17. transform: translate3d(-50%, -50%, 0);
  18. .modal-title {
  19. text-align: center;
  20. font-size: 18px;
  21. font-weight: bold;
  22. }
  23. .modal-content {
  24. min-height: 100px;
  25. }
  26. .modal-footer {
  27. text-align: center;
  28. button {
  29. margin: 0 20px;
  30. padding: 8px 27px;
  31. font-size: 16px;
  32. border-radius: 2px;
  33. background-color: #ffd900;
  34. border: 0;
  35. outline: none;
  36. &:hover {
  37. cursor: pointer;
  38. background-color: #fff000;
  39. }
  40. }
  41. }
  42. }
  43. }

基础部分写完之后,我们可以来验证一下自己的组件是否能够正常运行了。

我们在直接在containers里面的hello里面引入Modal测试即可:

  1. import React, { Component } from 'react';
  2. import Modal from 'components/modal';
  3. export default class Hello extends Component {
  4. render() {
  5. return (
  6. <Modal title="Demo" okText="确认" cancelText="取消" > <div>Hello world!</div> </Modal> ); } }

node启动开发机,登录到localhost:8088,可以看到我们的组件运行良好:
J4BPWi5m2y5p9CQ1OFo.png

但是似乎还是有一点瑕疵,我们的Modal不可能只有一个状态,因此我们需要一个type接口,来控制我们显示哪一种Modal,比如success、error等。

继续改造Modal.js

  1. Modal.PropTypes = {
  2. // ...
  3. type: PropTypes.oneOf(['alert', 'confirm', 'error'])
  4. };
  5. Modal.defaultProps = {
  6. // ...
  7. type: 'alert',
  8. };

我们在scss中稍微改变一点样式,能让我们分辨出来。
基本上都是使用特定的icon图片来作区分,这里为了简化代码量,直接使用emoji字符来代替了。

  1. .modal-title {
  2. // ...
  3. &.error:before {
  4. content: '❌';
  5. display: inline-block;
  6. }
  7. &.success:before {
  8. content: '✔';
  9. color: rgb(75, 231, 14);
  10. display: inline-block;
  11. }
  12. &.confirm:before {
  13. content: '❓';
  14. display: inline-block;
  15. }
  16. &.alert:before {
  17. content: '❕';
  18. display: inline-block;
  19. }
  20. }

现在在看我们的组件,可以看到已经有区分度了:

IEN9n6TA1QWssA007Wo.png

正常情况下,我们会继续细分很多东西,比如什么情况下不显示按钮组,什么情况下只显示确认按钮等。这里就不进行细分工作了。

挂载方法

Modal组件的骨架搭好之后,我们可以开始考虑组件需要的方法了。

首先组件是要可以关闭的,并且我们无论点击确认或者取消或者黑色弹层都要可以关闭组件。

而且当我们组件打开的时候,需要给body加上类名,方便我们之后的一切操作。

  1. const modalOpenClass = 'modal-open';
  2. const toggleBodyClass = isOpen => {
  3. const body = document.body;
  4. if(isOpen) {
  5. body.classList.add(modalOpenClass);
  6. } else {
  7. body.classList.remove(modalOpenClass);
  8. }
  9. }
  10. export default class Modal extends Component {
  11. /// ...
  12. constructor(props) {
  13. // ...
  14. toggleBodyClass(props.isOpen);
  15. }
  16. // 关闭弹层函数
  17. close() {
  18. this.setState() {
  19. isOpen: false
  20. };
  21. toggleBodyClass(false);
  22. }
  23. // 点击确认回调函数
  24. onOkClick() {
  25. this.props.onOk();
  26. this.close();
  27. }
  28. // 点击取消的回调函数
  29. onCancelClick() {
  30. this.props.onCancel();
  31. this.close();
  32. }
  33. // ...
  34. }

这些函数因为都要绑定到dom节点上,因此要提前绑定this,因此我们可以写一个工具函数,创建一个lib文件夹,在lib下创建一个util.js文件。

  1. // lib/util
  2. export default {
  3. bindMethods(methods, obj) {
  4. methods.forEach(func => {
  5. if(typeof func === 'function') {
  6. obj[func] = obj[func].bind(this);
  7. }
  8. })
  9. }
  10. }

然后在我们的Modal组件中引入util文件,绑定函数的this。

  1. // Modal.js
  2. import util from 'lib/util';
  3. // ...
  4. constructor(props) {
  5. // ...
  6. util.bindMethods(['onCancelClick', 'onOkClick', 'close'], this);
  7. }
  8. // ...

然后我们就可以将刚才的点击函数都替换掉:

  1. render() {
  2. // ...
  3. return (
  4. <div className={`mocal-container ${ className}`} onClick={maskClosable ? this.close : () => {}}> <div className="modal-body"> <div className={`modal-title ${ type}`}>{title}</div> <div className="modal-content">{children}</div> <div className="modal-footer"> <button className="ok-btn" onClick={this.onOkClick}>{okText}</button> <button className="cancel-btn" onClick={this.onCancelClick}>{cancelText}</button> </div> </div> </div> ); }

去实验一下代码,发现确实可以关闭了。

控制器

Modal组件主体部分写完之后,我们还要考虑考虑实际业务场景。

我们都知道React是一个组件化的框架,我们写好这个Modal组件后,不可能是将这个组件嵌套在其他组件内部使用的,而是要直接在body下面占满全屏显示,所以写到这里为止是肯定不够的。

并且在网站中,一般都是有一个按钮,当用户点击之后,才弹出Modal提示用户。

因此,我们现在这种通过组件调用的方式是肯定不行的,因此还要对这个Modal组件进行封装。

modal目录下创建一个index.js文件,代表我们整个Modal组件的入口文件。

然后在index.js中书写我们的主要控制器代码:

  1. // index.js
  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. import Modal from './modal';
  5. const show = (props) => {
  6. let component = null;
  7. const div = document.createElement('div');
  8. document.body.appendChild(div);
  9. const onClose = () => {
  10. ReactDOM.unmountComponentAtNode(div);
  11. document.body.removeChild(div);
  12. if(typeof props.onClose === 'function') {
  13. props.onClose();
  14. }
  15. }
  16. ReactDOM.render(
  17. <Modal { ...props} onClose={onClose} ref={c => component = c} isOpen >{props.content}</Modal>, div ); return () => component.close(); } const ModalBox = {}; ModalBox.confirm = (props) => show({ ...props, type: 'confirm' }); ModalBox.alert = (props) => show({ ...props, type: 'alert' }); ModalBox.error = (props) => show({ ...props, type: 'error' }); ModalBox.success = (props) => show({ ...props, type: 'success' }); export default ModalBox;

这段控制器的代码比较简单。

show函数用来控制Modal组件的显示,当show之后,在body下面创建一个div,然后将Modal组件熏染到这个div下面,并且在删除的时候一起将div和Modal组件都删除掉。

ModalBox就负责我们平时动态调用,根据我们传入不同的type值而显示不同type的Modal组件。

现在我们可以去改造一下container的入口文件了:

  1. // hello.js
  2. import React, { Component } from 'react';
  3. import Modal from 'components/modal';
  4. export default class Hello extends Component {
  5. render() {
  6. return (
  7. <div> <button onClick={() => Modal.confirm({ title: 'Demo', content: 'Hello world!', okText: '确认', cancelText: '取消', onOk: () => console.log('ok'), onCancel: () => console.log('cancel') })}>click me!</button> </div> ); } }

到此为止,我们点击click me的按钮之后,可以正常显示和关闭Modal组件了,并且点击确认和取消按钮的时候,都会调用相对应的回调函数来显示'ok' 'cancel'字样。

动画效果

生硬的Modal组件自然不是我们最终追求的效果,所以我们还要加上最后一个部分:动画效果。

React实现动画的方式有很多,但是总结起来可能只有两种:

  1. 使用css3实现动画。
  2. 根据react的状态管理利用js实现动画。

在复杂动画的情况下,一般选择第二种,因此我这里也是使用第三方react动画库来实现Modal的动画效果。

考虑到动画结束,删除组件之后还应该有一个回调函数,因此这里采用的是react-motion动画库,而不是常见的CSSTransitionGroup动画库。

在增加动画效果之前,我们要增加一个刚才提到的动画结束之后的回调函数,因此还需要增加一个接口。

onRest: PropTypes.func

并且将这个接口的默认值改为空函数:

onRest: () => {}

这里就不介绍具体的react-motion的使用方法了,直接展示最终的代码:

  1. import { Motion, spring, presets } from 'react-motion';
  2. export default class Modal extends Component {
  3. constructor(props) {
  4. // ...
  5. util.bindMethods(['onCancelClick', 'onOkClick', 'close', 'onRest'], this);
  6. }
  7. // ...
  8. // 动画结束之后的回调函数
  9. onRest() {
  10. const { isOpen } = this.state;
  11. if(!isOpen) {
  12. this.props.onClose();
  13. }
  14. this.props.onRest();
  15. }
  16. render() {
  17. // ...
  18. return (
  19. <Motion defaultStyle={ { opacity: 0.8, scale: 0.8 }} style={ { opacity: spring(isOpen ? 1 : 0, presets.stiff), scale: spring(isOpen ? 1 : 0.8, presets.stiff) }} onRest={this.onRest} > { ({ opacity, scale }) => ( <div className={`modal-container ${ className}`} style={ {opacity}} onClick={maskClosable ? this.close : () => {}} > <div className="modal-body" style={ { opacity, transform: `translate3d(-50%, -50%, 0) scale(${ scale})` }} > <div className={`modal-title ${ type}`}>{title}</div> <div className="modal-content">{children}</div> <div className="modal-footer"> <button className="ok-btn" onClick={this.onOkClick}>{okText}</button> <button className="cancel-btn" onClick={this.onCancelClick}>{cancelText}</button> </div> </div> </div> ) } </Motion> ); } }

到此为止,整个Modal组件就已经完成了,希望这份demo对学习react的同学有所帮助。

结语

在设计基础组件的时候,一定要尽可能多的考虑业务场景,然后根据业务场景去设计接口,尽量保证基础组件能够在所有的场景中都可以正常使用。

这份Demo是在React15.6.0版本下书写的,因为React已经升级到16版本,并且16增加了新的createPortal()方法,所以Modal组件的实现方式会有所变化,具体的实现方法在下一篇文章介绍。React15.6.0实现Modal弹层组件

代码地址如下:
http://www.demodashi.com/demo/12315.html

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

发表评论

表情:
评论列表 (有 0 条评论,193人围观)

还没有评论,来说两句吧...

相关阅读