Webpack 自動重新整理和HMR

patrick_kibo發表於2019-03-13

Webpack 自動重新整理和HMR
最近想了解一下webpack的功能,基於現在的4.x版本,過了一遍官方文件,內容實在太多,根本記不住...,不過發現了幾個有意思的擴充套件內容,HMR就是其中之一(另一個是PWA的SW-Service Worker),所以就花點時間整理下

1、自動重新整理

webpack.config配置引數devServer中存在inline引數,引數可以設定devServer的兩種模式inlineiframe,預設使用inline

devServer: {
    contentBase: "./dist",
    // inline: false,
    hot: true,
},
複製程式碼

官方是這樣描述inline模式的

Toggle between the dev-server's two different modes. By default the application will be served with inline mode enabled. This means that a script will be inserted in your bundle to take care of live reloading, and build messages will appear in the browser console.

大致意思是說在inline模式下,在bundle.js中會插入程式碼來處理自動重新整理,由於沒看原始碼,根據開關inline模式以後,webpack-dev-server啟動時候的控制檯輸出,和官方文件說明,所以個人猜測,大致實現:

  1. bundle.js插入socket的片段,在瀏覽器載入後建立socket連線
  2. 利用webpack -watch來監聽檔案變化,傳送socket資訊告知瀏覽器,瀏覽器接受資訊呼叫reload來實現重新整理頁面重新整理

通過分析瀏覽器請求(inline模式會在載入bundle.js後發起一個websocket請求)和bundle.js程式碼確認了這一點(onSocketMsg-> reloadApp-> applyReload),也就是說使用webpack-dev-server啟動就可以開啟自動重新整理功能了

2. HMR

HMR(Hot Module Replacement),新增、修改模組(修改JS/CSS)後瀏覽器內容通過非重新整理的方式自動更新,提高開發效率,要注意HMR只應在開發環境使用

HMR在配置成功以後,修改CSS/JS不會進行頁面重新整理(注意儲存檔案變更的時候,瀏覽器Tab頁是否是自動重新整理)

2.1 HMR配置

HMR配置有四個關鍵點:

  1. 使用webpack-dev-server作為伺服器啟動
  2. webpack.configdevServer中配置hot: true
  3. webpack.config的plugins增加HotModuleReplacementPlugin
  4. 使用module.hot.accept增加HMR程式碼

webpack.config

module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',
  entry: {
    main: __dirname + '/app/main.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  devServer: {
    contentBase: "./dist",//本地伺服器所載入的頁面所在的目錄
    // inline: true, //實時重新整理
    hot: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: __dirname + '/app/index.tmpl.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
};
複製程式碼

main.js

import './extra.js'
import './style.css'

if (module.hot) {
    module.hot.accept('./extra.js', function() {
      console.log('Accepting the updated printMe module!');
    })
    // 關閉指定子模組的HMR
    // module.hot.decline('./extra.js')
}
複製程式碼

extra.js

if(module.hot){
    // 監聽當前模組HMR異常
    // module.hot.accept(() => {
    //     console.log('error handler')
    // })
    // 關閉當前模組HMR
    // module.hot.decline()
    // 儲存模組資料
    // module.hot.dispose(disposeHander)
    // if (module.hot.data) {
    //    console.log(module.hot.data.a)
    // }
    // 移除模組資料
    // module.hot.removeDisposeHandler(disposeHander)
}
複製程式碼

配置成功以後修改extra.js或者style.css儲存,都會觸發HMR,控制檯會輸出[HMR]開頭的日誌

PS:

  1. css檔案我們會配置style-loaderstyle-loader中已經增加了module.hot.accept的支援,所以即使不配置module.hot.accept,對於css也可以HMR,但是如果JS沒有呼叫module.hot.accept,HMR執行找不到對應的內容,則會直接重新整理頁面,可以設定引數hotOnly: true來防止自動重新整理

  2. webpack plugins未新增HotModuleReplacementPlugin使用hot: true的時候會報錯 Uncaught Error: [HMR] Hot Module Replacement is disabled.

  3. 官網配置分為Node版本和Web版本,由於自己也瞭解不深,所以不探討Node版本如何使用webpack的HMR

2.2 使用HMR API

要想使用HMR,需要利用到HMR的API,在新增了HotModuleReplacementPlugin模組後,就可以使用module.hot的API

Module API

用於模組處理的API

2.2.1 accept

module.hot.accept(dependencies,callback):新增需要HMR的模組,以及觸發變更後的回撥,dependencies支援字串或者字串陣列

module.hot.accept(errorHandler):被新增的模組內部使用可以捕獲HMR異常時候丟擲的異常

2.2.2 decline

module.hot.decline(dependencies):和accept操作相反,標記一個不需要HMR管理的模組

