搭建 webpack 环境
创立一个我的项目
mkdir dev-erver && cd dev-server
npm init -y // 疾速创立一个我的项目配置
npm i webpack webpack-dev-server webpack-cli --save-dev
mkdir src // 创立资源目录
mkdir dist // 输入目录
touch webpack.dev.js // 因为是在开发环境须要热更新,所以间接创立 dev 配置文件
目录构造
webpack 版本
这里阐明一下,webpack4 和 webpack5 的配置信息或者显示信息可能有点区别
"devDependencies": {
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
}
编写配置文件
// webpack.dev.js
'use strict';
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口文件
output: {path: path.resolve(__dirname, 'dist'), // 输入到哪个文件夹
filename: 'output.js' // 输入的文件名
},
mode: 'development', // 开发模式
devServer: {// contentBase: path.resolve(__dirname, 'dist') // contentBase 是用来指定被拜访 html 页面所在目录的;// 然而我本地报错了,应用上面的语句
static: path.resolve(__dirname, "dist")
}
};
新建文件
// src/index.js
'use strict'
document.write('hello world~')
package.json 增加一条命令
"scripts": {
"test": "echo \"Error: no test specified\"&& exit 1",
"dev": "webpack-dev-server --config webpack.dev.js --open"
},
npm run dev 运行
咱们看到文件曾经打包实现了,然而在 dist
目录里并没有看到文件,这是因为 WDS
是把编译好的文件放在缓存中,没有放在磁盘上,然而咱们是能够拜访到的,
output.js 对应你在 webpack 配置文件中的输入文件,配置的是什么就拜访什么
http://localhost:8080/output.js
显然咱们想看成果而不是打包后的代码,所以咱们在 dist
目录里创立一个 html
文件引入即可,
<script src="./output.js"></script>
感触 webpack 的热更新
内容进去了,咱们接下来批改 index.js
文件,来看下是否能够主动刷新
'use strict'
document.write('hello world~byebye world')
这的确是热更新,然而这种是每一次批改会从新刷新整个页面,大家能够关上控制台查看。webpack-dev-server 提供了实时重加载的性能,然而不能部分刷新。必须配合后两步的配置能力实现部分刷新,这两步的背地其实是借助了 HotModuleReplacementPlugin。
webpack-dev-server 搭配 HotModuleReplacementPlugin 实现热更新
咱们须要的是,更新批改的模块,然而不要刷新页面。这个时候就须要用到模块热替换。
模块热替换 (
Hot Module Replacement
或HMR
) 是webpack
提供的最有用的性能之一。它容许在运行时更新各种模块,而无需进行齐全刷新。
个性
模块热替换 (HMR - Hot Module Replacement
) 性能会在利用程序运行过程中替换、增加或删除模块,而无需从新加载整个页面。次要是通过以下几种形式,来显著放慢开发速度:
- 保留在齐全从新加载页面时失落的应用程序状态。
- 只更新变更内容,以节俭贵重的开发工夫。
- 调整款式更加疾速 – 简直相当于在浏览器调试器中更改款式。
启用
// webpack.dev.js
const path = require('path');
const webpack = require('webpack'); // 次要多了这一行
module.exports = {
entry: './src/index.js', // 入口文件
output: {path: path.resolve(__dirname, 'dist'), // 输入到哪个文件夹
filename: 'output.js' // 输入的文件名
},
mode: 'development', // 开发模式
devServer: {// contentBase: path.resolve(__dirname, 'dist') // contentBase 是用来指定被拜访 html 页面所在目录的;然而我本地报错了,应用上面的语句
static: path.resolve(__dirname, "dist"),
hot: true // 次要多了这一行
},
plugins: [ // 次要多了这一行
new webpack.HotModuleReplacementPlugin()]
};
参考 前端进阶面试题具体解答
咱们批改一下文件,造成援用关系
//index.js
import {test} from './page1.js'
document.write('hello world~1234')
test()
//page1.js
module.exports = {test: function () {console.log(11111)
}
}
在入口页 index.js 面再增加一段
if (module.hot) {module.hot.accept();
}
思考💡:为什么平时批改代码的时候不必监听 module.hot.accept
也能实现热更新?
那是因为咱们应用的 loader 曾经在幕后帮咱们实现了。
接下来执行 npm run dev
而后咱们批改 page1.js,会发现页面并没有刷新,只是更新了局部文件
这样咱们的热更新就实现了。
热更新原理
第一步,在 webpack 的 watch 模式下,文件系统中某一个文件产生批改,webpack 监听到文件变动,依据配置文件对模块从新编译打包,并将打包后的代码通过简略的 JavaScript 对象保留在内存中。
第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,次要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 裸露的 API 对代码变动进行监控,并且通知 webpack,将代码打包到内存中。
第三步是 webpack-dev-server 对文件变动的一个监控,这一步不同于第一步,并不是监控代码变动从新打包。当咱们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中动态文件的变动,变动后会告诉浏览器端对利用进行 live reload。留神,这儿是浏览器刷新,和 HMR 是两个概念。
第四步也是 webpack-dev-server 代码的工作,该步骤次要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建设一个 websocket 长连贯,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包含第三步中 Server 监听动态文件变动的信息。浏览器端依据这些 socket 音讯进行不同的操作。当然服务端传递的最次要信息还是新模块的 hash 值,前面的步骤依据这一 hash 值来进行模块热替换。
webpack-dev-server/client 端并不可能申请更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是依据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有前面那些步骤了。
HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接管到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 申请,服务端返回一个 json,该 json 蕴含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 申请,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
而第 10 步是决定 HMR 胜利与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行比照,决定是否更新模块,在决定更新模块后,查看模块之间的依赖关系,更新模块的同时更新模块间的依赖援用。
最初一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
在初步领会了 webpack 的热更新之后,可能须要思考以下的问题
思考💡:为什么须要热更新?
Hot Module Replacement(以下简称 HMR)是 webpack 倒退至今引入的最令人兴奋的个性之一,当你对代码进行批改并保留后,webpack 将对代码从新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就可能对利用进行更新。例如,在开发 Web 页面过程中,当你点击按钮,呈现一个弹窗的时候,发现弹窗题目没有对齐,这时候你批改 CSS 款式,而后保留,在浏览器没有刷新的前提下,题目款式产生了扭转。感觉就像在 Chrome 的开发者工具中间接批改元素款式一样。
思考💡:HMR 是怎么实现主动编译的?
webpack 通过 watch 能够监听文件编译实现和监听文件的变动,webpack-dev-middleware 能够调用 webpack 的 API 监听代码的变动,webpack-dev-middleware 利用 sockjs 和 webpack-dev-server/client 建设 webSocket 长连贯。将 webpack 的编译编译打包的各个阶段通知浏览器端。次要通知新模块 hash 的变动,而后 webpack-dev-server/client 是无奈获取更新的代码的,通过 webpack/hot/server 获取更新的模块,而后 HMR 比照更新模块和模块的依赖。
思考💡:模块内容的变更浏览器又是如何感知的?
webpack-dev-middleware 利用 sockjs 和 webpack-dev-server/client 建设 webSocket 长连贯。将 webpack 的编译编译打包的各个阶段通知浏览器端。
思考💡:以及新产生的两个文件又是干嘛的?
d04feccfa446b174bc10.hot-update.json
告知浏览器新的 hash 值,并且是哪个 chunk 产生了扭转
main.d04feccfa446b174bc10.hot-update.js
告知浏览器,main 代码块中的 /src/title.js
模块变更的内容
首先是通过 XMLHttpRequest 的形式,利用上一次保留的 hash 值申请 hot-update.json 文件。这个形容文件的作用就是提供了批改的文件所在的 chunkId。
而后通过 JSONP 的形式,利用 hot-update.json 返回的 chunkId 及 上一次保留的 hash 拼接文件名进而获取文件内容。
思考💡:怎么实现部分更新的?
当 hot-update.js 文件加载好后,就会执行 window.webpackHotUpdate,进而调用了 hotApply。hotApply 依据模块 ID 找到旧模块而后将它删除,而后执行父模块中注册的 accept 回调,从而实现模块内容的部分更新。
思考💡:webpack 能够将不同的模块打包成 bundle 文件或者几个 chunk 文件,然而当我通过 webpack HMR 进行开发的过程中,我并没有在我的 dist 目录中找到 webpack 打包好的文件,它们去哪呢?
原来 webpack 将 bundle.js 文件打包到了内存中,不生成文件的起因就在于拜访内存中的代码比拜访文件系统中的文件更快,而且也缩小了代码写入文件的开销,这所有都归功于 memory-fs,memory-fs 是 webpack-dev-middleware 的一个依赖库,webpack-dev-middleware 将 webpack 本来的 outputFileSystem 替换成了 MemoryFileSystem 实例,这样代码就将输入到内存中。
思考💡:通过查看 webpack-dev-server 的 package.json 文件,咱们晓得其依赖于 webpack-dev-middleware 库,那么 webpack-dev-middleware 在 HMR 过程中表演什么角色?
webpack-dev-middleware 表演是中间件的角色,一头能够调用 webpack 裸露的 API 检测代码的变动,一头能够通过 sockjs 和 webpack-dev-server/client 建设 webSocket 长连贯,将 webapck 打包编译的各个阶段发送给浏览器端。
思考💡:应用 HMR 的过程中,通过 Chrome 开发者工具我晓得浏览器是通过 websocket 和 webpack-dev-server 进行通信的,然而 websocket 的 message 中并没有发现新模块代码。打包后的新模块又是通过什么形式发送到浏览器端的呢?为什么新的模块不通过 websocket 随音讯一起发送到浏览器端呢?
功能块的解耦,各个模块各司其职,dev-server/client 只负责音讯的传递而不负责新模块的获取,而这些工作应该有 HMR runtime 来实现,HMR runtime 才应该是获取新代码的中央。再就是因为不应用 webpack-dev-server 的前提,应用 webpack-hot-middleware 和 webpack 配合也能够实现模块热更新流程,在应用 webpack-hot-middleware 中有件有意思的事,它没有应用 websocket,而是应用的 EventSource。综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 音讯中。
思考💡:浏览器拿到最新的模块代码,HMR 又是怎么将老的模块替换成新的模块,在替换的过程中怎么解决模块之间的依赖关系?
思考💡:当模块的热替换过程中,如果替换模块失败,有什么回退机制吗?
模块热更新的错误处理,如果在热更新过程中呈现谬误,热更新将回退到刷新浏览器
面试题:说一下 webpack 的热更新原理?
webpack 通过 watch 能够监测代码的变动;webpack-dev-middleware 能够调用 webpack 裸露的 API 检测代码变动,并且通知 webpack 将代码保留到内存中;webpack-dev-middleware 通过 sockjs 和 webpack-dev-server/client 建设 webSocket 长连贯,将 webpack 打包阶段的各个状态告知浏览器端,最重要的是新模块的 hash 值。webpack-dev-server/client 通过 webpack/hot/dev-server 中的 HMR 去申请新的更新模块,HMR 次要借助 JSONP。先拿到 hash 的 json 文件,而后依据 hash 拼接出更新的文件 js,而后 HotModulePlugin 比照新旧模块和模块依赖实现更新。