Webpack-HMR

一 定义

Hot Module Replacement(以下简称 HMR)是指当我们对代码修改并保存后,webpack将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面

二 使用

1. 安装

npm i webpack webpack-cli webpack-dev-server html-webpack-plugin -D

2. 目录结构

public/index.html
src/index.js
src/title.js
package.json
webpack.config.js

3. 代码文件

  • webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

module.exports = {
	mode: 'development',
	devtool: false,
	entry: './src/index.js',
	output:{
		filename: '[name].[hash].js',
		path: path.resolve(__dirname, 'dist'),
		hotUpdateGlobal: 'webpackHotUpdate' // 修改热更新返回的模块名称
	},
	devServer:{
		hot: true,// 支持热更新
		port: 8080, // 端口
		contentBase: path.resolve(__dirname, 'static') // 额外静态资源目录
	},
	plugins:[
		new HtmlWebpackPlugin({
			template: './public/index.html'
		}),
		// 此处可写可不写,因为如果devServer.hot==true的话,webpack会自动帮你添加此插件
		new HotModuleReplacementPlugin()
	]
}
  • public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>hmr</title>
</head>
<body>
	<input/>
	<div id="root"></div>
</body>
</html>
  • src/index.js
let render = () => {
	let title = require('./title.js');
	root.innerText = title;
}
// 初始化渲染一次
render();

// 热更新渲染
if (module.hot) {
	module.hot.accept(['./title.js'], render);
}
  • src/title.js
module.exports = 'title'
  • package.json
"scripts": {
  "build": "webpack",
  "dev": "webpack serve"
}

4. 运行

npm run dev

三 工作流程

1. 原理图

hmr

2. 相关代码

3. 服务器端

(1) 关键节点

  • 启动webpack-dev-server服务器
  • 创建webpack实例
  • 创建Server服务器
  • 添加webpack的done事件回调,在编译完成后会向浏览器发送消息
  • 创建express应用app
  • 使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中
  • 设置文件系统为内存文件系统
  • 添加webpack-dev-middleware中间件,用来预览产出的资源文件
  • 创建http服务器并启动服务
  • 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些socket消息进行不同的操作。当然服务端传递的最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换

(2) 具体步骤

步骤说明源码位置
1启动webpack-dev-server服务器webpack-dev-server.js#L159open in new window
2创建webpack实例webpack-dev-server.js#L89open in new window
3创建Server服务器webpack-dev-server.js#L107open in new window
4更改config的entry属性Server.js#L57open in new window
updateCompiler中调用addEntriesupdateCompiler.js#L47open in new window
entry添加dev-server/client/index.jsaddEntries.js#L22open in new window
entry添加webpack/hot/dev-server.jsaddEntries.js#L30open in new window
5添加钩子setupHooksServer.js#L122open in new window
6添加webpack的done事件回调Server.js#L183open in new window
编译完成向websocket客户端推送消息,最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换Server.js#L178open in new window
7创建express应用appServer.js#L166open in new window
8添加webpack-dev-middleware中间件Server.js#L206open in new window
以watch模式启动webpack编译,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包index.js#L41open in new window
设置文件系统为内存文件系统index.js#L65open in new window
返回一个中间件,负责返回生成的文件middleware.js#L20open in new window
app中使用webpack-dev-middlerware返回的中间件Server.js#L128open in new window
9创建http服务器并启动服务Server.js#L135open in new window
10建立一个 websocket 长连接Server.js#L745open in new window
创建socket服务器并监听connection事件Server.js#L745open in new window

4. 客户端

(1) 关键节点

  • webpack-dev-server/client-src/default/index.js端会监听到此hash消息,会保存此hash值
  • 客户端收到ok的消息后会执行reloadApp方法进行更新
  • 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器
  • 在webpack/hot/dev-server.js会监听webpackHotUpdate事件,然后执行check()方法进行检查
  • 在check方法里会调用module.hot.check方法
  • 它通过调用 JsonpMainTemplate.runtime的hotDownloadManifest方法,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件,该 Manifest 包含了所有要更新的模块的 hash 值和chunk名
  • 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码
  • 补丁JS取回来后会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法,里面会调用hotAddUpdateChunk方法,用新的模块替换掉旧的模块
  • 然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码
  • 然后调用hotApply方法进行热更新

(2) 具体步骤

