教你怎麼使用 webpack3 的 HMR 模塊熱加載

前注:

文檔全文請查看 根目錄的文檔說明css

若是能夠,請給本項目加【Star】和【Fork】持續關注。html

有疑義請點擊這裏,發【Issues】。vue

點擊這裏查看DEMOwebpack

七、模塊熱加載 HMR

7.0、使用說明

安裝:git

npm install
複製代碼

運行(注意,是 dev):github

npm run dev
複製代碼

結論放前面,適合場景:web

  1. 當使用 style-loader 時,修改 css 樣式;
  2. 當使用 vue-loader 之類帶 HMR 功能的 loader 時,修改對應的模塊;
  3. 當僅僅只是須要修改代碼後,頁面能夠自動刷新,以保證當前頁面上是最新的代碼;

7.一、現象和原理

當談到 HMR 的時候,首先要明確一個概念,HMR 究竟是什麼?npm

若是用過帶 HMR 功能的腳手架,例如我分享的這個 Vue的腳手架,大約能給出一個答案:json

  1. 修改代碼後,不須要刷新頁面,修改後的代碼效果,就能馬上體如今頁面上;
  2. 已有的效果,好比輸入框裏輸入了一些內容,代碼更新後,每每內容還在;
  3. 彷佛想不到其餘的了;

從現象來看,在必定程度上,這個描述問題不大,但不嚴謹。數組

咱們須要分析 HMR 究竟是什麼?

  1. webpack 是模塊化的,每一個 js, css 文件,或者相似的東西,都是一個模塊;
  2. webpack 有 模塊依賴圖(Dependency Graph),也就是說,知道每一個模塊之間的依賴關係;
  3. HMR 是模塊熱加載,指模塊被修改後,webpack檢測並用新模塊更新;
  4. 也就是原來的模塊被移除後,用修改後的模塊替換;
  5. 表現效果(一般):若是是 js,會從新執行一遍;若是是 css,並用了 style-loader,原有的會被替換;

以上是理論基礎,實際流程以下:

  1. 假如 B 模塊的代碼被更改了,webpack 能夠檢測到,而且能夠知道是哪一個更改了;
  2. 而後根據 依賴圖,發現 A 模塊依賴於 B 模塊,因而向上冒泡到 A 模塊中,判斷 A 模塊裏有沒有處理熱加載的代碼;
  3. 若是沒有,則繼續向上冒泡,直到冒泡到頂層,最後觸發頁面刷新(若是引用了多個chunk,當一個冒泡到頂層而且沒有被處理的話,整個頁面都會觸發刷新);
  4. 若是中途被捕獲,那麼將只從新加載冒泡路徑上的模塊,並觸發對應 HMR 處理函數的回調函數;

更具體的內容,請查看官網的說明,附連接以下:

依賴圖(Dependency Graph)

模塊熱替換(Hot Module Replacement)(注:原理)

模塊熱替換(注:使用方法)

7.二、應用場景

HMR 的應用場景,最合適的是帶 HMR 功能的loader。

例如 style-loader,或 vue-loader

緣由很簡單,本身在頁面裏寫 HMR 冒泡捕獲功能,寫起來很麻煩,也很容易致使遺漏。

最重要的是,這些代碼並非業務代碼,而是 HMR 專用代碼,這些代碼會在webpack打包時被打包進去(能夠查看打包好後的源代碼)Z,但這沒有意義。

所以在 loader 裏進行處理,對模塊的加載纔是更加有效的。

固然,假如你只是但願保存修改的代碼,會自動觸發頁面刷新,以保證頁面上的代碼是最新的,那麼也是能夠的。

這種狀況只須要啓用 HMR 功能,不須要寫 HMR 的捕獲代碼,讓觸發的 update 行爲自動冒泡到頂層,觸發頁面刷新就行了(參照 開發環境中的 6.2)。

具體能夠參照下面的示例DEMO。

7.三、使用說明

7.3.一、HMR 的冒泡

先假設引用關係: A -> B -> C

【1】HMR 是向上冒泡的:

  1. C 被更改後,會去找他的父模塊 B,查看 B 中有沒有關於 C 的 HMR 的處理代碼:
  2. 若是沒有,那麼會繼續向上冒泡到 A,查看 A 中有沒有關於 B 的 HMR 的處理代碼;
  3. 若是 A 沒有,由於 A 是入口文件,因此會刷新整個頁面;

【2】冒泡過程當中只有父子關係:

