史上最详细的webpack讲解(了不起的WebpackHMR)

史上最详细的webpack讲解(了不起的WebpackHMR)(1)

作者:王平安

转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q

目录

了不起的 Webpack HMR 学习指南(上)「含源码讲解」 本篇

了不起的 Webpack HMR 学习指南(下)「含源码讲解」

一、HMR 介绍

Hot Module Replacement(以下简称:HMR 模块热替换)是 Webpack 提供的一个非常有用的功能,它允许在 JavaScript 运行时更新各种模块,而无需完全刷新

Hot Module Replacement (or HMR) is one of the most useful features offered by webpack. It allows all kinds of modules to be updated at runtime without the need for a full refresh. --《Hot Module Replacement》

史上最详细的webpack讲解(了不起的WebpackHMR)(2)

当我们修改代码并保存后,Webpack 将对代码重新打包,HMR 会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。HMR 主要通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面时丢失的应用程序状态;
  • 只更新变更内容,以节省宝贵的开发时间;
  • 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。

需要注意:HMR 不适用于生产环境,这意味着它应当只在开发环境使用。

二、HMR 使用方式

在 Webpack 中启用 HMR 功能比较简单:

1. 方式一:使用 devServer1.1 设置 devServer 选项

只需要在 webpack.config.js 中添加 devServer 选项,并设置 hot 值为 true ,并使用HotModuleReplacementPlugin 和 NamedModulesPlugin (可选)两个 Plugins :

//webpack.config.js constpath=require('path') constwebpack=require('webpack') module.exports={ entry:'./index.js', output:{ filename:'bundle.js', path:path.join(__dirname,'/') }, devServer:{ hot:true,//启动模块热更新HMR open:true,//开启自动打开浏览器页面 }, plugins:[ newwebpack.NamedModulesPlugin(), newwebpack.HotModuleReplacementPlugin() ] }

1.2 添加 scripts

然后再 package.json 中为 scripts 命令即可:

//package.json { //... "scripts":{ "start":"webpack-dev-server" }, //... }

2. 方式二、使用命令行参数

另一种是通过添加 --hot 参数来实现。添加 --hot 参数后,devServer 会告诉 Webpack 自动引入 HotModuleReplacementPlugin ,而不需要我们手动引入。另外常常也搭配 --open 来自动打开浏览器到页面。这里移除掉前面添加的两个 Plugins :

//webpack.config.js constpath=require('path') constwebpack=require('webpack') module.exports={ //... -plugins:[ -newwebpack.NamedModulesPlugin(), -newwebpack.HotModuleReplacementPlugin() -] }

然后修改 package.json 文件中的 scripts 配置:

//package.json { //... "scripts":{ -"start":"webpack-dev-server" "start":"webpack-dev-server--hot--open" }, //... }

3. 简单示例

基于上述配置,我们简单实现一个场景:index.js 文件中导入hello.js 模块,当 hello.js 模块发生变化时, index.js 将更新模块。模块代码如下实现:

//hello.js exportdefault()=>'hileo!'; //index.js importhellofrom'./hello.js' constdiv=document.createElement('div'); div.innerHTML=hello(); document.body.appendChild(div);

然后再 index.html 中导入打包后的 JS 文件,并执行 npm start 运行项目:

<!DOCTYPEhtml> <htmllang="en"> <head> <metacharset="UTF-8"> </head> <body> <div>了不起的WebpackHMR学习指南</div> <scriptsrc="bundle.js"></script> </body> </html>

4. 实现监听更新

当我们通过 HotModuleReplacementPlugin 插件启用了 HMR,则它的接口将被暴露在全局 module.hot 属性下面。通常,可以先检查这个接口是否可访问,然后再开始使用它。举个例子,你可以这样 accept 一个更新的模块:

if(module.hot){ module.hot.accept('./library.js',function(){ //使用更新过的library模块执行某些操作... }) }

关于 module.hot 更多 API ,可以查看官方文档《Hot Module Replacement API》 。回到上面示例,我们测试更新模块的功能。这时我们修改 index.js 代码,来监听 hello.js 模块中的更新:

importhellofrom'./hello.js'; constdiv=document.createElement('div'); div.innerHTML=hello(); document.body.appendChild(div); if(module.hot){ module.hot.accept('./hello.js',function(){ console.log('现在在更新hello模块了~'); div.innerHTML=hello(); }) }

然后修改 hello.js 文件内容,测试效果:

-exportdefault()=>'hileo!'; exportdefault()=>'hileo!helloworld';

当我们保存代码时,控制台输出 "现在在更新 hello模块了~" ,并且页面中"hi leo!" 也更新为 "hi leo! hello world" ,证明我们监听到文件更新了。

史上最详细的webpack讲解(了不起的WebpackHMR)(3)

简单 Webpack HMR 使用方式就介绍到这,更多介绍,还请阅读官方文档《Hot Module Replacement》。

5. devServer 常用配置和技巧5.1 常用配置

根据目录结构的不同,contentBase、openPage 参数要配置合适的值,否则运行时应该不会立刻访问到你的首页。同时要注意你的 publicPath,静态资源打包后生成的路径是一个需要思考的点,取决于你的目录结构。

devServer:{ contentBase:path.join(__dirname,'static'),//告诉服务器从哪里提供内容(默认当前工作目录) openPage:'views/index.html',//指定默认启动浏览器时打开的页面 index:'views/index.html',//指定首页位置 watchContentBase:true,//contentBase下文件变动将reload页面(默认false) host:'localhost',//默认localhost,想外部可访问用'0.0.0.0' port:8080,//默认8080 inline:true,//可以监控js变化 hot:true,//热启动 open:true,//启动时自动打开浏览器(指定打开chrome,open:'GoogleChrome') compress:true,//一切服务都启用gzip压缩 disableHostCheck:true,// true:不进行host检查 quiet:false, https:false, clientLogLevel:'none', stats:{//设置控制台的提示信息 chunks:false, children:false, modules:false, entrypoints:false,//是否输出入口信息 warnings:false, performance:false,//是否输出webpack建议(如文件体积大小) }, historyApiFallback:{ disableDotRule:true, }, watchOptions:{ ignored:/node_modules/,//略过node_modules目录 }, proxy:{//接口代理(这段配置更推荐:写到package.json,再引入到这里) "/api-dev":{ "target":"http://api.test.xxx.com", "secure":false, "changeOrigin":true, "pathRewrite":{//将url上的某段重写(例如此处是将api-dev替换成了空) "^/api-dev":"" } } }, before(app){}, }

5.2 技巧1:文件形式输出 dev-server 代码

dev-server 输出的代码通常在内存中,但也可以写入硬盘,产出实体文件:

devServer:{ writeToDisk:true, }

通常可以用于代理映射文件调试,编译时会产出许多带 hash 的 js 文件,不带 hash 的文件同样也是实时编译的

5.3 技巧2:默认使用本地 IP 启动服务

有的时候,启动服务时,想要默认使用本地的 ip 地址打开:

devServer:{ disableHostCheck:true,// true:不进行host检查 //useLocalIp:true,//建议不在这里配置 //host:'0.0.0.0',//建议不在这里配置 }

同时还需要将 host 配置为 0.0.0.0,这个配置建议在 scripts 命令中追加,而非在配置中写死,否则将来不想要这种方式往回改折腾,取巧一点,配个新命令:

"dev-ip":"yarnrundev--host0.0.0.0--useLocalIp"

5.4 技巧3:指定启动的调试域名

有时启动的时候希望是指定的调试域名,例如:local.test.baidu.com:

devServer:{ open:true, public:'local.test.baidu.com:8080',//需要带上端口 port:8080, }

同时需要将 127.0.0.1 修改为指定的 host,可以借助 iHost 等工具去修改,各个工具大同小异,格式如下:

127.0.0.1local.test.baidu.com

服务启动后将自动打开 local.test.baidu.com:8080 访问

5.5 技巧4:启动 gzip 压缩

devServer:{ compress:true, }

三、HMR 基本原理介绍

从前面介绍中,我们知道:HMR 主要功能是会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。那么,Webpack 编译源码所产生的文件变化在编译时,替换模块实现在运行时,两者如何联系起来?带着这两个问题,我们先简单看下 HMR 核心工作流程(简化版):

史上最详细的webpack讲解(了不起的WebpackHMR)(4)

HMR 工作流程图.png

接下来开始 HMR 工作流程分析:

  1. 当 Webpack(Watchman) 监听到项目中的文件/模块代码发生变化后,将变化通知 Webpack 中的构建工具(Packager)即 HMR Plugin;
  2. 然后经过 HMR Plugin 处理后,将结果发送到应用程序(Application)的运行时框架(HMR Runtime);
  3. 最后由 HMR Runtime 将这些发生变化的文件/模块更新(新增/删除或替换)到模块系统中。

其中,HMR Runtime 是构建工具在编译时注入的,通过统一的 Module ID 将编译时的文件与运行时的模块对应起来,并且对外提供一系列 API 供应用层框架(如 React)调用。

注意:建议先理解上面这张图的大致流程,再进行后续阅读。放心,我等着大家~

四、HMR 完整原理和源码分析

通过上一节内容,我们大概知道 HMR 简单工作流程,那么或许你现在可能还有很多疑惑:文件更新是什么通知 HMR Plugin?HMR Plugin 怎么发送更新到 HMR Runtime?等等问题。

那么接下来我们开始详细结合源码分析整个 HMR 模块热更新流程,首先还是先看流程图,可以先不了解图中方法名称(红色字体黄色背景色部分):

史上最详细的webpack讲解(了不起的WebpackHMR)(5)

Webpack HMR.png

上图展示了从我们修改代码,到模块热更新完成的一个 HMR 完整工作流程,图中已用红色阿拉伯数字符号将流程标识出来。

要了解上面工作原理,我们先理解图中这几个名称概念:

  • Webpack-dev-server :一个服务器插件,相当于 express 服务器,启动一个 Web 服务,只适用于开发环境;
  • Webpack-dev-middleware :一个 Webpack-dev-server 的中间件,作用简单总结为:通过watch mode,监听资源的变更,然后自动打包。
  • Webpack-hot-middleware :结合 Webpack-dev-middleware 使用的中间件,它可以实现浏览器的无刷新更新,也就是 HMR;

史上最详细的webpack讲解(了不起的WebpackHMR)(6)

下面一起学习 HMR 整个工作原理吧:

1.监控代码变化,重新编译打包

首先根据 devServer 配置,使用 npm start 将启动 Webpack-dev-server 启动本地服务器并进入 Webpack 的 watch 模式,然后初始化 Webpack-dev-middleware ,在 Webpack-dev-middleware 中通过调用 startWatch() 方法对文件系统进行 watch:

//webpack-dev-server\bin\webpack-dev-server.js //1.启动本地服务器Line386 server=newServer(compiler,options); //webpack-dev-server\lib\Server.js //2.初始化Webpack-dev-middlewareLine109 this.middleware=webpackDevMiddleware(compiler,Object.assign({},options,wdmOptions)); //webpack-dev-middleware\lib\Shared.js //3.开始watch文件系统Line171 startWatch:function(){ //... //startwatching if(!options.lazy){ varwatching=compiler.watch(options.watchOptions,share.handleCompilerCallback); context.watching=watching; } //... } share.startWatch(); //...

当 startWatch() 方法执行后,便进入 watch 模式,若发现文件中代码发生修改,则根据配置文件对模块重新编译打包

2.保存编译结果

Webpack 与 Webpack-dev-middleware 交互,Webpack-dev-middleware 调用 Webpack 的 API 对代码变化进行监控,并通知 Webpack 将重新编译的代码通过 JavaScript 对象保存在内存中

我们会发现,在 output.path 指定的 dist 目录并没有保存编译结果的文件,这是为什么?

其实, Webpack 将编译结果保存在内存中,因为访问内存中的代码比访问文件系统中的文件快,这样可以减少代码写入文件的开销。

Webpack 能将代码保存到内存中,需要归功于 Webpack-dev-middleware 的memory-fs 依赖库,它将原本 outputFileSystem 替换成了 MemoryFileSystem 的实例,便实现代码输出到内存中。其中部分源码如下:

//webpack-dev-middleware\lib\Shared.jsLine108 //storeourfilesinmemory varfs; varisMemoryFs=!compiler.compilers&& compiler.outputFileSysteminstanceofMemoryFileSystem; if(isMemoryFs){ fs=compiler.outputFileSystem; }else{ fs=compiler.outputFileSystem=newMemoryFileSystem(); } context.fs=fs;

上述代码先判断 fileSystem 是否是 MemoryFileSystem 的实例,若不是,则用 MemoryFileSystem 的实例替换 compiler 之前的 outputFileSystem。这样 bundle.js 文件代码就作为一个简单 JavaScript 对象保存在内存中,当浏览器请求 bundle.js 文件时,devServer 就直接去内存中找到上面保存的 JavaScript 对象并返回给浏览器端。

3.监控文件变化,刷新浏览器

Webpack-dev-server 开始监控文件变化,与第 1 不不同的是,这里并不是监控代码变化重新编译打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true ,Webpack-dev-server 会监听配置文件夹中静态文件的变化,发生变化时,通知浏览器端对应用进行浏览器刷新,这与 HMR 不一样。

//webpack-dev-server\lib\Server.js //1.读取参数Line385 if(options.watchContentBase){defaultFeatures.push('watchContentBase');} //2.定义_watch方法Line697 Server.prototype._watch=function(watchPath){ //... constwatcher=chokidar.watch(watchPath,options).on('change',()=>{ this.sockWrite(this.sockets,'content-changed'); }); this.contentBaseWatchers.push(watcher); }; //3.执行_watch()监听文件变化Line339 watchContentBase:()=>{ if(/^(https?:)?\/\//.test(contentBase)||typeofcontentBase==='number'){ thrownewError('Watchingremotefilesisnotsupported.'); }elseif(Array.isArray(contentBase)){ contentBase.forEach((item)=>{ this._watch(item); }); }else{ this._watch(contentBase); } }

4.建立 WS,同步编译阶段状态

这一步都是 Webpack-dev-server 中处理,主要通过 sockjs(Webpack-dev-server 的依赖),在 Webpack-dev-server 的浏览器端(Client)和服务器端(Webpack-dev-middleware)之间建立 WebSocket 长连接

然后将 Webpack 编译打包的各个阶段状态信息同步到浏览器端。其中有两个重要步骤:

  • 发送状态

Webpack-dev-server 通过 Webpack API 监听 compile 的 done 事件,当 compile 完成后,Webpack-dev-server 通过 _sendStats 方法将编译后新模块的 hash 值用 socket 发送给浏览器端。

  • 保存状态

浏览器端将_sendStats 发送过来的 hash 保存下来,它将会用到后模块热更新

史上最详细的webpack讲解(了不起的WebpackHMR)(7)

//webpack-dev-server\lib\Server.js //1.定义_sendStats方法Line685 //sendstatstoasocketormultiplesockets Server.prototype._sendStats=function(sockets,stats,force){ //... this.sockWrite(sockets,'hash',stats.hash); }; //2.监听done事件Line86 compiler.plugin('done',(stats)=>{ //将最新打包文件的hash值(stats.hash)作为参数传入_sendStats() this._sendStats(this.sockets,stats.toJson(clientStats)); this._stats=stats; }); //webpack-dev-server\client\index.js //3.保存hash值Line74 varonSocketMsg={ //... hash:functionhash(_hash){ currentHash=_hash; }, //... } socket(socketUrl,onSocketMsg);

未完结,请看下一篇

推荐JavaScript经典实例学习资料文章

《10个打开了我新世界大门的 WebAPI(上)「实践」》

《10个打开了我新世界大门的 WebAPI(中)「实践」》

《10个打开了我新世界大门的 WebAPI(下)「实践」》

《「图文」ESLint 在中大型团队的应用实践》

《Deno是代码的浏览器,你认同吗?》

《前端存储除了 localStorage 还有啥?》

《Javascript 多线程编程​的前世今生》

《微前端方案 qiankun(实践及总结)》

《「图文」V8 垃圾回收原来这么简单?》

《Webpack 5模块联邦引发微前端的革命?》

《基于 Web 端的人脸识别身份验证「实践」》

《「前端进阶」高性能渲染十万条数据(时间分片)》

《「前端进阶」高性能渲染十万条数据(虚拟列表)》

《图解 Promise 实现原理(一):基础实现》

《图解 Promise 实现原理(二):Promise 链式调用》

《图解 Promise 实现原理(三):Promise 原型方法实现》

《图解 Promise 实现原理(四):Promise 静态方法实现》

《实践教你从零构建前端 Lint 工作流「干货」》

《高性能多级多选级联组件开发「JS篇」》

《深入浅出讲解Node.js CLI 工具最佳实战》

《延迟加载图像以提高Web网站性能的五种方法「实践」》

《比较 JavaScript 对象的四种方式「实践」》

《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》

《前端如何一次性处理10万条数据「进阶篇」》

《推荐三款正则可视化工具「JS篇」》

《如何让用户选择是否离开当前页面?「JS篇」》

《JavaScript开发人员更喜欢Deno的五大原因》

《仅用18行JavaScript实现一个倒数计时器》

《图文细说JavaScript 的运行机制》

《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》

《推荐Web程序员常用的15个源代码编辑器》

《10个实用的JS技巧「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》

《深入JavaScript教你内存泄漏如何防范》

《手把手教你7个有趣的JavaScript 项目-上「附源码」》

《手把手教你7个有趣的JavaScript 项目-下「附源码」》

《JavaScript 使用 mediaDevices API 访问摄像头自拍》

《手把手教你前端代码如何做错误上报「JS篇」》

《一文让你彻底搞懂移动前端和Web 前端区别在哪里》

《63个JavaScript 正则大礼包「值得收藏」》

《提高你的 JavaScript 技能10 个问答题》

《JavaScript图表库的5个首选》

《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》

《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》

《教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」》

《可视化的 js:动态图演示 Promises & Async/Await 的过程》

《原生JS封装拖动验证滑块你会吗?「实践」》

《如何实现高性能的在线 PDF 预览》

《细说使用字体库加密数据-仿58同城》

《Node.js要完了吗?》

《Pug 3.0.0正式发布,不再支持 Node.js 6/8》

《纯JS手写轮播图(代码逻辑清晰,通俗易懂)》

《JavaScript 20 年 中文版之创立标准》

《值得收藏的前端常用60余种工具方法「JS篇」》

《箭头函数和常规函数之间的 5 个区别》

《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》

《「前端篇」不再为正则烦恼》

《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》

《深入细品浏览器原理「流程图」》

《JavaScript 已进入第三个时代,未来将何去何从?》

《前端上传前预览文件 image、text、json、video、audio「实践」》

《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》

《推荐13个有用的JavaScript数组技巧「值得收藏」》

《前端必备基础知识:window.location 详解》

《不要再依赖CommonJS了》

《犀牛书作者:最该忘记的JavaScript特性》

《36个工作中常用的JavaScript函数片段「值得收藏」》

《Node H5 实现大文件分片上传、断点续传》

《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》

《【实践总结】关于小程序挣脱枷锁实现批量上传》

《手把手教你前端的各种文件上传攻略和大文件断点续传》

《字节跳动面试官:请你实现一个大文件上传和断点续传》

《谈谈前端关于文件上传下载那些事【实践】》

《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》

《最全的 JavaScript 模块化方案和工具》

《「前端进阶」JS中的内存管理》

《JavaScript正则深入以及10个非常有意思的正则实战》

《前端面试者经常忽视的一道JavaScript 面试题》

《一行JS代码实现一个简单的模板字符串替换「实践」》

《JS代码是如何被压缩的「前端高级进阶」》

《前端开发规范:命名规范、html规范、css规范、js规范》

《【规范篇】前端团队代码规范最佳实践》

《100个原生JavaScript代码片段知识点详细汇总【实践】》

《关于前端174道 JavaScript知识点汇总(一)》

《关于前端174道 JavaScript知识点汇总(二)》

《关于前端174道 JavaScript知识点汇总(三)》

《几个非常有意思的javascript知识点总结【实践】》

《都2020年了,你还不会JavaScript 装饰器?》

《JavaScript实现图片合成下载》

《70个JavaScript知识点详细总结(上)【实践】》

《70个JavaScript知识点详细总结(下)【实践】》

《开源了一个 JavaScript 版敏感词过滤库》

《送你 43 道 JavaScript 面试题》

《3个很棒的小众JavaScript库,你值得拥有》

《手把手教你深入巩固JavaScript知识体系【思维导图】》

《推荐7个很棒的JavaScript产品步骤引导库》

《Echa哥教你彻底弄懂 JavaScript 执行机制》

《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》

《深入解析高频项目中运用到的知识点汇总【JS篇】》

《JavaScript 工具函数大全【新】》

《从JavaScript中看设计模式(总结)》

《身份证号码的正则表达式及验证详解(JavaScript,Regex)》

《浏览器中实现JavaScript计时器的4种创新方式》

《Three.js 动效方案》

《手把手教你常用的59个JS类方法》

《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》

《深入浅出讲解 js 深拷贝 vs 浅拷贝》

《手把手教你JS开发H5游戏【消灭星星】》

《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》

《手把手教你全方位解读JS中this真正含义【实践】》

《书到用时方恨少,一大波JS开发工具函数来了》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《手把手教你JS 异步编程六种方案【实践】》

《让你减少加班的15条高效JS技巧知识点汇总【实践】》

《手把手教你JS开发H5游戏【黄金矿工】》

《手把手教你JS实现监控浏览器上下左右滚动》

《JS 经典实例知识点整理汇总【实践】》

《2.6万字JS干货分享,带你领略前端魅力【基础篇】》

《2.6万字JS干货分享,带你领略前端魅力【实践篇】》

《简单几步让你的 JS 写得更漂亮》

《恭喜你获得治疗JS this的详细药方》

《谈谈前端关于文件上传下载那些事【实践】》

《面试中教你绕过关于 JavaScript 作用域的 5 个坑》

《Jquery插件(常用的插件库)》

《【JS】如何防止重复发送ajax请求》

《JavaScript Canvas实现自定义画板》

《Continuation 在 JS 中的应用「前端篇」》

作者:王平安

转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页