webpack源碼分析之六:hot module replacement

前言

在web開發中,webpack的hot module replacement(HMR)功能是能夠向運行狀態的應用程序定向的注入並更新已經改變的modules。它的出現能夠避免像LiveReload那樣,任意文件的改變而刷新整個頁面。html

這個特性能夠極大的提高開發擁有運行狀態,以及資源文件廣泛過多的前端應用型網站的效率。完整介紹能夠看官網文檔前端

本文是先從使用者的角度去介紹這個功能,而後從設計者的角度去分析並拆分須要實現的功能和實現的一些細節。node

功能介紹

對於使用者來講,體驗到這個功能須要如下的配置。linux

webpack.config.js:webpack

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
    entry: {
        app: './src/index.js'
    },
    devServer: {
        contentBase: './dist',
        hot: true,
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new HtmlWebpackPlugin()
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

代碼: index.js 依賴print.js,使用module.hot.accept接受print.js的更新:git

import './print';

if (module.hot) {
    module.hot.accept('./print', function () {
        console.log('i am updated');
    })
}

改變print.js代碼:github

console.log('print2')
console.log('i am change');

此時服務端向瀏覽器發送socket信息,瀏覽器收到消息後,開始下載以hash爲名字的下載的json,jsonp文件,以下圖:web

圖片描述

瀏覽器會下載對應的hot-update.js,並注入運行時的應用中:json

webpackHotUpdate(0,{

/***/ 30:
/***/ (function(module, exports) {

console.log('print2')
console.log('i am change');




/***/ })

})

0 表明着所屬的chunkid,30表明着所屬的moduleid。bootstrap

替換完以後,執行module.hot.accept的回調函數,以下圖:

clipboard.png

簡單來說,開啓了hmr功能以後,處於accepted狀態的module的改動將會以jsonp的形式定向的注入到應用程序中。

一張圖來表示HMR的總體流程:

clipboard.png

功能分析

提出問題

當翻開bundle.js的時候,你會發現Runtime代碼多了許多如下的代碼:

/******/     function hotDownloadUpdateChunk(chunkId) {
/******/            ...
/******/     }
/******/    function hotDownloadManifest(requestTimeout) {
/******/            ...
/******/     }
/******
/******/     function hotSetStatus(newStatus) {
/******/            ...
/******/     }
/******/

打包的時候,明明只引用了4個文件,可是整個打包文件卻有30個modules之多:

圖片描述

/* 30 */
/***/ (function(module, exports) {

console.log('print3')
console.log('i am change');




/***/ })

到如今你可能會有如下幾個疑問:

  1. hmr模式下Runtime是怎麼加上HMR Runtime代碼的。
  2. 業務代碼並無打包socketjs,hot代碼的,瀏覽器爲何會有這些代碼的。
  3. 瀏覽器是如何判斷並下載如:501eaf61104488775d2e.hot-update.json,。501eaf61104488775d2e.hot-update.js文件的,而且如何將js內容替換應用程序的內容。
  4. 編譯器如何監聽資源文件的變化,並將改動的文件輸出到Server裏面供客戶端下載,如501eaf61104488775d2e.hot-update.json,0.501eaf61104488775d2e.hot-update.js。
  5. 服務器如何監聽編譯器的生命週期,並在其編譯開始,結束的時候,向瀏覽器發送socket信息。
  6. 瀏覽器替換新的module以後,如何及時清理緩存。

分析問題

  1. Runtime代碼是根據MainTemplate內部實現的,有多種場景如normal,jsonp,hot模式,則能夠考慮將字符串拼接改爲事件。
  2. 編譯開始時候,若是是hot模式,在編譯器層面將socketjs,hot代碼一併打包進來。
  3. 監聽文件變化,webpack 封裝了watchpack模塊去監聽如window,linux,mac系統的文件變化
  4. 編譯結束後生成hash,文件變化後對比最近一次的hash。有變更則生成新的變更文件。
  5. server層監聽編譯器的編譯開始,結束的事件,如compile,watch,done事件,觸發事件後,像瀏覽器發送對應的websocket消息。
  6. 瀏覽器接受到了websocket消息後,根據hash信息,獲得[hash].hot-update.json文件,從中解析到chunkId,在根據chunkId,hash信息去下載[chunkId].[hash]-update.js。
  7. 瀏覽器替換新的module以前,installedModules對象中刪除緩存的module,在替換後,執行__webpack_require__(id),將其併入到installedModules對象中。

功能實現

以上問題,能夠從三個不一樣的角度去解決。server,webpack,brower。

webpack-dev-server

  • 對入口entry作包裝處理,如將
entry:{app:'./src/index.js'}

,轉換爲

entry:{app:['/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/_webpack-dev-server@2.11.2@webpack-dev-server/client/index.js?http://localhost:8082'],'webpack/hot/dev-server','./src/index.js'}

構建業務代碼時,附帶上socketjs,hot代碼。

  • 初始化服務端sockjs,並註冊connection事件,向客戶端發送hot信息,開啓hmr功能。

Server.js

if (this.hot) this.sockWrite([conn], 'hot');

瀏覽器

hot: function hot() {
    _hot = true;
    log.info('[WDS] Hot Module Replacement enabled.');
  }
  • 監聽編譯器的生命週期模塊。

    • socket

      • 監聽compiler的compile事件,經過webSocket向客戶端發送invalid 信息
      • 監聽compiler的done事件,經過webSocket向客戶端發送still-ok,hash以及hash內容,並將全部請求資源文件設置爲可用狀態
compiler.plugin('compile', invalidPlugin);
  compiler.plugin('invalid', invalidPlugin);
  compiler.plugin('done', (stats) => {
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
  });
  • 資源文件鎖定

    • 監聽compiler的invalid,watch-run,run事件。將全部請求資源文件設置爲pending狀態,直到構建結束。
    • 監聽compiler的done事件,將全部請求資源文件從新設置爲可用狀態
context.compiler.plugin("done", share.compilerDone);
    context.compiler.plugin("invalid", share.compilerInvalid);
    context.compiler.plugin("watch-run", share.compilerInvalid);
    context.compiler.plugin("run", share.compilerInvalid);

webpack

Template

MainTemplate增長module-obj,module-require事件

module-obj事件負責生成如下代碼

/******/         var module = installedModules[moduleId] = {
/******/             i: moduleId,
/******/             l: false,
/******/             exports: {},
/******/             hot: hotCreateModule(moduleId),
/******/             parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
/******/             children: []
/******/         };
/******/

module-require事件負責生成如下代碼

/******/         modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

Compiler

新增Watching類支持watch模式,並結合watchpack監聽文件變化。

class Watching {
    ....
}

Module

新增updateHash實現

updateHash(hash) {
       this.updateHashWithSource(hash);
       this.updateHashWithMeta(hash);
       super.updateHash(hash);
   }

Chunk

新增updateHash實現

updateHash(hash) {
        hash.update(`${this.id} `);
        hash.update(this.ids ? this.ids.join(",") : "");
        hash.update(`${this.name || ""} `);
        this._modules.forEach(m => m.updateHash(hash));
    }

Compilation

增長createHash方法,默認調用md5計算compilation hash。調用依賴樹module,chunk的updateHash方法。

createHash() {
    ....
}

Parser

  • 增長對ifStatement的statement類的解析支持

如:

if(module.hot){}

編譯後

if(true){}

MultiEntryPlugin

  • 增長MultiEntryDependency,MultiModule,MultiModuleFactory。將數組的entry對象,打包爲如下的資源文件。
entry:{app:['/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/_webpack-dev-server@2.11.2@webpack-dev-server/client/index.js?http://localhost:8082'],'webpack/hot/dev-server','./src/index.js'}

打包後

/* 5 */
/***/ (function(module, exports, __webpack_require__) {

// webpack-dev-server/client/index.js
__webpack_require__(6); 
//webpack/hot/dev-server
__webpack_require__(26); 
// .src/index.js
module.exports = __webpack_require__(28);

/***/ })

HotModuleReplacementPlugin

  • 監聽module-require,require-extensions,hash,bootstrap,current-hash,module-obj等事件生成HMR Runtime 代碼
  • 監聽record事件,存儲最近一次的compilation hash。
compilation.plugin("record", function(compilation, records) {
                if(records.hash === this.hash) return;
                records.hash = compilation.hash;
                records.moduleHashs = {};
                this.modules.forEach(module => {
                    const identifier = module.identifier();
                    const hash = require("crypto").createHash("md5");
                    module.updateHash(hash);
                    records.moduleHashs[identifier] = hash.digest("hex");
                });
                records.chunkHashs = {};
                this.chunks.forEach(chunk => {
                    records.chunkHashs[chunk.id] = chunk.hash;
                });
                records.chunkModuleIds = {};
                this.chunks.forEach(chunk => {
                    records.chunkModuleIds[chunk.id] = chunk.mapModules(m => m.id);
                });
            });
  • 監聽additional-chunk-assets 事件,對比record的最近一次hash,判斷變化以後。生成以[hash].hot-update.json,[chunkId].[hash].hot-update.js爲名稱的assets對象。
compilation.plugin("additional-chunk-assets", function() {
                ....
                this.assets[filename] = source;
            });

Brower

  • 初始化runtime,將全部附加的模塊代碼統一增長parents,children等屬性。並提供check,以及apply方法去管理hmr的生命週期。

    • check,發送http請求請求並更新manifest,請求成功以後,會將待更新的chunk hash與當前chunk hash作比較。多個chunk,則會等待相應的chunk 完成下載以後,將狀態轉回ready狀態,表示更新已準備並可用。
    • apply,當應用狀態爲ready時,將全部待更新模塊置爲無效(清除客戶端緩存),更新中調用新模塊(更新緩存),更新完成以後,應用程序切回idle狀態。
  • 初始化websocket,與server端創建長連接,並註冊事件。如ok,invalid,hot,hash等事件。
  • 初始化hot 代碼,註冊事件對比新老hash,不相等則調用check方法開啓模塊更新功能。
module.hot.check(true).then(function(updatedModules) {
    ....
})

代碼實現

本人的簡易版webpack實現simple-webpack

(完)

相關文章
相關標籤/搜索