一、描述

rax-modal 是 rax 内置的并且也是 rax 内部实现的模态框组件,主要基于 rax-viewrax-touchableuniversal-transition 实现。

官方文档地址:

rax-modal 优势在于,用户能够自定义内部的全部内容,包括样式等,所以它不是一个写死的东西,通用性很强,其内部的实现原理也蛮好的。

我之前有自己实现过一个在 weex 中使用的 mask 组件,实际上就是一个 modal,不过通用性做的很一般,因为只是写一个示例,文章地址:http://www.ptbird.cn/weex-mask.html,不过那是基于 vue 的。

之后我在 rax 中也在某种业务场景下自己实现了 modal 组件,因为内置的 rax-modal 在我的那种业务场景下会发生一些问题,自己实现的主要原理和使用 vue 实现的原理差不多,尤其是在动画过渡方面,仍旧是采用 :hack 的方式保证在 DOM 加载后在进行动画过渡。

当时没有考虑 react 的 setState 的机制以及携带的第二个参数是回调,不需要基于 hack 的方式,而 rax 的同步 setState 机制,更加方便实现这种过渡效果。

二、源码

1、支持的 props

从文档上看,rax-modal 只支持 4 个 props,但是源码中声明的 propTypes 有6个,分别是:

  • onHide: PropTypes.func,
  • onShow: PropTypes.func,
  • visible: PropTypes.bool,
  • maskCanBeClick: PropTypes.bool,
  • delay: PropTypes.number,
  • duration: PropTypes.number

其中 delayduration 分别是过渡动画的时间

2、维护的 state

组件本身是有状态的,维护以下两个状态,默认值如下:

  • visible: false,
  • visibility: 'hidden'

其中 visible 目前是用来在 componentWillReceiveProps() 中判断合并的,只有 visible 变动,才会触发隐藏和显示。

visibility 是用在 style 中,用在实际控制内容的显示或隐藏。

3、show() 方法和 hide() 方法

show()hide() 是用来控制显示和隐藏的最直接的方法,两个方法放在一起说,主要是因为他们整体上没有太大的差异,无非就是 state 的控制上存在不同。

show() 方法举例:

show() {
  const currentState = {
    visible: true,
    visibility: 'visible'
  };
  this.setState;
  this.setState(
    currentState,
    () => this.animated(currentState, () => this.props.onShow && this.props.onShow(currentState))
  );
}

