写点什么

Webpack 热刷新与热加载的原理分析

  • 2019-11-01
  • 本文字数:5635 字

    阅读完需:约 18 分钟

Webpack热刷新与热加载的原理分析

Webpack 功能非常强大,一款优秀的脚手架可以给我们的工作省去众多繁琐且无意义的工作,其中热刷新、热加载相较于传统开发大大提高了开发效率。

首先简单介绍下热刷新与热加载:

热刷新:文件内动改动后,整个页面自动刷新,不保留任何状态,相当于 webpack 帮你摁了 F5 刷新;

热加载:文件改动后,以最小的代价改变被改变的区域。尽可能保留改动文件前的状态(修改 js 代码之后可以把页面输入的部分信息保留下来)

本文主要讲下我在配置热刷新、热加载时遇到的坑,边踩坑边成长,希望对初学者追查定位问题有帮助(基于 Webpack4)。

内容

首先介绍下研究问题的项目场景,整个项目是基于前端 JS (使用 webpack 构建启动) + Node (中间服务器)+PHP 后端的开发模式,由于项目需要登录登录信息,故使用 Node 做登录验证,需要本地绑定 Host(127.0.0.1 test.link.lianjia.com),使用域名 test.link.lianjia.com 在浏览器访问。我惊奇的发现前端居然未使用任何热更新,每次 js 代码修改之后需要手动刷新页面,强迫症的我开始给这个项目加热更新,就有了下述这篇文章。


1.Webpack 热刷新


简单来说 Webpack 热刷新就是 Webpack-dev-server 会启动一个 Socket 服务,来监听 webpack 打包文件变化,有文件更新时 webpack-dev-server 通知浏览器某个文件变化了,浏览器就会刷新当前页面。


在项目中我使用了以下配置:


devServer: {    contentBase: './dist',    port: '7008',    inline: true,    host: '0.0.0.0'}
复制代码


在项目中使用以上配置时发现当代码更新时,浏览器并不会自动刷新页面,感觉很奇怪,配置貌似没有问题。为了避免其它因素干扰,自己写了一个小 Demo(只有前端,无 Node 服务),用了相同的配置(仅修改了端口号)去尝试热更新,在浏览器内使用 IP 127.0.0.1:7008 访问,发现配置是没问题的,可以实现正常的热刷新。(伏笔:在小 Demo 中使用的是 IP 去访问)但是本项目在浏览器中却发现收不到 Websocket 文件。


问题原因


此时认为配置文件是没有问题的,质疑热刷新失败是否与 Node 服务有关系,然后就是各种源码阅读尝试,后来发现问题与 Node 无关,这里就不细说排查 Node 的过程。接下来质疑是否与 Host 文件有关系,因为之前在 Demo 使用 IP 去访问并没有问题,而本项目中是通过域名来访问。再次通过域名访问 Demo 项目,发现无法访问了,报错如下:



原因历经千辛万苦终于定位到了问题在哪里,那么究竟是 Webpack-dev-server 的问题,还是我系统配置的 Host 问题呢?接下来查看了 Webpack-dev-server 源码。


2.Webpack-dev-server 源码分析


在 webpack-dev-server\lib\Server.js 中,看到以下代码:


