• React15.6.0实现Modal弹层组件


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

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

    本文适合对象

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

    项目说明

    项目结构截图

    项目运行说明

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

    Modal组件分析

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

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

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

    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中创建出组件的基础部分。

    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import './modal.scss';
    
    export default class Modal extends Component {
    	constructor(props) {
    		super(props);
    	}
    	render() {
    		return (
    			<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

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

    // 刚才的代码部分
    Modal.propTypes = {
    	isOpen: PropTypes.bool.isRequired,
    	title: PropTypes.string.isRequired,
    	children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired,
    	className: PropTypes.string,
    	maskClosable: PropTypes.bool,
    	onCancel: PropTypes.func,
    	onOk: PropTypes.func,
    	okText: PropTypes.string,
    	cancelText: PropTypes.string
    };
    
    Modal.defaultProps = {
    	className: '',
    	maskClosable: true,
    	onCancel: () => {},
    	onOk: () => {},
    	okText: 'OK',
    	cancelText: 'Cancel'
    };
    
    

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

    export default class Modal extends Component {
    	constructor(props) {
    		super(props);
    		this.state = {
    			isOpen: props.isOpen || false
    		};
    	}
    	componentWillReceiveProps(nextProps) {
    		if('isOpen' in nextProps) {
    		  this.setState({
    			isOpen: nextProps.isOpen
    		  });
    		}
      }
    	render() {
    		const {
    			title,
    			children,
    			className,
    			okText,
    			cancelText,
    			onOk,
    			onCancel,
    			maskClosable
    		} = this.props;
    		return (
    			<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组件的样式:

    .modal-container {
    	background-color: rgba(33, 33, 33, .4);
    	position: fixed;
    	top: 0;
    	left: 0;
    	right: 0;
    	bottom: 0;
    	opacity: 1;
    	.modal-body {
    		background-color: #fff;
    		border-radius: 5px;
    		padding: 30px;
    		 400px;
    		position: absolute;
    		left: 50%;
    		top: 40%;
    		transform: translate3d(-50%, -50%, 0);
    		.modal-title {
    			text-align: center;
    			font-size: 18px;
    			font-weight: bold;
    		}
    		.modal-content {
    			min-height: 100px;
    		}
    		.modal-footer {
    			text-align: center;
    			button {
    				margin: 0 20px;
    				padding: 8px 27px;
    				font-size: 16px;
    				border-radius: 2px;
    				background-color: #ffd900;
    				border: 0;
    				outline: none;
    				&:hover {
    					cursor: pointer;
    					background-color: #fff000;
    				}
    			}
    		}
    	}
    }
    

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

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

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

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

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

    继续改造Modal.js

    Modal.PropTypes = {
    	// ...
    	type: PropTypes.oneOf(['alert', 'confirm', 'error'])
    };
    Modal.defaultProps = {
    	// ...
    	type: 'alert',
    };
    

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

    .modal-title {
    	// ...
    	&.error:before {
    			content: '❌';
    			display: inline-block;
          }
          &.success:before {
    			content: '✔';
    			color: rgb(75, 231, 14);
    			display: inline-block;
          }
          &.confirm:before {
    			content: '❓';
    			display: inline-block;
          }
          &.alert:before {
    			content: '❕';
    			display: inline-block;
          }
    }
    

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

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

    挂载方法

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

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

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

    
    const modalOpenClass = 'modal-open';
    
    const toggleBodyClass = isOpen => {
    	const body = document.body;
    	if(isOpen) {
    		body.classList.add(modalOpenClass);
    	} else {
    		body.classList.remove(modalOpenClass);
    	}
    }
    
    export default class Modal extends Component {
    	/// ...
    	constructor(props) {
    		// ...
    		toggleBodyClass(props.isOpen);
    	}
    	// 关闭弹层函数
    	close() {
    		this.setState() {
    			isOpen: false
    		};
    		toggleBodyClass(false);
    	}
    	// 点击确认回调函数
    	onOkClick() {
    		this.props.onOk();
    		this.close();
    	}
    	// 点击取消的回调函数
    	onCancelClick() {
    		this.props.onCancel();
    		this.close();
    	}
    	// ...
    }
    

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

    // lib/util
    export default {
    	bindMethods(methods, obj) {
    		methods.forEach(func => {
    			if(typeof func === 'function') {
    				obj[func] = obj[func].bind(this);
    			}
    		})
    	}
    }
    

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

    // Modal.js
    import util from 'lib/util';
    
    // ...
    constructor(props) {
    	// ...
    	util.bindMethods(['onCancelClick', 'onOkClick', 'close'], this);
    }
    // ...
    

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

    render() {
    	// ...
    	return (
    			<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中书写我们的主要控制器代码:

    // index.js
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Modal from './modal';
    
    const show = (props) => {
    	let component = null;
    	const div = document.createElement('div');
    	document.body.appendChild(div);
    
    	const onClose = () => {
    		ReactDOM.unmountComponentAtNode(div);
    		document.body.removeChild(div);
    
    		if(typeof props.onClose === 'function') {
    			props.onClose();
    		}
    	}
    
    	ReactDOM.render(
    		<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的入口文件了:

    // hello.js
    
    import React, { Component } from 'react';
    import Modal from 'components/modal';
    
    export default class Hello extends Component {
    	render() {
    		return (
    			<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的使用方法了,直接展示最终的代码:

    import { Motion, spring, presets } from 'react-motion';
    
    export default class Modal extends Component {
    	constructor(props) {
    		// ...
    		util.bindMethods(['onCancelClick', 'onOkClick', 'close', 'onRest'], this);
    	}
    	// ...
    	// 动画结束之后的回调函数
    	onRest() {
    		const { isOpen } = this.state;
    		if(!isOpen) {
    			this.props.onClose();
    		}
    		this.props.onRest();
      }
      render() {
    	// ...
    	return (
    		<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大师代发,拒绝转载,转载需要作者授权

  • 相关阅读:
    精心总结的Linux运维面试题汇总
    [DevExpress]DxValidationProvider分享
    PivotGridField 中对Unbound的列赋值(除法)--数据钻取
    PivotGridField 中对Unbound的列赋值(除法)
    2020后端/前段开发工程师应当掌握的专业技能一览(源自github)
    跨域的CRUD
    水印图片和文字
    导入 导出 压缩 解压
    winForm的CRUD 加上传图片 的DAL
    winForm的CRUD 加上传图片
  • 原文地址:https://www.cnblogs.com/demodashi/p/8512647.html
Copyright © 2020-2023  润新知