结合 Redux 实现一个优雅的弹窗系统

4,439 阅读6分钟

前言

关于提示类组件,在Android里有Toast、SnackBar,React Native里我也发现了一个非常不错的SnackBar风格的开源组件——react-native-message-bar, 具体使用方法就是在应用的顶级容器里注册,然后在你需要invoke的地方调用所给的show方法即可。这让我不禁想起了Redux的状态管理机制,有兴趣的同学们可以研究一下具体的实现方式( 我尽量克服懒惰研究一下哈 ! )

开发缘由

在工作中,因为要给Applean添加各种操作的提示框,最后结合了react-native-message-bar的思路并实现了多层级对话框的叠加显示功能,有兴趣的可以去Applean体验一下

具体实现

这里假定我们已经接入了redux-thunk这个简单而又非常实用的中间件,并且已经处理好了Redux的异步操作(很简单,就是用applyMiddleware接入所需要的Middleware即可),这里因为不符合文章主题,不予讲解。

实现概述

  1. 这里的弹窗是利用react-bootstrap ( bootstrap对reactjs的一个嫁接 ),当然,这个非常非常的次要
  2. 将所需要展示的整个View以message字段的方式通过ActionCreator传递给Reducer,而在Reducer里则以一个数组的形式来管理,这也是为了提高所展示视图灵活性的一个方案。
  3. 在视图的渲染展示上,还是出现了不小的问题,可以详细讲解一下解决方案

ActionCreator部分逻辑

let promptCounter = 0
export function showPrompt (payload) {
  const promptMessage = React.cloneElement(payload.message, {key: promptCounter++, duration: payload.duration})
  return {
    type: SHOW_PROMPT,
    promptMessage: promptMessage
  }
}
export function hidePrompt (message) {
  return {
    type: HIDE_PROMPT,
    promptMessage: message
  }
}
  • 可以看到,这里我们给传进来的message提示框组件通过cloneElement方法给组件key属性加了一个随调用次数自增长的变量值,如果在渲染时再根据遍历所得的index来进行赋值会造成随着数组的变化,元素的key值不断变化。而因为React的diff算法对数组的更新策略是针对unique key值的,如果数组元素没有key值,一来会有系统warning,二来如果某个元素发生变化,会导致系统重绘整个数组。
  • cloneElement方法中可以看到我们又加了一个duration属性,主要是用来在UI显示层容易拿到这个参数来控制弹框的显示,后文会详细讲到。

reducer中的处理逻辑

const initialState = {promptMessage: []}
export default function reducer (state = initialState, action = {}) {
  case SHOW_PROMPT: 
    return {
      ...state,
      promptMessage: [...state.promptMessages, action.message]
  case HIDE_PROMPT:
    return {
      ...state,
      promptMessage: state.promptMessages.filter((x) => x !== action.promptMessage)
    }
  default:
    return state  
  }
}

这里很简单,我们在遵循不修改原有state的原则下,采取了解构对象以及使用filter函数来实现增加、删除数组元素。当然,assign、拆分数组也未尝不可。

发起一个对话框

this.props.dispatch(showPrompt{
  message: Example,
  duration: 1000
})

就这样,非常简单的发起了一个提示框的Action。

UI展示层处理

这里也是多级提示框展示的地方,具体在实现上,也有一些耐人寻味的几点需要注意一下。

批量化渲染组件

renderPromptMessage = (promptMessage) => {
    const onHide = () => {
      setTimeout(() => {
        this.props.dispatch(hidePrompt(promptMessage))
      }, 1000)

      promptMessage.props.onHide && promptMessage.props.onHide()
    }
    return 
};
  • 以上是对单个promptMessage弹窗的渲染函数,当然,使用的时候,只需要用从State树中获取到的数组调一下map函数就可以啦!
  • 这里需要注意的是,如果我们直接采取更新数据源的方式来更新提示窗的数量时,整个View都是以一种非常生硬的方式直接从页面中消失,而非存在一个淡出的过程,这时我们就需要通过在合适的时机调用Modal自带的show参数来让它执行自己的淡出逻辑,而我们是通过ModalWrapper组件来实现的。
  • 当然,这里也有几个参数作为属性被传递了进去
    • child: 这个在数据逻辑上对应视图层级上关系的一个属性,我们在这里将promptMessage作为属性直接传入作处理,当然也可以用标签对包裹。
    • duration: 传入后由Wrapper来控制其fade-out逻辑
    • onHide: 这里传入的是在当前函数中写好的onHide方法,主要用来响应Modal组件的onHide回调。可以看到在onHide方法中,我们先是在onHide回调存在时执行了它,然后在一秒后dispatch了我们的hidePrompt动作,这里是数据层面的hide,为什么延时下边会做介绍。

ModalWrapper包装组件

class ModalWrapper extends React.Component {
  static propTypes = {
    child: React.PropTypes.element.isRequired,
    onHide: React.PropTypes.func.isRequired,
    duration: React.PropTypes.number
  };
  state = {
    show: true
  };
  onHide = () => {
    this.setState({
      show: false
    })
    this.props.onHide()
  };
  componentDidMount () {
    if (this.props.duration) {
      this.timer = setTimeout(() => {
        this.onHide()
      }, this.props.duration)
    }
  }
  render () {
    return React.cloneElement(this.props.child, {show: this.state.show, onHide: this.onHide})
  }
  componentWillUnmount () {
    clearTimeout(this.timer)
  }
}

作为一个比较核心的类,我们看到它在render函数中返回了一个再次被我们Hack了的child组件(传进来的promptMessage对象)

  • show: react-bootstrap 中Modal组件的显示和淡出主要由show属性控制,在正常使用时,可以通过将该属性与state单向绑定来实现对弹框的控制。可以看到在这个组件中,show的初始值是true。
  • onHide: 既然show可以直接控制Modal,为何还要写到onHide里呢?这其实也是模拟了Modal.Header中的关闭按钮的逻辑,点击后淡出弹框并执行onHide回调。这个逻辑在没有关闭按钮时是肯定不会触发的。在设置show为false是从视图层面的hide,然后调用传进来的onHide方法,让它执行数据层面的hide,注意这里存在1s的延时,主要用于给Modal足够的时间淡出,否则这个fade-out动画是无法执行完的。
  • componentDidMount: 这个也是我们duration的用武之地。在视图渲染完毕后,倒计时duration然后调用上边的onHide方法。
  • 干完事情要记得清理战场,为了防止组件被卸载后才执行其对应的逻辑,造成对unmounted componet 操作的bug,我们在组建即将卸载时及时清理掉了这个timer。

结语

本来这篇文章上周就可以完结的,无奈在写的过程中,发现之前使用的thunk中间件可以去掉,而且为了更好地结合关闭按钮和自动关闭逻辑,将hide数据层面的操作从dispatch接收的函数中提取到了Wrapper中。其实最为核心的问题大致总结为以下几点:

  • reducer存储数组中message对象的唯一性
  • 数组中key值的稳定不可变性
  • 数据层和视图层hide逻辑的分离和延迟

生活不止眼前的苟且
还有诗和远方的田野