一步一步帶你封裝基於react的modal元件

binnear發表於2018-09-22

中秋放假,一個人有點無聊,於是寫點博文暖暖心,同時祝大家中秋快樂~ ?

接下來將一步步帶領大家實現一個基本的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;
複製程式碼

完成以上步驟後,我們瀏覽器中就會如下圖顯示了:

一步一步帶你封裝基於react的modal元件

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;
}
複製程式碼

修改完成後,我們瀏覽器中就會如下圖顯示:

一步一步帶你封裝基於react的modal元件

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元件顯隱的效果

未點選按鈕效果如下圖:

一步一步帶你封裝基於react的modal元件

點選按鈕後效果如下圖:

一步一步帶你封裝基於react的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;
複製程式碼

接著我們點選頁面中的按鈕,結果顯示如下:

一步一步帶你封裝基於react的modal元件

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;
複製程式碼

儲存後,我們再瀏覽器中分別點選取消和確認,控制檯中將會出現如下圖所示:

一步一步帶你封裝基於react的modal元件

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層:

一步一步帶你封裝基於react的modal元件

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一樣,接下來看看瀏覽器中看看效果是否如我們所想:

一步一步帶你封裝基於react的modal元件

可以說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;
複製程式碼

效果如下:

一步一步帶你封裝基於react的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~

相關文章