美文网首页React Native实践
React组件ActionSheet的简单封装

React组件ActionSheet的简单封装

作者: DaveJump | 来源:发表于2017-08-08 17:44 被阅读0次

前段时间在用React重构一个项目,由于项目本身没多复杂,且都在业余时间进行开发,就自己造轮子练练手,把需要用到的基础组件都封装了一遍。
作为一个React新手,踩坑必不可少。这其中我就遇到了一个疑点,是关于移动端ActionSheet或Toast组件的编写。

这种组件和其他嵌入JSX的组件不同,他们是通过类似静态方法实时调用显示的,而且全局只有一个,并不像其他组件可以有多个嵌入渲染模板里。例如:Toast.show('Hello!')ActionSheet.open(options)。这是Antd-mobile库里的ActionSheet:

ActionSheet.gif

通过向群里、网上发帖发问,得出了初步思路。我就拿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,后续也可以扩展出具有类似分享等其他的功能,使之变得更灵活通用,在这里只是提供一个思路。

由于敲码能力有限,当中有什么不妥之处或者需要优化(特别是性能方面的优化)之处,请各位大佬评论指点谢谢!

相关文章

网友评论

    本文标题:React组件ActionSheet的简单封装

    本文链接:https://www.haomeiwen.com/subject/flazlxtx.html