概述
在使用 Webpack 構建開發期時,Webpack 提供熱更新功能為開發帶來良好的體驗和開發效率,那熱更新機制是怎么實現的呢?
代碼實現
- Webpack 配置添加 HotModuleReplacementPlugin 插件
new webpack.HotModuleReplacementPlugin({
// Options...
})
- Node Server 引入
webpack-dev-middlerware
和webpack-hot-middleware
插件,如果是 koa 引入對應的koa-webpack-dev-middlerware
和koa-webpack-hot-middleware
const devMiddleware = require('koa-webpack-dev-middleware')(compiler, {
publicPath,
stats: {
colors: true,
children: true,
modules: false,
chunks: false,
chunkModules: false,
},
watchOptions: {
ignored: /node_modules/,
}
});
app.use(devMiddleware);
const hotMiddleware = require('koa-webpack-hot-middleware')(compiler, {
log: false,
reload: true
});
app.use(hotMiddleware);
- entry 注入熱更新代碼
webpack-hot-middleware/client?path=http://127.0.0.1:9000/__webpack_hmr&noInfo=false&reload=true&quiet=false
這里注入熱更新代碼是關鍵,保證服務端和客戶端能夠通信。
熱更新一探
首先我們啟動 egg-vue-webpack-boilerplate 應用,通過 chrome-dev-tool 看看首次打開頁面前端和后端通信是如何建立的?

有一個webpack_hmr的請求:http://127.0.0.1:9000/webpack_hmr.
從這張圖里面看到幾個信息
- 這里看到內容類型為 eventStream,具體是啥請看之后介紹的 EventSource。
- 返回類型為message, 內容關鍵的有個 action: sync 和 hash:73c528ba5b06e7e9ab26, 這個幾個信息在后面會用到。 這里的 hash 為 Webpack 初始化的一個hash,在 vendor.js 文件里面可以看到, 每次頁面首次加載時,都會重新生成一個。(var hotCurrentHash = “73c528ba5b06e7e9ab26”)
- 有一個空的message信息,通過觀察發現和后面查看代碼發現,這個是為了保證后端與客戶端通信保持連接,后端隔一段時間會向客戶端發送一段信息。
然后修改 about.vue 文件保存后,發現控制臺 Webpack 馬上重新編譯了,UI 無刷新更新了。
1.這時候會發現 Webpack 編譯結果多了兩個update的文件, 而且文件名包含上面的 hash 信息。
4.73c528ba5b06e7e9ab26.hot-update.js 2.93 kB 4 [emitted] about/about
73c528ba5b06e7e9ab26.hot-update.json 43 bytes [emitted]
2.同時,chrome-dev-tool 請求面板下多了兩個請求,其中 hot-update.json 為 ajax請求, hot-update.js 為 GET 請求, 也就是插入 script 鏈接到文檔中的script 請求。

3.頁面內容插入了 4.73c528ba5b06e7e9ab26.hot-update.js
script文件

4.我們來初步看一下兩個文件的內容:
- 4.73c528ba5b06e7e9ab26.hot-update.js
{"h":"540f0a679c8bcbf12848","c":{"4":true}}
- 73c528ba5b06e7e9ab26.hot-update.json
webpackHotUpdate(4,{
(function(module, __webpack_exports__, __webpack_require__) {
// ...... 此處為 about.vue 組件代碼邏輯
/* hot reload */
if (true) {(function () {
var hotAPI = __webpack_require__(1)
hotAPI.install(__webpack_require__(0), false)
if (!hotAPI.compatible) return
module.hot.accept()
if (!module.hot.data) {
hotAPI.createRecord("data-v-80abbab2", Component.options)
} else {
hotAPI.reload("data-v-80abbab2", Component.options)
' + ' }
module.hot.dispose(function (data) {
disposed = true
})
})()}
})
})
5.進行多次熱更新效果

