写点什么

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

评论

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

Java基础25~XML和JSON解析,springbootoauth2教程

Java 程序员 后端

Java实现经典算法,阿里java技术专家面试

Java 程序员 后端

Java基础总结三(泛型、异常,阿里java面试题及答案

Java 程序员 后端

【架构设计模块九】:设计电商秒杀系统

Ryoma

Java岗开发3年,公司临时抽查算法,离职后这几题我记一辈子

Java 程序员 后端

架构实战模块二作业

吴霏

「架构实战营」

Java多线程 高频面试题,java架构师工作内容

Java 程序员 后端

Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day16

Java 程序员 后端

Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day26

Java 程序员 后端

Java实现图的存储和创建,linux编程基础课后答案第二章

Java 程序员 后端

架构实战营毕业总结

VE

架构实战课

Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day14

Java 程序员 后端

Java岗大厂面试百日冲刺【Day42】,java基础面试宝典

Java 程序员 后端

Java学习笔记——正则表达式,平安金服java面试题

Java 程序员 后端

Java实现RPC(源码),java常见面试题

Java 程序员 后端

Java岗开发者福利!Github热度飙升的多线程学习手册,已获31k+star

Java 程序员 后端

Java基础 - 多态,抽象类,接口,linux源码学习

Java 程序员 后端

Java基础面试题整理,java数组排序sort原理

Java 程序员 后端

架构实战营模块二学习总结

吴霏

「架构实战营」

Java如何支持函数式编程?,大专生面试阿里P7居然过了

Java 程序员 后端

033云原生之云服务测评指标体系

穿过生命散发芬芳

云原生 10月月更

Java基础09 面向对象~类和对象,阿里面试官必问

Java 程序员 后端

Java基础27~使用JDBC+连接池,这篇文章可以满足你80%日常工作

Java 程序员 后端

第二模块作业

River Tree

架构实战营

Java学习路线和方法推荐,拿下我人生中第7个Offer

Java 程序员 后端

Java多态大乱炖,mysql基础教程视频

Java 程序员 后端

linux去掉空行的几种方法

入门小站

Linux

Java基础面试题——IO流,springboot实战项目百度云

Java 程序员 后端

自由职业者靠什么赚钱?

石云升

职场经验 10月月更

java实现简单二叉树,深入分布式缓存从原理到实践技术分享

Java 程序员 后端

Java市场饱和了吗?现在转行学习Java会不会太晚了?

Java 程序员 后端

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