美文网首页
webpack-hot-middleware解读

webpack-hot-middleware解读

作者: 前端大飞 | 来源:发表于2017-08-11 11:09 被阅读0次

    wepack-hot-middleware 深入解读

    1. webpack-hot-middleware 做什么的?
      webpack-hot-middleware 是和webpack-dev-middleware配套使用的。从上一篇文章中知道,webpack-dev-middleware是一个express中间件,实现的效果两个,一个是在fs基于内存,提高了编译读取速度;第二点是,通过watch文件的变化,动态编译。但是,这两点并没有实现浏览器端的加载,也就是,只是在我们的命令行可以看到效果,但是并不能在浏览器动态刷新。那么webpack-hot-middleware就是完成这件小事的。没错,这就是一件小事,代码不多,后面我们就深入解读。

    2. webpack-hot-middleware 怎么使用的?
      官方文档已经很详细的介绍了,那么我再重复一遍。

      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. 重点,重点,重点,源代码分析。
      通过上面的配置,我们可以发现webpack实现了热加载,那么是如何实现的呢?进入正题。

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

      文件结构能够看到line6-36是核心。那么,我们深入分析,

      line7-10定义了基本的option,分别定义了option的log方法,path路径以及server的socket响应时间。(哈哈,log方法怎么在插件都进行了定义!!!)

      line 12创建了eventStream对象,这是个什么呢?line38-76,


      好简单,就是执行了这两行,

      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编译时,加入一个回调处理函数。

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

      上述这种通过node定义webpack插件的方式很常见(其实可以理解为加入了一个事件处理函数)。它做了什么呢?对你的代码(如果参考我的代码的话,改变index.js即可),会发生什么呢?

      也就是通过客户端EventStream,向浏览器发送消息("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);
      });
      

      另一个函数啊,publishStats().函数内部,又调用了extractBundles(),以及buildModuleMap

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

      很简单的几行,就是将stats包装为数组,有子元素children,直接用,没有,就[stats]。

      buildModuleMap就简单了,建立了一个key,value的map映射。

      这就简单了,回到compiler的done回调函数,整个流程就是执行了抽取bundle,每个bundle执行一次eventStream的publish回调。

      实例效果,也就是上图展示的那样了,那个built,看清楚了吧。

      那么,接着继续!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(默认,可配置),不是的话跳过,执行next()。是请求__webpack_hmr的话呢,执行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,就是判断如果已经编译完成,就向浏览器publish一个sync的消息。

      至此,这个hot-middleware的服务器端的整个执行过程就分析完了。我们上文一直提到eventStream,作为EventStream,如果少了客户端怎么行呢?哈哈,别漏了这么重要的角色。

      1. client.js
        你应该还记得,上文中的配置webpack.config.js中,在entry中引入的“webpack-hot
        -middleware/client.js”,对,这个就是client.js登上舞台的入口。

        line 4-33,做了一件事,就是配置,根据__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执行了哪些呢?

        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];
        }
        

        上述主要执行了就是创建了EventSourceWrapper()的对象。那么EventSourceWrapper()执行了什么呢?

        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);
            }
          };
        }
        

        主要执行逻辑就是在init()方法中,就是新建一个window.EventSource(options.path)对象。然后通过每隔10s轮询判断是否,已经20s(两次)连接失败了,就断开本次连接,然后在timeout20s后,重新尝试建立连接。最后返回一个对象,也就是对外抛出一个可以添加listener的口子。

        line106-117,添加了这个事件监听处理函数,handleMessage(event)方法。这个方法,首先根据服务端返回的数据,是不是(心形 没错 "\uD83D\uDC93"就是红心),如果是,表示正常的轮询,直接return就可以。如果不是,调用processMessage处理,根据不同action,有不同的行为。这也就是middleware.js中的action行为。

        我们再深入processMessage()方法,前两个action:"building","built"就很简单了,就是一个console.log()提示。如果是sync就分多种情况了,warn,error。通过reporter(下文创建的)去处理。最后调用了processUpdate(),下文再详解。

        还有两部分没有分析,就是createReporter().line134-190分析得到如下结论,reporter简单的区分了warn,error,并以不同的style(console控制台样式,不知道的自行恶补)提示信息。并且,如果是编译错误信息,通过overlay.js展示错误信息(创建一个遮罩层,打印出错误信息)。

        现在,还遗漏了一个文件的分析,也就是processUpdate()方法。这其实是核心的玩意儿啊,不容忽视。分析后,就会发现,至此为止,还少了至关重要的一部,EventStream只是将变化,通知给了client端,但是client端怎么实现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是否已经过期,也就是webpack进行了重新打包,manifest有变化,是的话,就check()检查变化的资源。

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

        检查hash是否过期的方法,使用了这样一个__webpack_hash,这个是webpack给出的一个常量(可以通过webpack官网查询),它表示资源的hash值,也就是已经在浏览器端加载的资源的hash值。而hash,从上文中,我们还记得,这个玩意儿是EventStream传过来的新的hash值。对的,没有看错,判断文件是否变化,就是这么简单。

        继续,check()方法。

        check()执行,从64行开始,module.hot.check(false, cb);源文档

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

        这就明白了这里module.hot.check检查webpack打包资源是否变化,将会沿着依赖树往上遍历。

        再看一下回调函数cb


        line33, 如果error,handleError()处理。handleError在line112-125很简单,就根据module.hot.status()获取hot module Replacement 进程的状态。(官方原话:Retrieve the current status of the hot module replacement process.),如果状态是abort或者fail,表示检查失败。。那么执行performReload().也就是刷新浏览器(window.location.reload()).

        回归正题,line35-42可以看到,虽然不是error,但是updatedModules为undefined,也就是同样没有找到,执行了一样的逻辑,performReload().

        line 52行,module.hot.apply()方法。为什么呢,还记得module.hot.check(false, cb)么,这里第一个参数为false表示需要手动调用module.hot.apply()。继续。

        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是否出现,出现的话,handleError处理。然后再次检查了hash是否最新,如果不是的话,重新执行了check().不知道会不会有死循环的风险。。。最后,就是logUpdate().没什么技术了就,简单区分了下,然后打印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()方法,看看官网是怎么介绍的。
      翻译成中文就是,当只有这一个timer处于active态时,并不需要事件循环去维持这个玩意儿,也就是process进程会退出。当然,如果有其他timer或者活动需要事件循环时,它也是可以跑着的。还不理解,这就是俗话:哎呀,我随便,你们要是没有吃的,我也不要了,要是有要的,就也给我一份。

      哈哈。
      测试一下!

      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);
      

    相关文章

      网友评论

          本文标题:webpack-hot-middleware解读

          本文链接:https://www.haomeiwen.com/subject/dtkjtxtx.html