步骤说明源码位置
1连接websocket服务器socket.js#L25open in new window
2websocket客户端监听事件socket.js#L53open in new window
监听hash事件,保存此hash值index.js#L55open in new window
3监听ok事件,执行reloadApp方法进行更新index.js#L93open in new window
4在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器reloadApp.js#L7open in new window
5在webpack/hot/dev-server.js会监听webpackHotUpdate事件dev-server.js#L55open in new window
6在check方法里会调用module.hot.check方法dev-server.js#L13open in new window
7调用hotDownloadManifest,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lastHash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名HotModuleReplacement.runtime.js#L180open in new window
8调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码JsonpMainTemplate.runtime.js#L14open in new window
9补丁JS取回来后会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法JsonpMainTemplate.runtime.js#L8open in new window
10然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码HotModuleReplacement.runtime.js#L222open in new window
11然后调用hotApply方法进行热更新HotModuleReplacement.runtime.js#L257open in new window
12从缓存中删除旧模块HotModuleReplacement.runtime.js#L510open in new window
13执行accept的回调HotModuleReplacement.runtime.js#L569open in new window

四 源码实现

1 配置文件

// webpack.config.js

//1.引入核心模块
const webpack = require('./webpack');
//2.加载配置文件
const options = require('./webpack.config');
// 3. 执行webpack得到编译对象Compiler,就是一个大管理,是核心编译对象
const compiler = webpack(options);
// 4. 调用它的run方法开始启动编译
compiler.run();

2 启动入口

// package.json

"scripts": {
    "start": "node ./startDevServer.js"
  },

3 启动开发服务器

// startDevServer.js

// 1.准备创建开发服务器
const webpack = require('webpack');
const config = require('./webpack.config');
const Server = require('./webpack-dev-server/lib/Server');

function startDevServer(compiler, config) {
	const devServerArgs = config.devServer || {};
	// 创建http服务器,负责打包项目,提供预览项目,访问打包后的文件
	const server = new Server(compiler, devServerArgs);
	const {port = 8080, host = 'localhost'} = devServerArgs;
	server.listen(port, host, (err) => {
		console.log(`Project is running at http://${host}:${port}/`);
	});
}

// 2.创建complier实例
const compiler = webpack(config);
// 3.启动服务HTTP服务器
startDevServer(compiler, config);

module.exports = startDevServer; 

4 创建服务器类

// webpack-dev-server/lib/Server.js

const express = require('express');
const http = require('http');

const updateCompiler = require('./utils/updateCompiler');
const webpackDevMiddleware = require('../../webpack-dev-middleware');

const io = require('socket.io');
class Server {
	constructor(compiler, devServerArgs) {
		this.sockets = []; // 存放客户端
		this.compiler = compiler; // webpack编译器实例
		this.devServerArgs = devServerArgs; // 配置参数
		updateCompiler(compiler); // 添加客户端入口
		this.setupHooks(); // 开始启动webpack的编译
		this.setupApp(); // 实例化express
		this.routes(); // 添加路由
		this.setupDevMiddleware(); // 添加中间件
		this.createServer(); // 创建http服务器
		this.createSocketServer(); // 创建socket服务
	}

	setupHooks() {
		// 当webpack完成一次完整的编译之后,会触发的done这个钩子的回调函数执行
        // 编译成功后的成果描述(modules,chunks,files,assets,entries)会放在stats里
		this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
			console.log('新的一编译已经完成,新的hash值为',stats.hash);
			//以后每一次新的编译成功后,都要向客户端发送最新的hash值和ok
            this.sockets.forEach(socket=>{
                socket.emit('hash',stats.hash);
                socket.emit('ok');
			});
			// 保存上一次的stats信息
			this._stats = stats;
		});
	}

	routes(){
		if(this.devServerArgs.contentBase){
			//此目录将会成为静态文件根目录
			this.app.use(express.static(this.devServerArgs.contentBase));
		}
	}
	setupApp(){
		//this.app并不是一个http服务,它本身其实只是一个路由中间件
		this.app = express();
	}

	setupDevMiddleware(){
        this.middleware = webpackDevMiddleware(this.compiler);
        this.app.use(this.middleware);
	}
	
	createServer(){
		this.server = http.createServer(this.app);
	}

	createSocketServer(){
        //websocket通信之前要握手,握手的时候用的HTTP协议
        const websocketServer = io(this.server);
        //监听客户端的连接
        websocketServer.on('connection',(socket)=>{
            console.log('一个新的websocket客户端已经连接上来了');
            //把新的客户端添加到数组里,为了以后编译成功之后广播做准备
            this.sockets.push(socket);
            //监控客户端断开事件
            socket.on('disconnect',()=>{
                let index = this.sockets.indexOf(socket);
                this.sockets.splice(index, 1);
            });
            //如果以前已经编译过了,就把上一次的hash值和ok发给客户端
            if(this._stats){
                socket.emit('hash',this._stats.hash);
                socket.emit('ok');
            }
        });
    }
	
	listen(port, host, callback){
		this.server.listen(port, host, callback);
	}
}
module.exports = Server;

5 添加客户端

// webpack-dev-server/utils/updateCompiler.js


6 执行命令输出结果

// socket.io安装报错,后续再更新