React组件ActionSheet的简单封装

前段时间在用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,后续也可以扩展出具有类似分享等其他的功能,使之变得更灵活通用,在这里只是提供一个思路。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容