C 更改,冒泡到 B(B 無 HMR 處理代碼),而後冒泡到 A。

此時,在 A 這裏,視爲 B 被更改(而不是 C),

所以 A 裏面處理 HMR 代碼,捕獲的模塊,應該是 B,而不是 C,

若是 A 的目標是 C,那麼該段代碼不會響應(雖然冒泡的起點是 C);

【3】HMR 觸發,會執行整個冒泡流程中涉及到的模塊中的代碼:

例如上面的 C 更改,B 捕獲到了,從新執行 C;

B 無捕獲代碼向上冒泡,A捕獲到了,從新執行 B 和 C;

假如引用關係是:A -> B -> C 和 D,即 B 裏面同時引用 C 和 D 兩個模塊,而且 B 沒有處理 HMR 的代碼,A 有:

  1. 冒泡起點是 B:B 從新執行一遍本身的代碼,C 和 D 不會執行;
  2. 冒泡起點是 C:B 和 C 從新執行一遍本身的代碼, D 不會執行;
  3. 冒泡起點是 D:B 和 D 從新執行一遍本身的代碼, C 不會執行;

【4】冒泡行爲起點的子模塊,其代碼不會被從新執行:

先假設引用關係:A -> B -> C -> D,B 沒有 處理 HMR 的代碼,C 有沒有無所謂,A 有。

冒泡起點是 C,所以冒泡到 A。

從上面咱們能夠得知,B 和 C 會被從新執行,那麼 D 呢?

答案是不會,由於 D 不在冒泡路線上。

總結:

總結以上四點,得出一個結論:

  1. 從修改的模塊開始冒泡,直到被捕獲爲止;
  2. 冒泡路徑上的代碼(不包含捕獲到的模塊),都會被從新執行;
  3. 非冒泡路徑上的代碼,不論是子模塊,或者是兄弟模塊,都不會被從新執行(除非是整個頁面被刷新)Z。

7.3.二、HMR 的隱患

以上特色這就可能帶來一些後果(主要是 js 代碼):

  1. 假如我代碼裏,有綁定事件,那麼當修改代碼並從新執行一遍後,顯然會再綁定一次,所以會致使重複綁定的問題(所以要考慮到解綁以前的事件);
  2. 相似的,若是代碼裏添加了 DOM,那麼當從新執行的時候,本來 DOM 節點還在,從新執行的時候又添加了一次;
  3. 若是有某些一次性操做,好比代碼裏移除了某個 DOM,那麼極可能 HMR 不能解決你的問題,也許須要從新刷新後,表現才正常;

7.3.三、HMR 的一個坑

那就是引用時候的名字,和處理的 API,引用的文件名,須要相同;

舉例:

// 引入
import foo from './foo.js';

// 處理
module.hot.accept('./foo.js', callback);
複製代碼

若是不同,會致使第一次響應正常,後面就可能致使沒法正常觸發 HMR ,雖然提示模塊更新,但不會從新執行模塊的代碼。

7.四、示例

爲了說明 HMR 是怎麼使用和生效,這裏將給一個最簡單的示例,包含 html、css、和 js 代碼,來解釋其的使用方法。

能夠直接 fork 本項目參看源碼,如下是分析做用,以及如何生效的。

須要使用的東西:

  1. 使用 webpack-dev-server,參考上一篇六、開發環境
  2. 兩個HMR插件:webpack.NamedModulesPluginwebpack.HotModuleReplacementPlugin
  3. 配置一下 package.json,添加一行 scripts :"dev": "webpack-dev-server --open --config webpack.config.js"
  4. style-loader,用於實現 css 的 HMR(使用後默認開啓);

依賴圖:

app.js        入口文件,在其中配置了 foo.js 和 bar.js 的 HMR 處理函數
├─style.css   樣式文件
├─img
│  ├─1.jpg    圖片1
│  └─2.jpg    圖片2
├─foo.js      模塊foo,配置了 HMR 模塊熱替換的接口
│  └─bar.js   模塊bar,是foo的子模塊
└─DOM.js      抽象出一個創造 DOM,並插入到 body 標籤的函數
複製代碼

一、先分析 js 部分

app.js

// 引入資源
import './style.css';
import foo from './foo.js';
import createDOM from './DOM.js'

// 建立一個DOM並插入<body>標籤
let el = createDOM({
    id: 'app-box',
    innerHTML: 'app.js<input>'
})
document.body.appendChild(el);

