QCon北京|3天沉浸式学习,跳出信息茧房。 了解详情
写点什么

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

评论

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

瓴羊Quick BI自助式报表分析工具,令企业的运营服务更高效

夏日星河

基于云边协同架构的五大应用场景革新

阿里云CloudImagine

阿里云 边缘计算 边缘云

ansible2.4安装和体验

程序员欣宸

DevOps ansible 11月月更

我代码就加了一行log日志,结果引发了P1的线上事故

小小怪下士

Java 程序员 log 代码

如何通过链路追踪进行定时任务诊断

阿里巴巴云原生

阿里云 云原生 SchedulerX

面试合集:数据库+数据结构+JVM+网络+JAVA+分布式+操作系统

钟奕礼

Java 程序员 java面试 java编程

前端培训程序员不好招吗,应该怎么学习

小谷哥

Service Mesh 的下一站是 Sidecarless 吗?

SOFAStack

MOSN

java程序员,是不是很想进字节跳动?开发三年的我拿到了入职通知

钟奕礼

Java 程序员 java面试 java编程

深入了解瓴羊Quick BI,对于商业智能BI发展情况更好分析

巷子

K3S +Helm+NFS最小化测试安装部署只需十分钟

京东科技开发者

Docker k8s 软件测试 k3s 应用程序

java培训学习中怎么来提升开发水平

小谷哥

web前端和java培训学编程哪个更好

小谷哥

华夏银行:详解iDo平台一体化运维的落地过程

嘉为蓝鲸

运维 金融 银行 数字化

降价背后,函数计算规格自主选配功能揭秘

阿里巴巴云原生

阿里云 云原生 函数计算

阿里P8面试官总结的《2022最新年底java面试题》,搞定90%以上的技术面

钟奕礼

Java 程序员 java面试 java编程

MobPush 推送查询API

MobTech袤博科技

磐久网络|揭秘阿里云HAIL数据中心网络

云布道师

阿里云 数据中心 基础设施建设

Intel Arch SIG:介绍下一代数据中心互联协议CXL及在龙蜥的规划 | 第 54 期

OpenAnolis小助手

开源 直播 intel 龙蜥大讲堂 CXL

一个小而美的项目如何进行跨端选型

Onegun

移动端 跨端开发

New Features | NFTScan 推出 BlueChip、Watch List、Activity Overview

NFT Research

区块链 NFT 数据基础设施

火山引擎DataLeap的Data Catalog系统公有云实践

字节跳动数据平台

大数据 火山引擎 大数据研发

LeetCode题解:938. 二叉搜索树的范围和,栈,JavaScript,详细注释

Lee Chen

JavaScript LeetCode

制造业数字化发展,瓴羊Quick BI引起了需求者的关注

夏日星河

为什么mysql不推荐使用雪花ID作为主键

京东科技开发者

MySQL 数据 雪花id 主键 自增

TCL 基于 StarRocks 构建统一的数据分析平台

StarRocks

#数据库

供应链-数字化招投标/采购/供应商管理系统

金陵老街

数字化 Java‘’ Vue 3 spring-boot

又一创新!阿里云 Serverless 调度论文被云计算顶会 ACM SoCC 收录

Serverless Devs

教你用JavaScript完成进度条

小院里的霍大侠

JavaScript 编程开发 初学者 入门实战

前端培训程序员学习就业还有前途吗?

小谷哥

参加大数据培训学习还来得及吗

小谷哥

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