React中的模式對話方塊

溜達向日葵發表於2018-07-30

在16.x版本之後React提供了Protals功能來解決模式對話方塊不在Dom根節點導致的一些BUG。除了Protal還有更多的方法去解決這些問題,本文來自David Gilbertson的部落格,詳細解釋了React中模式對話方塊的一些問題,以及他給出的解決方案,在瞭解Protals之前閱讀這篇內容,能讓你更加明白Protal的重要性。

對於React的模式對話方塊,有很多方法可以實現但是並沒有一個絕對正確的方法。這句話怎麼理解呢?讓我們先看看一個模式對話方塊的特性:

  1. 能夠浮現在最上層,阻止使用者的其他操作。
  2. 能夠處理滑鼠和鍵盤事件,例如關閉視窗事件。
  3. 接受外部傳入一個回撥函式,當使用者進行某些操作的時候呼叫他,例如點選“確定”或“取消”按鈕。
  4. 接受外部引數,可以設定大小、文字、處理器等等。

模式對話方塊的實現思路

下面的這些圖片是常見模式對話方塊的例子:

React中的模式對話方塊

React中的模式對話方塊

React中的模式對話方塊

這些模式對話方塊都有一個全域性的背景遮罩層、有頭部或描述內容、有一些功能按鈕、可以隨意設定的寬度和高度、位置居中。

在React中有三種方式實現模式對話方塊:

  1. 使用一個常規的元件作為一個模式對話方塊的包裝元件,然後將我們自定義的內容作為子元件傳遞給模式對話方塊。例如這個專案:https://github.com/reactjs/react-modal
  2. 將模式對話方塊放置到HTML結構的頂層,將其設定為 document.body 的子元素。例如:https://github.com/tajo/react-portal
  3. 將模式對話方塊作為整個元件結構中的頂層元件(根元素的子元件),通過全域性的資料來控制他顯示或隱藏。

那這三種實現方式有什麼問題呢:

第一種方式有定位問題。如果你用這種方式實現模式對話方塊,你的HTML上下文會影響當前模式對話方塊的展示效果,所以這種方式很有可能會出現一些意向不到的問題。你真的認為 position: fixed 可以讓某個元素相對與瀏覽器視窗絕對定位嗎?請看這個例子: https://output.jsbin.com/fepime/,使用開發人員工具看看 .top-div和 .fixed-div 的樣式你就懂了。

第二種方式首先對於單元測試不友好,因為我們不得不把對話方塊作為body的子元素(或者其他某個真實DOM的子元素)來顯示,那麼得有瀏覽器的真實DOM才能看到效果。而且這種方式看起來挺“駭客”的,我們按照單向資料流的思路開發了整套個標準合理的React元件,最後不得不用 ReactDOM.unstable_renderSubtreeIntoContainer() 方法裝載一個元件到body元素中,最終可能會導致虛擬DOM與真實DOM不一致或者服務端渲染遇到問題。‘unstable’字首的含義是React官方明確告訴你:這玩意有坑,踩上了別怪我。詳情請看React官方對unstable_renderSubtreeIntoContainer的說明。

第三種方式在筆者看來是最合理最優秀的,下面就談談這種實現方式的思路。

全域性資料流控制模式對話方塊

實際上就是用flux或redux的方式去控制對話方塊顯示或關閉。如果之前用過flux之類思路的工具,後面的內容分分鐘就理解了。

先看下模式對話方塊的元件結構:

React中的模式對話方塊

  • App.jsx——整個工程的根元件,通常不會在這裡有什麼特殊的處理。它首先會渲染其他所有的頂層元件,然後再最後渲染模式對話方塊元件。
  • ModalConductor.jsx——模式框的管理元件,由他來控制當前應該渲染哪個模式框。
  • SignIn.jsx、EditScreen.jsx等元件——具體樣式的模式對話方塊。

在這些元件之外,還有store來儲存全域性模式對話方塊的相關資料。store.currentModal 用於指示顯示哪個模式框的字串,如果為 null 則表示沒有任何模式框要顯示,所以整個工程一次只顯示一個模式框。

下面我們看看元件實現過程。

首先我們在任何位置都可以修改 store 。當我們通過某種方式將 store.currentModal 的值修改為 signIn 後,ModalConductor 會觸發重新渲染並在內部判斷要渲染 SignIn 元件。

這是 ModalConductor 的示意程式碼,通過switch語句判斷要顯示的元件:

import React from `react`;

import ExportDataModal from `./ExportDataModal.jsx`;
import SignInModal from `./SignInModal.jsx`;
import FeedbackModal from `./FeedbackModal.jsx`;
import BoxDetailsModal from `./BoxDetailsModal.jsx`;

const ModalConductor = props => {
  switch (props.currentModal) {
    case `EXPORT_DATA`:
      return <ExportDataModal {...props}/>;

    case `SOCIAL_SIGN_IN`:
      return <SignInModal {...props}/>;

    case `FEEDBACK`:
      return <FeedbackModal {...props}/>;

    case `EDIT_BOX`:
      return <BoxDetailsModal {...props}/>;

    default:
      return null;
  }
};

export default ModalConductor;

下面模式對話方塊元件的程式碼結構:

import React from `react`;

import ModalWrapper from `../ModalWrapper.jsx`;

const SignIn = props => {
  const signIn = provider => {
    props.hideModal();
    props.signIn(provider);
  };

  return (
    <ModalWrapper
      {...props}
      title="Sign in"
      width={400}
      showOk={false}
    >
      <p>Choose your flavor</p>
      <button onClick={() => signIn(`facebook`)}>Facebook</button>
      <button onClick={() => signIn(`google`)}>Google</button>
      <button onClick={() => signIn(`twitter`)}>Twitter</button>
    </ModalWrapper>
  );
};

export default SignIn;

他內部使用了一個名為 ModalWrapper 的包裝元件,用來顯示模式對話方塊的效果,可以直接使用https://github.com/reactjs/react-modal或者自己實現,如下是一個模式框的包裝元件:

import React from `react`;
const {PropTypes} = React;

const ModalWrapper = props => {
  const handleBackgroundClick = e => {
    if (e.target === e.currentTarget) props.hideModal();
  };

  const onOk = () => {
    props.onOk();
    props.hideModal();
  };

  const okButton = props.showOk
    ? (
      <button
        onClick={onOk}
        disabled={props.okDisabled}
      >
        {props.okText}
      </button>
    ) : null;

  return (
    <div onClick={handleBackgroundClick}>
      <header>
        <h1>{props.title}</h1>

        <button onClick={props.hideModal}>Close</button>
      </header>

      {props.children}

      {okButton}
    </div>
  );
};

ModalWrapper.defaultProps = {
  title: ``,
  showOk: true,
  okText: `OK`,
  okDisabled: false,
  width: 400,
  onOk: () => {}
};

export default ModalWrapper;


相關文章