構(gòu)建工具03 Webpack模塊熱重載(HMR)

使用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 原理解析,寫的非常好吸奴,簡單易懂允扇,把道理也說的很明白缠局。

image

上圖展示了從修改代碼到模塊熱更新完成的一個周期:

第一步: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.watchContentBasetrue的時候)刷允,當靜態(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接收到typehash的消息后會將hash保存起來囊榜,接收到typeok的消息后會執(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方法主要分為了三個階段:

  1. 找出陳舊的模塊outdatedModules和依賴outdatedDependencies
  2. 從緩存中刪除過期的模塊和依賴
  3. 將新的模塊和依賴添加到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ù)
  })
}

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末核蘸,一起剝皮案震驚了整個濱河市巍糯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌客扎,老刑警劉巖祟峦,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異徙鱼,居然都是意外死亡宅楞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門袱吆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厌衙,“玉大人,你說我怎么就攤上這事绞绒∩粝#” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵蓬衡,是天一觀的道長喻杈。 經(jīng)常有香客問我,道長狰晚,這世上最難降的妖魔是什么筒饰? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮壁晒,結(jié)果婚禮上瓷们,老公的妹妹穿的比我還像新娘。我一直安慰自己讨衣,他們只是感情好换棚,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著反镇,像睡著了一般固蚤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歹茶,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天夕玩,我揣著相機與錄音,去河邊找鬼惊豺。 笑死燎孟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的尸昧。 我是一名探鬼主播揩页,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烹俗!你這毒婦竟也來了爆侣?” 一聲冷哼從身側(cè)響起萍程,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兔仰,沒想到半個月后茫负,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡乎赴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年忍法,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榕吼。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡饿序,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出友题,到底是詐尸還是另有隱情嗤堰,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布度宦,位于F島的核電站踢匣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏戈抄。R本人自食惡果不足惜离唬,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望划鸽。 院中可真熱鬧输莺,春花似錦、人聲如沸裸诽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丈冬。三九已至嘱函,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間埂蕊,已是汗流浹背往弓。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蓄氧,地道東北人函似。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像喉童,于是被迫代替她去往敵國和親撇寞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 原文首發(fā)于:Webpack 3露氮,從入門到放棄 Update (2017.8.27) : 關(guān)于 output.pub...
    昵稱都被用完了衰閱讀 1,897評論 4 19
  • Hot Module Replacement(簡稱 HMR) 包含以下內(nèi)容: 熱更新圖 熱更新步驟講解 第一步:w...
    zhongmeizhi閱讀 9,063評論 1 5
  • 在現(xiàn)在的前端開發(fā)中,前后端分離钟沛、模塊化開發(fā)、版本控制局扶、文件合并與壓縮恨统、mock數(shù)據(jù)等等一些原本后端的思想開始...
    Charlot閱讀 5,439評論 1 32
  • 1.早上送孩子上學回來8點多,困得眼鏡睜不開三妈,就睡了畜埋,一覺醒來11點半。太不可思議了畴蒲,我怎么這么瞌睡悠鞍。 感受是:睡...
    Sunflower語閱讀 159評論 0 0
  • 第一次看電影遲到30分鐘,但接下來的90分鐘模燥,依然讓自己感動。 《綠皮書》改編自真人真事,講述了意裔美國人保鏢托尼...
    lovexuxu_閱讀 1,163評論 0 0