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

使用webpack-dev-server 實現的Hot Moudle Replacement(HMR)讓我們在開發時修改代碼并保存后,不必手動刷新瀏覽器,而是讓瀏覽器通過新的模塊替換老的模塊。這樣可以讓我們在保證當前頁面狀態的前提下,讓新的代碼生效,就如同在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:服務器基本運行路徑
  • host:服務器運行地址
  • compress:服務器壓縮式,一般為true
  • port:服務運行端口

package.json中定義相關命令:

"scripts": {
  "dev": "webpack-dev-server --hot --open",
},

然后執行npm run dev就可以開啟webpack的服務,并且實現模塊熱重載,并且自動打開瀏覽器。

增加--open屬性可以自動打開瀏覽器。

原理解析

原來只是在各種Cli工具中使用了模塊熱重載,知道是利用了Webpack的HMR特性,但是它是怎么實現的卻不了解。今天在清理收藏夾攢的知識時看到了餓了么前端專欄的這篇文章Webpack HMR 原理解析,寫的非常好,簡單易懂,把道理也說的很明白。

image

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

第一步:Webpack在watch模式下打包更改的文件到內存中(對應圖中的①②③)

Webpack-dev-middleware調用Webpack的API對文件系統watch,監聽到文件變化時,根據配置文件對模塊重新編譯打包,將打包后的代碼以JavaScript對象的形式保存在內存中。

// webpack-dev-middleware/lib/Shared.js
if (!options.lazy) {
  var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);
  context.watching = watching;
}

Webpack會將打包的文件保存在內存中,而不是打包到output.path目錄下,是因為訪問內存中的代碼比訪問文件系統中的代碼更快,也減少了寫入文件的開銷。這個過程利用了memory-fs這個庫,它提供了一個簡單的基于內存的文件系統,所有數據都保存在JavaScript對象中。

圖中的第③步也是對文件變化的監控,只不過這一步監聽的不是代碼,而是在配置文件制定的靜態文件目錄下的靜態文件的變化(當配置文件中配置了devServer.watchContentBasetrue的時候),當靜態文件發生變化時通知瀏覽器對應用進行刷新(注意是瀏覽器刷新,而非HRM)

第二步:webpack-dev-Server通知瀏覽器端文件發生變化(對應④)

瀏覽器端和服務端之間是通過Websocket長連接進行通信的,利用的是sockjs建立的。通過Websocket長連接,webpack-dev-Server將編譯打包的各個階段狀態告知瀏覽器(包括第③步中監聽的靜態文件的變化)。

同時webpack-dev-Server調用Webpack的API監聽complie的done事件,在編譯完成后,webpack-dev-Server通過_sendStatus方法將編譯打包后的新模塊的hash值發送給瀏覽器,后面的步驟都會利用這個hash值來進行模塊熱替換。

// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
  // stats.hash 是最新打包文件的 hash 值,發送給瀏覽器
  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'); 
  }
  // 調用 sockWrite 方法將 hash 值通過 websocket 發送到瀏覽器端
  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接收到服務端消息做出響應(對應⑤?)

webpack-dev-server/client端并不能夠請求更新的代碼,也不會執行熱更模塊操作,而是在接收到通過長連接收到的服務端的消息后,對信息進行處理,而具體的更新操作又交回給了Webpack。

webpack/hot/dev-server的工作就是根據webpack-dev-server/client傳給它的信息以及dev-server的配置決定是刷新瀏覽器呢還是進行模塊熱更新。當然如果僅僅是刷新瀏覽器,也就沒有后面那些步驟了。

我們并沒有在業務代碼里添加Websocket客戶端的代碼,也沒有在webpack.config.js中的entry屬性中添加新的入口文件,那么bundle.js中的接受Websocket信息的代碼是從哪來的呢?答案是webpack-dev-server會自動修改Webpack配置中的entry屬性,在里面添加了webpck-dev-client的代碼。

具體來看,webpack-dev-server/client接收到typehash的消息后會將hash保存起來,接收到typeok的消息后會執行relooad操作,在reload操作中會根據hot的配置是刷新瀏覽器還是執行熱更新(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消息后調用reloadApp方法,在其內部根據hot配置,決定是調用webpack/hot/emitter將最新的hash值發送給Webpack執行熱更新,還是直接調用location.reload刷新頁面。

第四步:Webpack接收新的hash值并請求模塊代碼(對應⑥⑦⑧⑨)

首先webpack/hot/dev-server監聽上一步webpack-dev-server/client發送的webpackHotUpdate消息,然后調用webpack/lib/HotModuleReplacement.runtime(簡稱HMR runtime),HMR runtime是客戶端HMR的中樞,它首先通過JsonpMainTemplate.runtime調用hotDownloadManifest方法向server端發送JSONP請求,檢查是否有更新的文件,如果有的話服務端返回一個JSON響應,包含了所有要更新的模塊的hash值。

獲取到更新列表后,該模塊通過hotDownloadUpdateChunk再次發送JSONP請求,獲取到最新的模塊代碼,并返回給HMR runtime。

上面為了獲取最新的Hash值和最新的代碼,HMR runtime向服務端發送了兩次Ajax請求,為什么不在第三步的Websocket長連接中發送給瀏覽器呢?可能的原因:

(1)包括了功能模塊的解耦,webpack-dev-server/client只負責消息的傳遞而不負責新模塊的拉取,HRM runtime來負責獲取新代碼

(2)可以使用webpack-hot-middleware來代替webpack-dev-server實現HMR,webpack-hot-middleware沒有使用Websocket,而是使用EventSource來實現客戶端與服務端通信。

第五步:HMR runtime對模塊進行熱更新(對應⑩)

HMR runtime會對新舊模塊進行對比,決定是否更新模塊,在決定更新模塊后,檢查模塊之間的依賴關系,更新模塊的同時更新模塊間的依賴引用。

這一切都發生在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中,當下次調用_webpack_require方法時就獲取到新的代碼

如果HMR失敗后,回退到live reload操作,也就是進行瀏覽器刷新來獲取最新打包代碼,相關的代碼在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();
  }
});

第六步:業務代碼改造

當新的模塊代替老的模塊后,舊的業務代碼并不能知道代碼發生變化,所以需要在業務代碼的入口調用HMR的accept方法,添加模塊更新后的處理函數:

// index.js
if (module.hot) {
  module.hot.accept('./hello.js', function() {
    // 更新后的處理函數
  })
}

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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