美文网首页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