阅读 14020

一步一步带你封装基于react的modal组件

中秋放假,一个人有点无聊,于是写点博文暖暖心,同时祝大家中秋快乐~ 🙃

接下来将一步步带领大家实现一个基本的modal弹窗组件,封装一个简单的动画组件,其中涉及到的一些知识点也会在代码中予以注释讲解。

一. modal组件的实现;

1. 环境搭建

我们使用create-react-app指令,快速搭建开发环境:

create-react-app modal
复制代码

安装完成后,按照提示启动项目,接着在src目录下新建modal目录,同时创建modal.jsx modal.css两个文件

modal.jsx内容如下:

import React, { Component } from 'react';
import './modal.css';
class Modal extends Component {
  render() {
    return <div className="modal">
      这是一个modal组件
    </div>
  }
}
export default Modal;
复制代码

回到根目录,打开App.js,将其中内容替换成如下:

import Modal from './modal/modal';
import React, { Component } from 'react';
import './App.css';
class App extends Component {
  render() {
    return <div className="app">
      <Modal></Modal>
    </div>
  }
}
export default App;
复制代码

完成以上步骤后,我们浏览器中就会如下图显示了:

2. modal样式完善

写之前,我们先回想一下,我们平时使用的modal组件都有哪些元素,一个标题区,内容区,还有控制区,一个mask;

modal.jsx内容修改如下:

import React, { Component } from 'react';
import './modal.css';
class Modal extends Component {
  render() {
    return <div className="modal-wrapper">
      <div className="modal">
        <div className="modal-title">这是modal标题</div>
        <div className="modal-content">这是modal内容</div>
        <div className="modal-operator">
          <button className="modal-operator-close">取消</button>
          <button className="modal-operator-confirm">确认</button>
        </div>
      </div>
      <div className="mask"></div>
    </div>
  }
}
export default Modal;
复制代码

modal.css内容修改如下:

.modal {
  position: fixed;
  width: 300px;
  height: 200px;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  border-radius: 5px;
  background: #fff;
  overflow: hidden;
  z-index: 9999;
  box-shadow: inset 0 0 1px 0 #000;
}

.modal-title {
  width: 100%;
  height: 50px;
  line-height: 50px;
  padding: 0 10px;
}

.modal-content {
  width: 100%;
  height: 100px;
  padding: 0 10px;
}

.modal-operator {
  width: 100%;
  height: 50px;
}

.modal-operator-close, .modal-operator-confirm {
  width: 50%;
  border: none;
  outline: none;
  height: 50px;
  line-height: 50px;
  opacity: 1;
  color: #fff;
  background: rgb(247, 32, 32);
  cursor: pointer;
}

.modal-operator-close:active, .modal-operator-confirm:active {
  opacity: .6;
  transition: opacity .3s;
}

.mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #000;
  opacity: .6;
  z-index: 9998;
}
复制代码

修改完成后,我们浏览器中就会如下图显示:

3. modal功能开发

到这里我们的准备工作已经完成,接下就具体实现modal功能,再次回想,我们使用modal组件的时候,会有哪些基本的功能呢?

  1. 可以通过visible控制modal的显隐;
  2. title,content可以自定义显示内容;
  3. 点击取消关闭modal,同时会调用名为onClose的回调,点击确认会调用名为confirm的回调,并关闭modal,点击蒙层mask关闭modal
  4. animate字段可以开启/关闭动画;

3.1. 添加visible字段控制显隐

modal.jsx修改如下:

import React, { Component } from 'react';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    // 通过父组件传递的visile控制显隐
    const { visible } = this.props;
    return visible && <div className="modal-wrapper">
      <div className="modal">
        <div className="modal-title">这是modal标题</div>
        <div className="modal-content">这是modal内容</div>
        <div className="modal-operator">
          <button className="modal-operator-close">取消</button>
          <button className="modal-operator-confirm">确认</button>
        </div>
      </div>
      <div className="mask"></div>
    </div>
  }
}
export default Modal;
复制代码

App.js修改如下:

import Modal from './modal/modal';
import React, { Component } from 'react';

