webpack之HMR

什么是HMR

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

使用HMR

安装

js 复制代码
yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin socket.io socket.io-client events mime fs-extra --dev

使用

webpack.config.js

webpack.config.js

js 复制代码
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
let HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
    mode: "development",
    entry:"./src/index.js",
    output: {
        filename: "[name].js",
        path: path.resolve(__dirname, "dist")
    },
    devServer:{
        hot:true,
        port:8000,
        contentBase:path.join(__dirname,'static')
    },
    plugins: [
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new HotModuleReplacementPlugin()
    ]
}

src\index.html

src\index.html

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

src\index.js

js 复制代码
let render = () => {
    let title = require("./title.js");
    root.innerText = title;
}
render();

if (module.hot) {
    module.hot.accept(["./title.js"], render);
}

title.js

src\title.js

js 复制代码
module.exports = "title";

package.json

json 复制代码
"scripts": {
  "build": "webpack",
  "dev": "webpack serve"
}

debugger

diff 复制代码
  "scripts": {
    "build": "webpack",
    "dev": "webpack serve",
+    "debug": "webpack serve"
  },

基础知识

module和chunk

  • 在 webpack里有各种各样的模块
  • 一般一个入口会依赖多个模块
  • 一个入口一般会对应一个chunk,这个chunk里包含这个入口依赖的所有的模块

HotModuleReplacementPlugin

  • webpack\lib\HotModuleReplacementPlugin.js
  • 它会生成两个补丁文件
    • 上一次编译生成的hash.hot-update.json,说明从上次编译到现在哪些代码块发生成改变
    • chunk名字.上一次编译生成的hash.hot-update.js,存放着此代码块最新的模块定义,里面会调用webpackHotUpdate方法
  • 向代码块中注入HMR runtime代码,热更新的主要逻辑,比如拉取代码、执行代码、执行accept回调都是它注入的到chunk中的
  • hotCreateRequire会帮我们给模块 module的parentschildren赋值

webpack的监控模式

  • 如果使用监控模式编译webpack的话,如果文件系统中有文件发生了改变,webpack会监听到并重新打包
  • 每次编译会产生一个新的hash值

工作流程

服务器部分

  1. 启动webpack-dev-server服务器
  2. 创建webpack实例
  3. 创建Server服务器
  4. 添加webpack的done事件回调,在编译完成后会向浏览器发送消息
  5. 创建express应用app
  6. 使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中
  7. 设置文件系统为内存文件系统
  8. 添加webpack-dev-middleware中间件
  9. 创建http服务器并启动服务
  10. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些socket消息进行不同的操作。当然服务端传递的最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换
步骤 代码位置
1.启动webpack-dev-server服务器 webpack-dev-server.js#L159
2.创建webpack实例 webpack-dev-server.js#L89
3.创建Server服务器 webpack-dev-server.js#L100
4.更改config的entry属性 webpack-dev-server.js#L157
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#L169
8. 添加webpack-dev-middleware中间件 Server.js#L208
以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. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接 Server.js#L745
创建socket服务器并监听connection事件 SockJSServer.js#L33

客户端部分

  1. webpack-dev-server/client-src/default/index.js端会监听到此hash消息,会保存此hash值
  2. 客户端收到ok的消息后会执行reloadApp方法进行更新
  3. 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器
  4. webpack/hot/dev-server.js会监听webpackHotUpdate事件,然后执行check()方法进行检查
  5. 在check方法里会调用module.hot.check方法
  6. 它通过调用 JsonpMainTemplate.runtimehotDownloadManifest方法,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件,该 Manifest 包含了所有要更新的模块的 hash 值和chunk名
  7. 调用JsonpMainTemplate.runtimehotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码
  8. 补丁JS取回来后会调用JsonpMainTemplate.runtime.jswebpackHotUpdate方法,里面会调用hotAddUpdateChunk方法,用新的模块替换掉旧的模块
  9. 然后会调用HotModuleReplacement.runtime.jshotAddUpdateChunk方法动态更新模块代 码
  10. 然后调用hotApply方法进行热更新
步骤 代码
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.runtimehotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码 JsonpMainTemplate.runtime.js#L14
9. 补丁JS取回来后会调用JsonpMainTemplate.runtime.jswebpackHotUpdate方法 JsonpMainTemplate.runtime.js#L8
10. 然后会调用HotModuleReplacement.runtime.jshotAddUpdateChunk方法动态更新模块代码 HotModuleReplacement.runtime.js#L222
11.然后调用hotApply方法进行热更新 HotModuleReplacement.runtime.js#L257 HotModuleReplacement.runtime.js#L278
12.从缓存中删除旧模块 HotModuleReplacement.runtime.js#L510
13.执行accept的回调 HotModuleReplacement.runtime.js#L569

相关代码

启动开发服务器

startDevServer.js

startDevServer.js

js 复制代码
const webpack = require("webpack")
const Server = require('./webpack-dev-server/lib/Server');
const config = require("./webpack.config")
function startDevServer(compiler,options) {
    const devServerOptions = options.devServer||{};
    const server = new Server(compiler, devServerOptions);
    const {host='localhost',port=8080}=devServerOptions;
    server.listen(port, host, (err) => {
        console.log(`Project is running at http://${host}:${port}`);
    });
}
const compiler = webpack(config);
startDevServer(compiler,config);

Server.js

webpack-dev-server\lib\Server.js

