使用webpack-dev-server
實現(xiàn)的Hot Moudle Replacement(HMR)讓我們在開發(fā)時修改代碼并保存后其掂,不必手動刷新瀏覽器,而是讓瀏覽器通過新的模塊替換老的模塊顾患。這樣可以讓我們在保證當前頁面狀態(tài)的前提下,讓新的代碼生效个唧,就如同在Chrome的控制臺修改CSS樣式一樣描验。
使用
安裝webpack-dev-server
npm install webpack-dev-server --save-dev
在webpack.config.js
中進行配置
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
host: 'localhost',
compress: true,
port: 8080
}
其中:
-
contentBase
:服務(wù)器基本運行路徑 -
host
:服務(wù)器運行地址 -
compress
:服務(wù)器壓縮式,一般為true
-
port
:服務(wù)運行端口
在package.json
中定義相關(guān)命令:
"scripts": {
"dev": "webpack-dev-server --hot --open",
},
然后執(zhí)行npm run dev
就可以開啟webpack的服務(wù)坑鱼,并且實現(xiàn)模塊熱重載,并且自動打開瀏覽器絮缅。
增加--open
屬性可以自動打開瀏覽器鲁沥。
原理解析
原來只是在各種Cli工具中使用了模塊熱重載,知道是利用了Webpack的HMR特性耕魄,但是它是怎么實現(xiàn)的卻不了解画恰。今天在清理收藏夾攢的知識時看到了餓了么前端專欄的這篇文章Webpack HMR 原理解析,寫的非常好吸奴,簡單易懂允扇,把道理也說的很明白缠局。
上圖展示了從修改代碼到模塊熱更新完成的一個周期:
第一步:Webpack在watch
模式下打包更改的文件到內(nèi)存中(對應(yīng)圖中的①②③)
Webpack-dev-middleware調(diào)用Webpack的API對文件系統(tǒng)watch,監(jiān)聽到文件變化時考润,根據(jù)配置文件對模塊重新編譯打包狭园,將打包后的代碼以JavaScript對象的形式保存在內(nèi)存中。
// webpack-dev-middleware/lib/Shared.js
if (!options.lazy) {
var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);
context.watching = watching;
}
Webpack會將打包的文件保存在內(nèi)存中糊治,而不是打包到output.path
目錄下唱矛,是因為訪問內(nèi)存中的代碼比訪問文件系統(tǒng)中的代碼更快,也減少了寫入文件的開銷井辜。這個過程利用了memory-fs這個庫绎谦,它提供了一個簡單的基于內(nèi)存的文件系統(tǒng),所有數(shù)據(jù)都保存在JavaScript對象中粥脚。
圖中的第③步也是對文件變化的監(jiān)控窃肠,只不過這一步監(jiān)聽的不是代碼,而是在配置文件制定的靜態(tài)文件目錄下的靜態(tài)文件的變化(當配置文件中配置了devServer.watchContentBase
為true
的時候)刷允,當靜態(tài)文件發(fā)生變化時通知瀏覽器對應(yīng)用進行刷新(注意是瀏覽器刷新冤留,而非HRM)
第二步:webpack-dev-Server通知瀏覽器端文件發(fā)生變化(對應(yīng)④)
瀏覽器端和服務(wù)端之間是通過Websocket長連接進行通信的,利用的是sockjs建立的恃锉。通過Websocket長連接搀菩,webpack-dev-Server將編譯打包的各個階段狀態(tài)告知瀏覽器(包括第③步中監(jiān)聽的靜態(tài)文件的變化)。
同時webpack-dev-Server調(diào)用Webpack的API監(jiān)聽complie的done
事件破托,在編譯完成后肪跋,webpack-dev-Server通過_sendStatus
方法將編譯打包后的新模塊的hash值發(fā)送給瀏覽器,后面的步驟都會利用這個hash值來進行模塊熱替換土砂。
// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
// stats.hash 是最新打包文件的 hash 值州既,發(fā)送給瀏覽器
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');
}
// 調(diào)用 sockWrite 方法將 hash 值通過 websocket 發(fā)送到瀏覽器端
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接收到服務(wù)端消息做出響應(yīng)(對應(yīng)⑤?)
webpack-dev-server/client端并不能夠請求更新的代碼,也不會執(zhí)行熱更模塊操作萝映,而是在接收到通過長連接收到的服務(wù)端的消息后吴叶,對信息進行處理,而具體的更新操作又交回給了Webpack序臂。
webpack/hot/dev-server的工作就是根據(jù)webpack-dev-server/client傳給它的信息以及dev-server的配置決定是刷新瀏覽器呢還是進行模塊熱更新蚌卤。當然如果僅僅是刷新瀏覽器,也就沒有后面那些步驟了奥秆。
我們并沒有在業(yè)務(wù)代碼里添加Websocket客戶端的代碼逊彭,也沒有在webpack.config.js
中的entry
屬性中添加新的入口文件,那么bundle.js
中的接受Websocket信息的代碼是從哪來的呢构订?答案是webpack-dev-server會自動修改Webpack配置中的entry
屬性侮叮,在里面添加了webpck-dev-client的代碼。
具體來看悼瘾,webpack-dev-server/client接收到type
為hash
的消息后會將hash
保存起來囊榜,接收到type
為ok
的消息后會執(zhí)行relooad
操作审胸,在reload
操作中會根據(jù)hot
的配置是刷新瀏覽器還是執(zhí)行熱更新(HMR):
// 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-dev-server/client首先將接收到的hash
值存儲到currentHash
變量中卸勺,當接收到ok
消息后調(diào)用reloadApp
方法砂沛,在其內(nèi)部根據(jù)hot
配置,決定是調(diào)用webpack/hot/emitter
將最新的hash
值發(fā)送給Webpack執(zhí)行熱更新孔庭,還是直接調(diào)用location.reload
刷新頁面尺上。
第四步:Webpack接收新的hash
值并請求模塊代碼(對應(yīng)⑥⑦⑧⑨)
首先webpack/hot/dev-server監(jiān)聽上一步webpack-dev-server/client發(fā)送的webpackHotUpdate
消息,然后調(diào)用webpack/lib/HotModuleReplacement.runtime(簡稱HMR runtime)圆到,HMR runtime是客戶端HMR的中樞怎抛,它首先通過JsonpMainTemplate.runtime調(diào)用hotDownloadManifest
方法向server端發(fā)送JSONP請求,檢查是否有更新的文件芽淡,如果有的話服務(wù)端返回一個JSON響應(yīng)马绝,包含了所有要更新的模塊的hash值。
獲取到更新列表后挣菲,該模塊通過hotDownloadUpdateChunk
再次發(fā)送JSONP請求富稻,獲取到最新的模塊代碼,并返回給HMR runtime白胀。
上面為了獲取最新的Hash值和最新的代碼椭赋,HMR runtime向服務(wù)端發(fā)送了兩次Ajax請求,為什么不在第三步的Websocket長連接中發(fā)送給瀏覽器呢或杠?可能的原因:
(1)包括了功能模塊的解耦哪怔,webpack-dev-server/client只負責消息的傳遞而不負責新模塊的拉取,HRM runtime來負責獲取新代碼
(2)可以使用webpack-hot-middleware來代替webpack-dev-server實現(xiàn)HMR向抢,webpack-hot-middleware沒有使用Websocket认境,而是使用EventSource來實現(xiàn)客戶端與服務(wù)端通信。
第五步:HMR runtime對模塊進行熱更新(對應(yīng)⑩)
HMR runtime會對新舊模塊進行對比挟鸠,決定是否更新模塊叉信,在決定更新模塊后,檢查模塊之間的依賴關(guān)系艘希,更新模塊的同時更新模塊間的依賴引用硼身。
這一切都發(fā)生在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];
}
}
// ...
}
hotApply
方法主要分為了三個階段:
- 找出陳舊的模塊
outdatedModules
和依賴outdatedDependencies
- 從緩存中刪除過期的模塊和依賴
- 將新的模塊和依賴添加到
moudles
中,當下次調(diào)用_webpack_require
方法時就獲取到新的代碼
如果HMR失敗后覆享,回退到live reload
操作鸠姨,也就是進行瀏覽器刷新來獲取最新打包代碼,相關(guān)的代碼在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();
}
});
第六步:業(yè)務(wù)代碼改造
當新的模塊代替老的模塊后淹真,舊的業(yè)務(wù)代碼并不能知道代碼發(fā)生變化,所以需要在業(yè)務(wù)代碼的入口調(diào)用HMR的accept
方法连茧,添加模塊更新后的處理函數(shù):
// index.js
if (module.hot) {
module.hot.accept('./hello.js', function() {
// 更新后的處理函數(shù)
})
}