Webpack 的 HMR 运行机制

调试工具

首先利用 Chrome 的 dev-tools 中的 network 观察,代码改变的时候,页面与后端之间发生了什么?示例中的项目代码地址已上传 Github。

页面初始加载

运行 dev 模式,本地打开页面并开启 dev-tools,我们看到除了加载页面所依赖的文件外,多了一个连接__webpack_hmr,这是一个叫做 Server-sent Events 的长连接,主要用于后端不断的向前端推送数据,其原理图如下所示,相关的介绍可以参考这篇文章。

后端每次推送的数据内容主要在 EventStream 中的 Data 字段中,如下图所示:

Data 中存储的是一个对象,对象有 action、hash、modules 等字段。

  • action:sync 操作;
  • hash:0c0d327c2abaa1fd4b88,是 bundle 的 hash,因为和产出文件 app.0c0d327c2abaa1fd4b88.js 的内容 hash 值相同;
  • modules:产出 bundle 中的 module id 和对应的文件地址。

修改代码

修改一处代码,webpack 自动编译后,发现 network 中发生了几处变化,首先是客户端收到后端发出的若干事件。

  • action:built 操作,通知浏览器 webpack 完成了编译;
  • hash:最新产出 bundle 的内容 hash 值为215d3b813666fbaea5a3;
  • modules:bundle 中的模块id 和对应模块的文件地址。

在前端收到 built 事件之后,前端向后端发起了两个请求,请求了 0c0d327c2abaa1fd4b88.hot-update.json 和 0.0c0d327c2abaa1fd4b88.hot-update.js 两个文件,文件的 hash 值正好是未发生修改之前后端发送前端的 bundle hash 值。

json文件的内容:

  • h:215d3b813666fbaea5a3,bundle 内容的最新 hash 值;
  • c:"0": true, 表示 bundle id 为 0 的文件被修改了;

js文件的内容:

内容是一个函数,类似 jsonp 的返回形式,也就是页面收到请求后执行了 webpackHotUpdate 函数,对 bundle id 为 0 的文件中的 moudle id 为 11 的模块进行修改。

推测结论

根据上面 network 中的信息,我们可以推测出这个交互过程:

  1. webpack 首次编译时,为前端页面注入后端推送事件监听(event-source)和拉取、更新模块的方法(update-method)的代码,并打包到 bundle 之中;
  2. webpack 进入 watch 模式,在项目代码发生变化的时候重新编译,并将编辑的进展实时通知前端;
  3. 将编译产出存放在 dev-server,此处的编译只针对变动的模块,产出应该包含上文中提到的 oldbundlehash.hot-update.json 和 oldbundlehash.hot-update.js 文件;
  4. dev-server 中使用 hot-middleware 中间件向前端发送 built 事件;
  5. 前端收到通知后,向后端请求最新的变动文件,请求到的 js 文件通过 script 标签加载后执行(类似 Jsonp),其实就是执行已经预埋到 bundle 中的函数(update-method),从而修改 bundle 文件。

以上过程可以用下图表示:

配置文件

接下来我们从项目的配置文件来验证一下,配置文件主要参考 vue-cli 中的 webapck 项目(1.1.2),不同的版本会存在差异。

  • webpack.dev.conf.js

涉及到 Hot Module Replacement 的地方主要有两处:

1. entry 的配置:

在每个入口 bundle 开头引入了 event-source,即在页面中接收后端发送的事件

    /*********./build/webpack.dev.conf.js********/
    // 将 event-source 相关代码,添加到每个入口 chunk 中,作为 HRM Runtime 的一部分。
    // 后端相应的配置见 dev-server 的 hotMiddleware 部分
    Object.keys(baseWebpackConfig.entry).forEach(function (name) {
        baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
    })    /*********./build/dev-client.js********/

    // Event-Source 对象用于接收服务器端推送事件
    // eventsource-polyfill 用于扩展 Event-Source 对象在 IE 浏览器下的兼容性
    require('eventsource-polyfill')    var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')    // 主要用于接受后端 hotMiddleware 的 reload 通知,执行相应操作
    hotClient.subscribe(function (event) {        if (event.action === 'reload') {            window.location.reload()
        }
    }

2. 插件的配置:

引入 HotModuleReplacementPlugin 插件,将 update-method 的代码打入 bundle

    plugins: [
            ...            // HMR 插件将 HMR Runtime 代码嵌入到 bundle 中,能够操作 APP 代码,完成代码替换
            new webpack.HotModuleReplacementPlugin(),            // 报错提示插件:报错不阻塞,但是编译后给出提示
            new webpack.NoEmitOnErrorsPlugin(),            new FriendlyErrorsPlugin()
        ]

 - dev-server.js

涉及到 Hot Module Replacement 的地方主要有两处:

1. 将 compiler 挂载在 devMiddleware 上:

对编译产出提供静态文件服务

    // 将 compiler 挂载在 dev-server 上,监听本地代码变化,变化则启动编译并将编译后的文件暂存到内存中
    var devMiddleware = require('webpack-dev-middleware')(compiler, {        publicPath: config.dev.assetsPublicPath === './' ? '' : config.dev.assetsPublicPath,        quiet: true
    })

2. 将 compiler 挂载在 hotMiddleware 上:

通知前端 event-source 对象发生了 rebuilt

    // 编译后发送通知到 HRM Runtime,HRM Runtime 收到 update 通知后,下载更新的模块,通知 APP 更新,APP 收到通知,然后要求 HRM Runtime 执行模块替换
    var hotMiddleware = require('webpack-hot-middleware')(compiler, {        log: () => {}
    })

由配置文件可以基本验证之前通过 network debug 得到的推论,接下来去看一

下官方文档验证一下。

官方文档

官方文档中先是总体介绍了一下 Hot Module Replacement 的基本原理,然后将原理中涉及到几个知识点进行了介绍。

1. 基本原理

webapck 在编译的过程中,将 HMR Runtime 嵌入到 bundle 中;编译结束后,webpack 对项目代码文件进行监视,发现文件变动重新编译变动的模块,同时通知 HMR Runtime,然后 HMR Runtime 加载变动的模块文件,尝试执行热更新操作。更新的逻辑是:先检查模块是否能支持 accept 方法,不支持的话,则冒泡查找模块树的父节点,直到入口模块,accept 方法也就是模块 hot-replace 的 handler。

2. 知识点

(1)compiler

这里的 compiler 也就是指 webapck,主要提供 update 的信息,也就是 update menifest(json 文件格式)和 update chunks(js文件格式);

(2)app

app 也就是指前端页面,app 中的代码主要调用 HMR Runtime 下载最新的模块代码,然后调用 HMR Runtime 执行 update 操作;

(3)HMR Runtime

HMR Runtime 是 webapck 内嵌到前端页面的代码,主要提供来能给个职能 check 和 apply。check 用来下载最新模块代码,runtime 能够接收后端发送的事件和发送请求;apply 用于更新模块,主要将要更新的模块打上 tag,然后调用模块的(也有可能是父模块)的更新 handler 执行更新。

(4)module

HRM是一个可插拔的工具,只能影响包含HMR code的模块。通常情况下,没有必要为每个模块写入HMR code,更新的时候会进行冒泡检查HMR code的是否存在。

根据官方文档的介绍,基本和我们的推论吻合,区别在于官方文档引入了 HMR Runtime 的概念,这个可以看作是推论中的 event-source 和update-method的结合体。

现在大家应该清楚了 webpack的Hot Module Replacement 的基本原理了,官方文档中提到了如何根据最新的模块替换旧模块的方法,这个知识点暂不在本文进行介绍。

原文发布于微信公众号 - GitChat精品课(CSDN_Tech)

原文发表时间:2018-05-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏卡少编程之旅

webpack4配置入门和进阶

1.5K12
来自专栏前端儿

Webpack打包构建太慢了?试试几个方法

webpack支持监听模式,此时需要重新编译时就可以进行增量构建,增量构建是很快的,基本不到一秒或几秒之内就能重新编译好

8192
来自专栏SDNLAB

【连载-4】数据中心网络虚拟化 配置管理技术

在构建虚拟网络时,管理员需要进行大量的配置工作,例如端口的ip地址和VXLAN配置等等。显然,没有人愿意在系统每次启动时都将繁琐的配置工作重复一遍,所以将配置信...

2825
来自专栏黑白安全

中间件安全加固之Apache

使用命令“chmod 600 /etc/httpd/conf/httpd.conf”设置配置文件为属主可读写,其他用户无权限

1922
来自专栏飞雪无情的博客

Go语言经典库使用分析(三)| Gorilla Handlers 详细介绍

在我们编写web服务端程序的时候,我们可能会对一些甚至全部的Http Request统一处理,比如我们记录每个访问的Request,对提交的Form表单进行映射...

1221
来自专栏张戈的专栏

Linux系统zip压缩命令详细参数,附文件排除选项的正确用法

这次给博客添加了纯代码缓存之后,发现缓存文件竟然达到了 50 多 MB!而我的服务器每次还需要同步至七牛,文件体积的增加都会增加文件远程同步备份的时间。因此,我...

3265
来自专栏人人都是极客

Linux下so动态库一些不为人知的秘密

Linux 下有动态库和静态库,动态库以.so为扩展名,静态库以.a为扩展名。二者都使用广泛。本文主要讲动态库方面知识。

2002
来自专栏大闲人柴毛毛

Linux系统服务——Daemon

什么是Daemon? Daemon是Linux的一些系统服务,它们是一些常驻内存的进程。 Daemon分类 Daemon拥有两种分类方式,按照“daemon是...

3774
来自专栏PHP在线

Socket 通信原理

什么是Socket? Socket的中文翻译过来就是“套接字”。套接字是什么,我们先来看看它的英文含义:插座。 Socket就像一个电话插座,负责连通两端的电话...

6646
来自专栏pangguoming

创建GitHub技术博客全攻略

说明: 首先,你需要注册一个 github 账号,最好取一个有意义的名字,比如姓名全拼,昵称全拼,如果被占用,可以加上有意义的数字. 1. 注册账号: 地址:...

4077

扫码关注云+社区

领取腾讯云代金券