// 本行代碼表示app.js已經被執行了一遍
console.log('%c%s', 'color:red;', 'app.js is running...')

// 兩個子模塊建立DOM並插入<body>標籤
foo()

// 這裏是控制 HMR 的函數
// 注:
// 這裏引用的 foo.js 模塊,那麼處理 foo.js HMR 效果的代碼必須寫在這裏;
// 特別提示:這段代碼不能抽象封裝到另一個js文件中(即便那個js文件也被 app.js import進來)
// 推測是根據webpack的依賴圖,向上找父模塊,而後在父模塊的代碼中,找有沒有處理 HMR 的代碼
if (module.hot) {
    module.hot.accept('./foo.js', function (url) {
        // 回調函數只有url一個參數,類型是數組
        // 執行時機是 foo.js 中的代碼執行完畢後執行
        console.log('%c%s', 'color:#FF00FF;', `[${url}] is update`)
    })
}
複製代碼

foo.js

// 引入資源
import createDOM from './DOM'
import bar from "./bar.js";
// bar 中建立的DOM邏輯,在 foo 中執行
bar()

// 執行本段代碼的時候,表示 foo.js 被從新執行了
console.log('%c%s', 'color:green;', 'foo.js is running...')

function Foo() {
    let el = createDOM({
        id: 'foo-box',
        classList: 'foo',
        innerHTML: 'foo.js<input>'
    })

    document.body.appendChild(el);
}

// 導出給 app.js 執行
export default Foo

// 這裏寫 bar.js 的 HMR 邏輯
if (module.hot) {
    module.hot.accept('./bar.js', function (args) {
        console.log('%c%s', 'color:#FF00FF', `[${args}] is update`)
    })
}
複製代碼

bar.js

// 引入資源
import createDOM from './DOM'

// 執行本段代碼的時候,表示 bar.js 被從新執行了
console.log('%c%s', 'color:blue;', 'bar.js is running...')

function Bar() {
    let el = createDOM({
        id: 'bar-box',
        classList: 'bar',
        innerHTML: 'bar.js<input>'
    })

    document.body.appendChild(el);
}

// 導出給 foo.js 執行
export default Bar
複製代碼

簡單總結一下以上代碼:

  1. app.js 做爲入口文件,他引入了本身的子模塊 foo.js,以及 css 資源文件,而且處理本身子模塊的 HMR 行爲;
  2. foo.js 做爲 app.js 的子模塊,他引入了本身的子模塊 bar.js ,而且處理本身子模塊的 HMR行爲;
  3. bar.js 沒作什麼特殊的;
  4. 三個模塊裏,都有一行 console.log() 代碼,當出如今瀏覽器的控制檯裏的時候,表示該模塊代碼被從新執行了一遍;
  5. 父模塊處理子模塊的 HMR 時,回調函數裏有一行 console.log() 代碼,表示該子模塊已經從新加載完畢;
  6. 所以,理論上,咱們修改 foo.js 或者 bar.js 文件後,首先會看到該模塊的 console.log() 代碼,其次會看到其父模塊處理 HMR 的回調函數中的 console.log() 代碼;

首次刷新頁面後,控制檯先輸出三條 log,和幾行 HMR代碼,略略略。

修改 foo.js

當咱們修改 foo.js 的 log 代碼:console.log('%c%s', 'color:green;', 'foo.js is running...I change it')

控制檯輸出:

foo.js is running...I change it
[./foo.js] is update
[HMR] Updated modules:
[HMR]  - ./foo.js
[HMR] App is up to date.
複製代碼

正如咱們所料,foo.js 代碼被從新執行了一遍,而後觸發了 app.js 裏面 module.hot.accept() 的回調函數(注意,有前後順序)。

而且,頁面上多了一個 DOM 節點(來自 bar.js的,由於在 foo.js 裏面執行了 bar()),這正是咱們前面所提出來的,HMR 機制的天生缺陷之一。

另外請注意,因此 bar.js 是 foo.js 的子模塊,但因爲 bar.js 並無被修改,因此 bar.js 裏面的代碼沒有從新執行一遍(除了他暴露給 foo.js 的接口)。

修改 bar.js

當咱們修改 bar.js 的 log 代碼:console.log('%c%s', 'color:blue;', 'bar.js is running...and bar has been changed')

控制檯輸出:

bar.js is running...and bar has been changed
[./bar.js] is update
[HMR] Updated modules:
[HMR]  - ./bar.js
[HMR] App is up to date.
複製代碼

