注:本文Demo環境使用的是我平時開發用的配置:這里是地址。
本文適合對象
- 了解React。
- 使用過webpack3。
- 熟悉es6語法。
項目說明
項目結構截圖
項目運行說明
npm install
npm run start
npm run startfe
- 登錄
localhost:8088
查看demo
Modal組件分析
Modal組件是屬於一個網站中比較常用的基礎組件,但是在實現方面上稍微復雜一些,對場景支持的需求度較高。
這里是Antd中Modal組件的演示Demo。
首先分析這個組件的組成結構:
- title Modal彈層的標題部分。
- content Modal彈層的主體部分。
- footer Modal彈層最后的button部分。
- 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 = {};
接下來分析我們的組件都需要預留哪些接口:
- 開關狀態isOpen
- Modal標題title
- Modal主體內容children
- Modal類名className
- 點擊黑色區域是否可以關閉maskClosable
- 關閉按鈕文案 cancelText
- 確認按鈕文案 okText
- 關閉按鈕回調函數 onCancel
- 確認按鈕回調函數 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;
width: 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實現動畫的方式有很多,但是總結起來可能只有兩種:
- 使用css3實現動畫。
- 根據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彈層組件
注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權