前段时间在用React重构一个项目,由于项目本身没多复杂,且都在业余时间进行开发,就自己造轮子练练手,把需要用到的基础组件都封装了一遍。
作为一个React新手,踩坑必不可少。这其中我就遇到了一个疑点,是关于移动端ActionSheet或Toast组件的编写。
这种组件和其他嵌入JSX的组件不同,他们是通过类似静态方法实时调用显示的,而且全局只有一个,并不像其他组件可以有多个嵌入渲染模板里。例如:Toast.show('Hello!')
,ActionSheet.open(options)
。这是Antd-mobile库里的ActionSheet:
通过向群里、网上发帖发问,得出了初步思路。我就拿ActionSheet的例子简单敞开总结一下思路,希望对大家有点帮助。
组件分离的确定
一个ActionSheet我们可以把它分成两部分:一部分是整个遮罩和弹出框,另一部分是里面的标题描述、options和取消按钮。我们可以把第一部分命名为ActionSheetContainer、第二部分命名为ActionSheetPanel。
组件间的关系和逻辑的确定
ActionSheetContainer承接了它的子组件和顶层设计,是一座“桥”;ActionSheetPanel是ActionSheetContainer的一个子组件,接收props来触发行为;顶层设计向外暴露API由用户动态触发。
先考虑把组件渲染到页面
这就是本例子的一个核心点。对普通的已经构造好的组件实例,我们可以直接插在render函数里渲染出来,然后通过改变父组件的state,通过props间接改变该组件的状态,这很好操作。但是这种通过类似全局方法调用的方式来动态显示组件,即模板里没有提前预设好的react组件,要渲染出来该如何做。
我们可以想象先提前在body里创建一个DOM节点,然后通过React提供的API把React组件渲染到该节点里,然后外部调用就直接改变已经存在的组件实例的状态。再看了看官网文档,的确可以这么做。ReactDOM.render方法返回一个渲染过后的组件实例,其实就相当于一个对象的实例,我们可以访问该对象的任何成员,而这些成员,即属性和方法,都已经在定义组件,即对应class的时候已经定义好了。OK,大概思路就对了,开工,直接上代码:
首先我们从ActionSheetContainer入手,因为上面说了,这是具有承接关系的组件,从此处入手较为方便。
ActionSheetContainer.jsx
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
//引入ActionSheetPanel作为子组件,先可以做一个只渲染空节点的纯函数
import ActionSheetPanel from './ActionSheetPanel';
class ActionSheetContainer extends Component {
constructor(props){
super(props);
//确定初始状态,options为ActionSheet的选项,desc为描述,maskClosable为遮罩可否关闭,callback为选择之后的回调,active为是否激活显示ActionSheet
this.state = {
options: [],
desc: '',
maskClosable: true,
callback: undefined,
active: false
}
this.onClose = this.onClose.bind(this);
this.onMaskClose = this.onMaskClose.bind(this);
}
//声明onOpen函数,因为每次打开,都有可能是一个新的状态,显示时先更新相应状态
onOpen(props){
const { options, desc, callback } = props;
//此处设置延迟0秒后执行,让setState在第三次事件循环后执行,保证能正确获取到动态改变后的props。
setTimeout(() => {
this.setState({
options,
desc,
callback,
active: true
});
}, 0);
}
//关闭
onClose(){
this.setState({
active: false
});
}
//遮罩关闭
onMaskClose(e){
const { maskClosable } = this.state;
if(maskClosable && (e.target === e.currentTarget)){
this.onClose();
}
}
//选择选项后的回调,这里只传入选择的索引
onSelect(selectedIndex){
const { callback } = this.state;
callback && callback(selectedIndex);
this.onClose();
}
render(){
const { options, desc, active} = this.state;
const actionSheetMaskClass = classnames({
'grp-action-sheet': true,
'active': active
});
return (
<div
className={actionSheetMaskClass}
onClick={e => this.onMaskClose(e)}
>
//此处子组件和我们可以传入的props
<ActionSheetPanel
options={options}
desc={desc}
onCancel={this.onClose}
onSelect={selectedIndex => this.onSelect(selectedIndex)}
/>
</div>
);
}
}
//这里我们为类ActionSheetContainer添加一个静态方法,用于初次渲染组件
ActionSheetContainer.renderActionSheet = () => {
const actionSheetWrap = document.createElement('div');
document.body.appendChild(actionSheetWrap);
//将ActionSheetContainer渲染到创建好的div里并返回ActionSheetContainer组件实例
const actionSheetInstance = ReactDOM.render(
React.createElement(
ActionSheetContainer
),
actionSheetWrap
);
return {
open(props){
//调用ActionSheetContainer实例的onOpen方法并传入外部props
actionSheetInstance.onOpen(props);
},
close(){
actionSheetInstance.onClose();
},
//添加销毁组件的方法
distroy(){
ReactDOM.unmountComponentAtNode(actionSheetWrap);
document.body.removeChild(actionSheetWrap);
}
}
}
export default ActionSheetContainer;
接着构造子组件ActionSheetPanel,这里最简单,只是负责desc、options的显示和绑定每个option的事件处理。
ActionSheetPanel.jsx
import React from 'react';
const ActionSheetPanel = ({ options, desc, onSelect, onCancel }) => (
<div className="action-sheet-panel">
<h1 className="action-sheet-header">{desc}</h1>
<ul className="action-sheet-options">
{
options.map((option, index) => (
<li
key={option.toString()}
onClick={e => onSelect(index, e)}
>{option}</li>
))
}
</ul>
<span className="action-sheet-spliter"></span>
<div
className="action-sheet-cancel"
onClick={onCancel}
>
取消
</div>
</div>
);
export default ActionSheetPanel;
最后是顶层代码的编写,为了方便webpack通过alias找到对应组件,我们把顶层命名为index.js,并把ActionSheetContainer.jsx、ActionSheetPanel.jsx和对应的sass/less/css放入同一个文件夹components/ActionSheet 。
index.js
import ActionSheetContainer from './ActionSheetContainer';
import './ActionSheet.css';
let newActionSheet;
const initActionSheet = (() => {
//这里保证ActionSheet只在页面中渲染一次,类似单例
if (!newActionSheet) {
newActionSheet = ActionSheetContainer.renderActionSheet();
}
return newActionSheet;
})();
//这里就设置暴露出的API
const ActionSheet = {
openActionSheetWithOptions(props = {}, callback) {
const { options = [], desc = '', maskClosable = true } = props;
initActionSheet.open({options, desc, maskClosable, callback});
},
close(){
initActionSheet.close();
},
distroy() {
if(newActionSheet){
initActionSheet.distroy();
newActionSheet = null;
}
}
}
export default ActionSheet;
到这里,一个简单的ActionSheet的封装就完成了。使用的时候也是很方便:
import ActionSheet from 'components/ActionSheet';
//......
<a
onClick={e => {
ActionSheet.openActionSheetWithOptions(
{
options: ['option1', 'option2', 'option3']
},
selectedIndex => {
switch(selectedIndex){
case 1:
//do something
break;
case 2:
//do something
break;
default:
break;
}
}
);
e.preventDefault();
}}
>
按钮
</a>
//......
ActionSheet2.gif
这个例子只是一个简化版的ActionSheet组件,只有简单的几个option,连里面的删除按钮也省了。设计思想参考了antd-mobile,后续也可以扩展出具有类似分享等其他的功能,使之变得更灵活通用,在这里只是提供一个思路。
由于敲码能力有限,当中有什么不妥之处或者需要优化(特别是性能方面的优化)之处,请各位大佬评论指点谢谢!
网友评论