bar.js 是 foo.js 的子模塊,並且 foo.js 裏面有關於處理 bar.js 的模塊 HMR 功能的代碼。

所以 bar.js 被修改後,冒泡到本身的父模塊時就被捕獲到,並無繼續向上冒泡。

讓 bar.js 的修改冒泡到 app.js

假如讓 bar.js 的修改冒泡到 app.js 會發生什麼事情呢?先修改代碼:

app.js 嘗試讓 app.js 同時捕獲 foo.js 和 bar.js 的修改

// from
module.hot.accept('./foo.js', function (url) {

// to
module.hot.accept(['./foo.js', './bar.js'], function (url) {
複製代碼

foo.js 註釋掉對 bar.js 的 HMR 功能的處理代碼

// from 
if (module.hot) {
    module.hot.accept('./bar.js', function (args) {
        console.log('%c%s', 'color:#FF00FF', `[${args}] is update`)
    })
}

// to 
// if (module.hot) {
//     module.hot.accept('./bar.js', function (args) {
//         console.log('%c%s', 'color:#FF00FF', `[${args}] is update`)
//     })Z
// }
複製代碼

恢復以前 foo.js 和 bar.js 的 console.log() 的修改

// foo.js
// from
console.log('%c%s', 'color:green;', 'foo.js is running...I change it')

// to
console.log('%c%s', 'color:green;', 'foo.js is running...')


// bar.js
// from
console.log('%c%s', 'color:blue;', 'bar.js is running...and bar has been changed')

// to
console.log('%c%s', 'color:blue;', 'bar.js is running...')
複製代碼

修改完畢,此時刷新一下頁面,重置狀態。而後咱們給 bar.js 添加一行代碼 console.log('bar.js is be modified')

控制檯輸出:

bar.js is running...
bar.js is be modified
foo.js is running...
[./foo.js] is update
[HMR] Updated modules:
[HMR]  - ./bar.js
[HMR]  - ./foo.js
[HMR] App is up to date.
複製代碼

這說明,webpack 成功捕捉到了 bar.js 的修改,而且更新了 bar.js 和 foo.js 。

而且,雖然在 app.js 裏去嘗試捕獲 bar.js ,然而,由於 bar.js 並非 app.js 的子模塊(而是子模塊的子模塊),所以是捕獲不到的。

複數監視

module.hot.accept這個函數中,參數一能夠接受一個數組,表示監視的模塊能夠是複數。

因此不須要寫多個函數來監視多個模塊,若是他們之間邏輯是複用的話,那麼一個模塊就好了。

總結:

js 文件被修改,會致使冒泡過程當中,涉及到的 js 文件,都被從新執行一遍。


二、再分析 css 部分

在使用 style-loader 後,咱們不須要配置任何東西,就能夠實現 HMR 效果。

style.css

#app-box {
    color: red;
}
複製代碼

默認打開頁面,會發現頁面上 app.js 那一行的字體顏色是紅色。

修改這個css樣式爲:

#app-box {
    color: red;
    font-size: 24px;
}
複製代碼

在保存這個css文件後,會發現頁面在沒有刷新的狀況下,樣式已經改變了。

因爲咱們開發通常都會採用 style-loader,並且 css 因爲是替代效果,也不是可執行代碼,所以天生適用於 HMR 場景。

7.五、總結

css 文件沒有什麼好說的,只要使用 style-loader 便可。

由於 HMR 的特性(會從新執行 js 文件),因此若是沒有 loader 輔助的話,寫在 HMR 下可用的 js 代碼是很麻煩的。

想象一下,你的js代碼裏有一個建立並插入 DOM 的操做,而後在你每次修改這個模塊裏的代碼時,都會建立一個新的 DOM,並插入。

例如本 DEMO 裏,修改 foo.js 文件,會致使從新執行 foo 模塊時,執行 bar.js 暴露出來的接口 bar,

因而頁面被重複插入一個 DOM,這顯然不符合咱們的預期。

固然了,也有解決辦法,刷新頁面便可恢復正常。

相似的還有 綁定事件(致使重複綁定),發起異步請求(致使屢次發起異步請求)等。

那麼有沒有解決辦法呢?

答案是使用相關的 loader,而且寫符合相關格式的代碼。

例如 vue-loader 能夠處理 .vue 結尾的文件。在你修改 .vue 文件的時候,就能夠自動處理。假如你 .vue 文件不按要求寫,而是本身亂寫,那麼顯然就不能正常運行。

相關文章
相關標籤/搜索