Hot Module Replacement
,簡稱HMR
,無需徹底刷新整個頁面的同時,更新模塊。HMR
的好處,在平常開發工做中體會頗深:節省寶貴的開發時間、提高開發體驗。javascript
刷新咱們通常分爲兩種:html
window.location.reload()
。WDS (Webpack-dev-server)
的模塊熱替換,只須要局部刷新頁面上發生變化的模塊,同時能夠保留當前的頁面狀態,好比複選框的選中狀態、輸入框的輸入等。HMR
做爲一個Webpack
內置的功能,能夠經過HotModuleReplacementPlugin
或--hot
開啓。那麼,HMR
究竟是怎麼實現熱更新的呢?下面讓咱們來了解一下吧!java
項目啓動後,進行構建打包,控制檯會輸出構建過程,咱們能夠觀察到生成了一個 Hash值:a93fd735d02d98633356
。 node
Compiling…
字樣,觸發新的編譯中...能夠在控制檯中觀察到:
a61bdd6e82294ed06fa3
a93fd735d02d98633356.hot-update.json
index.a93fd735d02d98633356.hot-update.js
首先,咱們知道Hash
值表明每一次編譯的標識。其次,根據新生成文件名能夠發現,上次輸出的Hash
值會做爲本次編譯新生成的文件標識。依次類推,本次輸出的Hash
值會被做爲下次熱更新的標識。webpack
而後看一下,新生成的文件是什麼?每次修改代碼,緊接着觸發從新編譯,而後瀏覽器就會發出 2 次請求。請求的即是本次新生成的 2 個文件。以下: git
json
文件,返回的結果中,
h
表明本次新生成的
Hash
值,用於下次文件熱更新請求的前綴。
c
表示當前要熱更新的文件對應的是
index
模塊。
再看下生成的js
文件,那就是本次修改的代碼,從新編譯打包後的。 github
d2e4208eca62aa1c5389
a61bdd6e82294ed06fa3.hot-update.json
可是咱們發現,並無生成新的js
文件,由於沒有改動任何代碼,同時瀏覽器發出的請求,能夠看到c
值爲空,表明本次沒有須要更新的代碼。 web
小聲說下,webapck
之前的版本這種狀況hash
值是不會變的,後面可能出於什麼緣由改版了。細節不用在乎,瞭解原理纔是真諦!!!面試
最後思考下🤔,瀏覽器是如何知道本地代碼從新編譯了,並迅速請求了新生成的文件?是誰告知了瀏覽器?瀏覽器得到這些文件又是如何熱更新成功的?那讓咱們帶着疑問看下熱更新的過程,從源碼的角度看原理。ajax
相信你們都會配置webpack-dev-server
熱更新,我就不示意例子了。本身網上查下便可。接下來咱們就來看下webpack-dev-server
是如何實現熱更新的?(源碼都是精簡過的,第一行會註明代碼路徑,看完最好結合源碼食用一次)。
咱們根據webpack-dev-server
的package.json
中的bin
命令,能夠找到命令的入口文件bin/webpack-dev-server.js
。
// node_modules/webpack-dev-server/bin/webpack-dev-server.js
// 生成webpack編譯主引擎 compiler
let compiler = webpack(config);
// 啓動本地服務
let server = new Server(compiler, options, log);
server.listen(options.port, options.host, (err) => {
if (err) {throw err};
});
複製代碼
本地服務代碼:
// node_modules/webpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp();
this.createServer();
}
setupApp() {
// 依賴了express
this.app = new express();
}
createServer() {
this.listeningApp = http.createServer(this.app);
}
listen(port, hostname, fn) {
return this.listeningApp.listen(port, hostname, (err) => {
// 啓動express服務後,啓動websocket服務
this.createSocketServer();
}
}
}
複製代碼
這一小節代碼主要作了三件事:
webpack
,生成compiler
實例。compiler
上有不少方法,好比能夠啓動 webpack
全部編譯工做,以及監聽本地文件的變化。express
框架啓動本地server
,讓瀏覽器能夠請求本地的靜態資源。server
啓動以後,再去啓動websocket
服務,若是不瞭解websocket
,建議簡單瞭解一下websocket速成。經過websocket
,能夠創建本地服務和瀏覽器的雙向通訊。這樣就能夠實現當本地文件發生變化,立馬告知瀏覽器能夠熱更新代碼啦!上述代碼主要乾了三件事,可是源碼在啓動服務前又作了不少事,接下來便看看webpack-dev-server/lib/Server.js
還作了哪些事?
啓動本地服務前,調用了updateCompiler(this.compiler)
方法。這個方法中有 2 段關鍵性代碼。一個是獲取websocket
客戶端代碼路徑,另外一個是根據配置獲取webpack
熱更新代碼路徑。
// 獲取websocket客戶端代碼
const clientEntry = `${require.resolve( '../../client/' )}?${domain}${sockHost}${sockPath}${sockPort}`;
// 根據配置獲取熱更新代碼
let hotEntry;
if (options.hotOnly) {
hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server');
}
複製代碼
修改後的webpack
入口配置以下:
// 修改後的entry入口
{ entry:
{ index:
[
// 上面獲取的clientEntry
'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
// 上面獲取的hotEntry
'xxx/node_modules/webpack/hot/dev-server.js',
// 開發配置的入口
'./src/index.js'
],
},
}
複製代碼
爲何要新增了 2 個文件?在入口默默增長了 2 個文件,那就意味會一同打包到bundle
文件中去,也就是線上運行時。
(1)webpack-dev-server/client/index.js
首先這個文件用於websocket
的,由於websoket
是雙向通訊,若是不瞭解websocket
,建議簡單瞭解一下websocket速成。咱們在第 1 步 webpack-dev-server
初始化 的過程當中,啓動的是本地服務端的websocket
。那客戶端也就是咱們的瀏覽器,瀏覽器尚未和服務端通訊的代碼呢?總不能讓開發者去寫吧hhhhhh。所以咱們須要把websocket
客戶端通訊代碼偷偷塞到咱們的代碼中。客戶端具體的代碼後面會在合適的時機細講哦。
(2)webpack/hot/dev-server.js
這個文件主要是用於檢查更新邏輯的,這裏你們知道就好,代碼後面會在合適的時機(第5步)細講。
修改好入口配置後,又調用了setupHooks
方法。這個方法是用來註冊監聽事件的,監聽每次webpack
編譯完成。
// node_modules/webpack-dev-server/lib/Server.js
// 綁定監聽事件
setupHooks() {
const {done} = compiler.hooks;
// 監聽webpack的done鉤子,tapable提供的監聽方法
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
複製代碼
當監聽到一次webpack
編譯結束,就會調用_sendStats
方法經過websocket
給瀏覽器發送通知,ok
和hash
事件,這樣瀏覽器就能夠拿到最新的hash
值了,作檢查更新邏輯。
// 經過websoket給客戶端發消息
_sendStats() {
this.sockWrite(sockets, 'hash', stats.hash);
this.sockWrite(sockets, 'ok');
}
複製代碼
每次修改代碼,就會觸發編譯。說明咱們還須要監聽本地代碼的變化,主要是經過setupDevMiddleware
方法實現的。
這個方法主要執行了webpack-dev-middleware
庫。不少人分不清webpack-dev-middleware
和webpack-dev-server
的區別。其實就是由於webpack-dev-server
只負責啓動服務和前置準備工做,全部文件相關的操做都抽離到webpack-dev-middleware
庫了,主要是本地文件的編譯和輸出以及監聽,無非就是職責的劃分更清晰了。
那咱們來看下webpack-dev-middleware
源碼裏作了什麼事:
// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
if (err) { /*錯誤處理*/ }
});
// 經過「memory-fs」庫將打包後的文件寫入內存
setFs(context, compiler);
複製代碼
(1)調用了compiler.watch
方法,在第 1 步中也提到過,compiler
的強大。這個方法主要就作了 2 件事:
webpack
的一系列編譯流程。爲何代碼的改動保存會自動編譯,從新打包?這一系列的從新檢測編譯就歸功於compiler.watch
這個方法了。監聽本地文件的變化主要是經過文件的生成時間是否有變化,這裏就不細講了。
(2)執行setFs
方法,這個方法主要目的就是將編譯後的文件打包到內存。這就是爲何在開發的過程當中,你會發現dist
目錄沒有打包後的代碼,由於都在內存中。緣由就在於訪問內存中的代碼比訪問文件系統中的文件更快,並且也減小了代碼寫入文件的開銷,這一切都歸功於memory-fs
。
咱們已經能夠監聽到文件的變化了,當文件發生變化,就觸發從新編譯。同時還監聽了每次編譯結束的事件。當監聽到一次webpack
編譯結束,_sendStats
方法就經過websoket
給瀏覽器發送通知,檢查下是否須要熱更新。下面重點講的就是_sendStats
方法中的ok
和hash
事件都作了什麼。
那瀏覽器是如何接收到websocket
的消息呢?回憶下第 2 步驟增長的入口文件,也就是websocket
客戶端代碼。
'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080'
複製代碼
這個文件的代碼會被打包到bundle.js
中,運行在瀏覽器中。來看下這個文件的核心代碼吧。
// webpack-dev-server/client/index.js
var socket = require('./socket');
var onSocketMessage = {
hash: function hash(_hash) {
// 更新currentHash值
status.currentHash = _hash;
},
ok: function ok() {
sendMessage('Ok');
// 進行更新檢查等操做
reloadApp(options, status);
},
};
// 鏈接服務地址socketUrl,?http://localhost:8080,本地服務地址
socket(socketUrl, onSocketMessage);
function reloadApp() {
if (hot) {
log.info('[WDS] App hot update...');
// hotEmitter其實就是EventEmitter的實例
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
}
}
複製代碼
socket
方法創建了websocket
和服務端的鏈接,並註冊了 2 個監聽事件。
hash
事件,更新最新一次打包後的hash
值。ok
事件,進行熱更新檢查。熱更新檢查事件是調用reloadApp
方法。比較奇怪的是,這個方法又利用node.js
的EventEmitter
,發出webpackHotUpdate
消息。這是爲何?爲何不直接進行檢查更新呢?
我的理解就是爲了更好的維護代碼,以及職責劃分的更明確。websocket
僅僅用於客戶端(瀏覽器)和服務端進行通訊。而真正作事情的活仍是交回給了webpack
。
那webpack
怎麼作的呢?再來回憶下第 2 步。入口文件還有一個文件沒有講到,就是:
'xxx/node_modules/webpack/hot/dev-server.js'
複製代碼
這個文件的代碼一樣會被打包到bundle.js
中,運行在瀏覽器中。這個文件作了什麼就顯而易見了吧!先瞄一眼代碼:
// node_modules/webpack/hot/dev-server.js
var check = function check() {
module.hot.check(true)
.then(function(updatedModules) {
// 容錯,直接刷新頁面
if (!updatedModules) {
window.location.reload();
return;
}
// 熱更新結束,打印信息
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function(err) {
window.location.reload();
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
check();
});
複製代碼
這裏webpack
監聽到了webpackHotUpdate
事件,並獲取最新了最新的hash
值,而後終於進行檢查更新了。檢查更新呢調用的是module.hot.check
方法。那麼問題又來了,module.hot.check
又是哪裏冒出來了的!答案是HotModuleReplacementPlugin
搞得鬼。這裏留個疑問,繼續往下看。
前面好像一直是webpack-dev-server
作的事,那HotModuleReplacementPlugin
在熱更新過程當中又作了什麼偉大的事業呢?
首先你能夠對比下,配置熱更新和不配置時bundle.js
的區別。內存中看不到?直接執行webpack
命令就能夠看到生成的bundle.js
文件啦。不要用webpack-dev-server
啓動就行了。
(1)沒有配置的。
HotModuleReplacementPlugin
或
--hot
的。
moudle
新增了一個屬性爲
hot
,再看
hotCreateModule
方法。 這不就找到
module.hot.check
是哪裏冒出來的。
通過對比打包後的文件,__webpack_require__
中的moudle
以及代碼行數的不一樣。咱們均可以發現HotModuleReplacementPlugin
原來也是默默的塞了不少代碼到bundle.js
中呀。這和第 2 步驟非常類似哦!爲何,由於檢查更新是在瀏覽器中操做呀。這些代碼必須在運行時的環境。
你也能夠直接看瀏覽器Sources
下的代碼,會發現webpack
和plugin
偷偷加的代碼都在哦。在這裏調試也很方便。
HotModuleReplacementPlugin
如何作到的?這裏我就不講了,由於這須要你對
tapable
以及
plugin
機制有必定了解,能夠看下我寫的文章
Webpack插件機制之Tapable-源碼解析。固然你也能夠選擇跳過,只關心熱更新機制便可,畢竟信息量太大。
經過第 6 步,咱們就能夠知道moudle.hot.check
方法是如何來的啦。那都作了什麼?以後的源碼都是HotModuleReplacementPlugin
塞入到bundle.js
中的哦,我就不寫文件路徑了。
hash
值,調用hotDownloadManifest
發送xxx/hash.hot-update.json
的ajax
請求;Hash
標識,並進入熱更新準備階段。hotAvailableFilesMap = update.c; // 須要更新的文件
hotUpdateNewHash = update.h; // 更新下次熱更新hash值
hotSetStatus("prepare"); // 進入熱更新準備狀態
複製代碼
hotDownloadUpdateChunk
發送xxx/hash.hot-update.js
請求,經過JSONP
方式。function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
if (null) script.crossOrigin = null;
document.head.appendChild(script);
}
複製代碼
這個函數體爲何要單獨拿出來,由於這裏要解釋下爲何使用JSONP
獲取最新代碼?主要是由於JSONP
獲取的代碼能夠直接執行。爲何要直接執行?咱們來回憶下/hash.hot-update.js
的代碼格式是怎麼樣的。
能夠發現,新編譯後的代碼是在一個webpackHotUpdate
函數體內部的。也就是要當即執行webpackHotUpdate
這個方法。
再看下webpackHotUpdate
這個方法。
window["webpackHotUpdate"] = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
} ;
複製代碼
hotAddUpdateChunk
方法會把更新的模塊moreModules
賦值給全局全量hotUpdate
。hotUpdateDownloaded
方法會調用hotApply
進行代碼的替換。function hotAddUpdateChunk(chunkId, moreModules) {
// 更新的模塊moreModules賦值給全局全量hotUpdate
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId];
}
}
// 調用hotApply進行模塊的替換
hotUpdateDownloaded();
}
複製代碼
熱更新的核心邏輯就在hotApply
方法了。 hotApply
代碼有將近 400 行,仍是挑重點講了,看哭😭
經過hotUpdate
能夠找到舊模塊
var queue = outdatedModules.slice();
while (queue.length > 0) {
moduleId = queue.pop();
// 從緩存中刪除過時的模塊
module = installedModules[moduleId];
// 刪除過時的依賴
delete outdatedDependencies[moduleId];
// 存儲了被刪掉的模塊id,便於更新代碼
outdatedSelfAcceptedModules.push({
module: moduleId
});
}
複製代碼
appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
複製代碼
for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i];
moduleId = item.module;
try {
// 執行最新的代碼
__webpack_require__(moduleId);
} catch (err) {
// ...容錯處理
}
}
複製代碼
hotApply
的確比較複雜,知道大概流程就行了,這一小節,要求你對webpack打包後的文件如何執行的有一些瞭解,你們能夠自去看下。
仍是以閱讀源碼的形式畫的圖,①-④的小標記,是文件發生變化的一個流程。
本次是以閱讀源碼的方式講解原理,是由於以爲熱更新這塊涉及的知識量比較多。因此知識把關鍵性代碼拿出來,由於每個塊細節提及來都能寫一篇文章了,你們能夠本身對着源碼再理解下。
仍是建議提早了解如下知識會更好理解熱更新:
bundle
文件如何運行的。webpack
啓動流程,webpack
生命週期。參考的文章你們也能夠看下,可是因爲源碼版本不一樣,因此不要太糾結與細節。
原創不易,走過路過點個贊吧~😊