基于webpack的热重载live reload和热更新HMR(中)

本文涉及的产品
视频直播,500GB 1个月
简介: 在前端应用框架中不管是react还是vue,官方都提供了相应的脚手架方便开发者快速入手,当我们在开发时修改某个js或者css文件时,webpack会自动编译我们的文件,我们刷新浏览器就可以看到编译后的文件。为此我们会想,如果我们修改保存之后,文件被编译、浏览器自动刷新、或者浏览器局部刷新(不刷新整个浏览器),这样的话多好。当然,基于webpack打包工具的相关库已经实现了。下面对此部分流程做简单的分析

'webpack/hot/dev-server' 文件如下


// => module.hot 被替换成true:在前期ast语法树分析过程中标识代码位置,然后在webpack assets阶段被替换
// => module.hot 被替换成true:在前期ast语法树分析过程中标识代码位置,然后在webpack assets阶段被替换
if(module.hot) {
var lastHash;
var upToDate = function upToDate() {
  return lastHash.indexOf(__webpack_hash__) >= 0;
};
var check = function check() {
  module.hot.check(true).then(function(updatedModules) {
    if(!updatedModules) {
      console.warn("[HMR] Cannot find update. Need to do a full reload!");
      console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
      window.location.reload();
      return;
    }
    if(!upToDate()) {
      check();
    }
    require("./log-apply-result")(updatedModules, updatedModules);
    if(upToDate()) {
      console.log("[HMR] App is up to date.");
    }
  }).catch(function(err) {
    var status = module.hot.status();
    if(["abort", "fail"].indexOf(status) >= 0) {
      console.warn("[HMR] Cannot apply update. Need to do a full reload!");
      console.warn("[HMR] " + err.stack || err.message);
      // window.location.reload();
    } else {
      console.warn("[HMR] Update failed: " + err.stack || err.message);
    }
  });
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
  lastHash = currentHash;
  if(!upToDate() && module.hot.status() === "idle") {
    console.log("[HMR] Checking for updates on the server...");
    check();
  }
});
console.log("[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}


结论:被insert到客户端浏览器中的这段代码决定了 webpack热更新HMR 的开始,当热更新HMR模式失败时,就直接刷新浏览器了


const { setup } = require('../../util'); 文件如下


module.exports = {
  setup(config) {
    const defaults = { plugins: [], devServer: {} };
    const result = Object.assign(defaults, config);
    result.plugins.push(new webpack.HotModuleReplacementPlugin());
    result.plugins.push(new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.join(__dirname, '.assets/layout.html'),
      title: exampleTitle
    }));
    return result;
  }
};


webpack.HotModuleReplacementPlugin 插件的作用就是:在webpack打包生成的代码中添加功能代码,当我们开发时,修改某个文件并保存后,浏览器会拿到修改的模块代码,然后执行并更新依赖, 当然浏览器如何拿到代码以及如何执行更新,下面会讲到,这里先提一下这个插件的作用


webpack entry 入口文件app.js


'use strict';
require('./example');
if (module.hot) {
  module.hot.accept((err) => {
    if (err) {
      console.error('Cannot apply HMR update.', err);
    }
  });
}


webpack entry 入口文件example.js


'use strict';
const target = document.querySelector('#target');
target.innerHTML = 'Modify to update this element without reloading the page.';


html Template 模板文件


<!doctype html>
<html>
  <head>
    <title>WDS ▻ API: Simple Server</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="shortcut icon" href="/.assets/favicon.ico"/>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600|Source+Sans+Pro:400,400i,500,600"/>
    <link rel="stylesheet" href="/.assets/style.css"/>
  </head>
  <body>
    <main>
      <header>
        <h1>
          <img src="/.assets/icon-square.svg" style="width: 35px; height: 35px;"/>
          webpack-dev-server
        </h1>
      </header>
      <section>
        <h2>API: Simple Server</h2>
        <div id="target"></div>
      </section>
       <section>
        <div id="targetmodule"></div>
      </section>
    </main>
  <script type="text/javascript" src="main.js"></script></body>
</html>


以上是涉及到的一些文件...


下面来看具体的效果:运行 node --inspect-brk  server.js 文件, 访问http://localhost:8080



上图左侧是 webpack-dev-server 中 websockets server端的代码,借助webpack-dev-middleware注册webapck打包生命周期事件回调函数,将打包过程关键生命点同步到客户端浏览器(右侧) ,从console处可以知道收到了消息类型type:hot、hash、ok。其中hot类型是告诉客户端浏览器更新代码的方式采用 热更新HMR 的方式, 而不是采用热重载live reload 直接刷新浏览器的方式,hash是本次webpack打包后的hash值, ok标识webpack打包生命周期已经完成,可以进行客户端浏览器代码的更新操作了,也就是 webpack热更新HMR的过程。


下面当修改 example.js 文件 也就是浏览器如何更新代码流程 关键时刻到了


//target.innerHTML = 'Modify to update this element without reloading the page.';
target.innerHTML = '热更新HMR的模式';


文件变化后,webpack.HotModuleReplacementPlugin 插件 中关键的 webpack Compilation 对象事件回调函数如下


compilation.plugin("record", function(compilation, records) {
  // 生成的 records 用于当文件变化后找出变话的模块
  debugger
  if(records.hash === this.hash) return;
  records.hash = compilation.hash;
  records.moduleHashs = {};
  // 循环每个module, webpack中一个文件就是一个module,且通过hash值判断文件是否有更改
  this.modules.forEach(function(module) {
    var identifier = module.identifier();
    var hash = require("crypto").createHash("md5");
    module.updateHash(hash);
    records.moduleHashs[identifier] = hash.digest("hex");
  });
  records.chunkHashs = {};
  // this webpack compilation 对象
  this.chunks.forEach(function(chunk) {
    records.chunkHashs[chunk.id] = chunk.hash;
  });
  records.chunkModuleIds = {};
  this.chunks.forEach(function(chunk) {
    records.chunkModuleIds[chunk.id] = chunk.modules.map(function(m) {
      return m.id;
    });
  });
});
var initialPass = false;
var recompilation = false;
compilation.plugin("after-hash", function() {
  // records 相应的hash 决定模块变化之后的标识
  debugger
  var records = this.records;
  if(!records) {
    initialPass = true;
    return;
  }
  if(!records.hash)
    initialPass = true;
  var preHash = records.preHash || "x";
  var prepreHash = records.prepreHash || "x";
  if(preHash === this.hash) {
    recompilation = true;
    this.modifyHash(prepreHash);
    return;
  }
  records.prepreHash = records.hash || "x";
  records.preHash = this.hash;
  // complain 对象的hash值
  this.modifyHash(records.prepreHash);
});
compilation.plugin("additional-chunk-assets", function() {
  // 这里当modul变化之后,找出变化的module 并并生成json 和对应的module Template模板信息
  debugger
  var records = this.records;
  if(records.hash === this.hash) return;
  if(!records.moduleHashs || !records.chunkHashs || !records.chunkModuleIds) return;
  // 循环遍历module 通过hash值标识module是否变化了
  this.modules.forEach(function(module) {
    var identifier = module.identifier();
    var hash = require("crypto").createHash("md5");
    module.updateHash(hash);
    hash = hash.digest("hex");
    module.hotUpdate = records.moduleHashs[identifier] !== hash;
  });
  // this.hash  webpack Compilation 对象的hash值
  var hotUpdateMainContent = {
    h: this.hash,
    c: {}
  };
  // records.chunkHashs 包含了 所有chunk的hash值信息
  Object.keys(records.chunkHashs).forEach(function(chunkId) {
    chunkId = isNaN(+chunkId) ? chunkId : +chunkId;
    // 修改文件导致module 变化 => 找到对应的chunk
    var currentChunk = this.chunks.find(chunk => chunk.id === chunkId);
    if(currentChunk) {
      // 通过chunk 来确定是哪个module变化了
      var newModules = currentChunk.modules.filter(function(module) {
        return module.hotUpdate;
      });
      var allModules = {};
      currentChunk.modules.forEach(function(module) {
        allModules[module.id] = true;
      });
      // 如果项目中有某个模块没有引用了 就会找出改模块
      var removedModules = records.chunkModuleIds[chunkId].filter(function(id) {
        return !allModules[id];
      });
      // 如果发生了模块module的变化
      if(newModules.length > 0 || removedModules.length > 0) {
        // 根据变化的module 得到 module字符串模板
        var source = hotUpdateChunkTemplate.render(chunkId, newModules, removedModules, this.hash, this.moduleTemplate, this.dependencyTemplates);
        var filename = this.getPath(hotUpdateChunkFilename, {
          hash: records.hash,
          chunk: currentChunk
        });
        this.additionalChunkAssets.push(filename);
        // filename 就是: `${currentChunk}.${records.hash}.hot-update.js}` => 0.9236d98784cee1af7a96.hot-update.js文件
        this.assets[filename] = source;
        // 标识module变化了
        hotUpdateMainContent.c[chunkId] = true;
        currentChunk.files.push(filename);
        this.applyPlugins("chunk-asset", currentChunk, filename);
      }
    } else {
      hotUpdateMainContent.c[chunkId] = false;
    }
  }, this);
  // 下面是 `${records.hash}.hot-update.json` => 9236d98784cee1af7a96.hot-update.json 文件内容
  var source = new RawSource(JSON.stringify(hotUpdateMainContent));
  var filename = this.getPath(hotUpdateMainFilename, {
    hash: records.hash
  });
  this.assets[filename] = source;
  // 注: 以上添加到this.assets 的内容在 Compiler.emitAssets 阶段 生成文件内容
});