js 复制代码
const express = require("express");
const http = require("http");
class Server {
    constructor(compiler,devServerOptions) {
        this.compiler = compiler;
        this.devServerOptions = devServerOptions;
        this.setupApp();
        this.createServer();
    }
    setupApp() {
        this.app = new express();
    }
    createServer() {
        this.server = http.createServer(this.app);
    }
    listen(port, host = "localhost", callback = ()=>{}) {
         this.server.listen(port, host, callback);
    }
}
module.exports = Server;

package.json

package.json

diff 复制代码
  "scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server",
+   "start":"node ./startDevServer.js"
  },

给entry添加客户端

Server.js

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

diff 复制代码
const express = require("express");
+const updateCompiler = require('./utils/updateCompiler');
const http = require("http");
class Server {
    constructor(compiler,devServerOptions) {
        this.compiler = compiler;
        this.devServerOptions = devServerOptions;
+       updateCompiler(compiler);
        this.setupApp();
        this.createServer();
    }
    setupApp() {
        this.app = new express();
    }
    createServer() {
        this.server = http.createServer(this.app);
    }
    listen(port, host = "localhost", callback = ()=>{}) {
         this.server.listen(port, host, callback);
    }
}
module.exports = Server;

updateCompiler.js

webpack-dev-server\lib\utils\updateCompiler.js

js 复制代码
const path = require("path");
let updateCompiler = (compiler) => {
    const config = compiler.options;
    //来自webpack-dev-server/client/index.js 在浏览器启动WS客户端
    config.entry.main.import.unshift(require.resolve("../../client/index.js"),);
    //webpack/hot/dev-server.js 在浏览器监听WS发射出来的webpackHotUpdate事件
    config.entry.main.import.unshift(require.resolve("../../../webpack/hot/dev-server.js"));
    console.log(config.entry);
    compiler.hooks.entryOption.call(config.context, config.entry);
}
module.exports = updateCompiler;

client\index.js

webpack-dev-server\lib\client\index.js

js 复制代码
console.log('webpack-dev-server\client\index.js');

dev-server.js

webpack-dev-server\lib\client\hot\dev-server.js

js 复制代码
console.log('webpack-dev-server\lib\client\hot\dev-server.js');

添加webpack的done事件回调

Server.js

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

diff 复制代码
const express = require("express");
const updateCompiler = require('./utils/updateCompiler');
const http = require("http");
class Server {
    constructor(compiler,devServerOptions) {
        this.compiler = compiler;
        this.devServerOptions=devServerOptions;
        updateCompiler(compiler);
+       this.sockets = [];
+       this.setupHooks();
        this.setupApp();
        this.createServer();
    }
+    setupHooks() {
+        this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
+            console.log("stats.hash", stats.hash);
+            this.sockets.forEach((socket) => {
+                socket.emit("hash", stats.hash);
+                socket.emit("ok");
+            });
+            this._stats = stats;
+        });
+    }
    setupApp() {
        this.app = new express();
    }
    createServer() {
        this.server = http.createServer(this.app);
    }
    listen(port, host = "localhost", callback = ()=>{}) {
         this.server.listen(port, host, callback);
    }
}
module.exports = Server;

webpack-dev-middleware中间件

  • webpack-dev-middleware 实现webpack编译和文件相关操作

Server.js

webpack-dev-server\lib\Server.js

diff 复制代码
const express = require("express");
const updateCompiler = require('./utils/updateCompiler');
+const webpackDevMiddleware = require('../../webpack-dev-middleware');
const http = require("http");
class Server {
    constructor(compiler,devServerOptions) {
        this.compiler = compiler;
        this.devServerOptions = devServerOptions;
        updateCompiler(compiler);
        this.sockets = [];
        this.setupHooks();
        this.setupApp();
+       this.setupDevMiddleware();
        this.createServer();
    }
+   setupDevMiddleware() {
+        if(this.devServerOptions.contentBase)
+            this.app.use(express.static(this.devServerOptions.contentBase));
+        this.middleware = webpackDevMiddleware(this.compiler);
+        this.app.use(this.middleware);
+   }
    setupHooks() {
        this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
            console.log("stats.hash", stats.hash);
            this.sockets.forEach((socket) => {
                socket.emit("hash", stats.hash);
                socket.emit("ok");
            });
            this._stats = stats;
        });
    }
    setupApp() {
        this.app = new express();
    }
    createServer() {
        this.server = http.createServer(this.app);
    }
    listen(port, host = "localhost", callback = ()=>{}) {
         this.server.listen(port, host, callback);
    }
}
module.exports = Server;

webpack-dev-middleware\index.js

webpack-dev-middleware\index.js

js 复制代码
const middleware = require("./middleware");
const MemoryFileSystem = require("memory-fs");
let memoryFileSystem = new MemoryFileSystem();
function webpackDevMiddleware(compiler) {
    compiler.watch({}, () => {
        console.log("start watching!");
    });
    let fs = compiler.outputFileSystem = memoryFileSystem;
    return middleware({
        fs,
        outputPath:compiler.options.output.path
    });
}

module.exports = webpackDevMiddleware;

middleware.js

webpack-dev-middleware\middleware.js

js 复制代码
const mime = require('mime');
const path = require("path");
module.exports = function wrapper(context) {
    return function middleware(req, res, next) {
        let url = req.url;
        if (url === "/") { url = "/index.html"; }
        let filename = path.join(context.outputPath, url);
        try {
            let stat = context.fs.statSync(filename);
            if (stat.isFile()) {
                let content = context.fs.readFileSync(filename);
                res.setHeader("Content-Type", mime.getType(filename));
                res.send(content);
            } else {
                res.sendStatus(404);
            }
        } catch (error) {
            res.sendStatus(404);
        }
    };
};
相关推荐
热爱编程的小曾8 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin20 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox33 分钟前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758102 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox