React 整合 Hot Module Replacement
增加 React 專案的開發效率
在「用開源專案一起熱血世大運」一文發表後,陸續有網友看了原始碼對我們使用的 boilerplate 感興趣而提了些問題,其中一部份跟 webpack 的 Hot Module Replacement(以下簡稱 HMR)有關。上星期和前公司的同事一起午餐時,又剛好有人提到在某些情況 HMR 似乎無法保留 React 元件的狀態,想了想於是決定寫篇文章整理一下給大家參考。
在專案中使用過 webpack-dev-server 的開發者們應該會注意到當原始碼變更時,瀏覽器會收到 webpack-dev-server 的通知並自動重新整理頁面,這個行為稱之為 live reload。對於簡單的應用來說,live reload 已經可以省去許多修改原始碼後要手動更新頁面的麻煩。
不過對於比較複雜的應用來說,有些功能往往需要經過一連串的操作才看得到效果,若每次 live reload 後都要重覆同樣的操作,將會大大降低開發的效率。這樣的情況下,使用 HMR 的 hot reload 處理異動是個比較理想的方法。如同 webpack 文件所提,HMR 主要解決幾個問題:
- 套用異動後維持頁面當下的狀態
- 僅更新異動部份以節省開發時間
- 更快速地套用樣式異動
接下來的文章中會分別說明:
HMR 的原理
這張圖是 webpack 編譯原始碼後的結果示意圖,圓角矩型的部份代表編譯出來的 chunk,圓型的部份代表各個 module,而虛線的部份則代表 module 和 chunk 的從屬關係。另外有箭頭的部份,黑色代表 module 之間的引用,而綠色則代表 chunk 透過哪些 module 被程式使用。
接著以一個簡單的修改為例,這裡 module 4 不再繼續引用 module 9,而 module 11 則是多引用了新的 module 12 。在這些 modules 異動後,modules 所屬的 chunks 會產生各自的更新檔並透過 HMR 通知瀏覽器,再由瀏覽器下載這些更新檔套用至對應的 chunk。
由於 chunk 3 不再被程式使用,套用更新後所屬的 module 9、10 就完整被移出程式了。chunk 1、4 則是根據更新檔進行異動,調整自己與 module 4、11、12 的關係。當所有的更新檔都被套用後,HMR 就會回復到閒置狀態等待下一次的更新到來。
在 webpack 開啟 HMR
要開啟 HMR 只要在 webpack 中將 devServer
的 hot
選項設定成 true,並在 plugins
中加入 HotModuleReplacementPlugin
就完成了。
另外為了在瀏覽器的 console 比較好檢查更新的狀況,通常會在 plugins
中再加上 NamedModulesPlugin
,讓 HMR 在更新時能以實際的檔案名稱顯示會被更新的項目。
使用 React Hot Loader 整合 HMR 到 React
由於 React 有一套處理元件生命週期的機制,單純使用 HMR 做 hot reload 時 React 會認為更新前後的元件不同,導致原本的元件被 unmount 而失去當下的狀態。為了讓 React 能夠在 hot reload 後保留原有狀態,開發者需要 React Hot Loader 作為 HMR 和 React 溝通的橋梁。
$ npm install --save react-hot-loader@next
過去 React Hot Loader 曾經被棄用一段時間,但隨著 v3.0 加入幾個重要的功能後又重回開發行列中。由於目前 v3.0 仍在測試階段,安裝時需要特別指定 next
tag 才會安裝到這個版本。
安裝完首先要在編譯過程加上 React Hot Loader 的支援,這邊可以選擇 Babel 或是 webpack 設定的其中一種加入就好。
接著 webpack 的 entry
也要加上 React Hot Loader 的支援,讓瀏覽器知道如何 patch 透過 React Hot Loader 處理過的 HMR 更新。
最後在 render
App 時加上一層 AppContainer
HOC,讓 React Hot Loader 能透過它處理下層的元件更新。同時透過 HMR 註冊原始碼異動後的 callback,在 callback 裡用 React 重新 render
更新後的 App 就完成了。
使用 HMR 可能遇到的疑難雜症
瀏覽器 console 出現 HMR 更新訊息但頁面卻沒有反應
這兩段程式碼分別是 webpack v1.x 和 v2.x 以後的版本用在 hot reload 時重新 render 頁面的片段。由於 webpack v2.x 以後的版本支援 ES6 modules,開發者不需要在 hot reload 的 callback 中重新用 require
引用更新的元件進行 render。
有些情況第二種寫法會發生看得到 HMR 更新訊息但頁面卻沒有反應,這時候如果改用第一種寫法可以運作的話,問題很有可能來自 Babel。在「Tree-shaking ES6 Modules in webpack 2」一文提到,Babel 轉譯後的 module 結構會讓 webpack 無法套用 module 的更新,只要停用 Babel 的轉譯功能讓 webpack 處理 module 就可以解決這個問題。
只要把 Babel 設定中 es2015
preset 的 modules
選項改成 false,webpack 就能正常處理 ES6 modules 讓 hot reload 正常運作了。
特定 component 更新後無法維持原本的狀態
這是透過 webpack 使用 react-hot-loader 處理原始碼時會遇到的問題。在使用 HOC 包裝 SkillListContainer 的情況下,由於 SkillListContainer 並沒有被 export
導致 webpack 無法辨認它,因此 React 在更新後以為它們是不同的 components 而 unmount 了原本的 SkillListContainer。
透過 Babel 使用 react-hot-loader 處理就可以解決這個問題,詳細的說明可以參考 React Hot Loader 的 issue #276。
無法更新 Redux 的 reducer
在沒有設定 Redux 支援 HMR 的情況下,修改 reducer 後會發現程式沒有任何變動,這時候打開瀏覽器的 console 會看到類似上圖的訊息。造成這個問題的主要原因是 React Redux v2.0 之後的版本不再主動處理 reducer 的 hot reload,開發者必需自行加入相關的設定。
替 Redux 加入 HMR 支援的方式也跟 React 本身類似,同樣透過 HMR 註冊一個在 reducer 更新時執行的 callback,接著在 callback 中使用 store
的 replaceReducer
取代原本的 reducer 就完成了。
以上是這篇文章的內容,如果你覺得這篇文章有幫助,或是有任何相關經驗想要討論,歡迎你分享或留言讓我知道。
在此感謝 Amdis Liu 在文章撰寫過程中協助校閱與提供文案。