webpack-hot-middleware解讀

wepack-hot-middleware 深入解讀

  1. webpack-hot-middleware 做什么的?
    webpack-hot-middleware 是和webpack-dev-middleware配套使用的。從上一篇文章中知道,webpack-dev-middleware是一個express中間件,實現(xiàn)的效果兩個,一個是在fs基于內(nèi)存,提高了編譯讀取速度;第二點是,通過watch文件的變化,動態(tài)編譯。但是,這兩點并沒有實現(xiàn)瀏覽器端的加載,也就是,只是在我們的命令行可以看到效果,但是并不能在瀏覽器動態(tài)刷新。那么webpack-hot-middleware就是完成這件小事的。沒錯,這就是一件小事,代碼不多,后面我們就深入解讀。

  2. webpack-hot-middleware 怎么使用的?
    官方文檔已經(jīng)很詳細的介紹了,那么我再重復一遍。

    1. 在plugins中增加HotModuleReplacementPlugin().
    2. 在entry中新增webpack-hot-middleware/client
    3. 在express中加入中間件webpack-hot-middleware.
    4. 在入口文件添加
    //灰常重要,知會 webpack 允許此模塊的熱更新
    if (module.hot) {
        module.hot.accept();
    }
    

    詳細的配置文件也可以看我的github

    // webpack.config.js
    const path = require('path');
    const webpack = require('webpack');
    module.exports = {
        entry: ['webpack-hot-middleware/client.js', './app.js'],
        output: {
            publicPath: "/assets/",
            filename: 'bundle.js',
        },
        plugins: [
            new webpack.HotModuleReplacementPlugin(),
            new webpack.NoEmitOnErrorsPlugin()
        ]
    };
    // express.config.js
    app.use(require("webpack-hot-middleware")(compiler, {
        path: '/__webpack_hmr',
    }));
    
    app.get("/", function(req, res) {
        res.render("index");
    });
    
    app.listen(3333);
    
  3. 重點,重點,重點,源代碼分析。
    通過上面的配置,我們可以發(fā)現(xiàn)webpack實現(xiàn)了熱加載,那么是如何實現(xiàn)的呢?進入正題。

    1. webpack-hot-middleware中的入口文件middleware.js,哇,只有100多行。
      整個文件的結構如下:
    文件結構

    文件結構能夠看到line6-36是核心。那么,我們深入分析,

    line7-10定義了基本的option,分別定義了option的log方法,path路徑以及server的socket響應時間。(哈哈,log方法怎么在插件都進行了定義?。。。?/p>

    line 12創(chuàng)建了eventStream對象,這是個什么呢?line38-76,


    好簡單,就是執(zhí)行了這兩行,

    setInterval(function heartbeatTick() {
      everyClient(function(client) {
        client.write("data: \uD83D\uDC93\n\n");
      });
    }, heartbeat).unref();
    

    每間隔heartbeat秒,遍歷clients,每個client socket eventStream 寫一個心(紅心)。


    紅心

    最后返回了一個handler以及publish方法的對象,也就是eventStream。

    下一步就是line 15,在compiler編譯時,加入一個回調(diào)處理函數(shù)。

    compiler.plugin("compile", function() {
      latestStats = null;
      if (opts.log) opts.log("webpack building...");
      eventStream.publish({action: "building"});
    });
    

    上述這種通過node定義webpack插件的方式很常見(其實可以理解為加入了一個事件處理函數(shù))。它做了什么呢?對你的代碼(如果參考我的代碼的話,改變index.js即可),會發(fā)生什么呢?

    也就是通過客戶端EventStream,向瀏覽器發(fā)送消息("action: building").

    ok!還有另一個(一共只有兩個,竊喜,好簡單)。

    compiler.plugin("done", function(statsResult) {
      // Keep hold of latest stats so they can be propagated to new clients
      latestStats = statsResult;
      publishStats("built", latestStats, eventStream, opts.log);
    });
    

    另一個函數(shù)啊,publishStats().函數(shù)內(nèi)部,又調(diào)用了extractBundles(),以及buildModuleMap

    function extractBundles(stats) {
      if (stats.modules) return [stats];
      if (stats.children && stats.children.length) return stats.children;
      return [stats];
    }
    

    很簡單的幾行,就是將stats包裝為數(shù)組,有子元素children,直接用,沒有,就[stats]。

    buildModuleMap就簡單了,建立了一個key,value的map映射。

    這就簡單了,回到compiler的done回調(diào)函數(shù),整個流程就是執(zhí)行了抽取bundle,每個bundle執(zhí)行一次eventStream的publish回調(diào)。

    實例效果,也就是上圖展示的那樣了,那個built,看清楚了吧。

    那么,接著繼續(xù)!line25-35.

    var middleware = function(req, res, next) {
      if (!pathMatch(req.url, opts.path)) return next();
      eventStream.handler(req, res);
      if (latestStats) {
        // Explicitly not passing in `log` fn as we don't want to log again on
        // the server
        publishStats("sync", latestStats, eventStream);
      }
    };
    middleware.publish = eventStream.publish;
    return middleware;
    

    重點來了。返回的中間件middleware,流程是這樣滴:判斷是不是__webpack_hmr(默認,可配置),不是的話跳過,執(zhí)行next()。是請求__webpack_hmr的話呢,執(zhí)行eventStream的handler方法,處理請求。

    handler: function(req, res) {
      req.socket.setKeepAlive(true);
      res.writeHead(200, {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'text/event-stream;charset=utf-8',
        'Cache-Control': 'no-cache, no-transform',
        'Connection': 'keep-alive',
        // While behind nginx, event stream should not be buffered:
        // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
        'X-Accel-Buffering': 'no'
      });
      res.write('\n');
      var id = clientId++;
      clients[id] = res;
      req.on("close", function(){
        delete clients[id];
      });
    },
    

    其實就是,建立了一個eventStream。關鍵點:Content-Type: 'text/event-stream'。并且記錄了請求的clients.

    line29-32,就是判斷如果已經(jīng)編譯完成,就向瀏覽器publish一個sync的消息。

    至此,這個hot-middleware的服務器端的整個執(zhí)行過程就分析完了。我們上文一直提到eventStream,作為EventStream,如果少了客戶端怎么行呢?哈哈,別漏了這么重要的角色。

    1. client.js
      你應該還記得,上文中的配置webpack.config.js中,在entry中引入的“webpack-hot
      -middleware/client.js”,對,這個就是client.js登上舞臺的入口。

      line 4-33,做了一件事,就是配置,根據(jù)__resourceQuery,轉化查詢字符串中的配置項?不明白。__resourceQuery是webpack中的默認API,表示require某個模塊時的查詢字符串。例如,我們在entry的client.js這樣寫。

        entry: ['webpack-hot-middleware/client.js?name="zzf"', './app.js'],
      

      那么,在client.js中取到__resourceQuery的值就是?name="zzf"

      line 35-45,一次判斷是否為客戶端瀏覽器,是否支持EventSource。如果都支持的話,connect()連接server端。connect()方法就是這個client.js的全部了。

      function connect() {
        getEventSourceWrapper().addMessageListener(handleMessage);
      
        function handleMessage(event) {
          if (event.data == "\uD83D\uDC93") {
            return;
          }
          try {
            processMessage(JSON.parse(event.data));
          } catch (ex) {
            if (options.warn) {
              console.warn("Invalid HMR message: " + event.data + "\n" + ex);
            }
          }
        }
      }
      

      line104執(zhí)行了哪些呢?

      function getEventSourceWrapper() {
        if (!window.__whmEventSourceWrapper) {
          window.__whmEventSourceWrapper = {};
        }
        if (!window.__whmEventSourceWrapper[options.path]) {
          // cache the wrapper for other entries loaded on
          // the same page with the same options.path
          window.__whmEventSourceWrapper[options.path] = EventSourceWrapper();
        }
        return window.__whmEventSourceWrapper[options.path];
      }
      

      上述主要執(zhí)行了就是創(chuàng)建了EventSourceWrapper()的對象。那么EventSourceWrapper()執(zhí)行了什么呢?

      function EventSourceWrapper() {
        var source;
        var lastActivity = new Date();
        var listeners = [];
      
        init();
        var timer = setInterval(function() {
          if ((new Date() - lastActivity) > options.timeout) {
            handleDisconnect();
          }
        }, options.timeout / 2);
      
        function init() {
          source = new window.EventSource(options.path);
          source.onopen = handleOnline;
          source.onerror = handleDisconnect;
          source.onmessage = handleMessage;
        }
      
        function handleOnline() {
          if (options.log) console.log("[HMR] connected");
          lastActivity = new Date();
        }
      
        function handleMessage(event) {
          lastActivity = new Date();
          for (var i = 0; i < listeners.length; i++) {
            listeners[i](event);
          }
        }
      
        function handleDisconnect() {
          clearInterval(timer);
          source.close();
          setTimeout(init, options.timeout);
        }
      
        return {
          addMessageListener: function(fn) {
            listeners.push(fn);
          }
        };
      }
      

      主要執(zhí)行邏輯就是在init()方法中,就是新建一個window.EventSource(options.path)對象。然后通過每隔10s輪詢判斷是否,已經(jīng)20s(兩次)連接失敗了,就斷開本次連接,然后在timeout20s后,重新嘗試建立連接。最后返回一個對象,也就是對外拋出一個可以添加listener的口子。

      line106-117,添加了這個事件監(jiān)聽處理函數(shù),handleMessage(event)方法。這個方法,首先根據(jù)服務端返回的數(shù)據(jù),是不是(心形 沒錯 "\uD83D\uDC93"就是紅心),如果是,表示正常的輪詢,直接return就可以。如果不是,調(diào)用processMessage處理,根據(jù)不同action,有不同的行為。這也就是middleware.js中的action行為。

      我們再深入processMessage()方法,前兩個action:"building","built"就很簡單了,就是一個console.log()提示。如果是sync就分多種情況了,warn,error。通過reporter(下文創(chuàng)建的)去處理。最后調(diào)用了processUpdate(),下文再詳解。

      還有兩部分沒有分析,就是createReporter().line134-190分析得到如下結論,reporter簡單的區(qū)分了warn,error,并以不同的style(console控制臺樣式,不知道的自行惡補)提示信息。并且,如果是編譯錯誤信息,通過overlay.js展示錯誤信息(創(chuàng)建一個遮罩層,打印出錯誤信息)。

      現(xiàn)在,還遺漏了一個文件的分析,也就是processUpdate()方法。這其實是核心的玩意兒啊,不容忽視。分析后,就會發(fā)現(xiàn),至此為止,還少了至關重要的一部,EventStream只是將變化,通知給了client端,但是client端怎么實現(xiàn)hmr的呢?核心邏輯就在processUpdate()方法中。

      // Based heavily on https://github.com/webpack/webpack/blob/
      //  c0afdf9c6abc1dd70707c594e473802a566f7b6e/hot/only-dev-server.js
      

      依賴這個玩意兒啊,hot/only-dev-server.js,這個玩意兒的分析在下一篇webpack-dev-server會深入分析。

      返回正題,這里,line9-11判斷了module.hot如果不支持,直接拋出error,這也就是webpack-hot-middleware必須配合HotModuleReplacementPlugin使用的原因,它是給webpack添加了module.hot能力的啊。

      line24-132,也就是整個方法了,這個方法嵌套的還是蠻深的。

      var reload = options.reload;
      if (!upToDate(hash) && module.hot.status() == "idle") {
        if (options.log) console.log("[HMR] Checking for updates on the server...");
        check();
      }
      

      首先獲取options中的reload配置,還記得怎么配置的不?client.js?reload=true. 然后判斷hash是否已經(jīng)過期,也就是webpack進行了重新打包,manifest有變化,是的話,就check()檢查變化的資源。

      function upToDate(hash) {
        if (hash) lastHash = hash;
        return lastHash == __webpack_hash__;
      }
      

      檢查hash是否過期的方法,使用了這樣一個__webpack_hash,這個是webpack給出的一個常量(可以通過webpack官網(wǎng)查詢),它表示資源的hash值,也就是已經(jīng)在瀏覽器端加載的資源的hash值。而hash,從上文中,我們還記得,這個玩意兒是EventStream傳過來的新的hash值。對的,沒有看錯,判斷文件是否變化,就是這么簡單。

      繼續(xù),check()方法。

      check()執(zhí)行,從64行開始,module.hot.check(false, cb);源文檔

      module.hot.check(autoApply, (error, outdatedModules) => {
        // Catch errors and outdated modules...
      });
      

      這就明白了這里module.hot.check檢查webpack打包資源是否變化,將會沿著依賴樹往上遍歷。

      再看一下回調(diào)函數(shù)cb


      line33, 如果error,handleError()處理。handleError在line112-125很簡單,就根據(jù)module.hot.status()獲取hot module Replacement 進程的狀態(tài)。(官方原話:Retrieve the current status of the hot module replacement process.),如果狀態(tài)是abort或者fail,表示檢查失敗。。那么執(zhí)行performReload().也就是刷新瀏覽器(window.location.reload()).

      回歸正題,line35-42可以看到,雖然不是error,但是updatedModules為undefined,也就是同樣沒有找到,執(zhí)行了一樣的邏輯,performReload().

      line 52行,module.hot.apply()方法。為什么呢,還記得module.hot.check(false, cb)么,這里第一個參數(shù)為false表示需要手動調(diào)用module.hot.apply()。繼續(xù)。

      module.hot.apply(applyOptions, applyCallback);

      var applyOptions = { ignoreUnaccepted: true };
      

      這個option的意思就很清晰了。還記得第一步中的第四條么,這里就是它的用武之地了。

      var applyCallback = function(applyErr, renewedModules) {
        if (applyErr) return handleError(applyErr);
      
        if (!upToDate()) check();
      
        logUpdates(updatedModules, renewedModules);
      };
      

      這里代碼就好玩了。首先檢查applyErr是否出現(xiàn),出現(xiàn)的話,handleError處理。然后再次檢查了hash是否最新,如果不是的話,重新執(zhí)行了check().不知道會不會有死循環(huán)的風險。。。最后,就是logUpdate().沒什么技術了就,簡單區(qū)分了下,然后打印log信息。

      line52-60是promise的then處理。line64-71同樣是如此。

      That's all!

  4. 擴展。

    unref()方法。

    還記得在client.js中有這么一處代碼么?

    setInterval(function heartbeatTick() {
      everyClient(function(client) {
        client.write("data: \uD83D\uDC93\n\n");
      });
    }, heartbeat).unref();
    

    這里,我們就介紹一下這個unref()方法,看看官網(wǎng)是怎么介紹的。
    翻譯成中文就是,當只有這一個timer處于active態(tài)時,并不需要事件循環(huán)去維持這個玩意兒,也就是process進程會退出。當然,如果有其他timer或者活動需要事件循環(huán)時,它也是可以跑著的。還不理解,這就是俗話:哎呀,我隨便,你們要是沒有吃的,我也不要了,要是有要的,就也給我一份。

    哈哈。
    測試一下!

    var timer1 = setInterval(function() {
      console.log('timer1');
    }, 1000).unref();
    

    沒有任何輸出。

    var timer1 = setInterval(function() {
      console.log('timer1');
    }, 1000).unref();
    
    var timer2 = setInterval(function() {
      console.log('timer2');
    }, 1000);
    
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 原文http://www.cnblogs.com/libin-1/p/6596810.html 版本號 vue-c...
    tengrl閱讀 3,865評論 0 0
  • 構建一個小項目——FlyBird,學習webpack和react。(本文成文于2017/2/25) 從webpac...
    布蕾布蕾閱讀 17,107評論 31 98
  • webpack 介紹 webpack 是什么 為什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert閱讀 6,662評論 2 71
  • 一粒種子降落,包裹了一堆的情愫,我們注定與此糾葛癡纏,去承受生命獨一無二的苦痛和傷痕??v使千瘡百孔也不要忘...
    愫su閱讀 304評論 0 0
  • 《八月》,去年金馬獎的最佳,張大磊的處女作。 影片追憶九十年代,國企改革的轉型期,截取八月這個暑假的模...
    暢游0321閱讀 403評論 0 0

友情鏈接更多精彩內(nèi)容