Agentic AI、具身智能、强化学习框架、端侧大模型……来QCon上海站,感受AI的未来! 了解详情
写点什么

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:503421

评论

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

如何降低代码的复杂度?

代码生成器研究

云桌面是什么?好用的云桌面推荐?

青椒云云电脑

云桌面 云桌面解决方案

当我跑越野时,我在想为什么

escray

技术人写作 21 天技术人写作行动营 21 天

行业首家!百度通过DCMM 4级乙方云服务商最高认证,数据管理能力行业领先

百度安全

Mural在线白板最全解析!Mural功能|发展历程|替代软件推荐!

彭宏豪95

科技 在线白板 办公软件 在线协作 效率软件

如何降低代码的复杂度?

代码生成器研究

Last Week in Milvus

Zilliz

非结构化数据 Milvus Zilliz AIGC

卫龙 x 赛博威 | 为民族品牌数字化建设添砖加瓦

赛博威科技

营销费用管理 赛博威 卫龙

强大的自动化清理推荐Hazel 激活最新版

胖墩儿不胖y

Mac软件 自动化清理工具 Mac软件清理软件

阿里云数据库MongoDB版助力掌阅平滑上云,撬动数据红利

极客天地

直播预告 | 降本增效持续深化,如何找准 FinOps 关键着力点?

小红书技术REDtech

云原生 成本优化 FinOps

HashMap超全源码详解(JDK1.8)

是月月啊2023

Java 面试题

工作以来最有成就感的事(深度思考)

Java 工程师蔡姬

21 天技术人写作行动营 #个人总结 #工作总结 #最有成就感的事 #职场思考

SQL 数据库语句- 创建和管理数据库

小万哥

MySQL 数据库 程序员 sql 后端开发

[译]原生CSS嵌套使用

南城FE

CSS 前端 预处理器 嵌套

5 种主要的云电脑解决方案 - 不同之处

青椒云云电脑

云桌面 云电脑 云桌面解决方案

《21 天技术人写作行动营》--工作后最有成就感的一件事

IT蜗壳-Tango

2023,“科技无障碍”不谈价值观

脑极体

AI

分布式系统硬件资源池原理和接入实践

HarmonyOS开发者

HarmonyOS

Python必备:Pandas中rolling方法指南

秃头小帅oi

前端 爬虫 pandas Python Monad rolling

一种典型的负载均衡解决方案

极客罗杰

英特尔是如何实现玻璃基板的?

E科讯

双十一 |顺应平台趋势,在数据中寻找更多生意机会

赛博威科技

数据分析 双十一 电商大促

青椒云云桌面—低配电脑秒变高性能设计神器

青椒云云电脑

桌面云 云桌面 云桌面系统

Wireshark中的http协议包分析

小魏写代码

Java——编程语言中的王者

伤感汤姆布利柏

Java 后端 低代码

您距离一个成熟安全的 DevOps 平台,只差一个迁移

极狐GitLab

DevOps gitlab Atlassian Gartner Bamboo

一体化IT运维管理系统选哪家?理由是什么?

行云管家

运维 IT运维 安全运维 一体化运维

云桌面系统如何使用?云桌面的优势有哪些?

青椒云云电脑

云桌面 云桌面解决方案 云桌面系统

只要你想,你就能找到一种方法

学渣汪在央企打怪升级

输出内容价值 | 极客写作训练营

6点无痛早起学习的和尚

代码坏味道

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