webpack hot-module-replacement 原理&踩坑
webpack hot-module-replacement 原理&踩坑
起因
最近在做san框架的熱更新,記錄一下webpack HMR的原理和小坑。
什麼是HMR?
熱更新是webpack的一個特性,通過無重新整理實現程式碼更新。
在HMR之前,大多數開發體驗是live reload,儲存後自動重新整理瀏覽器,已經是比刀耕火種的年代強很多了,但是自從某天在油管上看到dan神的redux時間穿梭,瞬間被驚豔到(當然,HMR已經是這之前很久就出現了)。
HMR大幅提高了開發體驗,只更新變更內容,調整樣式迅速,避免了大部分的網路請求、瀏覽器重新渲染、app解析編譯顯示,讓我們來看下他是如何做到的。
hmr基本法
概念
compile: webpack的核心。js編譯、拆包。
hmr-server: 建立連線並完成模組熱更新的推送。
bundle-server: 靜態伺服器。
bundle.js: client端。
hmr-runtime: 注入到bundle.js中的程式碼。
更新流程
熱更新開啟後,當webpack打包時,會向client端注入一段HMR runtime程式碼,同時server端(webpack-dev-server或是webpack-hot-middware)啟動了一個HMR伺服器,它通過websocket和注入的runtime進行通訊。
當webpack檢測到檔案修改後,會重新構建,並通過ws向client端傳送更新訊息,瀏覽器通過jsonp拉取更新過的模組,回撥觸發模組熱更新邏輯。
1.修改了一個或多個檔案。
2.檔案系統接收更改並通知Webpack。
3.Webpack重新編譯構建一個或多個模組,並通知HMR伺服器進行了更新。
4.HMR Server使用websockets通知HMR Runtime需要更新。HMR執行時通過HTTP請求這些更新(jsonp)。
5.HMR執行時替換更新中的模組,如果確定這些模組無法更新,則觸發整個頁面重新整理(這是個大坑。。)。
坑
觸發頁面重新整理
// webpack/hot/dev-server if(module.hot) { var lastHash; //__webpack_hash__是每次編譯的hash值是全域性的 //Only available with the HotModuleReplacementPlugin or the ExtendedAPIPlugin var upToDate = function upToDate() { return lastHash.indexOf(__webpack_hash__) >= 0; }; var check = function check() { // check([autoApply], callback: (err: Error, outdatedModules: Module[]) => void // If autoApply is truthy the callback will be called with all modules that were disposed. apply() is automatically called with autoApply as options parameter.(傳入哪些程式碼已經被更新的模組) //If autoApply is not set the callback will be called with all modules that will be disposed on apply(). (不是true那麼傳入的是哪些需要被apply處理的模組) module.hot.check(true).then(function(updatedModules) { //檢查所有要更新的模組,如果沒有模組要更新那麼回撥函式就是null if(!updatedModules) { console.warn("[HMR] Cannot find update. Need to do a full reload!"); console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); window.location.reload(); return; } //如果還有更新 if(!upToDate()) { check(); } require("./log-apply-result")(updatedModules, updatedModules); //已經被更新的模組都是updatedModules if(upToDate()) { console.log("[HMR] App is up to date."); } }).catch(function(err) { var status = module.hot.status(); //如果報錯直接全域性reload if(["abort", "fail"].indexOf(status) >= 0) { console.warn("[HMR] Cannot apply update. Need to do a full reload!"); console.warn("[HMR] " + err.stack || err.message); window.location.reload(); } else { console.warn("[HMR] Update failed: " + err.stack || err.message); } }); }; var hotEmitter = require("./emitter"); //獲取MyEmitter物件 hotEmitter.on("webpackHotUpdate", function(currentHash) { lastHash = currentHash; if(!upToDate() && module.hot.status() === "idle") { //呼叫module.hot.status方法獲取狀態 console.log("[HMR] Checking for updates on the server..."); check(); } }); console.log("[HMR] Waiting for update signal from WDS..."); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); }
正常情況下,hmr只會更新模組,不會觸發頁面重新整理。
但是當bundle.js中的程式碼丟擲異常時,如果開發者沒有手動接收並處理,這個錯誤會冒泡到webpack-hmr-runtime中。
runtime接收error後會console.log一些資訊並立即重新整理,通常情況下是沒辦法看到那些log的,因為太快了。
// vue-hot-reload-api.js
// 不得不說,這個一開始確實沒搞懂是為啥要包一層
// 自己實現的時候才知道,當有error彈出時
// 如果不手動這樣接住error,webpack會接到然後立即location.reload()
// 根本來不及看reload之前給出的提示
// 所以要手動處理下error
function tryWrap (fn) {
return function (id, arg) {
try { fn(id, arg) } catch (e) {
console.error(e)
console.warn('Something went wrong during Vue component hot-reload. Full reload required.')
}
}
}
所以開發者需要自定義一個類似的高階函式手動處理下錯誤,防止看不到錯誤資訊而重新整理。
副作用
模組的熱更新是好事,但是老模組仍然有可能在client端留下了痕跡。試想一個元件被熱更新後,如果不處理之前的元件,那麼新老兩個元件都會在瀏覽器中出現。
所以別忘了在module.hot.accept中清除掉舊的元件。
類似的問題還有很多,事件繫結、手動插入並且沒有銷燬的dom、定時器等,記得把這些副作用一起幹掉。
如果做不到的話,老老實實重新整理你的瀏覽器吧。