Server.prototype.checkHost = function (headers) {   if (this.disableHostCheck) return true; // 1  const hostHeader = headers.host;  if (!hostHeader) return false; //2  const hostname = url.parse(`//${hostHeader}`, false, true).hostname;  if (ip.isV4Format(hostname) || ip.isV6Format(hostname)) return true; //3  if (hostname === 'localhost') return true; //3  if (this.allowedHosts && this.allowedHosts.length) { // 4    for (let hostIdx = 0; hostIdx < this.allowedHosts.length; hostIdx++) {      const allowedHost = this.allowedHosts[hostIdx];      if (allowedHost === hostname) return true;      if (allowedHost[0] === '.') {        if (hostname === allowedHost.substring(1)) return true;        if (hostname.endsWith(allowedHost)) return true;      }    }  }  if (hostname === this.listenHostname) return true; //5  if (typeof this.publicHost === 'string') { // 6    const idxPublic = this.publicHost.indexOf(':');    const publicHostname = idxPublic >= 0 ? this.publicHost.substr(0, idxPublic) : this.publicHost;    if (hostname === publicHostname) return true;  }  return false; // 7};
复制代码


而调用 checkHost 的代码如下:


  app.all('*', (req, res, next) => {       if (this.checkHost(req.headers)) { return next(); }      res.send('Invalid Host header');  });
复制代码


可以看到 如果 checkHost 返回了 fasle,那就只会返回 Invalid Host header,与我们看到的现象一致。


再来看看 checkHost 函数的返回值会受到哪些参数影响:


1)如果使用了 disableHostCheck,关闭校验,则直接返回 true;


2)如果请求头的 host 是空,返回 false;


3)如果请求网站时用的 host 是 localhost,返回 true;


4)启用白名单,如果访问的 host 地址在白名单中,返回 true;


5)访问的 host 与 webpack 配置的 host 相同时 返回 true;


6)返回的 host 与设置的 publicHost 相同时,返回 true;


7)不满足以上规则,返回 false


根据本项目的情况,hostname是test.link.lianjia.com,设置的 host 是 0.0.0.0(即本地 IP 或者 127.0.0.1),不满足上述所有规则,因此最终返回了 false。待我设置了白名单后,就可以愉快的开启 webpack 的自动刷新功能了。


3.更进一步,开启热加载


自动刷新成功了,现在想要更进一步开启热加载。参考新版官网,新的配置如下:


