想必做爲前端大佬的你,工做中應該用過 webpack,而且對熱更新的特性也有了解。若是沒有,固然也不要緊。javascript
下面我要講的,是我對 Webpack 熱更新機制的一些認識和理解,不足之處,歡迎指正。html
首先:前端
熱更新,是指 Hot Module Replacement,縮寫爲 HMR。vue
從名字上解讀,就是把「熱」的模塊進行替換。熱,是指這個模塊已經在運行中。java
不知道你有沒有聽過或看過這樣一段話:「在高速公路上將汽車引擎換成波音747飛機引擎」。webpack
雖然有點牽強,可是放在這裏,從某些角度上來講,也還算合適吧。git
再扯遠一點,說下我目前工做中的遇到的狀況,相信不少人也遇到過。github
微信小程序的開發工具,沒有提供相似 Webpack 熱更新的機制,因此在本地開發時,每次修改了代碼,預覽頁面都會刷新,因而以前的路由跳轉狀態、表單中填入的數據,都沒了。web
哪怕只是一個文案或屬性配置的修改,都會致使刷新,而要從新進入特定頁面和狀態,有時候很麻煩。對於開發時須要頻繁修改代碼的狀況,這樣比較浪費時間。小程序
而若是有相似 Webpack 熱更新的機制存在,則是修改了代碼,不會致使刷新,而是保留現有的數據狀態,只將模塊進行更新替換。也就是說,既保留了現有的數據狀態,又能看到代碼修改後的變化。
很美好,可是想一想就以爲是一件確定不簡單的事情。
因此,熱更新是啥呢?
引用官方文檔,熱更新是:
使得應用在運行狀態下,不重載刷新就能更新、增長、移除模塊的機制
那麼熱更新要解決的問題,在上面也解釋了。用個人話來闡述,就是 在應用程序的開發環境,方便開發人員在不刷新頁面的狀況下,就能修改代碼,而且直觀地在頁面上看到變化的機制。
簡單來講,就是爲了 提高開發效率。
聯想到我在微信小程序上的開發體驗,真心以爲若是有熱更新機制的話,開發效率要高不少。
若是你知道微信小程序已經或計劃支持熱更新,或者有大佬已經作了相似的工做,歡迎告訴我,感謝!
進一步介紹前,咱們來看下 Webpack 熱更新如何配置。
若是你以前作的項目是其餘人搭建配置了 Webpack 和熱更新,那麼這裏能夠了解下熱更新是怎麼配置的。
個人示例採用 Webpack 4,想直接看代碼的話,在這裏:
https://github.com/luobotang/...
除了 Webpack,還須要 webpack-dev-server
(或 webpack-dev-middleware
)。
爲 Webpack 開發環境開啓熱更新,要作兩件事:
HotModuleReplacementPlugin
插件webpack-dev-server
的熱更新開關HotModuleReplacementPlugin
插件是 Webpack 自帶的,在 webpack.config.js
加入就好:
// webpack.config.js module.exports = { // ... plugins: [ webpack.HotModuleReplacementPlugin(), // ... ] }
若是直接經過 webpack-dev-server 啓動 Webpack 的開發環境,那麼能夠這樣打開 webpack-dev-server 的熱更新開關:
// webpack.config.js module.exports = { // ... devServer: { hot: true, // ... } }
也很簡單。
下面經過例子來進一步解釋熱更新機制。若是你以前對 Webpack 熱更新的體驗,是 Vue 經過 vue-loader 提供給你的,也就是說你在本身的代碼中從沒有寫過或者見到過相似:
if (module.hot) { module.hot.accept(/* ... */) // ... }
這樣的代碼,那麼下面的例子就恰好適合看一看了。
這些例子就在上面的 webpack-hmr-demo,若是你對代碼更親切,那直接去看吧,首頁文檔裏有簡單的說明。
這個例子只是把示例頁面的功能簡單介紹下,而且讓你體會下每次修改代碼都要從新刷新頁面的痛苦。
頁面上只有一個元素,用來展現數值:
<div id="root" class="number"></div>
入口模塊(index.js)引用了兩個模塊:
入口模塊的功能很簡單,調用 timer.start()
,再傳入的回調函數中,每次將獲得的數值更新到頁面上顯示:
import { start } from './timer' import { message } from './foo' var current = 0 var root = document.getElementById('root') start(onUpdate, current) console.log(message) function onUpdate(i) { current = i root.textContent = '#' + i }
將這個項目運行起來,打開的頁面中就是在一直刷新展現增長的數值而已,相似這樣:
一旦修改任何模塊的代碼,例如改變 timer 中定時器的間隔時間(如從1秒改爲3秒),或者 onUpdate 中展現的內容(如 '#' + i
改爲 '*' + i
),頁面都會刷新,已經有的狀態清除,從新從0開始計數。
接下來的例子,展現在 index.js 如何處理其餘模塊的更新。
依賴的模塊發生更新,要麼是接受變動(頁面不用刷新,模塊替換下就好),要麼不接受(必須得刷新)。
Webpack 將熱更新相關接口以 module.hot
暴露到模塊中,在使用前,最好判斷下當前的環境是否支持熱更新,也就是上面看到的這樣的代碼:
if (module.hot) { // ... }
延續上一個例子,選擇接受並處理 timer 的更新,但對於 foo 模塊,不接受:
if (module.hot) { module.hot.accept('timer', () => { // ... }) module.hot.decline('./foo') }
因此,在熱更新的機制中,實際上是以這種「聲明」的方式告知 Webpack,哪些模塊的更新是被處理的,哪些模塊的更新又不被處理。固然對於要處理的模塊的更新,自行在 module.hot.accept() 的第二個參數即回調函數中進行處理,會在聲明的模塊被替換後執行。
下面來看對 timer 模塊更新的處理。
timer 模塊的 start 函數調用後返回一個能夠終止定時器的 stop 函數,藉助它咱們實現對舊的 timer 模塊的清理,並基於當前狀態從新調用新的 timer 模塊的 start 函數:
var stop = start(onUpdate, current) // 先記錄下返回的 stop 函數 // ... if (module.hot) { module.hot.accept('timer', () => { stop() stop = start(onUpdate, current) }) // ... }
處理邏輯如上所述,先經過以前記錄的 stop 中止舊模塊的定時器,而後調用新模塊的 start 繼續計數,而且傳入當前數值從而沒必要從0開始從新計數。
看起來仍是比較簡單的吧。運行起來的效果是,若是修改 timer 中的定時器間隔時間,當即在頁面上就能看到效果,並且頁面並不會刷新致使從新從0開始計數:
在運行幾秒後,修改 timer 模塊中定時器的間隔時間爲 100ms
修改 foo 中的 message,頁面仍是會刷新。
有幾點額外說明下:
此外,除了聲明其餘模塊更新的處理,模塊也能夠聲明自身更新的處理,也是一樣的接口,不傳參數便可:
module.hot.accept()
告訴 Webpack,當前模塊更新不用刷新module.hot.decline()
告訴 Webpack,當前模塊更新時必定要刷新並且,依賴同一個模塊的不一樣模塊,能夠有各自不一樣的聲明,這些聲明多是衝突的,好比有的容許依賴模塊更新,有的不容許,Webpack 怎麼協調這些呢?
Webpack 的實現機制有點相似 DOM 事件的冒泡機制,更新事件先由模塊自身處理,若是模塊自身沒有任何聲明,纔會向上冒泡,檢查使用方是否有對該模塊更新的聲明,以此類推。若是最終入口模塊也沒有任何聲明,那麼就刷新頁面了。這也就是爲何在上一個例子中,雖然開啓了熱更新,可是模塊修改後仍舊刷新頁面的緣由,由於沒有任何模塊對更新進行處理。
自身模塊的更新處理與依賴模塊相似,也是要經過 module.hot 的接口向 Webpack 聲明。不過模塊自身的更新,可能須要在模塊被 Webpack 替換以前就作一些處理,更新後的處理則沒必要經過特別接口來作,直接寫到新模塊代碼裏面就好。
module.hot.dispose()
用於註冊當前模塊被替換前的處理函數,而且回調函數接收一個 data 對象,能夠向其寫入須要保存的數據,這樣在新的模塊執行時能夠經過 module.hot.data
獲取到:
var current = 0 if (module.hot && module.hot.data) { current = module.hot.data.current }
首先,模塊執行時,先檢查有沒有舊模塊留下來的數據,若是有,就恢復。
而後在模塊被替換前的執行處理,這裏就是記錄數據、停掉現有的定時器:
if (module.hot) module.hot.accept() module.hot.dispose(data => { data.current = current stop() }) }
作了這些處理以後,修改 index.js 的 onUpdate,使得渲染到頁面的數值改變,也能夠在不刷新的狀況下體現:
在運行幾秒後,修改 onUpdate() 中的'#' + i
爲'*' + i
看過上面的例子,咱們來總結下。
Webpack 的熱更新,其實只是提供一套接口和基礎的模塊替換的實現。做爲開發者,須要在代碼中經過熱更新接口(module.hot.xxx)向 Webpack 聲明依賴模塊和當前模塊是否可以更新,以及更新的先後進行的處理。
若是接受更新,那麼須要開發者本身來在模塊被替換前清理或保留必要的數據、狀態,並在模塊被替換後恢復以前的數據、狀態。
固然,像咱們在使用 Vue 或 React 進行開發時,vue-loder 等插件已經幫咱們作了這些事情,而且對於 *.vue 文件在更新時要若是進行處理,不少細節也只有 vue-loader 內部比較清楚,咱們就放心使用好了。
可是對於 Webpack 熱更新是怎麼一回事,若是可以有深刻了解固然更好,我就遇到過同事在 Vue 組件中自行對 DOM 進行處理(爲了封裝一個直接操做 DOM 的組件),結果因爲熱更新的存在,致使一些狀態的清除有問題的狀況。
這種狀況,只有開發者本身才能處理,vue-loader 可無法處理這樣的特殊狀況。至少知道如何使用 Webpack 的熱更新接口,這種狀況下開發者就能自行處理了。
本文對於 Webpack 熱更新機制的介紹還只是在接口使用的層面,或者大致的機制上,沒有深刻說明熱更新的實現原理和細節。時間、篇幅有限,那就先放一張圖出來,或許有時間再細說一下。
上圖來源:Webpack & The Hot Module Replacement
https://medium.com/@rajaraodv/webpack-hot-module-replacement-hmr-e756a726a07這篇英文文章對 Webpack 熱更新實現原理方面有深刻介紹。