9 月 13 日,2025 Inclusion・外滩大会「开源嘉年华」正在限量报名中! 了解详情
写点什么

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

评论

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

“祥龙守神州,舞瑞中国年”,京东超市携手王牌驼喜迎新春

科技热闻

浅谈LocalCache | 京东云技术团队

京东科技开发者

Palworld幻兽帕鲁世界参数修改最佳实践(Ubuntu)

天翼云开发者社区

云计算 最佳实践 云服务器

OurBMC大咖说 | OurBMC,共创国产软硬件开源发展新纪元

OurBMC

大咖说 软硬件开源 BMC技术全栈

高效率软件开发工具,提速开发,真的很赞!

互联网工科生

软件开发 低代码 JNPF

如何使用低代码+定制,打造一个个性化的社交媒体平台?

天津汇柏科技有限公司

低代码 定制软件开发 软件开发定制

有了ERP和MES,还需要质量管理QMS系统吗?

万界星空科技

数字化 生产管理系统 mes 万界星空科技 QMS

大文件上传原理及实现方案 | 京东物流技术团队

京东科技开发者

假期想学习,送你测试开发+人工智能大礼包

霍格沃兹测试开发学社

迎龙年接新春,来华为手机里寻找祥龙

最新动态

全新 Amazon S3 Express One Zone 高性能存储类服务,震撼发布!

亚马逊云科技 (Amazon Web Services)

4份报告简读Java生态

4ye

JVM, Java’

KubeEdge v1.16.0 版本发布!10项新增特性

华为云开发者联盟

k8s 开发 华为云 kubeedge 华为云开发者联盟

OurBMC 社区 SIG 建设月报(2023 年 10 月)

OurBMC

SIG月报 SIG进展

探索大模型训练与多模态数据处理

百度开发者中心

人工智能 图像 大模型训练

通义灵码——灵动指间,快码加编,你的智能编码助手

阿里巴巴云原生

阿里云 云原生

【教程】一个比较良心的C++代码混淆器

Kubeadmiral 开源编程挑战 —— 我觉得不错

miraclejzd

字节跳动 Kubernetes 云原生 Kubeadmiral

WiFi 7/QCN9274: Connecting the super network of the future

wallysSK

这篇深入浅出贴 助你早日实现Stable diffusion自由

京东科技开发者

Wireshark中的http协议包分析

小齐写代码

Seal 新春大挑战等你来参与!

SEAL安全

AI DevOps Walrus

100%中奖、会员回馈礼…星河会员新春福利到!

飞桨PaddlePaddle

百度 飞桨 飞桨AI 飞桨星河社区

自动化测试,有最佳实践吗?

老张

软件测试 自动化测试

部署Palworld幻兽帕鲁服务器最佳实践(Ubuntu)

天翼云开发者社区

云计算 最佳实践 服务器 云服务器

OurBMC社区首场Meetup成功举办,共建BMC产业生态

OurBMC

Meetup 汇聚智力 共建BMC

玩转OurBMC第一期:社区操作指南-功能篇

OurBMC

玩转OurBMC 操作指南 基本功能

OpenSPG新版发布:大模型知识抽取与快速知识图谱构建

百度开发者中心

人工智能 知识图谱 智能客服 大模型

IT工单治理野史:由每周最高150+治理到20+ | 京东物流技术团队

京东科技开发者

测试开发+人工智能大礼包,让你在假期实现弯道超车

测试人

软件测试

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