iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 6
0
Modern Web

從頭建立一套 web-based 服務架構系列 第 6

從頭建立一套 web-based 服務架構 (6) Webpack HMR

從頭建立一套 web-based 服務架構 (6) Webpack HMR

Webpack 內建名為 Hot Module Replacement (HMR) 的機制,和 LiveReload 等工具類似,能夠在開發時動態重新載入更改的程式碼。和 LiveReload 不同的是,Webpack 以 module 為單位進行重載,能夠在不重新整理頁面的情況下執行新的 module 並覆蓋舊的結果。對 React.js 的開發而言,HMR 能夠自動重載 component 內容以大幅增加開發速度。

啟用 HMR 機制

動態重載的原理是另啟一個 server 去監聽本地端的檔案系統變化,並即時通知前端頁面來實行變動。此專案使用 webpack-dev-middlewarewebpack-hot-middleware 來打造這台 server,並由 gulp server-hot 這個 Gulp task 啟用。gulp server-hot 會啟動一台 Express.js server:

import express from 'express';
import webpack from 'webpack';
import webpackDev from 'webpack-dev-middleware';
import webpackHot from 'webpack-hot-middleware';
import makeWebpackConfig from '../config.webpack';

const app = express();

const webpackConfig = makeWebpackConfig(process.env);
const compiler = webpack(webpackConfig);

app.use(webpackDev(compiler, {
    headers: { 'Access-Control-Allow-Origin': '*' },
    noInfo: true,
    publicPath: webpackConfig.output.publicPath
}));

app.use(webpackHot(compiler));

app.listen(8080);

以上的兩個 Express middleware 到底實際上做了什麼事情?首先,webpack-dev-middleware 將 Webpack 的 compile 結果 host 在 server 中,而非寫入 output 所指定的檔案路徑。由於 bundling 的結果存放在記憶體,dev middleware 在執行 compile 的速度比起直接 build Webpack 快上不少。

webpack-hot-middleware 會將 dev middleware 的產出提供給前端存取。具體的作法是在 Webpack 的設定中,加入 webpack.optimize.OccurrenceOrderPluginwebpack.HotModuleReplacementPluginwebpack.NoErrorsPlugin 三個 plugin,以及在 entry 的 hot server endpoint:

entry: {
    app: [
        `webpack-hot-middleware/client?path=http://${ip.address()}:8080/__webpack_hmr`,
        './src/client/main.js'
    ]
},

以及在 render HTML 內容時,將 JS bundle URL 指定到 hot server 提供的 endpoint:

const scriptSrc = isProduction ? '/dist/app.js' : `//${ip.address()}:8080/dist/app.js`;

加入 HMR 程式碼

只靠 hot server 並沒有真正達到動態重新載入的效果;實際上,HMR 機制還需要仰賴程式碼本身的設定才能運作。道理很簡單,因為 HMR 不會預先假設開發者撰寫的程式碼該如何去處理 module 重載的行為,這必須由開發者自行設定。

在 Webpack 載入 JS code 時,會對程式碼內用到的 requiremodule 變數做靜態轉換,建立 dependency graph(Webpack 的術語稱為 module tree)(註1);而開發者需要使用 Wepback 提供給 module 的 HMR API 來動態接受新的 module。HMR 機制可以藉由 module.hot.accept 設定:

if (module.hot && typeof module.hot.accept === 'function') {
    module.hot.accept('./root.view', renderRoot);
}

function renderRoot() {
    const Root = require('./root.view').default;
    ReactDOM.render(<Root />, document.getElementById('app'));
}

以這段程式為例,當 root.view.js 內容更動時,module.hot.accept 會接收此事件並呼叫開發者設定的 renderRoot callback。renderRoot 的內容就是單純重新 require 新版的 Root component 並重新執行 ReactDOM.render。而 HMR 真正的強大之處,在於它會監聽 dependency graph 內的任何 module 變動,並將此變動事件沿著 dependency graph 的 root 方向 bubble 出去,直到被 module.hot.accept 攔截為止。因此,我們不需要在每個 module 都加上 HMR 相關的程式碼,只需要在接近根部的位置(以此例而言,root.view.js)設定好 HMR 即可一次監聽所有 child nodes 的變動事件。

真的能夠動態重載任何 module 嗎?

若是 HMR 設定成功,Webpack 會在 browser console 輸出相關的 debug 訊息,例如和 hot server 連線成功的 [HMR] connected、module 重新載入完成的 [HMR] bundle rebuilt in OOOms 等。當發生 HMR 事件時,Webpack 也會將變動的 module 依照 dependency graph 一路印出到 root 的結果:

Module Tree in Console

反過來,若是 Webpack 偵測到無法被 hot replace 的 module 變動,也會以 console.warn 的形式輸出到 browser console:

HMR failed

這個警告訊息代表此 module 的變動事件在 bubbling 過程沒有被任何 module.hot.accept 攔截到。另外,由於一個 module 往往會被複數的 module 所 require,當有任何一條往 root 的 bubbling 的路線沒能被 module.hot.accept 攔截時,仍會出現這個 hot reload 失敗的訊息。若是想設定一個最上層的 module.hot.accept 攔截行為的話,以下模仿 LiveReload 行為的最簡易版本可以提供參考:它會攔截所有 module 的變動事件,然後直接重新整理頁面。

if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => {
        window.location.reload();
    });
}

使用 HMR 的幾個建議

  1. 在程式碼內儘量越少出現 HMR 相關的程式碼越好,以將程式本身和 Webpack 之間的耦合度限制在最小範圍(例如只撰寫於 Webpack 的 entry file 內)
  2. 不用擔心 HMR 造成的 build bundle size 增加問題或是讓 production bundle 增加無謂的程式碼,由於 module.hot 在沒有使用 HMR 時會是空值,這些 if 區塊會整個被 UglifyJS 拔掉(UglifyJS 的 unreachable code removal 功能)
  3. 有些 loader 會偷塞 HMR 機制的程式碼(例如 style-loader),除了方便開發外,也可以研究一下不同套件對 HMR 的寫法。

註1:雖然「由 entry file 去 require 其他所有的 module」這一行為用 tree 會是比較好的形容方式,但考量到 module 是允許複數 parent (被不同 module 所 require)的狀況下,作者個人認為它更像是一個 directed graph,因此文內均以 dependency graph 一詞替代 Webpack 內部使用的 module tree 一詞。


上一篇
從頭建立一套 web-based 服務架構 (5) Tuning Webpack
下一篇
從頭建立一套 web-based 服務架構 (7) 啟動 Frontend Server
系列文
從頭建立一套 web-based 服務架構9

尚未有邦友留言

立即登入留言