结论: 当文件变化后,webpack 就会编译生成 hot-update.json、以及对应的文件模块hot-update.js信息 用于在Compiler.emitAssets 阶段生成js文件


相关文章
|
1月前
|
API 开发工具 开发者
webpack热更新原理
Webpack的Hot Module Replacement(HMR)提升开发效率,无需刷新页面即可更新模块。开启HMR需在配置中设`devServer.hot: true`。Webpack构建时插入HMR Runtime,通过WebSocket监听并处理文件变化。当模块改变,Webpack发送更新到浏览器,HMR Runtime找到对应模块进行热替换,保持应用状态。开发者可利用`module.hot` API处理热替换逻辑。
|
1月前
|
前端开发 测试技术 开发者
深入理解 Webpack 热更新原理:提升开发效率的关键
深入理解 Webpack 热更新原理:提升开发效率的关键
|
1月前
|
自然语言处理 JavaScript 前端开发
webpack 的热更新是如何做到的?原理是什么?
webpack 的热更新是如何做到的?原理是什么?
48 0
|
1月前
|
前端开发 JavaScript 算法
面试官:【webpack和vite的区别?vite一定比webpack快吗?vite的缺点是什么?webpack的热更新和vite的热更新区别?】
面试官:【webpack和vite的区别?vite一定比webpack快吗?vite的缺点是什么?webpack的热更新和vite的热更新区别?】
377 0
|
资源调度 前端开发 JavaScript
配置多入口 Webpack 热更新失效? #120
配置多入口 Webpack 热更新失效? #120
141 0
|
移动开发 前端开发 JavaScript
Webpack 如何配置热更新 #96
Webpack 如何配置热更新 #96
211 0
|
前端开发
【前端】解决 webpack5 热更新时间太长的问题
【前端】解决 webpack5 热更新时间太长的问题
440 0
Golang:使用air实现gin应用的live reload热重载
Golang:使用air实现gin应用的live reload热重载
327 0
|
资源调度 JavaScript
vue-cli3+webpack热更新失效问题
A项目中遇到问题,热更新失效,百思不得其解,查询搜索vuecli3热更新失效、vue histroy 模式热更新失效,网上看到不少方法,npm重新安装,不要用淘宝镜像cnpm安装;npm安装yarn,再用yarn重新install,yarn serve启动,在npm run serve 启动等方法都不好用。github有类似问题3.0.0-beta.16 热更新失效 · Issue #1559 · vuejs/vue-cli · GitHub,有次得到启发可能版本不同导致。 对比热更新正常的项目B的几个配置文件,重点查看package.json文件,发现有webpack版本不同。
394 0
vue-cli3+webpack热更新失效问题
Webpack插件使用及热更新打包
Webpack插件使用及热更新打包
113 0