Webpack 熱更新機制

想必做爲前端大佬的你,工做中應該用過 webpack,而且對熱更新的特性也有了解。若是沒有,固然也不要緊。javascript

下面我要講的,是我對 Webpack 熱更新機制的一些認識和理解,不足之處,歡迎指正。html

首先:前端

熱更新是啥?

熱更新,是指 Hot Module Replacement,縮寫爲 HMRvue

從名字上解讀,就是把「熱」的模塊進行替換。熱,是指這個模塊已經在運行中。java

不知道你有沒有聽過或看過這樣一段話:「在高速公路上將汽車引擎換成波音747飛機引擎」。webpack

雖然有點牽強,可是放在這裏,從某些角度上來講,也還算合適吧。git

再扯遠一點,說下我目前工做中的遇到的狀況,相信不少人也遇到過。github

微信小程序的開發工具,沒有提供相似 Webpack 熱更新的機制,因此在本地開發時,每次修改了代碼,預覽頁面都會刷新,因而以前的路由跳轉狀態、表單中填入的數據,都沒了。web

哪怕只是一個文案或屬性配置的修改,都會致使刷新,而要從新進入特定頁面和狀態,有時候很麻煩。對於開發時須要頻繁修改代碼的狀況,這樣比較浪費時間。小程序

而若是有相似 Webpack 熱更新的機制存在,則是修改了代碼,不會致使刷新,而是保留現有的數據狀態,只將模塊進行更新替換。也就是說,既保留了現有的數據狀態,又能看到代碼修改後的變化。

很美好,可是想一想就以爲是一件確定不簡單的事情。

因此,熱更新是啥呢?

引用官方文檔,熱更新是:

使得應用在運行狀態下,不重載刷新就能更新、增長、移除模塊的機制

熱更新解決的問題

那麼熱更新要解決的問題,在上面也解釋了。用個人話來闡述,就是 在應用程序的開發環境,方便開發人員在不刷新頁面的狀況下,就能修改代碼,而且直觀地在頁面上看到變化的機制

簡單來講,就是爲了 提高開發效率

聯想到我在微信小程序上的開發體驗,真心以爲若是有熱更新機制的話,開發效率要高不少。

若是你知道微信小程序已經或計劃支持熱更新,或者有大佬已經作了相似的工做,歡迎告訴我,感謝!

進一步介紹前,咱們來看下 Webpack 熱更新如何配置。

熱更新配置

若是你以前作的項目是其餘人搭建配置了 Webpack 和熱更新,那麼這裏能夠了解下熱更新是怎麼配置的。

個人示例採用 Webpack 4,想直接看代碼的話,在這裏:

https://github.com/luobotang/...

除了 Webpack,還須要 webpack-dev-server(或 webpack-dev-middleware)。

爲 Webpack 開發環境開啓熱更新,要作兩件事:

  • 使用 HotModuleReplacementPlugin 插件
  • 打開 webpack-dev-server 的熱更新開關

HotModuleReplacementPlugin 插件是 Webpack 自帶的,在 webpack.config.js 加入就好:

// webpack.config.js
module.exports = {
  // ...
  plugins: [
    webpack.HotModuleReplacementPlugin(),
   // ...
  ]
}

若是直接經過 webpack-dev-server 啓動 Webpack 的開發環境,那麼能夠這樣打開 webpack-dev-server 的熱更新開關:

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    hot: true,
    // ...
  }
}

也很簡單。

熱更新示例

下面經過例子來進一步解釋熱更新機制。若是你以前對 Webpack 熱更新的體驗,是 Vue 經過 vue-loader 提供給你的,也就是說你在本身的代碼中從沒有寫過或者見到過相似:

if (module.hot) {
  module.hot.accept(/* ... */)
  // ...
}

這樣的代碼,那麼下面的例子就恰好適合看一看了。

這些例子就在上面的 webpack-hmr-demo,若是你對代碼更親切,那直接去看吧,首頁文檔裏有簡單的說明。

示例1:沒有熱更新的狀況

這個例子只是把示例頁面的功能簡單介紹下,而且讓你體會下每次修改代碼都要從新刷新頁面的痛苦。

頁面上只有一個元素,用來展現數值:

<div id="root" class="number"></div>

入口模塊(index.js)引用了兩個模塊:

  • timer.js:只提供了一個 start 接口,傳入回調函數,而後 timer 會間隔一段時間調用回調函數,並傳入一個每次增長的數值
  • foo.js:沒啥功能,就簡單暴露一個 message,引入它單純是區別 timer.js 展現不一樣的模塊更新處理方法

入口模塊的功能很簡單,調用 timer.start(),再傳入的回調函數中,每次將獲得的數值更新到頁面上顯示:

import { start } from './timer'
import { message } from './foo'

var current = 0
var root = document.getElementById('root')
start(onUpdate, current)

console.log(message)

function onUpdate(i) {
  current = i
  root.textContent = '#' + i
}

將這個項目運行起來,打開的頁面中就是在一直刷新展現增長的數值而已,相似這樣:

hmr-demo-1

一旦修改任何模塊的代碼,例如改變 timer 中定時器的間隔時間(如從1秒改爲3秒),或者 onUpdate 中展現的內容(如 '#' + i 改爲 '*' + i),頁面都會刷新,已經有的狀態清除,從新從0開始計數。

示例2:處理依賴模塊的熱更新

接下來的例子,展現在 index.js 如何處理其餘模塊的更新。

依賴的模塊發生更新,要麼是接受變動(頁面不用刷新,模塊替換下就好),要麼不接受(必須得刷新)。

Webpack 將熱更新相關接口以 module.hot 暴露到模塊中,在使用前,最好判斷下當前的環境是否支持熱更新,也就是上面看到的這樣的代碼:

if (module.hot) {
  // ...
}

延續上一個例子,選擇接受並處理 timer 的更新,但對於 foo 模塊,不接受:

if (module.hot) {
  module.hot.accept('timer', () => {
    // ...
  })
  module.hot.decline('./foo')
}

因此,在熱更新的機制中,實際上是以這種「聲明」的方式告知 Webpack,哪些模塊的更新是被處理的,哪些模塊的更新又不被處理。固然對於要處理的模塊的更新,自行在 module.hot.accept() 的第二個參數即回調函數中進行處理,會在聲明的模塊被替換後執行。

下面來看對 timer 模塊更新的處理。

timer 模塊的 start 函數調用後返回一個能夠終止定時器的 stop 函數,藉助它咱們實現對舊的 timer 模塊的清理,並基於當前狀態從新調用新的 timer 模塊的 start 函數:

var stop = start(onUpdate, current) // 先記錄下返回的 stop 函數

// ...

if (module.hot) {
  module.hot.accept('timer', () => {
    stop()
    stop = start(onUpdate, current)
  })
  // ...
}

處理邏輯如上所述,先經過以前記錄的 stop 中止舊模塊的定時器,而後調用新模塊的 start 繼續計數,而且傳入當前數值從而沒必要從0開始從新計數。

看起來仍是比較簡單的吧。運行起來的效果是,若是修改 timer 中的定時器間隔時間,當即在頁面上就能看到效果,並且頁面並不會刷新致使從新從0開始計數:

hmr-demo-2

在運行幾秒後,修改 timer 模塊中定時器的間隔時間爲 100ms

修改 foo 中的 message,頁面仍是會刷新。

有幾點額外說明下:

  • timer 模塊若是修改後不返回 start 接口,那麼上述處理機制顯然會失效,因此這裏的處理是基於模塊的接口不變的狀況下
  • timer 模塊的 start 調用後顯然必須返回一個 stop 函數,不然在 index.js 是無法清除 timer 模塊內開啓的定時器的,這也很重要
  • 或許你也注意到了,就是對 timer 模塊的 start 函數的引用貌似一直沒有變過,那爲何在回調函數中的 start 就是新模塊了呢?這個實際上是有 Webpack 在編譯時處理掉的,編譯後的代碼並不是當前的樣式,對 start 會進行替換,使得回調中的 start 必定引用到的是新的 timer 模塊的 start。感興趣能夠看下 Webpack 文檔中對此的相關描述。

此外,除了聲明其餘模塊更新的處理,模塊也能夠聲明自身更新的處理,也是一樣的接口,不傳參數便可:

  • module.hot.accept() 告訴 Webpack,當前模塊更新不用刷新
  • module.hot.decline() 告訴 Webpack,當前模塊更新時必定要刷新

並且,依賴同一個模塊的不一樣模塊,能夠有各自不一樣的聲明,這些聲明多是衝突的,好比有的容許依賴模塊更新,有的不容許,Webpack 怎麼協調這些呢?

Webpack 的實現機制有點相似 DOM 事件的冒泡機制,更新事件先由模塊自身處理,若是模塊自身沒有任何聲明,纔會向上冒泡,檢查使用方是否有對該模塊更新的聲明,以此類推。若是最終入口模塊也沒有任何聲明,那麼就刷新頁面了。這也就是爲何在上一個例子中,雖然開啓了熱更新,可是模塊修改後仍舊刷新頁面的緣由,由於沒有任何模塊對更新進行處理。

示例3:處理自身模塊的熱更新

自身模塊的更新處理與依賴模塊相似,也是要經過 module.hot 的接口向 Webpack 聲明。不過模塊自身的更新,可能須要在模塊被 Webpack 替換以前就作一些處理,更新後的處理則沒必要經過特別接口來作,直接寫到新模塊代碼裏面就好。

module.hot.dispose() 用於註冊當前模塊被替換前的處理函數,而且回調函數接收一個 data 對象,能夠向其寫入須要保存的數據,這樣在新的模塊執行時能夠經過 module.hot.data 獲取到:

var current = 0
if (module.hot && module.hot.data) {
  current = module.hot.data.current
}

首先,模塊執行時,先檢查有沒有舊模塊留下來的數據,若是有,就恢復。

而後在模塊被替換前的執行處理,這裏就是記錄數據、停掉現有的定時器:

if (module.hot)
  module.hot.accept()
  module.hot.dispose(data => {
    data.current = current
    stop()
  })
}

作了這些處理以後,修改 index.js 的 onUpdate,使得渲染到頁面的數值改變,也能夠在不刷新的狀況下體現:

hmr-demo-3

在運行幾秒後,修改 onUpdate() 中的 '#' + i'*' + i

總結

看過上面的例子,咱們來總結下。

Webpack 的熱更新,其實只是提供一套接口和基礎的模塊替換的實現。做爲開發者,須要在代碼中經過熱更新接口(module.hot.xxx)向 Webpack 聲明依賴模塊和當前模塊是否可以更新,以及更新的先後進行的處理。

若是接受更新,那麼須要開發者本身來在模塊被替換前清理或保留必要的數據、狀態,並在模塊被替換後恢復以前的數據、狀態。

固然,像咱們在使用 Vue 或 React 進行開發時,vue-loder 等插件已經幫咱們作了這些事情,而且對於 *.vue 文件在更新時要若是進行處理,不少細節也只有 vue-loader 內部比較清楚,咱們就放心使用好了。

可是對於 Webpack 熱更新是怎麼一回事,若是可以有深刻了解固然更好,我就遇到過同事在 Vue 組件中自行對 DOM 進行處理(爲了封裝一個直接操做 DOM 的組件),結果因爲熱更新的存在,致使一些狀態的清除有問題的狀況。

這種狀況,只有開發者本身才能處理,vue-loader 可無法處理這樣的特殊狀況。至少知道如何使用 Webpack 的熱更新接口,這種狀況下開發者就能自行處理了。

本文對於 Webpack 熱更新機制的介紹還只是在接口使用的層面,或者大致的機制上,沒有深刻說明熱更新的實現原理和細節。時間、篇幅有限,那就先放一張圖出來,或許有時間再細說一下。

Webpack 熱更新流程

上圖來源:

Webpack & The Hot Module Replacement
https://medium.com/@rajaraodv/webpack-hot-module-replacement-hmr-e756a726a07

這篇英文文章對 Webpack 熱更新實現原理方面有深刻介紹。

相關文章
相關標籤/搜索