Hot Module Replacement(如下簡稱 HMR)是 webpack 發展至今引入的最使人興奮的特性之一 ,當你對代碼進行修改並保存後,webpack 將對代碼從新打包,並將新的模塊發送到瀏覽器端,瀏覽器經過新的模塊替換老的模塊,這樣在不刷新瀏覽器的前提下就可以對應用進行更新。react
基本實現原理大體這樣的,構建 bundle 的時候,加入一段 HMR runtime 的 js 和一段和服務溝通的 js 。文件修改會觸發 webpack 從新構建,服務器經過向瀏覽器發送更新消息,瀏覽器經過 jsonp 拉取更新的模塊文件,jsonp 回調觸發模塊熱替換邏輯。webpack
使用webpack-dev-server,設置 hot 屬性爲 true. 寫模塊時,按照如下寫法:web
if (module.hot) { //判斷是否有熱加載
module.hot.accept('./hmrTest.js', function() { //熱加載的模塊路徑
console.log('Accepting the updated printMe module!'); //熱加載的回調,即發生了模塊更新時,執行什麼 callback
printMe();
})
}
複製代碼
缺點:更新邏輯得本身寫。好比要使頁面顯示的內容生效,須要在回調中寫入document.append(xxx)json
react 的熱加載,使用 react-hot-loaderapi
import { hot } from 'react-hot-loader';
const Record = ()=>{
...
}
export default hot(module)(Record);
複製代碼
或promise
if (module.hot) {
module.hot.accept('./App', function () {
var NextApp = require('./App')
ReactDOM.render(<NextApp />, rootEl)
})
}
複製代碼
webpack-dev-server 裏引用了 webpack-dev-middleware,相關的 watch 邏輯就是在裏面實現的。瀏覽器
//webpack-dev-server/lib/Server.js
setupDevMiddleware() {
// middleware for serving webpack bundle
this.middleware = webpackDevMiddleware(
this.compiler,
Object.assign({}, this.options, { logLevel: this.log.options.level })
);
}
// webpack-dev-middleware/index.js
if (!options.lazy) {
context.watching = compiler.watch(options.watchOptions, (err) => {
if (err) {
context.log.error(err.stack || err);
if (err.details) {
context.log.error(err.details);
}
}
});
} else {
context.state = true;
}
複製代碼
以上代碼能夠看出,webpack-dev-middleware 是經過調用 webpack 的 api 對文件系統 watch 的。watchOptions 若是沒有配置的話,會取默認值。值的含義見:webpack.js.org/configurati…緩存
當文件發生變化時,從新編譯輸出 bundle.js。devServer 下,是沒有文件會輸出到 output.path 目錄下的,這時 webpack 是把文件輸出到了內存中。webpack 中使用的操做內存的庫是 memory-fs,它是 NodeJS 原生 fs 模塊內存版(in-memory)的完整功能實現,會將你請求的url映射到對應的內存區域當中,所以讀寫都比較快。服務器
// webpack-dev-middleware/lib/fs.js
const isMemoryFs =
!isConfiguredFs &&
!compiler.compilers &&
compiler.outputFileSystem instanceof MemoryFileSystem;
...
compiler.outputFileSystem = fs;
fileSystem = fs;
} else if (isMemoryFs) {
fileSystem = compiler.outputFileSystem;
} else {
fileSystem = new MemoryFileSystem();
compiler.outputFileSystem = fileSystem;
}
複製代碼
devServer 通知瀏覽器端文件發生改變,在啓動 devServer 的時候,sockjs 在服務端和瀏覽器端創建了一個 webSocket 長鏈接,以便將 webpack 編譯和打包的各個階段狀態告知瀏覽器,最關鍵的步驟仍是 webpack-dev-server 調用 webpack api 監聽 compile的 done 事件,當compile 完成後,webpack-dev-server經過 _sendStatus 方法將編譯打包後的新模塊 hash 值發送到瀏覽器端。websocket
// webpack-dev-server/lib/Server.js
const addHooks = (compiler) => {
...
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
...
// send stats to a socket or multiple sockets
_sendStats(sockets, stats, force) {
...
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-src 中,webpack-dev-server 修改了webpack 配置中的 entry 屬性,在裏面添加了 webpack-dev-client 的代碼,這樣在最後的 bundle.js 文件中就會有接收 websocket 消息的代碼了。
//webpack-dev-server/lib/utils/addEntries.js
/** @type {string} */
const clientEntry = `${require.resolve(
'../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;
/** @type {(string[] | string)} */
let hotEntry;
if (options.hotOnly) {
hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server');
}
...
[].concat(config).forEach((config) => {
...
const additionalEntries = checkInject(
options.injectClient,
config,
webTarget
)
? [clientEntry]
: [];
if (hotEntry && checkInject(options.injectHot, config, true)) {
additionalEntries.push(hotEntry);
}
config.entry = prependEntry(config.entry || './src', additionalEntries);
if (options.hot || options.hotOnly) {
config.plugins = config.plugins || [];
if (
!config.plugins.find(
(plugin) => plugin.constructor.name === 'HotModuleReplacementPlugin'
)
) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
}
});
複製代碼
以上代碼能夠看出,若是選擇了熱加載,輸出的 bundle.js 會包含接收 websocket 消息的代碼。並且 plugin 也會注入一個 HotModuleReplacementPlugin,構建過程當中熱加載相關的邏輯都在這個插件中。這個插件主要處理兩部分邏輯:
先看一張圖,看看 websocket 中的消息長什麼樣子:
能夠看到,接收的消息只有 type 和 hash 兩個內容。在 client 裏面的邏輯,他們分別對應不一樣的處理邏輯:
// webpack-dev-server/client-src/default/index.js
hash(hash) {
status.currentHash = hash;
},
...
ok() {
sendMessage('Ok');
if (options.useWarningOverlay || options.useErrorOverlay) {
overlay.clear();
}
if (options.initial) {
return (options.initial = false);
} // eslint-disable-line no-return-assign
reloadApp(options, status);
}
複製代碼
能夠看出,當接收到 type 爲 hash 消息後會將 hash 值暫存起來,當接收到 type 爲 ok 的消息後對應用執行 reload 操做,而 hash 消息是在 ok 消息以前的。再看看 reload 裏面的處理邏輯:
// webpack-dev-server/client-src/default/reloadApp.js
if (hot) {
...
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
if (typeof self !== 'undefined' && self.window) {
self.postMessage(`webpackHotUpdate${currentHash}`, '*');
}
}
else if (liveReload) {
...
}
複製代碼
能夠看出,若是配置了模塊熱更新,就調用 webpack/hot/emitter 將最新 hash 值發送給 webpack,而後將控制權交給 webpack 客戶端代碼。若是沒有配置模塊熱更新,就進行 liveReload 的邏輯。webpack/hot/dev-server 中會監聽 webpack-dev-server/client-src 發送的 webpackHotUpdate 消息,而後調用 webpack/lib/HotModuleReplacement.runtime 中的 check 方法,檢測是否有新的更新:
// webpack/hot/dev-server.js
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();
}
});
// webpack/lib/HotModuleReplacement.runtime
function hotCheck(apply) {
...
return hotDownloadManifest(hotRequestTimeout).then(function(update) {
...
/*globals chunkId */
hotEnsureUpdateChunk(chunkId);
...
return promise;
});
}
function hotEnsureUpdateChunk(chunkId) {
if (!hotAvailableFilesMap[chunkId]) {
hotWaitingFilesMap[chunkId] = true;
} else {
hotRequestedFilesMap[chunkId] = true;
hotWaitingFiles++;
hotDownloadUpdateChunk(chunkId);
}
}
複製代碼
以上代碼能夠看出,在 check 過程當中,主要調用了兩個方法 hotDownloadManifest 和 hotDownloadUpdateChunk。hotDownloadManifest 是經過 Ajax 向服務器請求十分有更新的文件,若是有就返回對應的文件信息,hotDownloadUpdateChunk 是經過Jsonp的方式,請求最新的代碼模塊。以下圖所示:
補充,這兩個文件的名稱是能夠配置的,若是沒有配置,則取定義在 WebpackOptionsDefaulter 中的默認配置。
this.set("output.hotUpdateChunkFilename", "[id].[hash].hot-update.js");
this.set("output.hotUpdateMainFilename", "[hash].hot-update.json");
複製代碼
綜上,咱們得到了更新的內容。接下來就能夠進行更新了。這部分的邏輯在 webpack/lib/HotModuleReplacement.runtime 中。
首先,更新過的模塊,如今都屬於 outdated 的模塊,因此先找出過時的模塊及其依賴:
//webpack/lib/HotModuleReplacement.runtime
function getAffectedStuff(updateModuleId) {
var outdatedModules = [updateModuleId];
var outdatedDependencies = {};
...
return {
type: "accepted",
moduleId: updateModuleId,
outdatedModules: outdatedModules,
outdatedDependencies: outdatedDependencies
};
複製代碼
}
根據調用的 Api 信息,對結果進行標註及處理。
switch (result.type) {
case "self-declined":
...
break;
case "declined":
...
break;
case "unaccepted":
...
break;
case "accepted":
if (options.onAccepted) options.onAccepted(result);
doApply = true;
break;
case "disposed":
if (options.onDisposed) options.onDisposed(result);
doDispose = true;
break;
default:
throw new Error("Unexception type " + result.type);
}
複製代碼
從緩存中刪除過時的模塊和依賴
// 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++) {
...
}
// remove outdated dependency from module children
var dependency;
var moduleOutdatedDependencies;
for (moduleId in outdatedDependencies) {
...
}
複製代碼
將新的模塊添加到 modules 中,當下次調用 webpack_require (webpack 重寫的 require 方法)方法的時候,就是獲取到了新的模塊代碼了。
// insert new code
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
複製代碼
最後就是錯誤的兼容了,若是熱加載失敗,將會刷新瀏覽器。
這裏只是對 HMR 的一個大概流程梳理,貼出的都是源碼。源碼比較龐大,細節處得本身慢慢理解才能吃透。