從上面截圖可以看到,每次服務端發送的消息(EventStrean) 的 hash 將作為下次 hot-update.json 和 hot-update.js 文件的 hash。
結合上面的分析,接下來從實現到代碼層面分析一下整個流程。
熱更新實現分析
EventSource 服務端與客戶端通信
首先通過查看代碼 webpack-hot-middleware/client
發現通信是用 window.EventSource
實現,那 EventSource 是什么東西呢?
EventSource 是 HTML5 中 Server-sent Events 規范的一種技術實現。EventSource 接口用于接收服務器發送的事件。它通過HTTP連接到一個服務器,以text/event-stream 格式接收事件, 不關閉連接。通過 EventSource 服務端可以主動給客戶端發現消息,使用的是 HTTP協議,單項通信,只能服務器向瀏覽器發送; 與 WebSocket 相比輕量,使用簡單,支持斷線重連。更多信息參考MDN
Node 端通信實現
Node 通過中間件 webpack-hot-middleware/middleware.js
創建 createEventStream 流
首先看一下中間件核心代碼,主要是向客戶端發送消息
- compile 發送 編譯中 消息給客戶端
- build 發送 編譯完成 消息給客戶端
- sync 文件修復熱更新或者報錯會發送該消息
// 初始化 EventStream 發送消息通道
var eventStream = {
handler: function(req, res) {
req.socket.setKeepAlive(true);
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
});
res.write('\n');
var id = clientId++;
clients[id] = res;
req.on("close", function(){
delete clients[id];
});
},
publish: function(payload) {
everyClient(function(client) {
client.write("data: " + JSON.stringify(payload) + "\n\n");
});
}
}
// 根據 Webpack 編譯狀態 主動發送消息給客戶端
function webpackHotMiddleware(compiler, opts) {
compiler.plugin("compile", function() {
latestStats = null;
if (opts.log) opts.log("webpack building...");
eventStream.publish({action: "building"});
});
compiler.plugin("done", function(statsResult) {
// Keep hold of latest stats so they can be propagated to new clients
latestStats = statsResult;
// 當首次編譯完成 和 修改代碼重新編譯(熱更新)完成時發送
publishStats("built", latestStats, eventStream, opts.log);
});
var middleware = function(req, res, next) {
if (!pathMatch(req.url, opts.path)) return next();
// 見下面的 handler 實現,中間件通過 `req.socket.setKeepAlive` 開啟長鏈接通道,
eventStream.handler(req, res);
if (latestStats) {
// 服務端向客戶端寫入數據,sync 表示告訴客戶端熱更新已經準備好
eventStream.publish({
name: stats.name,
action: "sync",
time: stats.time,
hash: stats.hash,
warnings: stats.warnings || [],
errors: stats.errors || [],
modules: buildModuleMap(stats.modules)
});
}
};
return middleware;
客戶端通信實現
服務端通過 EventSource 發送消息給客戶端了,我們來看看客戶端的通信實現。打開 webpack-hot-middleware/client.js 的代碼實現:
var source = new window.EventSource('(http://127.0.0.1:9000/__webpack_hmr)');
source.onopen = handleOnline; // 建立鏈接
source.onerror = handleDisconnect;
source.onmessage = handleMessage; // 接收服務端消息,然后進行相應處理
Node端會主動發送消息給客戶端, 客戶端 EventSource 關鍵代碼處理消息代碼如下:
function processMessage(obj) {
switch(obj.action) {
case "building":
if (options.log) {
console.log(
"[HMR] bundle " + (obj.name ? "'" + obj.name + "' " : "") +
"rebuilding"
);
}
break;
case "built": // 這里沒有break,所以 編譯完成會執行 build 和 sync 邏輯
if (options.log) {
console.log(
"[HMR] bundle " + (obj.name ? "'" + obj.name + "' " : "") +
"rebuilt in " + obj.time + "ms"
);
}
// fall through
case "sync":
processUpdate(obj.hash, obj.modules, options);
break;
default:
if (customHandler) {
customHandler(obj);
}
}
}
上面 building
, built
, sync
三種消息于服務端發送的消息對應, 這樣就完成了服務端和客戶端通信。
因 build 的 action 時, build case 沒有 break,所以當修改文件時,編譯完成發送 build 消息時,會依次執行 build 和 sync 邏輯, 也就是進入 processUpdate 流程。processUpdate 接收到信息( hash, module) 之后, 進入 module.hot.check 和 module.hot.apply 流程。
客戶端熱更新
首先我們再來看看 module.hot
初始化實現邏輯
module.hot 初始化
webpack_require 函數定義時,通過 hotCreateModule 為每個 module 初始化 hot 邏輯
function __webpack_require__(moduleId) {
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
hot: hotCreateModule(moduleId), // 前端通過 ajax 獲取熱更新文件內容
parents:xxxx,
children: []
};
return module.exports;
}
hotCreateModule 實現
function hotCreateModule(moduleId) {
var hot = {
accept: function(dep, callback) {
},
check: hotCheck,
apply: hotApply,
status: function(l) {},
.....
}
return hot;
}
hotCheck:前端通過 ajax 獲取熱更新文件內容
從 熱更新一探:[進行多次熱更新效果] 上面截圖可以看到,每次服務端發送的消息(EventStrean) 的 hash 將作為下次 hot-update.json 和 hot-update.js 文件的 hash。也就是下面客戶端更新當前
hotCurrentHash 值,作為下次的 hot-update.json 和 hot-update.js 更新請求。
function hotCheck(){
return new Promise(function(resolve, reject) {
var __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json";
var request = new XMLHttpRequest();
request.open("GET", requestPath, true);
request.timeout = requestTimeout;
request.send(null);
request.onreadystatechange = function() {
if(request.readyState === 4 && request.status === 200){
reject(new Error("Manifest request to " + requestPath + " failed."));
}else{
resolve(JSON.parse(request.responseText));
}
});
}).then(function(update) {
// {h: "dcc99b114b8c64461a2e", c: {5: true}}
// 新的hotUpdateHash
hotUpdateNewHash = update.h;
// 向文檔插入 hot-update.js script
hotEnsureUpdateChunk();
});
},
hotEnsureUpdateChunk 實現
hotEnsureUpdateChunk 函數的邏輯是向 HTML 文檔插入 hot-update.js script 腳本。 hotEnsureUpdateChunk 調用 hotDownloadUpdateChunk 函數
function hotDownloadUpdateChunk(chunkId) {
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.type = "text/javascript";
script.charset = "utf-8";
script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
head.appendChild(script);
}
開啟更新機制
- 開啟熱更新構建后, 每個 Vue 組件構建的代碼都有下面這么一段 hotAPI 代碼:
/* hot reload */
Component.options.__file = "app/web/page/about/about.vue"
if (true) {(function () {
var hotAPI = __webpack_require__(3)
hotAPI.install(__webpack_require__(0), false)
if (!hotAPI.compatible) return
module.hot.accept()
if (!module.hot.data) {
hotAPI.createRecord("data-v-aafed0d8", Component.options)
} else {
hotAPI.reload("data-v-aafed0d8", Component.options)
' + ' }
module.hot.dispose(function (data) {
disposed = true
})
})()}
- createRecord 和 reload 觸發 UI 更新
獲取 Vue 組件的 render,重新 render 組件, 繼而實現 UI 無刷新更新。
function makeOptionsHot(id, options) {
if (options.functional) {
// 獲取組件的render 方法,重新 render
var render = options.render
options.render = function (h, ctx) {
var instances = map[id].instances
if (instances.indexOf(ctx.parent) < 0) {
instances.push(ctx.parent)
}
return render(h, ctx)
}
} else {
injectHook(options, 'beforeCreate', function() {
var record = map[id]
if (!record.Ctor) {
record.Ctor = this.constructor
}
record.instances.push(this)
})
injectHook(options, 'beforeDestroy', function() {
var instances = map[id].instances
instances.splice(instances.indexOf(this), 1)
})
}
}
熱更新流程總結
- Webpack編譯期,為需要熱更新的 entry 注入熱更新代碼(EventSource通信)
- 頁面首次打開后,服務端與客戶端通過 EventSource 建立通信渠道,把下一次的 hash 返回前端
- 客戶端獲取到hash,這個hash將作為下一次請求服務端 hot-update.js 和 hot-update.json的hash
- 修改頁面代碼后,Webpack 監聽到文件修改后,開始編譯,編譯完成后,發送 build 消息給客戶端
- 客戶端獲取到hash,成功后客戶端構造hot-update.js script鏈接,然后插入主文檔
- hot-update.js 插入成功后,執行hotAPI 的 createRecord 和 reload方法,獲取到 Vue 組件的 render方法,重新 render 組件, 繼而實現 UI 無刷新更新。