为什么要搭建本地服务器?

目前我们开发的代码,为了运行需要有两个条件:

  1. npm run build:编译相关的代码
  2. 通过live server 或者直接通过浏览器,打开index.html 代码,查看效果

这样非常影响开发效率,我们希望当文件发生变化时,可以自动完成编译和展示:

为了实现自动编译,webpack提供了技工可选的方法:

  • webpack watch mode
  • webpack-dev-server
  • webpack-dev-middleware

目前的开发模式:

  1. watch方案来监听文件的变化
  2. 通过live-server插件提供本地服务(当文件变化时,自动刷新页面)

效率不是特别高:

  1. 对所有源代码都会重新进行编译
  2. 编译成功后会生成新的文件
  3. live-server插件属于vscode,不属于webpack

    webpack-dev-server

    我们可以通过这个webpack提供的插件实现热更新功能

安装webpack-dev-server

npm install --save-dev webpack-dev-server

添加一个新的scripts脚本
Untitled (6).png
webpack在编译后不会写入到任何输出文件,而是将bundle保留在内存中

wepack-dev-middleware

默认情况下,webpack-dev-server已经帮我们做好了一切

  • 比如通过express启动一个服务,比如HMR
  • 如果我们想要有更高的自由度,可以使用webpack-dev-middleware

    认识模块热替换(HMR)

    什么是HMR?

  • HMR的全称是Hot Module Replacement,翻译为模块热替换

  • 模块热替换是指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面

HMR通过如下几种方式来提高开发的速度

  • 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
  • 只更新需要变化的内容,节省开发时间
  • 修改了css、js源代码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改

开启方式,webpackconfig.js添加一个选项
Untitled (7).png
浏览器会输出以下内容
Untitled (8).png
但是当我们修改某个模块内容时,依然是全部刷新,而不是局部模块刷新,这是一位内我们自己去指定哪些模块发生变化时进行HMR

在入口文件中写以下代码
Untitled (9).png
当math.js文件中代码更改,就会打印如下内容,证明已经热更新
Untitled (10).png
module.hot.accept还可以写回调函数,在模块更新时执行回调函数的内容
Untitled (11).png
效果如下
Untitled (12).png

框架的HMR

在开发其他项目时,我们是否需要经常手动去写入module.hot.accept相关的API呢?

  • 比如开发Vue、React项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?
  • 事实上社区已经针对这些有很成熟的解决方案了
  • 比如vue开发中,我们使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验
  • 比如react开发中,有React Hot Loader,实时调整react组件(目前官方已弃用,改为react-refresh)

    React的HMR

    React之前借助于ReactHotLoader实现HMR,现在改为通过react-refresh来实现

安装所需依赖:

npm install -d @pmmmwh/react-refresh-webpack-plugin react-refresh

修改webpack.config.js和babel.config.js:
Untitled (2).png
Untitled (3).png
Untitled (4).png

Vue的HMR

Vue的加载需要使用vue-loader,vue-loader加载的组件默认会进行HMR处理

安装所需依赖:

npm install vue-loader vue-template-compiler -D

配置webpack.config.js:
Untitled.png
Untitled (1).png
如上操作就可以开启HMR

HMR的原理

初识 HMR 的时候觉得其很神奇,一直有一些疑问萦绕在脑海。

  1. webpack 可以将不同的模块打包成 bundle 文件或者几个 chunk 文件,但是当我通过 webpack HMR 进行开发的过程中,我并没有在我的 dist 目录中找到 webpack 打包好的文件,它们去哪呢?
  2. 通过查看 webpack-dev-server 的 package.json 文件,我们知道其依赖于 webpack-dev-middleware 库,那么 webpack-dev-middleware 在 HMR 过程中扮演什么角色?
  3. 使用 HMR 的过程中,通过 Chrome 开发者工具我知道浏览器是通过 websocket 和 webpack-dev-server 进行通信的,但是 websocket 的 message 中并没有发现新模块代码。打包后的新模块又是通过什么方式发送到浏览器端的呢?为什么新的模块不通过 websocket 随消息一起发送到浏览器端呢?
  4. 浏览器拿到最新的模块代码,HMR 又是怎么将老的模块替换成新的模块,在替换的过程中怎样处理模块之间的依赖关系?
  5. 当模块的热替换过程中,如果替换模块失败,有什么回退机制吗?

带着上面的问题,于是决定深入到 webpack 源码,寻找 HMR 底层的奥秘。
v2-f7139f8763b996ebfa28486e160f6378_r.jpg
上图是webpack 配合 webpack-dev-server 进行应用开发的模块热更新流程图。

  • 上图底部红色框内是服务端,而上面的橙色框是浏览器端。
  • 绿色的方框是 webpack 代码控制的区域。蓝色方框是 webpack-dev-server 代码控制的区域,洋红色的方框是文件系统,文件修改后的变化就发生在这,而青色的方框是应用本身。

上图显示了我们修改代码到模块热更新完成的一个周期,通过深绿色的阿拉伯数字符号已经将 HMR 的整个过程标识了出来。

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。