import './App.css';
class App extends Component {
  constructor(props) {
    super(props)
    // 这里绑定this因为类中的方法不会自动绑定指向当前示例,我们需要手动绑定,不然方法中的this将是undefined,这是其中一种绑定的方法,
    // 第二种方法是使用箭头函数的方法,如:showModal = () => {}
    // 第三种方法是调用的时候绑定,如:this.showModal.bind(this)
    this.showModal = this.showModal.bind(this)  
    this.state = {
      visible: false
    }
  }

  showModal() {
    this.setState({ visible: true })
  }

  render() {
    const { visible } = this.state
    return <div className="app">
      <button onClick={this.showModal}>click here</button>
      <Modal visible={visible}></Modal>
    </div>
  }
}
export default App;
复制代码

以上我们通过父组件App.js中的visible状态,传递给modal组件,再通过button的点击事件来控制visible的值以达到控制modal组件显隐的效果

未点击按钮效果如下图:

点击按钮后效果如下图:

3.2. titlecontent内容自定义

modal.jsx修改如下:

import React, { Component } from 'react';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    const { visible, title, children } = this.props;
    return visible && <div className="modal-wrapper">
      <div className="modal">
        {/* 这里使用父组件的title*/}
        <div className="modal-title">{title}</div>
        {/* 这里的content使用父组件的children*/}
        <div className="modal-content">{children}</div>
        <div className="modal-operator">
          <button className="modal-operator-close">取消</button>
          <button className="modal-operator-confirm">确认</button>
        </div>
      </div>
      <div className="mask"></div>
    </div>
  }
}
export default Modal;
复制代码

App.js修改如下:

import Modal from './modal/modal';
import React, { Component } from 'react';

import './App.css';
class App extends Component {
  constructor(props) {
    super(props)
    this.showModal = this.showModal.bind(this)
    this.state = {
      visible: false
    }
  }

  showModal() {
    this.setState({ visible: true })
  }

  render() {
    const { visible } = this.state
    return <div className="app">
      <button onClick={this.showModal}>click here</button>
      <Modal
        visible={visible}
        title="这是自定义title"
      >
        这是自定义content
      </Modal>
    </div>
  }
}
export default App;
复制代码

接着我们点击页面中的按钮,结果显示如下:

3.3. 取消与确认按钮以及蒙层点击功能添加

写前思考:我们需要点击取消按钮关闭modal,那么我们就需要在modal中维护一个状态,然后用这个状态来控制modal的显隐,好像可行,但是我们再一想,我们前面是通过父组件的visible控制modal的显隐,这样不就矛盾了吗?这样不行,那我们作一下改变,如果父组件的状态改变,那么我们只更新这个状态,modal中点击取消我们也只更新这个状态,最后用这个状态值来控制modal的显隐;至于onClose钩子函数我们可以再更新状态之前进行调用,确认按钮的点击同取消。

modal.jsx修改如下:

import React, { Component } from 'react';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
    this.confirm = this.confirm.bind(this)
    this.maskClick = this.maskClick.bind(this)
    this.closeModal = this.closeModal.bind(this)
    this.state = {
      visible: false
    }
  }

  // 首次渲染使用父组件的状态更新modal中的visible状态,只调用一次
  componentDidMount() {
    this.setState({ visible: this.props.visible })
  }

  // 每次接收props就根据父组件的状态更新modal中的visible状态,首次渲染不会调用
  componentWillReceiveProps(props) {
    this.setState({ visible: props.visible })
  }

  // 点击取消更新modal中的visible状态
  closeModal() {
    console.log('大家好,我叫取消,听说你们想点我?傲娇脸👸')
    const { onClose } = this.props
    onClose && onClose()
    this.setState({ visible: false })
  }

  confirm() {
    console.log('大家好,我叫确认,楼上的取消是我儿子,脑子有点那个~')
    const { confirm } = this.props
    confirm && confirm()
    this.setState({ visible: false })
  }

  maskClick() {
    console.log('大家好,我是蒙层,我被点击了')
    this.setState({ visible: false})
  }

  render() {
    // 使用modal中维护的visible状态来控制显隐
    const { visible } = this.state;
    const { title, children } = this.props;
    return visible && <div className="modal-wrapper">
      <div className="modal">
        <div className="modal-title">{title}</div>
        <div className="modal-content">{children}</div>
        <div className="modal-operator">
          <button
            onClick={this.closeModal}
            className="modal-operator-close"
          >取消</button>
          <button
            onClick={this.confirm}
            className="modal-operator-confirm"
          >确认</button>
        </div>
      </div>
      <div
        className="mask"
        onClick={this.maskClick}
      ></div>
    </div>
  }
}
export default Modal;
复制代码