首先修改 visible = truevisibility = 'visible' ,而利用 setState(obj,callback()) 的 callback() 来保证在设置完 state 之后再进行动画的输出,也就是调用 this.animated 方法执行动画,再利用 transition 的回调方法来触发 props.onShow`。

反之,hide() 方法无非是在设置了 visible = falsevisibility = 'hidden' 之后再进行其他顺序的流程。

4、animated() 方法

上面的 show()hide() 方法最终触发显示和隐藏都是触发的 animated() 方法,这个方法也是在整个 rax-modal 组件的核心,不过也很简单,如果熟悉 rax 的 universal-transition 用法,则更加容易明白。

animated = (state, callback) => {
  const {visible} = state;
  const {delay, duration} = this.props;
  transition(findDOMNode(this.refs.mask), {
    opacity: visible === true ? 1 : 0
  }, {
    timingFunction: 'ease',
    delay,
    duration
  }, () => {
    callback && callback();
  });
}

因为 transition 本身的要求,无法直接使用 this.refs.mask,而需要进行 findDOMNode(this.refs.mask),实现过渡的属性借助的是 opacity 至于是 0 还是 1 则是取决于 state.visible 的值。

对于 delayderation 目前使用的是默认值 200 200,callback 则是最终触发 props.onShowprops.onHide

5、toggle() 方法

看源码可以发现,rax-modal 并不是直接触发 show() 和 hide() ,总是通过 toggle() 方法中去判断 visible 再去调用 show() 和 hide()。

6、性能优化

对于性能的优化主要体现在 componentWillReceiveProps(nextProps) 上,只有当 visiblestate.visibleprops.visible 不一致的时候,才会触发 toggle 方法, 并且使用 nextProps.visible

  componentWillReceiveProps(nextProps) {
    if (
      nextProps.visible != this.props.visible &&
      nextProps.visible != this.state.visible
    ) {
      this.toggle(nextProps.visible);
    }
  }

7、第一次挂载

组件在第一次挂载的时候初始化了整个 mask 的高度,并且在不同的环境下获取高度的方式也不相同,在 weex 容器环境下,使用的是 weex 的 dom.getComponentRect('viewport',(e)=>{}) 来获取视窗的高度,而在 web 环境下,是通过 window.screen 进行比例计算得到的。

这是我觉得存在争议的地方,就 rax-modal 的使用方式上来说,基本上 mask 都是视窗高度的,为什么不直接通过 flex 布局的 flex:1 实现满屏,而是通过 width:750,然后计算高度去实现?

8、render() 方法

下面的渲染方法中,默认的 mask 样式高度是 3000,而最终渲染的高度还是在 componentDidMount 中计算得到的 height,而宽度则没有变动,还是 750,(这就是上面我提出的疑问)

mask 是整个 modal 组件的外层容器,代码中也提到了,对已 android 的点击透传需要通过一个 hack 来抵消掉。

mask 内部是一个 Touchable,子组件内容是 props.children,Touchable 的样式中,默认的 styles.main 宽度和高度分别是 750 和 340,当然,这个样式会被 props.contentStyle 覆盖掉,这也是传入的样式起到作用的原因。

render() {
  const {contentStyle, children, maskCanBeClick} = this.props;
  const {visible} = this.state;
  // HACK: register a empty click event to fix Android click penetration problem when in mask
  return (
    <View
      ref={'mask'}
      onClick={() => {
        maskCanBeClick && this.hide();
      }}
      style={{
        ...styles.mask,
        height: this.height,
        visibility: this.state.visibility,
      }}
    >
      <Touchable
        onPress={(e) => {
          if (isWeb) {
            e.stopPropagation && e.stopPropagation();
          }
        }}
        style={[styles.main, contentStyle]}
      >
        {children}
      </Touchable>
    </View>
  );
}

const styles = {
  mask: {
    position: 'fixed',
    top: 0,
    left: 0,
    width: 750,
    height: 3000,
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
    zIndex: 100,
    alignItems: 'center',
    justifyContent: 'center',
    overflow: 'hidden'
  },
  main: {
    width: 640,
    height: 340,
    backgroundColor: '#ffffff'
  }
};

三、使用

知道了源码,再去使用的话没有什么问题,对于样式,只要控制好 contentStyle 即可。

1、代码

import {createElement, Component} from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import styles from './App.css';
import Modal from 'rax-modal';

class App extends Component {
  toggleModalHandle = (flag) => {
    if (flag) {
      return this.refs.modal.show();
    }
    return this.refs.modal.hide();
  }
  render() {
    return (
      <View style={styles.app}>
        <Text 
          style={styles.btnText} 
          onClick={this.toggleModalHandle.bind(this,true)}
        >显示 modal</Text>
        <Modal ref="modal" contentStyle={styles.content} >
          <View>
            <Text>
              I am a dialog
            </Text>
          </View>
          <Text 
            style={styles.btnText} 
            onClick={this.toggleModalHandle.bind(this,false)}
          >隐藏Modal</Text>
        </Modal> 
      </View>
    );
  }
}

export default App;

2、样式

.app {
  flex: 1;
  justify-content: center;
  align-items: center;
}

.btnText{
  border-width:1;
  border-style:solid;
  border-color:red;
  padding:15;
}

.content{
  width:750;
  height:600;
  background-color:rgba(255,255,255,0.5);
  align-self: center;
  justify-content: center;
}

### 3、效果

22222.gif