構建專欄系列目錄入口javascript
朱海華: 微醫前端技術部平臺支撐組 我本地是好的,你再試試~🤔css
在使用Webpack Dev Server
之後 可讓咱們在開發工程中 專一於 Coding, 由於它能夠監聽代碼的變化 從而實現打包更新,而且最後經過自動刷新的方式同步到瀏覽器,便於咱們及時查看效果。 可是 Dev Server 從監聽到打包再到通知瀏覽器總體刷新頁面
就會致使一個讓人困擾的問題 那就是 沒法保存應用狀態
所以 針對這個問題,Webpack 提供了一個新的解決方案 Hot Module Replacement
前端
Hot Module Replacement 是指當咱們對代碼修改並保存後,Webpack 將會對代碼進行從新打包,並將新的模塊發送到瀏覽器端,瀏覽器用新的模塊替換掉舊的模塊,以實如今不刷新瀏覽器的前提下更新頁面。最明顯的優點就是相對於傳統的live reload
而言,HMR 並不會丟失應用的狀態,提升開發效率。在開始深刻了解 Webpack HMR 以前 咱們能夠先簡單過一下下面這張流程圖java
開啓 HMR 其實也極其容易 由於 HMR 自己就已經集成在了 Webpack 裏 開啓方式有兩種webpack
--hot
參數 直接開啓 HMR// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
// ...
devServer: {
// 開啓 HMR 特性 若是不支持 MMR 則會 fallback 到 live reload
hot: true,
},
plugins: [
// ...
// HMR 依賴的插件
new webpack.HotModuleReplacementPlugin()
]
}
複製代碼
經過翻閱 webpack-dev-server源碼 在這一過程當中,依賴於sockjs提供的服務端與瀏覽器端之間的橋樑,在 devServer 啓動的同時,創建了一個 webSocket 長連接,用於通知瀏覽器在 webpack 編譯和打包下的各個狀態,同時監聽 compile 下的 done 事件,當 compile 完成之後,經過 sendStats 方法, 將從新編譯打包好的新模塊 hash 值發送給瀏覽器。git
// webpack-dev-server/blob/master/lib/Server.js
sendStats(sockets, stats, force) {
const shouldEmit =
!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
(!stats.warnings || stats.warnings.length === 0) &&
stats.assets &&
stats.assets.every((asset) => !asset.emitted);
if (shouldEmit) {
this.sockWrite(sockets, 'still-ok');
return;
}
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) {
this.sockWrite(sockets, 'errors', stats.errors);
} else if (stats.warnings.length > 0) {
this.sockWrite(sockets, 'warnings', stats.warnings);
} else {
this.sockWrite(sockets, 'ok');
}
}
複製代碼
webpack-dev-server/client 當接收到 type 爲 hash 消息後會將 hash 值暫時緩存起來,同時當接收到到 type 爲 ok 的時候,對瀏覽器執行 reload 操做。 github
function reloadApp( { hotReload, hot, liveReload }, { isUnloading, currentHash } ) {
if (isUnloading || !hotReload) {
return;
}
if (hot) {
log.info('App hot update...');
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
if (typeof self !== 'undefined' && self.window) {
// broadcast update to window
self.postMessage(`webpackHotUpdate${currentHash}`, '*');
}
}
// allow refreshing the page only if liveReload isn't disabled
else if (liveReload) {
let rootWindow = self;
// use parent window for reload (in case we're in an iframe with no valid src)
const intervalId = self.setInterval(() => {
if (rootWindow.location.protocol !== 'about:') {
// reload immediately if protocol is valid
applyReload(rootWindow, intervalId);
} else {
rootWindow = rootWindow.parent;
if (rootWindow.parent === rootWindow) {
// if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways
applyReload(rootWindow, intervalId);
}
}
});
}
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
log.info('App updated. Reloading...');
rootWindow.location.reload();
}
複製代碼
經過翻閱 webpack-dev-server/client源碼,咱們能夠看到,首先會根據 hot 配置決定是採用哪一種更新策略,刷新瀏覽器或者代碼進行熱更新(HMR),若是配置了 HMR,就調用 webpack/hot/emitter
將最新 hash 值發送給 webpack,若是沒有配置模塊熱更新,就直接調用 applyReload
下的location.reload
方法刷新頁面。 web
在這一步,實際上是 webpack 中三個模塊(三個文件,後面英文名對應文件路徑)之間配合的結果,首先是 webpack/hot/dev-server(如下簡稱 dev-server) 監聽第三步 webpack-dev-server/client 發送的 webpackHotUpdate 消息,調用 webpack/lib/HotModuleReplacement.runtime(簡稱 HMR runtime)中的 check 方法,檢測是否有新的更新,在 check 過程當中會利用 webpack/lib/JsonpMainTemplate.runtime(簡稱 jsonp runtime)中的兩個方法 hotDownloadUpdateChunk 和 hotDownloadManifest , 第二個方法是調用 AJAX 向服務端請求是否有更新的文件,若是有將發更新的文件列表返回瀏覽器端,而第一個方法是經過 jsonp 請求最新的模塊代碼,而後將代碼返回給 HMR runtime,HMR runtime 會根據返回的新模塊代碼作進一步處理,多是刷新頁面,也多是對模塊進行熱更新。 npm
在這個過程當中,實際上是 webpack 三個模塊配合執行以後獲取的結果json
webpack/hot/dev-server
監聽 client 發送的webpackHotUpdate
消息// ....
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function (currentHash) {
lastHash = currentHash;
if (!upToDate() && module.hot.status() === "idle") {
log("info", "[HMR] Checking for updates on the server...");
check();
}
});
log("info", "[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
複製代碼
[HMR runtime/check()](https://github.com/webpack/webpack/blob/v4.41.5/lib/HotModuleReplacement.runtime.js)
檢測是否有新的更新,check 過程當中會利用webpack/lib/web/JsonpMainTemplate.runtime.js中的hotDownloadUpdateChunk
(經過 jsonp 請求新的模塊代碼而且返回給 HMR Runtime)以及hotDownloadManifest
(發送 AJAx 請求向 Server 請求是否有更新的文件,若是有則會將新的文件返回給瀏覽器)獲取更新文件列表 獲取模塊更新之後的最新代碼
這裏就是整個 HMR 最關鍵的步驟了,而其中 最關鍵的 無非就是hotApply這個方法了,因爲代碼量實在太多,這裏咱們直接進入過程解析(關鍵代碼),有興趣的同窗能夠閱讀一下源碼。
outdatedModules
和 outdatedDependencies
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
複製代碼
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
複製代碼
至此 一整個模塊替換的流程已經結束了,已經能夠獲取到最新的模塊代碼了,接下來就輪到業務代碼如何知曉模塊已經發生了變化~
因爲咱們編寫的 JavaScript 代碼是沒有任何規律可言的模塊,能夠導出的是一個模塊、函數、甚至於只是一個字符串 而對於這些毫無規律可言的模塊來講 Webpack 是沒法提供一個通用的模塊替換方案去處理的 所以在這種狀況下,還想要體驗完整的 HMR 開發流程 是須要咱們本身手動處理 當 JS 模塊更新之後,如何將更新之後的 JS 模塊替換至頁面當中 所以 HotModuleReplacementPlugin 爲咱們提供了一系列關於 HMR 的 API 而其中 最關鍵的部分則是hot.accept
。
接下來 咱們將嘗試 本身手動處理 JS 模塊更新 並通知到瀏覽器實現對應的局部刷新
:::info 當前主流開發框架 Vue、React 都提供了統一的模塊替換函數, 所以 Vue、React 項目並不須要針對 HMR 作手動的代碼處理,同時 css 文件也由 style-loader 統一處理 所以也不須要額外的處理,所以接下去的代碼處理邏輯,所有創建在純原生開發的基礎之上實現 :::
回到代碼中來 假設當前 main.js 文件以下
// ./src/main.js
import createChild from './child'
const child = createChild()
document.body.appendChild(child)
複製代碼
main.js 是 Webpack 打包的入口文件 在文件中引入了 Child 模塊 所以 當 Child 模塊裏的業務代碼更改之後 webpack 必然會從新打包,而且從新使用這些更新之後的模塊,因此,咱們須要在 main.js 裏實現去處理它所依賴的這些模塊更新後的熱替換邏輯
在 HMR 已開啓的狀況下,咱們能夠經過訪問全局的module
對象下的hot 成員
它提供了一個accept 方法
,這個方法用來註冊當某個模塊更新之後須要如何處理,它接受兩個參數 一個是須要監聽模塊的 path(相對路徑),第二個參數就是當模塊更新之後如何處理 其實也就是一個回調函數
// main.js
// 監聽 child 模塊變化
module.hot.accept('./child', () => {
console.log('老闆好,child 模塊更新啦~')
})
複製代碼
當作完這些之後,從新運行 npm run serve 同時修改 child 模塊 你會發現,控制檯會輸出以上的 console 內容,同時,瀏覽器也不會自動更新了,所以,咱們能夠得出一個結論 當你手動處理了某個模塊的更新之後,是不會出發自動刷新機制的,接下來 就來一塊兒看看 其中的原理 以及 如何實現 HMR 中的 JS 模塊替換邏輯
爲何咱們只有調用了moudule.hot.accept
才能夠實現熱更新, 翻看源碼 其實能夠發現實現以下
// 部分源碼
accept: function (dep, callback, errorHandler) {
if (dep === undefined) hot._selfAccepted = true;
else if (typeof dep === "function") hot._selfAccepted = dep;
else if (typeof dep === "object" && dep !== null) {
for (var i = 0; i < dep.length; i++) {
hot._acceptedDependencies[dep[i]] = callback || function () {};
hot._acceptedErrorHandlers[dep[i]] = errorHandler;
}
} else {
hot._acceptedDependencies[dep] = callback || function () {};
hot._acceptedErrorHandlers[dep] = errorHandler;
}
},
複製代碼
// module.hot.accept 其實等價於 module.hot._acceptedDependencies('./child) = render
// 業務邏輯實現
module.hot.accept('./child', () => {
console.log('老闆好,child 模塊更新啦~')
})
複製代碼
accept 往hot._acceptedDependencies
這個對象裏存入局部更新的 callback, 當模塊改變時,對模塊須要作的變動,蒐集到_acceptedDependencies
中,同時當被監聽的模塊內容發生了改變之後,父模塊能夠經過_acceptedDependencies
知道哪些內容發生了變化。
當了解了 accpet 方法之後,其實咱們要考慮的事情就很是簡單了,也就是如何實現 cb 裏的業務邏輯,其實當 accept 方法執行了之後,在其回調裏是能夠獲取到最新的被修改了之後的模塊的函數內容的
// ./src/main.js
import createChild from './child'
console.log(createChild) // 未更新前的函數內容
module.hot.accept('./child', ()=> {
console.log(createChild) // 此時已經能夠獲取更新之後的函數內容
})
複製代碼
既然是能夠獲取到最新的函數內容 其實也就很簡單了 咱們只須要移除以前的 dom 節點 並替換爲最新的 dom 節點便可,同時咱們也須要記錄節點裏的內容狀態,當節點替換爲最新的節點之後,追加更新本來的內容狀態
// ./src/main.js
import createChild from './child'
const child = createChild()
document.body.appendChild(child)
// 這裏須要額外注意的是,child 變量每一次都會被移除,因此其實咱們一個記錄一下每次被修改前的 child
let lastChild = child
module.hot.accept('./child', ()=> {
// 記錄狀態
const value = lastChild.innerHTML
// 刪除節點
document.body.remove(child)
// 建立最新節點
lastChild = createChild()
// 恢復狀態
lastChild.innerHTMl = value
// 追加內容
document.body.appendChild(lastChild)
})
複製代碼
到這裏爲止,對於如何手動實現一個 child 模塊的熱更新替換邏輯已經所有實現完畢了,有興趣的同窗能夠本身也手動實現一下~
:::tips tips: 手動處理 HMR 邏輯過程當中 若是 HMR 過程當中出現報錯 致使的 HRM 失效,其實只須要在配置文件中將hot: true 修改成 hotOnly: true
便可 :::
但願經過這篇文章,可以幫助到你們加深對 HMR 的理解,同時解決一下開發場景會遇到的問題(例如 脫離框架本身實現模塊熱更新),最後,歡迎你們一鍵三連~🎉🎉🎉