Hot Module Replacement(簡稱 HMR)前端
包含如下內容:webpack
webpack-dev-middleware 調用 webpack 的 api 對文件系統 watch,當文件發生改變後,webpack 從新對文件進行編譯打包,而後保存到內存中。git
webpack 將 bundle.js 文件打包到了內存中,不生成文件的緣由就在於訪問內存中的代碼比訪問文件系統中的文件更快,並且也減小了代碼寫入文件的開銷。github
這一切都歸功於memory-fs,memory-fs 是 webpack-dev-middleware 的一個依賴庫,webpack-dev-middleware 將 webpack 本來的 outputFileSystem 替換成了MemoryFileSystem 實例,這樣代碼就將輸出到內存中。web
webpack-dev-middleware 中該部分源碼以下:json
// compiler
// webpack-dev-middleware/lib/Shared.js
var isMemoryFs = !compiler.compilers &&
compiler.outputFileSystem instanceof MemoryFileSystem;
if(isMemoryFs) {
fs = compiler.outputFileSystem;
} else {
fs = compiler.outputFileSystem = new MemoryFileSystem();
}
複製代碼
在啓動 devServer 的時候,sockjs 在服務端和瀏覽器端創建了一個 webSocket 長鏈接,以便將 webpack 編譯和打包的各個階段狀態告知瀏覽器,最關鍵的步驟仍是 webpack-dev-server 調用 webpack api 監聽 compile的 done 事件,當compile 完成後,webpack-dev-server經過 _sendStatus 方法將編譯打包後的新模塊 hash 值發送到瀏覽器端。api
// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
// stats.hash 是最新打包文件的 hash 值
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});
...
Server.prototype._sendStats = function (sockets, stats, force) {
if (!force && stats &&
(!stats.errors || stats.errors.length === 0) && stats.assets &&
stats.assets.every(asset => !asset.emitted)
) { return this.sockWrite(sockets, 'still-ok'); }
// 調用 sockWrite 方法將 hash 值經過 websocket 發送到瀏覽器端
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 修改了webpack 配置中的 entry 屬性,在裏面添加了 webpack-dev-client 的代碼,這樣在最後的 bundle.js 文件中就會接收 websocket 消息的代碼了。瀏覽器
webpack-dev-server/client 當接收到 type 爲 hash 消息後會將 hash 值暫存起來,當接收到 type 爲 ok 的消息後對應用執行 reload 操做。bash
在 reload 操做中,webpack-dev-server/client 會根據 hot 配置決定是刷新瀏覽器仍是對代碼進行熱更新(HMR)。代碼以下:websocket
// webpack-dev-server/client/index.js
hash: function msgHash(hash) {
currentHash = hash;
},
ok: function msgOk() {
// ...
reloadApp();
},
// ...
function reloadApp() {
// ...
if (hot) {
log.info('[WDS] App hot update...');
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
// ...
} else {
log.info('[WDS] App updated. Reloading...');
self.location.reload();
}
}
複製代碼
首先 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)中的兩個方法 hotDownloadManifest 和 hotDownloadUpdateChunk。
hotDownloadManifest 是調用 AJAX 向服務端請求是否有更新的文件,若是有將發更新的文件列表返回瀏覽器端。該方法返回的是最新的 hash 值。
hotDownloadUpdateChunk 是經過 jsonp 請求最新的模塊代碼,而後將代碼返回給 HMR runtime,HMR runtime 會根據返回的新模塊代碼作進一步處理,多是刷新頁面,也多是對模塊進行熱更新。該 方法返回的就是最新 hash 值對應的代碼塊。
最後將新的代碼塊返回給 HMR runtime,進行模塊熱更新。
附:爲何更新模塊的代碼不直接在第三步經過 websocket 發送到瀏覽器端,而是經過 jsonp 來獲取呢?
個人理解是,功能塊的解耦,各個模塊各司其職,dev-server/client 只負責消息的傳遞而不負責新模塊的獲取,而這些工做應該有 HMR runtime 來完成,HMR runtime 才應該是獲取新代碼的地方。再就是由於不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也能夠完成模塊熱更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它沒有使用 websocket,而是使用的 EventSource。綜上所述,HMR 的工做流中,不該該把新模塊代碼放在 websocket 消息中。
這一步是整個模塊熱更新(HMR)的關鍵步驟,並且模塊熱更新都是發生在HMR runtime 中的 hotApply 方法中
// webpack/lib/HotModuleReplacement.runtime
function hotApply() {
// ...
var idx;
var queue = outdatedModules.slice();
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
// ...
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
// remove "parents" references from all children
for(j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(!child) continue;
idx = child.parents.indexOf(moduleId);
if(idx >= 0) {
child.parents.splice(idx, 1);
}
}
}
// ...
// insert new code
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
// ...
}
複製代碼
模塊熱更新的錯誤處理,若是在熱更新過程當中出現錯誤,熱更新將回退到刷新瀏覽器,這部分代碼在 dev-server 代碼中,簡要代碼以下:
module.hot.check(true).then(function(updatedModules) {
if(!updatedModules) {
return window.location.reload();
}
// ...
}).catch(function(err) {
var status = module.hot.status();
if(["abort", "fail"].indexOf(status) >= 0) {
window.location.reload();
}
});
複製代碼
當用新的模塊代碼替換老的模塊後,可是咱們的業務代碼並不能知道代碼已經發生變化,也就是說,當 hello.js 文件修改後,咱們須要在 index.js 文件中調用 HMR 的 accept 方法,添加模塊更新後的處理函數,及時將 hello 方法的返回值插入到頁面中。代碼以下
// index.js
if(module.hot) {
module.hot.accept('./hello.js', function() {
div.innerHTML = hello()
})
}
複製代碼
參考:餓了麼前端