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);
        }
    };
};
相关推荐
Watermelo6175 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248947 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356118 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript