谈: webpack的热更新做了什么?

1,317 阅读5分钟

前言

为了更好的提高开发效率,不用频繁刷新浏览器去看效果,webpack为我们提供了HotModuleReplacementPlugin,简称HRM.我相信大家应该都知道它是做什么的.是的,它最直接的作用就是让我们在修改完代码保存之后可以去浏览器直接看到修改后的效果, 不用每次更改都去刷新浏览器.这无疑是一个很强大的工具.那么它为什么可以做到这个事情,或者说它施了什么魔法.让我们来一探究竟.

使用说明

使用webpack的项目,都需要一个webpack.config.js的配置文件.那可以直接配置:

module.exports = {
 ...
  devServer: {
    hot: true,
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin()
  ]
 ...
}

下面的new webpack.HotModuleReplacementPlugin()实例化的步骤可以省略.如果我们在项目中webpack 或 webpack-dev-server 是通过 --hot 选项启动的,那么这个插件会被自动添加.当然配置上也没有不良反应.

这样子就可以做到修改保存在浏览器中自动刷新并修改视图的效果了.

那接下来这个功能就比较 op 了,请诸位看官暂时不要眨眼. 废话不说,先看代码.

let div = document.createElement('div')
document.body.appendChild(div)

let render = function () {
  let text = require('./text.js')
  div.innerHTML = text
}

render()

if (module.hot) {
  module.hot.accept(['./text.js'], render)
}

很简单的一个小功能,引入一个js脚本,并把它的内容赋值给div.关键是下面的module.hot.accept(),它是什么?

module.hot.accept()

这个东西是这样的,如果项目里通过 HotModuleReplacementPlugin 这个插件启用了Hot Module Replacement,那么就会在module.hot下面暴露出来它的接口,那accept()就是模块API.

简单的说,它的作用是对指定模块进行callback更新,那这优势显而易见,如果我们修改了accept()中配置的模块,那么这个模块会更新,对于页面来说就是局部更新,不会再刷新页面.这其实也是webpack对项目的优化点.

如果我们项目中那么多模块,是不是都要加在这个API里,那肯定不是的.比如vue项目的话,vue-loader会帮我们把这个事情做了.可以看一下vue-loader的源码就知道了.

module.hot.accept(
  dependencies, // 可以是一个字符串或字符串数组
  callback // 用于在模块更新后触发的函数
)

既然有指定模块更新,那就相对应有指定模块不更新,decline()就是做这个事情的具体请看这里.

项目中实际运转

我们可以看到实际的样子是在修改模块内容并保存后,在浏览器的Network中会生成两个文件:

WechatIMG222459.png

一个文件是hash值+'.hot-update.json'文件,一个是main+hash值+'.hot-update.js'文件.前者会返回修改了的模块与一个新的hash值在下次修改会用作文件名出现.后者会返回webpackHotUpdate()方法,里面包括,模块名与模块中修改后的内容.

其实webpack的更新就是经过webpackHotUpdate处理修改的模块与内容,再通过自定义的hotCreateRequire()方法把模块插入到页面中.

服务端流程

webpack监听文件

首先,webpack会通过compiler编译构建,然后调用watch监听文件.

const compiler = webpack(config)
...
compiler.watch({}, () => {
  console.log('监听编译成功')
})

web服务器

创建一个http服务与websoket链接.

const compiler = webpack(config)
const server = new Server(compiler)

建立webpack与web服务器连接

setupDevMiddleware() {
    this.middleware = this.webpackDevMiddleware() // 返回一个express中间件
}
webpackDevMiddleware(){
 // ...
 // 把打包之后的文件写入/读取内存
 let fs = new MemoryFS()
 this.fs = compiler.outputFileSystem = fs
 // 生成一个中间件 响应客户端请求文件
}

这里之所以要使用内存,是因为如果使用webpack的watch mode,每次打包后会存储到本地硬盘,IO操作很费时间,我们需要的是本地开发的实时性,效率为准.

浏览器与webpack服务器连接

Webpack-hot-middleware 插件的作用就是提供浏览器和 Webpack 服务器之间的通信机制、且在浏览器端接收 Webpack 服务器端的更新变化。

io.on('connection', (socket) => {
    ...
  socket.emit('hash', this.currentHash) // 返回新的哈希值
  socket.emit('ok')
    ...
})

上文中提到的两个hash.xx命名的文件就是这里返回的.那到这里我们改动页面代码之后,webpack重新编译,然后通知浏览器就完成了.这些是服务端做的事情.

客户端流程

连接websocket服务器

websocket客户端这边主要是监听hashok,保存hash值作为模块更新的文件名使用,监听ok主要是执行reloadApp方法进行更新.

reloadApp会做是否支持热更新的判断(是否使用HotModuleReplacementPlugin)

// reloadApp 逻辑
if (hot) {
    // 这里emit是用的events
    var hotEmitter = new EventEmitter();
    hotEmitter.emit('webpackHotUpdate', currentHash);
} else if (liveReload) {
    ... 
    window.location.reload();
}

hotCheck

向服务端发送请求拿到编译hash值与更新的模块名

这里会向server端发送ajax请求,服务端返回hash.hot-update.json文件,里面就包含了hash值与模块名

更新打包的js内容

hotCreateRequire方法会调用hotDownloadUpdateChunk,通过JSONP请求获取最新模块代码,也就是main.hash.hot-update.js

更新模块代码

通过调用hotApply方法,进行热更新.这个方法内的逻辑较多,调用hotUpdate找到旧模块,添加新的模块到modules中.然后在通过require执行新的代码.最后会从缓存中删除旧模块.这里只是记载大体流程.

总结

记录从起初对webpack-dev-server好奇,到查找文档资料,再到一段时间的学习.慢慢领会了解了热更新的事情.受益匪浅,这些框架原理很有魅力,开始了就不想停下来.虽说达不到手写的程度,但是能大概经历过程,也是不错的体验.这些在日常业务开发中用不到的东西,其实也在无形中服务于业务.

加油.