今天上午組內小朋友們談到 React 實踐,提到 React 模態框(彈窗)的使用。我發現很多一些 React 開發者對於 React 模態框的具體設計思路和實現存在一些疑惑。因而特寫此文,分享我對模態框這個“重要且典型”的前端互動,在 React 框架裡實現的一些想法。準備時間短促且匆忙,難免有遺漏之處,希望大神給予斧正。
這篇文章“進階式”漸進地,由淺入深分析三種實現。從最初的簡單粗暴到接近 react-modal 庫設計思想,一步步打磨分析,適合初學者閱讀思考。
原始級實現 —— 暴力美學
世界上大部分網站都離不開模態框互動。事實上,模態框就是我們俗稱的“彈窗”,只不過這個彈窗相比簡單的:
alert('我是一個簡單、原生的 alert~');複製程式碼
多了更多的資訊承載和互動行為。同時為了更佳美化和吸引眼球,模態框往往伴隨著深色透明的遮罩。比如下圖:
想想常見的使用者登入框、錯誤資訊提示等等,都是非常典型的模態框實現。
在傳統的 jQuery 操作 DOM 類庫的技術棧下,我們可以“肆無忌憚”地選擇 DOM 節點,完成 append, remove 等操作,實現模態框並不複雜。可是在 React 和 Redux 世界裡,我們該如何實現?
我們先來看一下場景和初版設計思路:
如圖,箭頭標記的元件需要觸發模態框的出現。
圖中元件樹對應基本頁面程式碼如下:
export default class App extends Component {
render() {
<div className="app">
<div className="left">
<h1>Hello left</h1>
// ...
</div>
<div className="right">
<h1>Hello right</h1>
// ...
<div>
<BadModal>
// 模態框內容
<h1> Modal title </h1>
<p> Modal content</p>
</BadModal>
</div>
</div>
</div>
}
}複製程式碼
細心的讀者會發現,作為 amazing 的程式設計師,儘管這是最初版本的實現,但是還是思考一些最基本的“複用”問題。
我們設計完成的模態框元件 ,因為每個模態框裡內容和互動不盡相同,所以在 元件內,我們渲染 child component,這個 child component 即業務對應的模態框內容,它將會由業務邏輯開發完成,實現模態框內容、互動的複用。如下程式碼:
class BadModal extends Comment {
render() {
return (
<div className="modal">
{ this.props.children }
</div>
)
}
}複製程式碼
至此,我們已經實現了最基本的模態框。可是為什麼說這是最原始、簡陋的方法呢?細想一下,似乎不完美的地方還很多。翻開我們的樣式表:
body .modal {
position: fixed;
// ...
}
.left {
z-index: 3
}
.right {
z-index: 1
}複製程式碼
你會發現惱人的 z-index 問題,我們模態框是 .right 節點的子孫節點,而 .right 的 z-index 小於 .left 的 z-index,這樣造成的直接問題就是模態框最終不能脫離頁面整體而“突出顯示”!
細想一下,這個問題的根本就出現在我們的元件設計圖中。
仔細觀察上圖,因為很深層次的子孫元件觸發模態框,而使得該元件內的模態框元件層級較深。如果你對 z-index 比較規則有所瞭解的話,這樣的情況很難完成模態框凌駕於頁面整體而出現的,遮罩也無法覆蓋整個頁面。
想想我們平時使用的 jQuery 是怎麼做的吧:
$('body').append('<div class="overlay"></div>');複製程式碼
一般情況,模態框和遮罩總是作為在 body 下的第一層子節點出現。由此,引出了我們的第二種進階思路。請讀者繼續閱讀。
實現方案二 —— 乾坤大挪移
解決方法很簡單,我們可以很自然地想到:只需要對 元件出現的位置進行移動。可是這就需要 元件和觸發模態框出現的深層次元件進行某種意義上的通訊。
傳統的 React 元件間通訊無外乎 props 和基於 props 的回撥實現(不考慮 context 的黑魔法)。可是這樣的做法太過複雜,也難以實現複用,更不利於維護。
至於我這裡採用的做法,還要從調整後的頁面元件樹設計出發:
如圖,我們在 document.body 下加入了 元件,並列於 Root Component。同時,至關重要的一步設計是,我們在觸發模態框的元件下,加入了一個 Fake Modal 元件。
這個神祕的 Fake Modal 元件做了什麼呢?
事實上,他並不渲染任何結果,而是藉助其生命週期函式,完成在 document.body 下新建並插入 元件的使命。
藉助程式碼進行理解:
class Modal extends Comment {
componentDidMount() {
this.modalTarget = document.createElement('div');
this.modalTarget.className = 'modal';
document.body.appendChild(this.modalTarget);
this.renderModal();
}
componentWillUpdate() {
this.renderModal();
}
componentWillUnmount() {
ReactDom.unmountComponentAtNode(this.modalTarget);
document.body.removeChild(this.modalTarget)l
}
renderModal() {
ReactDom.render(
<div>{ this.props.children }</div>,
this.modalTarget
);
}
render() {
return <noscript />
}
}複製程式碼
具體進行分析在真正的 render 方法中,我們不渲染任何實質的內容,而是:
return <noscript />;複製程式碼
同時,藉助生命週期函式 componentDidMount,我們使用原生 JavaScript 實現在 body 下的模態框建立:
this.modalTarget = document.createElement('div');
this.modalTarget.className = 'modal';
document.body.appendChild(this.modalTarget);
this.renderModal();複製程式碼
並最終呼叫 renderModal 方法完成插入:
ReactDom.render(
<div>{ this.props.children }</div>,
this.modalTarget
);複製程式碼
實現方案三 —— 搭配 Redux
相信很多 React 開發者都會使用 Redux 來做資料管理。仔細看上圖的結構中,我們難以實現對 Redux 的友好相容。
圖片
比如說,如果在 元件的子元件 child component 中,需要使用 Redux store 裡的資料,那麼因為 實質上是一個“高階元件”且不在 元件的元件鏈中,因為 child component 無法感知 Redux store 的存在。
為了解決這個問題,我們繼續改進元件樹結構為:
圖片
為此,我們引入應用的 store,以及 react-redux 包提供的 元件:
import { store } from '../index';
import { Provider } from 'react-redux';複製程式碼
同時改動先前的 renderModal 方法,加入對 的支援:
renderModal() {
ReactDom.render(
<Provider store={ store }>
<div>{ this.props.children }</div>
<Provider>,
this.modalTarget
);
}複製程式碼
著名的 react-modal 探祕和 React16 版本驚喜
在 React 開發中,我想很多工程師對 react-modal 非常熟悉。我們往往依賴它,完成模態框的使用。
這個庫設計良好,請封裝完善。如果你好奇它是如何實現的,原始碼又是如何組織?那麼我可以告訴你,你已經瞭解了他的設計哲學。事實上,文章介紹的思路就是它的奧祕。
瞭解了這些,你也可以動手實現“一個輪子”,或者擴充本文原始碼,實現更多的功能。比如樣式的自定義、彈出前後的回撥等等。相信一定會有很多收穫。
同時,React 最新版本 0.16 已經橫空出世,它帶來的很多新特性之一就與本文密切相關。那就是 —— Portal,Portal 我們把它翻譯為“傳送門、任意門”。Portals 允許將元件渲染到父節點之外的 DOM 節點中。它的基本使用如下程式碼示例:
render() {
return ReactDOM.createPortal(
this.props.children,
anyDomNode,
);
}複製程式碼
這裡 React 並不會在當前結構中渲染元件,而是向 anyDomNode 中渲染 this.props.children,這裡的 anyDomNode 是任何有效的DOM節點,無論它處於哪個層級位置。
瞭解了這些,我們當然能夠使用此特性,簡化上文邏輯。翻開 react-modal 最新提交的原始碼,便能夠發現對這一新特性的支援,react-modal/src/components/Modal.js 檔案中:
const isReact16 = ReactDOM.createPortal !== undefined;
const createPortal = isReact16
? ReactDOM.createPortal
: ReactDOM.unstable_renderSubtreeIntoContainer;複製程式碼
這裡對 React 版本進行判斷,並設定 isReact16 標識位表示是否支援 createPortal 的方法(個人認為這個標識位的命名非常不合適...)
最終在 render 方法內:
render() {
if (!canUseDOM || !isReact16) {
return null;
}
if (!this.node && isReact16) {
this.node = document.createElement("div");
}
return createPortal(
<ModalPortal
ref={this.portalRef}
defaultStyles={Modal.defaultStyles}
{...this.props}
/>,
this.node
);
}複製程式碼
非常明顯地看到,對於不支援 createPortal 的情況採用與我們類似的 return null; 否則愉快地使用 createPortal 方法。
總結
本文介紹內容雖然基礎,但是很好地貫穿了 React 思想以及實現一個“模態框輪子”的演進思路。同時介紹了 React 新版本的一項特性。
我的其他幾篇關於React技術棧的文章:
React Redux 中介軟體思想遇見 Web Worker 的靈感(附demo)
瞭解 Twitter 前端架構 學習複雜場景資料設計
React 探祕 - React Component 和 Element(文末附彩蛋demo和原始碼)
從setState promise化的探討 體會React團隊設計思想
通過例項,學習編寫 React 元件的“最佳實踐”
React 元件設計和分解思考
從 React 繫結 this,看 JS 語言發展和框架設計
React 服務端渲染如此輕鬆 從零開始構建前後端應用
做出Uber移動網頁版還不夠 極致效能打造才見真章**
React+Redux打造“NEWS EARLY”單頁應用 一個專案理解最前沿技術棧真諦**
Happy Coding!
PS: 作者 Github倉庫** 和 知乎問答連結 歡迎各種形式交流。