devServer: {        contentBase: './dist',        port: '7008',        inline: true,        host: '0.0.0.0',        disableHostCheck: true,//不使用白名单的原因是多人开发,每个人都需要绑定Host不方便,因此关闭Host检查        hot: true,  //开启热更新},plugins: [        new MiniCssExtractPlugin({            filename: "css/[name].css",        }),        new HtmlWebpackPlugin({            title: 'Output Management'        }),        new webpack.HotModuleReplacementPlugin() //开启热更新],// 在webpack 入口js底部加上以下代码if (module.hot) {    module.hot.accept();}
复制代码


晴天霹雳,又报错了,具体如下图:



遇见了平时后端常见的跨域问题,这是因为 webpack-dev-server 实质上是用 express 起了一个 Node 服务。看见这个问题,脑子一热上来就使用了 proxy 代理,配置如下:


proxy: {  '/': {      target: 'http://我的IP地址',      // target: 'http://127.0.0.1',      // target: 'http://localhost',      changeOrigin: true  }}
复制代码


发现怎么弄都不成功,后来查阅资料才知道 proxy 是用于向其他服务器请求的代理,而不是对于 webpack-dev-server 自身启动的服务。


在浏览官方文档后发现其提供了 Before API,用于在启动服务前处理事物,我在其中添加 CORS 尝试能否解决问题。



按照官方的 API 我们加上简单的 CORS 配置:


before: (app) => {  app.use('*', (req, res, next) => {    res.header("Access-Control-Allow-Origin", "*");    res.header("Access-Control-Allow-Methods", "POST,GET");    res.header("Access-Control-Allow-Headers", "Origin,Accept,Content-Type,Content-Length, Authorization, Accept,X-Requested-With");    next();  });}
复制代码


再保存更改的文件试试,热更新终于成功,终于可以愉快的 Coding 了。


4.源码分析


问题解决了,但我们再去看看 webpack 相关的源码看看上述配置都做了些什么。


文件在 webpack-server-dev\client\index.js


ok: function ok() {sendMsg('Ok');if (useWarningOverlay || useErrorOverlay) overlay.clear();if (initial) return initial = false; reloadApp();}
复制代码


这是监听到文件变化后 socket 调用的函数,可以看到它最主要功能就是调用 reloadAPP(),我们再来看看 reloadApp


 1function reloadApp() { 2  if (isUnloading || !hotReload) { 3    return; 4  } 5  if (_hot) { 6    log.info('[WDS] App hot update...'); 7    var hotEmitter = require('webpack/hot/emitter'); 8    hotEmitter.emit('webpackHotUpdate', currentHash); 9    if (typeof self !== 'undefined' && self.window) {10      self.postMessage('webpackHotUpdate' + currentHash, '*');11    }12  } else {13    ...//非重要代码省略14  }
复制代码


可以看到如果开启热更新后,会用 webpack/hot/emitter 触发一个名为 webpackHotUpdate 的事件,找到这个事件,位置在 webpack\hot\dev-server.js


 1var upToDate = function upToDate() { 2  return lastHash.indexOf(__webpack_hash__) >= 0; 3}; 4 5var check = function check() { 6  module.hot 7    .check(true) 8    .then(function(updatedModules) { 9      if (!updatedModules) {10        log("warning", "[HMR] Cannot find update. Need to do a full reload!");11        log(12          "warning",13          "[HMR] (Probably because of restarting the webpack-dev-server)"14        );15        window.location.reload();16        return;17      }1819      if (!upToDate()) {20        check();21      }2223      require("./log-apply-result")(updatedModules, updatedModules);2425      if (upToDate()) {26        log("info", "[HMR] App is up to date.");27      }28    })29    .catch(function(err) {30      var status = module.hot.status();31      if (["abort", "fail"].indexOf(status) >= 0) {32        log(33          "warning",34          "[HMR] Cannot apply update. Need to do a full reload!"35        );36        log("warning", "[HMR] " + err.stack || err.message);37        window.location.reload();38      } else {39        log("warning", "[HMR] Update failed: " + err.stack || err.message);40      }41    });42};
复制代码


upToDate 是判断最后一次前端的 hash 与 webpack 打包后的文件 hash 是否相同,相同返回 true(即表示最新的文件)。再看 module.hot 里的几个判断,updatedModules 是更新的模块,如果找不到更新的模块,则直接刷新网页。接下来通过判断文件 hash,如果两个文件 hash 相同了已经更新完了,调用


require("./log-apply-result")(updatedModules, updatedModules);
复制代码


打印 log,可以看看这个代码。


 1module.exports = function(updatedModules, renewedModules) { 2  //renewedModules表示要更新的模块,updatedModules表示已更新的模块 3    var unacceptedModules = updatedModules.filter(function(moduleId) { 4        return renewedModules && renewedModules.indexOf(moduleId) < 0; 5    }); 6    var log = require("./log"); 7 8    if (unacceptedModules.length > 0) { //不需要热更新的模块 9        log(10            "warning",11            "[HMR] The following modules couldn't be hot updated: (They would need a full reload!)"12        );13        unacceptedModules.forEach(function(moduleId) {14            log("warning", "[HMR]  - " + moduleId);15        });16    }1718    if (!renewedModules || renewedModules.length === 0) { //没有模块可以更新19        log("info", "[HMR] Nothing hot updated.");20    } else {21        log("info", "[HMR] Updated modules:");22        renewedModules.forEach(function(moduleId) { //热更新模块23            if (typeof moduleId === "string" && moduleId.indexOf("!") !== -1) {24                var parts = moduleId.split("!");25                log.groupCollapsed("info", "[HMR]  - " + parts.pop());26                log("info", "[HMR]  - " + moduleId);27                log.groupEnd("info");28            } else {29                log("info", "[HMR]  - " + moduleId);30            }31        });32        var numberIds = renewedModules.every(function(moduleId) {33            return typeof moduleId === "number";34        });35        if (numberIds) //如果renewedModules中都是数字,建议使用NamedModulesPlugin36            log(37                "info",38                "[HMR] Consider using the NamedModulesPlugin for module names."39            );40    }41};
复制代码


最后,简单梳理下一下热更新的流程图:


总结

如果是配置需求问题,先看官方文档,很多问题可以在文档中找到答案。如果在文档中找不到,可以花时间一步一步源码追踪,重点关注报错信息,按报错内容一步一步查找,终究能找到原因,从而解决问题。最后附上完整的 webpack4 配置代码。


在 webpack4.0 下热刷新配置:


devServer: {    contentBase: './dist',    port: '7008',    inline: true,   host: '0.0.0.0'   disableHostCheck: true, // 关闭Host检查  // allowedHosts: ['你的域名'] //白名单}
复制代码


热加载完整配置:


// webpack config 文件devServer: {  contentBase: './dist',  port: '7008',  inline: true,  host: '0.0.0.0',  disableHostCheck: true,  hot: true,  before: (app) => {    app.use('*', (req, res, next) => {      res.header("Access-Control-Allow-Origin", "*");      res.header("Access-Control-Allow-Methods", "POST,GET");      res.header("Access-Control-Allow-Headers", "Origin,Accept,Content-Type,Content-Length, Authorization, Accept,X-Requested-With");      next();    });  }},plugins: [  new MiniCssExtractPlugin({    filename: "css/[name].css",  }),  new HtmlWebpackPlugin({    title: 'Output Management'  }),  new webpack.HotModuleReplacementPlugin() ]// webpack 配置的入口js文件,在末尾加上if (module.hot) {    module.hot.accept();}
复制代码


见解有限,如有描述不当之处,请帮忙指出。


作者介绍:


张名攀,贝壳前端开发工程师,目前负责 NTS-APP 前端研发工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/NsQjFfDL_PR_gtmaeZkYHA


2019-11-01 11:503130

评论

发布
暂无评论
发现更多内容

AI绘图实践-用人工智能生图助力618大促

京东零售技术

AIGC 企业号2024年7月PK榜

深耕分析型数据库领域,火山引擎ByteHouse入围《2024爱分析数据库厂商全景报告》

字节跳动数据平台

数据库 大数据 云原生 bytehouse Click house

开源数据库Greenplu突然闭源?GaussDB(DWS)提供数仓新可能

华为云开发者联盟

数据库 postgresql 华为云 华为云开发者联盟 企业号2024年7月PK榜

软件测试学习笔记丨JUnit5自定义动态测试的执行顺序

测试人

软件测试

三连冠!百度文库再次登顶AI产品榜

Geek_2d6073

数字身份管理发展趋势:IAM 和数据安全相结合

芯盾时代

数字身份 身份安全 数据安全 iam 统一身份认证

万界星空科技汽车零部件行业MES介绍

万界星空科技

mes 汽车行业 汽车零部件 汽车零配件行业 汽车零部件mes

ICE启动AI:人工智能高频交易平台测试进入尾段

科技热闻

从0到100:4S店服务小程序开发历程

CC同学

IoTDB 征文活动评选结果新鲜出炉!

Apache IoTDB

透明LED显示屏:引领现代新视觉

Dylan

技术 LED LED显示屏 全彩LED显示屏 led显示屏厂家

解读jd.item_search_img API返回值:京东按图搜索的智能匹配

技术冰糖葫芦

API Explorer API 调试 API 文档 API 协议

【程序大侠传】大表分库分表切换数据库类型导致pagehelper生成sql语法报错

Disaster

飞书邀请ToB伙伴们一起共建“生态绿洲”!

ToB行业头条

ICE启动AI:人工智能高频交易平台测试进入尾段

科技汇

性能测试:行业流行性能剖析工具介绍

测试人

软件测试

深入理解JVM:内存管理与垃圾回收机制探索

乘云数字DataBuff

JVM 内存

千帆大模型平台升级十大能力,企业级 RAG 全面升级

Baidu AICLOUD

千帆大模型平台 rag

集业界领袖,话数据库未来,华为云数据库斯享会深圳站成功举办

Geek_2d6073

Webpack热刷新与热加载的原理分析_文化 & 方法_张名攀_InfoQ精选文章