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
评论