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. 原理图
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#L159 |
2 | 创建webpack实例 | webpack-dev-server.js#L89 |
3 | 创建Server服务器 | webpack-dev-server.js#L107 |
4 | 更改config的entry属性 | Server.js#L57 |
updateCompiler中调用addEntries | updateCompiler.js#L47 | |
entry添加dev-server/client/index.js | addEntries.js#L22 | |
entry添加webpack/hot/dev-server.js | addEntries.js#L30 | |
5 | 添加钩子setupHooks | Server.js#L122 |
6 | 添加webpack的done事件回调 | Server.js#L183 |
编译完成向websocket客户端推送消息,最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换 | Server.js#L178 | |
7 | 创建express应用app | Server.js#L166 |
8 | 添加webpack-dev-middleware中间件 | Server.js#L206 |
以watch模式启动webpack编译,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包 | index.js#L41 | |
设置文件系统为内存文件系统 | index.js#L65 | |
返回一个中间件,负责返回生成的文件 | middleware.js#L20 | |
app中使用webpack-dev-middlerware返回的中间件 | Server.js#L128 | |
9 | 创建http服务器并启动服务 | Server.js#L135 |
10 | 建立一个 websocket 长连接 | Server.js#L745 |
创建socket服务器并监听connection事件 | Server.js#L745 |
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#L25 |
2 | websocket客户端监听事件 | socket.js#L53 |
监听hash事件,保存此hash值 | index.js#L55 | |
3 | 监听ok事件,执行reloadApp方法进行更新 | index.js#L93 |
4 | 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器 | reloadApp.js#L7 |
5 | 在webpack/hot/dev-server.js会监听webpackHotUpdate事件 | dev-server.js#L55 |
6 | 在check方法里会调用module.hot.check方法 | dev-server.js#L13 |
7 | 调用hotDownloadManifest,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lastHash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名 | HotModuleReplacement.runtime.js#L180 |
8 | 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码 | JsonpMainTemplate.runtime.js#L14 |
9 | 补丁JS取回来后会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法 | JsonpMainTemplate.runtime.js#L8 |
10 | 然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码 | HotModuleReplacement.runtime.js#L222 |
11 | 然后调用hotApply方法进行热更新 | HotModuleReplacement.runtime.js#L257 |
12 | 从缓存中删除旧模块 | HotModuleReplacement.runtime.js#L510 |
13 | 执行accept的回调 | HotModuleReplacement.runtime.js#L569 |
四 源码实现
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安装报错,后续再更新