模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。
下面我们运行一个例子来更直观的感受什么是模块热更新。
视频中,我修改了字体颜色,页面会立即更新,但输入框中的内容依然保留着。HMR就是帮助我们实现了这样一个效果,不然我们在每次修改代码时,还需要手动刷新页面,且页面的内容不会保留。模块热更新的好处显而易见,它可以帮助我们节省开发时间,提升开发体验。
细心的同学可能会发现,webpack自动进行重新编译同时又多生成了两个文件。
下面让我们带着这些疑问,一起来探索模块热更新的原理。
在学习原理前,我们需要对模块热更新的配置有一个清晰的认识。因为平时的工作中很少需要我们自己手动去配置,所以会导致我们忽略一些细节的问题。现在我们来回顾一下配置流程,这样更有助于对源码的理解。
第一步:安装webpack-dev-server
npm install --save-dev. webpack-dev-server
第二步:在父模块中注册module.hot.accept事件
//src/index.js
let div = document.createElement('div');
document.body.appendChild(div);
let input = document.createElement('input');
document.body.appendChild(input);
let render = () => {
let title = require('./title.js')
div.innerHTML = title;
}
render()
//添加如下内容
+ if (module.hot) {
+ module.hot.accept(['./title.js'], render)
+ }
// 子模块 src/title.js
module.exports = 'Hello webpack'
第三步:在webpack.config.js中配置hot:true
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
+ devServer: {
+ hot: true
+ },
plugins: [
new HtmlWebpackPlugin(),
],
}
现在你可能会有一些疑问,为什么平时修改代码的时候不用监听module.hot.accept
也能实现热更新?那是因为我们使用的 loader 已经在幕后帮我们实现了。
webpack-dev-server 提供了实时重加载的功能,但是不能局部刷新。必须配合后两步的配置才能实现局部刷新,这两步的背后其实是借助了HotModuleReplacementPlugin
。
可以说HMR是webpack-dev-server
和HotModuleReplacementPlugin
共同的功劳。
下面就正式进入我们今天的主题。先来介绍第一位主角:webpack-dev-server。
通过node_modules/webpack-dev-server
下的package.json文件,根据 bin 的值可以找到命令实际运行的文件。./node_modules/webpack-dev-server/bin/webpack-dev-server.js
下面我们就顺着入口文件,来看一看webpack-dev-server都做了哪些事。为了减少篇幅,提高阅读质量,以下示例均为简易版的实现,感兴趣的可以参照源码一起来看。
首先通过webpack创建了一个compiler实例,然后通过创建自定义server实例,开启了一个本地服务。
// node_modules/webpack-dev-server/bin/webpack-dev-server.js
const webpack = require('webpack');
const config = require('../../webpack.config');
const Server = require('../lib/Server')
const compiler = webpack(config);
const server = new Server(compiler);
server.listen(8080, 'localhost', () => {})
这个自定义Server 不仅是创建了一个http服务,它还基于http服务创建了一个websocket服务,同时监听浏览器的接入,当浏览器成功接入时向它发送hash值,从而实现服务端和浏览器间的双向通信。
// node_modules/webpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp();
this.createServer();
}
//创建http应用
setupApp() {
this.app = express();
}
//创建http服务
createServer() {
this.server = http.createServer(this.app);
}
//监听端口号
listen(port, host, callback) {
this.server.listen(port, host, callback)
this.createSocketServer();
}
//基于http服务创建websocket服务,并注册监听事件connection
createSocketServer() {
const io = socketIO(this.server);
io.on('connection', (socket) => {
this.clientSocketList.push(socket);
socket.emit('hash', this.currentHash);
socket.emit('ok');
socket.on('disconnect', () => {
let index = this.clientSocketList.indexOf(socket);
this.clientSocketList.splice(index, 1)
})
})
}
}
module.exports = Server;
仅仅在建立websocket连接时,服务端向浏览器发送hash和拉取代码的通知还不够,我们还希望当代码改变时,浏览器也可以接到这样的通知。于是,在开启服务前,还需要对编译完成事件进行监听。
//监听编译完成,当编译完成后通过websocket向浏览器发送广播
setupHooks() {
let { compiler } = this;
compiler.hooks.done.tap('webpack-dev-server', (stats) => {
this.currentHash = stats.hash;
this.clientSocketList.forEach((socket) => {
socket.emit('hash', this.currentHash);
socket.emit('ok');
})
})
}
要想在代码修改的时候,触发重新编译,那么就需要对代码的变动进行监听。这一步,源码是通过webpackDevMiddleware
库实现的。库中使用了compiler.watch对文件的修改进行了监听,并且通过memory-fs
实现了将编译的产物存放到内存中,这也是为什么我们在dist目录下看不到变化的内容,放到内存的好处就是为了更快的读写从而提高开发效率。
// node_modules/webpack-dev-middleware/index.js
const MemoryFs = require('memory-fs')
compiler.watch({}, () => {})
let fs = new MemoryFs();
this.fs = compiler.outputFileSystem = fs;
前面提到要想实现浏览器和本地服务的通信,那么就需要浏览器接入到本地开启的websocket服务,然而浏览器本身并不具备这样的能力,这就需要我们自己提供这样的客户端代码将它运行在浏览器。因此自定Server在开启http服务之前,就调用了updateCompiler()
方法,它修改了webpack配置中的entry,使得插入的两个文件的代码可以一同被打包到 main.js 中,运行在浏览器。
//node_modules/webpack-dev-server/lib/utils/updateCompiler.js
const path = require('path');
function updateCompiler(compiler) {
compiler.options.entry = {
main: [
path.resolve(__dirname, '../../client/index.js'),
path.resolve(__dirname, '../../../webpack/hot/dev-server.js'),
config.entry,
]
}
}
module.exports = updateCompiler
node_modules /webpack-dev-server/client/index.js
这段代码会放在浏览器作为客户端代码,它用来建立 websocket 连接,当服务端发送hash广播时就保存hash,当服务端发送ok广播时就调用reloadApp()。
let currentHash;
let hotEmitter = new EventEmitter();
const socket = window.io('/');
socket.on('hash', (hash) => {
currentHash = hash;
})
socket.on('ok', () => {
reloadApp();
})
function reloadApp() {
hotEmitter.emit('webpackHotUpdate', currentHash)
}
webpack/hot/dev-server.js
reloadApp()继续调用module.hot.check(),当然第一次加载页面时是不会被调用的。至于这里为啥会分成两个文件,个人理解是为了解藕,每个模块负责不同的分工。
let lastHash;
hotEmitter.on('webpackHotUpdate', (currentHash) => {
if (!lastHash) {
lastHash = currentHash;
return;
}
module.hot.check();
})
module.hot.check()是哪来的?答案是HotModuleReplacementPlugin
。我们可以在浏览器的sources下看到,main.js被插入很多代码,这些代码就是被HotModuleReplacementPlugin
插入进来的。
它不仅在main.js中插入了代码,前面提到过的编译后生成的两个补丁包也是它生成的 。
现在,我们来看一下今天的第二位主角HotModuleReplacementPlugin
在main.js都悄悄插了哪些代码,从而实现的热更新。
前面提到过,当代码发生改动时,服务端会向浏览器发送ok消息,浏览器会执行module.hot.check进行模块热检查。check方法就是来源于这里了。
function hotCreateModule() {
let hot = {
_acceptedDependencies: {},
accept(deps, callback) {
deps.forEach(dep => hot._acceptedDependencies[dep] = callback);
},
check: hotCheck
}
return hot
}
module.hot.check()就是调用hotCheck,此时浏览器会向服务端获取两个补丁文件。
function hotCheck() {
hotDownloadManifest().then(update => {
//{"h":"eb861ba9f6408c42f1fd","c":{"main":true}}
let chunkIds = Object.keys(update.c) //['main']
chunkIds.forEach(chunkId => {
hotDownloadUpdateChunk(chunkId)
})
lastHash = currentHash;
}).catch(() => {
window.location.reload();
})
}
先看一眼这两个文件长什么样
告知浏览器新的hash值,并且是哪个chunk发生了改变
告知浏览器,main 代码块中的/src/title.js
模块变更的内容
首先是通过XMLHttpRequest的方式,利用上一次保存的hash值请求hot-update.json文件。这个描述文件的作用就是提供了修改的文件所在的chunkId。
function hotDownloadManifest() {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
let url = `${lastHash}.hot-update.json`
xhr.open('get', url);
xhr.responseType = 'json'
xhr.onload = function () {
resolve(xhr.response)
}
xhr.send()
})
}
然后通过JSONP的方式,利用hot-update.json返回的chunkId 及 上一次保存的hash 拼接文件名进而获取文件内容。
function hotDownloadUpdateChunk(chunkId) {
let script = document.createElement('script');
script.src = `${chunkId}.${lastHash}.hot-update.js`;
document.head.appendChild(script);
}
window.webpackHotUpdate = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
}
当hot-update.js文件加载好后,就会执行window.webpackHotUpdate,进而调用了hotApply。hotApply根据模块ID找到旧模块然后将它删除,然后执行父模块中注册的accept回调,从而实现模块内容的局部更新。
window.webpackHotUpdate = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
}
let hotUpdate = {}
function hotAddUpdateChunk(chunkId, moreModules) {
for (let moduleId in moreModules) {
modules[moduleId] = hotUpdate[moduleId] = moreModules[moduleId];
}
hotApply();
}
function hotApply() {
for (let moduleId in hotUpdate) {
let oldModule = installedModules[moduleId]
delete installedModules[moduleId]
oldModule.parents.forEach((parentModule) => {
let cb = parentModule.hot._acceptedDependencies[moduleId]
cb && cb()
})
}
}
模块热更新原理总结:
在执行npm run dev 后,首先会通过updateCompiler方法去修改compiler的entry,将两个文件的代码一起打包到main.js,这两个文件一个是用来与服务端进行通信的,一个是用来调用module.hot.check的。接着通过compiler.hooks.done.tap来监听编译完成,通过compiler.watch 监听代码的改动,通过createSocketServer()开启http服务和websocekt服务。
当用户访问http://localhost:8080时,浏览器会与服务端建立websocket连接。随后服务端向浏览器发送hash 和 ok ,用来通知浏览器当前最新编译版本的hash值和告诉浏览器拉取代码。同时服务端,会根据路由,将内存中的文件返回,此时浏览器保存hash,页面内容出现。
当修改本地代码时,会触发重新编译,此时webpackDevMiddleWare会将编译的产物保存到内存中,这得益于内置模块memory-fs的功劳。同时HotModuleReplacementPlugin
会生成两个补丁包,这两个补丁包一个是用来告诉浏览器哪个chunk变更了,一个是用来告诉浏览器变更模块及内容。当重新编译完成,浏览器会保存当前hash,然后通上一次的hash 值拼接出要请求的描述文件路径,再根据描述文件返回的内容,拼接出要另一个要请求的补丁包文件。请求成功就开始执行webpckHotUdate了,会继续调用 hotApply,实质就是执行了我们当初在配置模块热更新第二步中的回调事件,从而实现了页面内容的局部刷新。
[1] 模块热替换 | webpack 中文文档: https://webpack.docschina.org/guides/hot-module-replacement/
[2] 轻松理解webpack热更新原理 - 掘金:https://juejin.cn/post/6844904008432222215
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/s11mkXEK5ZHSbjHsKye-5A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。