module.hot.decline():JS模組中使用,將導致該模組無法被HMR

2.2.3 dispose

module.hot.addDisposeHandler/moduel.hot.dispose(data =>{}):即使使用HMR,每次更新後資料是不能進行保留的,如果想將資料傳遞給更新後的模組,使用dispose對全域性物件data賦值,之後可以通過module.hot.data來獲取

module.hot.removeDisposeHandler(callback):移除通過disposeaddDisposeHandler新增的回撥,移除後module.hot.data將變為空物件{}(呼叫dispose之前module.hot.dataundefined)

Management API

用於HMR狀態管理的API

2.2.4 status

module.hot.status(): HMR過程是存在變化狀態的,通過該函式來獲取到HMR的當前狀態

2.3 vue-cli3配置HMR

使用vue-cli3構建,通過vue-cli-service serve啟動的專案,其本身會新增HMR,在vue.config.jsdevServer中配置hot: false可以關閉。

但是專案中發現一個情況,當配置了https: true以後,必須指定host: localhost,HMR功能才可以開啟,否則HMR功能將失效(其實是整個自動更新功能都失效了,不會發起socket請求),而在webpack中並沒有出現該情況。

據此揣測,vue-cli-service在使用https: true的時候,socket連結必須指定使用hostname才可以進行構建,根據查詢bundle.js程式碼,確認該邏輯(可以在bundle.js查詢socket關鍵字,可以看到hostname相關檢測邏輯)。

3、HMR實現

3.1 工作原理

本小節內容都不是人話,可跳過~

一、應用程式(Applicationn):

  1. 應用程式在獲取到socket伺服器傳送的更新資訊
  2. 檢查HMR的執行狀態
  3. HMR執行環境非同步下載更新內容並通知應用程式
  4. 應用程式告訴HMR執行環境去應用更新
  5. HMR執行環境應用變更資訊

二、編譯部分(Compiler):

修改檔案以後,編譯器觸發更新操作,更新兩部分內容:manifestchunks

manifest包含了新的變化的所有模組的程式碼塊,編譯器確保module IDschunk IDs在本次構建中

三、模組(Module)

隻影響HMR中包含了的模組(module.hot.accept()),比如:style-loader,其中實現了HMR介面,所以在樣式更新的時候,新樣式替換老樣式

如果模組沒有HMR的處理,則更新操作持續冒泡,也就是模組樹中只要單個模組發生了更新,整個都會重新載入

四、執行過程(Runtime)

執行過程會持續使用checkapply來進行module.hot.staus的更新

check: 請求更新清單,請求成功後,比較更新的內容和當前的內容,將所有更新的內容進行儲存直到所有內容更下載完成,切換到ready狀態準備

apply: 將updated的模組標記為invalid,一直冒泡標記到entry point,所有無效模組處理完成後,更新所有的accepthandlers都會被觸發,然後狀態切換為idle狀態

3.2 原理解析

檢視bundle.js中的程式碼,將3.1中的內容翻譯一下:

  1. webpack-dev-server配置了hot:true的基礎上,bundle.js中的reloadApp()會進行HMR的邏輯(另一個邏輯是之前面的自動重新整理),這個時候會開始判斷HMRstatus,併發起一個AJAX請求chunk.hot-update.json,該請求會返回一個JSON物件
{
    c: {
        main: true
    },
    h: nextChunk
}
複製程式碼
  1. 請求成功以後,繼續更新HMR的status,會比較返回的nextChunk和當前的chunk是否相同,如果相同結束HMR,如果不同將nextChunk儲存並繼續進行HMR
  2. 根據chunk拼接<script>的src,並將<script>動態插入html的<head>中(比如我的配置就是:main.chunk.hot-update.js),操作後chunk陣列序號+1
  3. 由於<head>插入了新的指令碼<script src="main.chunk.hot-update.js"></script>,實現動態更新JS而不重新整理頁面

補充:對於新增了module.hot.dispose的部分,建立了全域性物件data={}來儲存資料,從而實現重新載入JS以後,仍然可以訪問該數值的內容

3. 總結

HMR的核心點有兩個:

  1. 如何在服務端檔案變化之後通知客戶端?使用socket
  2. 如何判斷檔案是否發生變化?客戶端和服務端都儲存chunk,比較是否發生變化

webpack的內容其實真的有點多,僅僅HMR包含的內容就很多,自己的總結也不夠完整,只能說是剛好入門。不過整個過程一邊自己猜想,一遍跟原始碼,然後驗證自己猜想,真的是很有意思的過程。

4. 參考

webpack官方文件:

HMR

HMR API

Dev Server

CSDN上一篇特別棒的資料:

徹底弄懂webpack-dev-server的熱更新

相關文章