App.js修改如下:

import Modal from './modal/modal';
import React, { Component } from 'react';

import './App.css';
class App extends Component {
  constructor(props) {
    super(props)
    this.confirm = this.confirm.bind(this)
    this.showModal = this.showModal.bind(this)
    this.closeModal = this.closeModal.bind(this)
    this.state = {
      visible: false
    }
  }

  showModal() {
    this.setState({ visible: true })
  }

  closeModal() {
    console.log('我是onClose回调')
  }

  confirm() {
    console.log('我是confirm回调')
  }

  render() {
    const { visible } = this.state
    return <div className="app">
      <button onClick={this.showModal}>click here</button>
      <Modal
        visible={visible}
        title="这是自定义title"
        confirm={this.confirm}
        onClose={this.closeModal}
      >
        这是自定义content
      </Modal>
    </div>
  }
}
export default App;
复制代码

保存后,我们再浏览器中分别点击取消和确认,控制台中将会出现如下图所示:

4. modal优化

以上就完成了一个基本的modal组件,但是我们还有一个疑问,就是现在引入的modal是在类名为App的元素之中,而一些被广泛使用的UI框架中的modal组件确实在body层,无论你在哪里引入,这样就可以防止modal组件受到父组件的样式的干扰。

而想要实现这种效果,我们必须得先了解React自带的特性:Portals(传送门)。这个特性是在16版本之后添加的,而在16版本之前,都是通过使用ReactDOMunstable_renderSubtreeIntoContainer方法处理,这个方法可以将元素渲染到指定元素中,与ReactDOM.render方法的区别就是,可以保留当前组件的上下文contextreact-redux就是基于context进行跨组件之间的通信,所以若是使用ReactDOM.render进行渲染就会导致丢失上下文,从而导致所有基于context实现跨组件通信的框架失效。

4.1. ReactDOM.unstable_renderSubtreeIntoContainer的使用

ReactDOM.unstable_renderSubtreeIntoContainer(
  parentComponent, // 用来指定上下文
  element,         // 要渲染的元素
  containerNode,   // 渲染到指定的dom中
  callback         // 回调
);
复制代码

接下来在我们的项目中使用它,src目录下新建oldPortal目录,并在其中新建oldPortal.jsxoldPortal.jsx中的内容如下:

import React from 'react';
import ReactDOM from 'react-dom';

class OldPortal extends React.Component {
  constructor(props) {
    super(props)
  }

  // 初始化时根据visible属性来判断是否渲染
  componentDidMount() {
    const { visible } = this.props
    if (visible) {
      this.renderPortal(this.props);
    }
  }

  // 每次接受到props进行渲染与卸载操作
  componentWillReceiveProps(props) {
    if (props.visible) {
      this.renderPortal(props)
    } else {
      this.closePortal()
    }
  }

  // 渲染
  renderPortal(props) {
    if (!this.node) {
      // 防止多次创建node
      this.node = document.createElement('div');
    }
    // 将当前node添加到body中
    document.body.appendChild(this.node);

    ReactDOM.unstable_renderSubtreeIntoContainer(
      this,           // 上下文指定当前的实例
      props.children, // 渲染的元素为当前的children
      this.node,      // 将元素渲染到我们新建的node中,这里我们不使用第四个参数回调.
    );
  }

  // 卸载
  closePortal() {
    if (this.node) {
      // 卸载元素中的组件
      ReactDOM.unmountComponentAtNode(this.node)
      // 移除元素
      document.body.removeChild(this.node)
    }
  }

  render() {
    return null;
  }
}

export default OldPortal
复制代码

保存后,我们在modal.jsx中使用它:

import React, { Component } from 'react';
import OldPortal from '../oldPortal/oldPortal';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
    this.confirm = this.confirm.bind(this)
    this.maskClick = this.maskClick.bind(this)
    this.closeModal = this.closeModal.bind(this)
    this.state = {
      visible: false
    }
  }

  componentDidMount() {
    this.setState({ visible: this.props.visible })
  }

  componentWillReceiveProps(props) {
    this.setState({ visible: props.visible })
  }

  closeModal() {
    console.log('大家好,我叫取消,听说你们想点我?傲娇脸👸')
    const { onClose } = this.props
    onClose && onClose()
    this.setState({ visible: false })
  }

  confirm() {
    console.log('大家好,我叫确认,楼上的取消是我儿子,脑子有点那个~')
    const { confirm } = this.props
    confirm && confirm()
    this.setState({ visible: false })
  }

  maskClick() {
    console.log('大家好,我是蒙层,我被点击了')
    this.setState({ visible: false })
  }

  render() {
    const { visible } = this.state;
    const { title, children } = this.props;
    return <OldPortal visible={visible}>
      <div className="modal-wrapper">
        <div className="modal">
          <div className="modal-title">{title}</div>
          <div className="modal-content">{children}</div>
          <div className="modal-operator">
            <button
              onClick={this.closeModal}
              className="modal-operator-close"
            >取消</button>
            <button
              onClick={this.confirm}
              className="modal-operator-confirm"
            >确认</button>
          </div>
        </div>
        <div
          className="mask"
          onClick={this.maskClick}
        ></div>
      </div>
    </OldPortal>
  }
}
export default Modal;
复制代码

可以看到,我们仅仅是在modalreturn的内容外层包裹一层OldPortal组件,然后将控制显隐的状态visible传递给了OldPortal组件,由OldPortal来实际控制modal的显隐;然后我们点击页面中的按钮,同时打开控制台,发现modal如我们所想,床送到了body层:

4.2. 16版本Portal使用

在16版本中,react-dom原生提供了一个方法ReactDOM.createPortal(),用来实现传送门的功能:

ReactDOM.createPortal(
  child,    // 要渲染的元素
  container // 指定渲染的父元素
)
复制代码

参数比之unstable_renderSubtreeIntoContainer减少了两个,接着我们在项目中使用它.

src目录下新建newPortal目录,在其中新建newPortal.jsx,newPortal.jsx内容如下:

import React from 'react';
import ReactDOM from 'react-dom';

class NewPortal extends React.Component {
  constructor(props) {
    super(props)
    // 初始化创建渲染的父元素并添加到body下
    this.node = document.createElement('div');
    document.body.appendChild(this.node);
  }

  render() {
    const { visible, children } = this.props;
    // 直接通过显隐表示
    return visible && ReactDOM.createPortal(
      children,
      this.node,
    );
  }
}
export default NewPortal
复制代码

可以很清晰的看到内容对比unstable_renderSubtreeIntoContainer的实现简化了很多,然后我们在modal.jsx中使用:

import React, { Component } from 'react';
import NewPortal from '../newPortal/newPortal';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
    this.confirm = this.confirm.bind(this)
    this.maskClick = this.maskClick.bind(this)
    this.closeModal = this.closeModal.bind(this)
    this.state = {
      visible: false
    }
  }

  componentDidMount() {
    this.setState({ visible: this.props.visible })
  }

  componentWillReceiveProps(props) {
    this.setState({ visible: props.visible })
  }

  closeModal() {
    console.log('大家好,我叫取消,听说你们想点我?傲娇脸👸')
    const { onClose } = this.props
    onClose && onClose()
    this.setState({ visible: false })
  }

  confirm() {
    console.log('大家好,我叫确认,楼上的取消是我儿子,脑子有点那个~')
    const { confirm } = this.props
    confirm && confirm()
    this.setState({ visible: false })
  }

  maskClick() {
    console.log('大家好,我是蒙层,我被点击了')
    this.setState({ visible: false })
  }

  render() {
    const { visible } = this.state;
    const { title, children } = this.props;
    return <NewPortal visible={visible}>
      <div className="modal-wrapper">
        <div className="modal">
          <div className="modal-title">{title}</div>
          <div className="modal-content">{children}</div>
          <div className="modal-operator">
            <button
              onClick={this.closeModal}
              className="modal-operator-close"
            >取消</button>
            <button
              onClick={this.confirm}
              className="modal-operator-confirm"
            >确认</button>
          </div>
        </div>
        <div
          className="mask"
          onClick={this.maskClick}
        ></div>
      </div>
    </NewPortal>
  }
}
export default Modal;
复制代码

使用上与OldPortal一样,接下来看看浏览器中看看效果是否如我们所想:

可以说Portals是弹窗类组件的灵魂,这里对Portals的使用仅仅是作为一个引导,讲解了其核心功能,并没有深入去实现一些复杂的公共方法,有兴趣的读者可以搜索相关的文章,都有更详细的讲解.

二. 出入场动画实现

1. 动画添加

从一个简单的效果开始(使用的代码是以上使用NewPortal组件的Modal组件),modal弹出时逐渐放大,放大到1.1倍,最后又缩小到1倍,隐藏时,先放大到1.1倍,再缩小,直到消失.

惯例先思考: 我们通过控制什么达到放大缩小的效果?我们如何将放大和缩小这个过程从瞬间变为一个渐变的过程?我们在什么时候开始放大缩小?又在什么时候结束放大缩小?

放大和缩小我们通过css3的属性transform scale进行控制,渐变的效果使用transition过度似乎是不错的选择,而放大缩小的时机,分为元素开始出现,出现中,出现结束,开始消失,消失中,消失结束六种状态,然后我们分别定义这六种状态的scale参数,再使用transition进行过度,应该就能实现我们需要的效果了:

modal.css添加如下代码:

.modal-enter {
  transform: scale(0);
}

.modal-enter-active {
  transform: scale(1.1);
  transition: all .2s linear;
}

.modal-enter-end {
  transform: scale(1);
  transition: all .1s linear;
}

.modal-leave {
  transform: scale(1);
}

.modal-leave-active {
  transform: scale(1.1);
  transition: all .1s linear;
}

.modal-leave-end {
  transform: scale(0);
  transition: all .2s linear;
}
复制代码

六种类名分别定义了出现与消失的六种状态,同时设置了各自的过度时间,接下来我们就在不同的过程给元素添加对应的类名,就能控制元素的显示状态了.

在我们写逻辑之前,我们还需要注意一点,之前我们组件的显隐是在NewPortal组件中实际控制的,但是我们在Modal组件中添加动画,就需要严格掌控显隐的时机,比如刚渲染就要开始动画,动画结束之后才能隐藏,这样就不适合在NewPortal组件中控制显隐了.有的读者就疑惑了,为什么不直接在NewPortal组件中添加动画呢?当然这个问题的答案是肯定的,但是NewPortal的功能是传送,并不复杂动画,我们要保持它的纯净,不宜与其他组件耦合.

修改newPortal.jsx的内容如下:

import React from 'react';
import ReactDOM from 'react-dom';

class NewPortal extends React.Component {
  constructor(props) {
    super(props)
    this.node = document.createElement('div');
    document.body.appendChild(this.node);
  }

  render() {
    const { children } = this.props;
    return ReactDOM.createPortal(
      children,
      this.node,
    );
  }
}
export default NewPortal
复制代码

修改modal.jsx的内容如下:

import React, { Component } from 'react';
import NewPortal from '../newPortal/newPortal';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
    this.confirm = this.confirm.bind(this)
    this.maskClick = this.maskClick.bind(this)
    this.closeModal = this.closeModal.bind(this)
    this.leaveAnimate = this.leaveAnimate.bind(this)
    this.enterAnimate = this.enterAnimate.bind(this)
    this.state = {
      visible: false,
      classes: null,
    }
  }

  componentDidMount() {
    this.setState({ visible: this.props.visible })
  }

  componentWillReceiveProps(props) {
    if (props.visible) {
      // 接收到父组件的props时,如果是true则进行动画渲染
      this.enterAnimate()
    }
  }

  // 进入动画
  enterAnimate() {
    // 这里定义每种状态的类名,就是我们之前modal.css文件中添加的类
    const enterClasses = 'modal-enter'
    const enterActiveClasses = 'modal-enter-active'
    const enterEndActiveClasses = 'modal-enter-end'
    // 这里定义了每种状态的过度时间,对应着modal.css中对应类名下的transition属性的时间,这里的单位为毫秒
    const enterTimeout = 0
    const enterActiveTimeout = 200
    const enterEndTimeout = 100
    // 将显隐状态改为true,同时将classes改为enter状态的类名
    this.setState({ visible: true, classes: enterClasses })
    // 这里使用定时器,是因为定时器中的函数会被加入到事件队列,带到主线程任务进行完成才会被调用,相当于在元素渲染出来并且加上初始的类名后enterTimeout时间后开始执行.
    // 因为开始状态并不需要过度,所以我们直接将之设置为0.
    const enterActiveTimer = setTimeout(_ => {
      this.setState({ classes: enterActiveClasses })
      clearTimeout(enterActiveTimer)
    }, enterTimeout)
    const enterEndTimer = setTimeout(_ => {
      this.setState({ classes: enterEndActiveClasses })
      clearTimeout(enterEndTimer)
    }, enterTimeout + enterActiveTimeout)

    // 最后将类名置空,还原元素本来的状态
    const initTimer = setTimeout(_ => {
      this.setState({ classes: '' })
      clearTimeout(initTimer)
    }, enterTimeout + enterActiveTimeout + enterEndTimeout)
  }

  // 离开动画
  leaveAnimate() {
    const leaveClasses = 'modal-leave'
    const leaveActiveClasses = 'modal-leave-active'
    const leaveEndActiveClasses = 'modal-leave-end'
    const leaveTimeout = 0
    const leaveActiveTimeout = 100
    const leaveEndTimeout = 200
    // 初始元素已经存在,所以不需要改变显隐状态
    this.setState({ classes: leaveClasses })
    const leaveActiveTimer = setTimeout(_ => {
      this.setState({ classes: leaveActiveClasses })
      clearTimeout(leaveActiveTimer)
    }, leaveTimeout)
    const leaveEndTimer = setTimeout(_ => {
      this.setState({ classes: leaveEndActiveClasses })
      clearTimeout(leaveEndTimer)
    }, leaveTimeout + leaveActiveTimeout)
    // 最后将显隐状态改为false,同时将类名还原为初始状态
    const initTimer = setTimeout(_ => {
      this.setState({ visible: false, classes: '' })
      clearTimeout(initTimer)
    }, leaveTimeout + leaveActiveTimeout + leaveEndTimeout)
  }

  closeModal() {
    console.log('大家好,我叫取消,听说你们想点我?傲娇脸👸')
    const { onClose } = this.props
    onClose && onClose()
    // 点击取消后调用离开动画
    this.leaveAnimate()
  }

  confirm() {
    console.log('大家好,我叫确认,楼上的取消是我儿子,脑子有点那个~')
    const { confirm } = this.props
    confirm && confirm()
    this.leaveAnimate()
  }

  maskClick() {
    console.log('大家好,我是蒙层,我被点击了')
    this.setState({ visible: false })
  }

  render() {
    const { visible, classes } = this.state;
    const { title, children } = this.props;
    return <NewPortal>
      <div className="modal-wrapper">
        {
          visible &&
          <div className={`modal ${classes}`}>
            <div className="modal-title">{title}</div>
            <div className="modal-content">{children}</div>
            <div className="modal-operator">
              <button
                onClick={this.closeModal}
                className="modal-operator-close"
              >取消</button>
              <button
                onClick={this.confirm}
                className="modal-operator-confirm"
              >确认</button>
            </div>
          </div>
        }
        {/* 这里暂时注释蒙层,防止干扰 */}
        {/* <div
          className="mask"
          onClick={this.maskClick}
        ></div> */}
      </div>
    </NewPortal>
  }
}
export default Modal;
复制代码

效果如下:

2. 动画组件封装

实现了动画效果,但是代码全部在modal.jsx中,一点也不优雅,而且也不能复用,因此我们需要考虑将之抽象成一个Transition组件。

思路:我们从需要的功能点出发,来考虑如何进行封装。首先传入的显隐状态值控制元素的显隐;给与一个类名,其能匹配到对应的六种状态类名;可以配置每种状态的过渡时间;可以控制是否使用动画;

src目录新建transition目录,创建文件transition.jsx,内容如下:

import React from 'react';
// 这里引入classnames处理类名的拼接
import classnames from 'classnames';

class Transition extends React.Component {
  constructor(props) {
    super(props)
    this.getClasses = this.getClasses.bind(this)
    this.enterAnimate = this.enterAnimate.bind(this)
    this.leaveAnimate = this.leaveAnimate.bind(this)
    this.appearAnimate = this.appearAnimate.bind(this)
    this.cloneChildren = this.cloneChildren.bind(this)
    this.state = {
      visible: false,
      classes: null,
    }
  }

  // 过渡时间不传入默认为0
  static defaultProps = {
    animate: true,
    visible: false,
    transitionName: '',
    appearTimeout: 0,
    appearActiveTimeout: 0,
    appearEndTimeout: 0,
    enterTimeout: 0,
    enterActiveTimeout: 0,
    enterEndTimeout: 0,
    leaveTimeout: 0,
    leaveEndTimeout: 0,
    leaveActiveTimeout: 0,
  }

  // 这里我们添加了首次渲染动画。只出现一次
  componentWillMount() {
    const { transitionName, animate, visible } = this.props;
    if (!animate) {
      this.setState({ visible })
      return
    }
    this.appearAnimate(this.props, transitionName)
  }

  componentWillReceiveProps(props) {
    const { transitionName, animate, visible } = props
    if (!animate) {
      this.setState({ visible })
      return
    }
    if (!props.visible) {
      this.leaveAnimate(props, transitionName)
    } else {
      this.enterAnimate(props, transitionName)
    }
  }

  // 首次渲染的入场动画
  appearAnimate(props, transitionName) {
    const { visible, appearTimeout, appearActiveTimeout, appearEndTimeout } = props
    const { initClasses, activeClasses, endClasses } = this.getClasses('appear', transitionName)
    this.setState({ visible, classes: initClasses })
    setTimeout(_ => {
      this.setState({ classes: activeClasses })
    }, appearTimeout)
    setTimeout(_ => {
      this.setState({ classes: endClasses })
    }, appearActiveTimeout + appearTimeout)
    setTimeout(_ => {
      this.setState({ classes: '' })
    }, appearEndTimeout + appearActiveTimeout + appearTimeout)
  }

  // 入场动画
  enterAnimate(props, transitionName) {
    const { visible, enterTimeout, enterActiveTimeout, enterEndTimeout } = props
    const { initClasses, activeClasses, endClasses } = this.getClasses('enter', transitionName)
    this.setState({ visible, classes: initClasses })
    const enterTimer = setTimeout(_ => {
      this.setState({ classes: activeClasses })
      clearTimeout(enterTimer)
    }, enterTimeout)
    const enterActiveTimer = setTimeout(_ => {
      this.setState({ classes: endClasses })
      clearTimeout(enterActiveTimer)
    }, enterActiveTimeout + enterTimeout)
    const enterEndTimer = setTimeout(_ => {
      this.setState({ classes: '' })
      clearTimeout(enterEndTimer)
    }, enterEndTimeout + enterActiveTimeout + enterTimeout)
  }

  // 出场动画
  leaveAnimate(props, transitionName) {
    const { visible, leaveTimeout, leaveActiveTimeout, leaveEndTimeout } = props
    const { initClasses, activeClasses, endClasses } = this.getClasses('leave', transitionName)
    this.setState({ classes: initClasses })
    const leaveTimer = setTimeout(_ => {
      this.setState({ classes: activeClasses })
      clearTimeout(leaveTimer)
    }, leaveTimeout)
    const leaveActiveTimer = setTimeout(_ => {
      this.setState({ classes: endClasses })
      clearTimeout(leaveActiveTimer)
    }, leaveActiveTimeout + leaveTimeout)
    const leaveEndTimer = setTimeout(_ => {
      this.setState({ visible, classes: '' })
      clearTimeout(leaveEndTimer)
    }, leaveEndTimeout + leaveActiveTimeout + leaveTimeout)
  }

  // 类名统一配置
  getClasses(type, transitionName) {
    const initClasses = classnames({
      [`${transitionName}-appear`]: type === 'appear',
      [`${transitionName}-enter`]: type === 'enter',
      [`${transitionName}-leave`]: type === 'leave',
    })
    const activeClasses = classnames({
      [`${transitionName}-appear-active`]: type === 'appear',
      [`${transitionName}-enter-active`]: type === 'enter',
      [`${transitionName}-leave-active`]: type === 'leave',
    })
    const endClasses = classnames({
      [`${transitionName}-appear-end`]: type === 'appear',
      [`${transitionName}-enter-end`]: type === 'enter',
      [`${transitionName}-leave-end`]: type === 'leave',
    })
    return { initClasses, activeClasses, endClasses }
  }


  cloneChildren() {
    const { classes } = this.state
    const children = this.props.children
    const className = children.props.className

    // 通过React.cloneElement给子元素添加额外的props,
    return React.cloneElement(
      children,
      { className: `${className} ${classes}` }
    )
  }


  render() {
    const { visible } = this.state
    return visible && this.cloneChildren()
  }
}

export default Transition
复制代码

modal.jsx内容修改如下:

import React, { Component } from 'react';
import NewPortal from '../newPortal/newPortal';
import Transition from '../transition/transition';
import './modal.css';
class Modal extends Component {
  constructor(props) {
    super(props)
    this.confirm = this.confirm.bind(this)
    this.maskClick = this.maskClick.bind(this)
    this.closeModal = this.closeModal.bind(this)
    this.state = {
      visible: false,
    }
  }

  componentDidMount() {
    this.setState({ visible: this.props.visible })
  }

  componentWillReceiveProps(props) {
    this.setState({ visible: props.visible })
  }

  closeModal() {
    console.log('大家好,我叫取消,听说你们想点我?傲娇脸👸')
    const { onClose } = this.props
    onClose && onClose()
    this.setState({ visible: false })
  }

  confirm() {
    console.log('大家好,我叫确认,楼上的取消是我儿子,脑子有点那个~')
    const { confirm } = this.props
    confirm && confirm()
    this.setState({ visible: false })
  }

  maskClick() {
    console.log('大家好,我是蒙层,我被点击了')
    this.setState({ visible: false })
  }

  render() {
    const { visible } = this.state;
    const { title, children } = this.props;
    return <NewPortal>
      {/* 引入transition组件,去掉了外层的modal-wrapper */}
      <Transition
        visible={visible}
        transitionName="modal"
        enterActiveTimeout={200}
        enterEndTimeout={100}
        leaveActiveTimeout={100}
        leaveEndTimeout={200}
      >
        <div className="modal">
          <div className="modal-title">{title}</div>
          <div className="modal-content">{children}</div>
          <div className="modal-operator">
            <button
              onClick={this.closeModal}
              className="modal-operator-close"
            >取消</button>
            <button
              onClick={this.confirm}
              className="modal-operator-confirm"
            >确认</button>
          </div>
        </div>
        {/* 这里的mask也可以用transition组件包裹,添加淡入淡出的过渡效果,这里不再添加,有兴趣的读者可以自己实践下 */}
        {/* <div
          className="mask"
          onClick={this.maskClick}
        ></div> */}
      </Transition>
    </NewPortal>
  }
}
export default Modal;
复制代码

文章到这里就写完了,为了阅读的完整性,每个步骤都是贴的完整的代码,导致全文篇幅过长,感谢您的阅读。

本文代码地址,欢迎star~

文章分类
前端