webpack热更新原理介绍

webpack热更新原理

  • 1、什么是HMR
  • 2、搭建HMR项目
  • 3、热更新流程图
  • 3、项目文件
  • 4.手写实现webpack-dev-server.js
  • 5、编写客户端更新模块webpackHotDevClient
  • 6、总结
  • * 集成 Socket.IO介绍 (只是普及和上面无关)

1、什么是HMR

Hot Module Replacement是指当你对代码修改并保存后,webpack将会对代码进行新打爆,并将新的模块发送到浏览器端。
相对于live reload 刷新页面的方案, HMR的有点在于保存应用的状态,提高开发效率。

2、搭建HMR项目

在这里插入图片描述
1、需要 cnpm i webpack webpack-cli webpack-dev-server html-webpack-plugin express socket.io events -S

// package.json
{
     
  "name": "15.hmr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
     
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
     
    "express": "^4.17.1"
  },
  "dependencies": {
     
    "express": "^4.17.1",
    "html-webpack-plugin": "^4.4.1",
    "mime": "^2.4.6",
    "socket.io": "^2.3.0",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }
}

项目结构
webpack热更新原理介绍_第1张图片

3、热更新流程图

webpack热更新原理介绍_第2张图片

3、项目文件

// webpack.config
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
     
  mode: "development",
  entry: "./src/index.js",
  output: {
     
    filename: "main.js",
    path: path.join(__dirname, "dist"),
  },
  devServer: {
     
    hot: true,
    contentBase: path.join(__dirname, "dist"),
  },
  plugins: [
    new HtmlWebpackPlugin({
     
      template: "./src/index.html",
      filename: "index.html",
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
};


<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>webpack热更新title>
head>

<body>
    <div id="root">div>
    <script src="/socket.io/socket.io.js">script>
body>

html>

// 入口文件:

// src/title.js
module.exports = 'title7';

// src/index.js  // 入口文件
import '../webpackHotDevClient';
let root = document.getElementById("root");
function render() {
     
  let title = require("./title");
  root.innerHTML = title;
}
render(); // 渲染页面用


if(module.hot){
     
  module.hot.accept(['./title'],()=>{
     
      render();
  });
}

4.手写实现webpack-dev-server.js

1、创建webpack实例
2、定义个server类 实例化时传入webpack的complier
3、手写个简易版webpack-dev-middleware中间件, 做静态服务器, 用来提供编译后产出的文件的静态文件服务
4、使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些socket消息进行不同的操作
当然服务端传递的最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换
启动webpack-dev-server服务器
5、compiler.watch监听编译
6、 每次文件编译后触发webpack-dev-server事件,执行回调的socket.emit事件, 发送编译完后把hash发给客户端

const path = require("path");
const fs = require("fs");
const express = require("express");
const mime = require("mime");
const webpack = require("webpack");
let config = require("./webpack.config");//配置对象
let compiler = webpack(config); // 使用webpack 配置 返回一个complier编译器 里面可以执行webpack操作
//1. 创建webpack实例
//2. 启动webpack-dev-server服务器
class Server {
     
  constructor(compiler) {
     
    //4. 添加webpack的`done`事件回调,在编译完成后会向浏览器发送消息
    let lastHash;
    let sockets = [];
    // 插件
    compiler.hooks.done.tap("webpack-dev-server", (stats) => {
     
      lastHash = stats.hash;
      sockets.forEach((socket) => {
     
        socket.emit("hash", stats.hash);//编译成功后先把hash值发给客户端,再发送ok事件
        socket.emit("ok");
      });
    });
    let app = new express();
    //webpack开始以监听模式进行编译
    compiler.watch({
     }, (err) => {
     
      console.log("编译成功");
    });

    //3. 添加webpack-dev-middleware中间件
    //用来提供编译后产出的文件的静态文件服务
    const webpackDevMiddleware = (req, res, next) => {
     
      if (req.url === "/favicon.ico") {
     
        return res.sendStatus(404);
      } else if (req.url === "/") {
     
        return res.sendFile(path.join(config.output.path,'index.html'));
      }
      // 比如请求/main.js 就拿到filename = dist/main.js
      let filename = path.join(config.output.path, req.url.slice(1));//main.js
      try {
     
        let stats = fs.statSync(filename);
        // 判断是文件就读取内容 返回给html
        if (stats.isFile()) {
     
          let content = fs.readFileSync(filename);
          res.header("Content-Type", mime.getType(filename));
          res.send(content);
        } else {
     
          next();
        }
      } catch (error) {
     
          return res.sendStatus(404);
      }
    };
    app.use(webpackDevMiddleware);
    this.server = require("http").createServer(app);
    //4. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
    //将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些`socket`消息进行不同的操作
    //当然服务端传递的最主要信息还是新模块的`hash`值,后面的步骤根据这一`hash`值来进行模块热替换
    let io = require("socket.io")(this.server);
    //启动一个websocket服务器
    io.on("connection", (socket) => {
     
      sockets.push(socket);
      if (lastHash) {
     
        //5.发送hash值
        socket.emit("hash", lastHash);
        socket.emit("ok");
      }
    });
  }
  //9. 创建http服务器并启动服务
  listen(port) {
     
    this.server.listen(port, () => {
     
      console.log(port + "服务启动成功!");
    });
  }
}
//3. 创建Server服务器
let server = new Server(compiler);
server.listen(8080);

5、编写客户端更新模块webpackHotDevClient

1、scoket在客户端连接后,就能on ‘hash’ 和 ‘ok’ 事件了
2、currentHash 记录当前的hash, lastHash记录上次文件的hash
3、客户端监听到ok后,执行hotCheck(),即拉取 ("/" + lastHash + “.hot-update.json”)的数据 里面是一个对象,如下
用lastHash是为了告诉服务器 我目前版本是这个 你给当前到最新版本的补丁hash我
webpack热更新原理介绍_第3张图片
h:当前hash值 留到下次才用到更新 每次请求描述文件都是传上一个的hash值
c:里面的main代表修改的模块

webpack热更新原理介绍_第4张图片

4、通过script标签加载 “/” + chunkId + “.” + lastHash+ “.hot-update.js”
即 /main.f147xxxxxx.hot-update.js js内容如下,调用webpackHotUpdate 传入 main 和 修改的js对象
webpack热更新原理介绍_第5张图片
调用下面的方法

//11. 补丁JS取回来后会调用`webpackHotUpdate`方法
window.webpackHotUpdate = (chunkId, moreModules) => {
     
  for (let moduleId in moreModules) {
     
    let oldModule = __webpack_require__.c[moduleId];//获取老模块
    let {
      parents } = oldModule;//父亲们 儿子们
    // 新的改变后的模块初始化
    var module = (__webpack_require__.c[moduleId] = {
     
      i: moduleId,
      exports: {
     },
      parents,
      children,
      hot: window.hotCreateModule(),
    });
    // 执行模块操作
    moreModules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    parents.forEach((parent) => {
     
      let parentModule = __webpack_require__.c[parent];
      parentModule.hot &&
        parentModule.hot._acceptedDependencies[moduleId] &&
        parentModule.hot._acceptedDependencies[moduleId]();
    });
    lastHash = currentHash;
  }
};

5、webpackHotUpdate 调用时
5.1、会创建新的模块对象 module 然后调用该模块方法
5.2、循环父亲(即哪个js依赖了他,就是他的父亲)比如改了title.js 父亲就是index.js 然后执行父亲的hot对象的_acceptedDependencies里面对应模块的callback方法
_acceptedDependencies用来装他的依赖模块的回调,哪个依赖改动了就执行该依赖回调
5.3 执行完 页面就拿到新的title之后 重新渲染 , 思路就是如此。

window.hotCreateModule = () => {
     
  var hot = {
     
    _acceptedDependencies: {
     }, //接收的依赖
    accept: function (dep, callback) {
     
      for (var i = 0; i < dep.length; i++) {
     
        hot._acceptedDependencies[dep[i]] = callback;
        //hot._acceptedDependencies['./title']=callback
      }
    },
  };
  return hot;
}

// index.js 调用了module.hot.accept 等于依赖了title.js  title改变后 会触发render();
if(module.hot){
     
  module.hot.accept(['./title'],()=>{
     
      render();
  });
}

6、webpackHotDevClient代码介绍

// webpackHotDevClient.js
/*
 * @description: 
 * @author: steve.deng
 * @Date: 2020-09-07 11:50:33
 * @LastEditors: steve.deng
 * @LastEditTime: 2020-09-15 10:14:26
 */
let socket = io("/");//先通过socket.io连接服务器
let currentHash;//当前的hash
let lastHash;//上一次的hash
const onConnected = () => {
     
  console.log("客户端已经连接");
  //6. 客户端会监听到此hash消息
  socket.on("hash", (hash) => {
     
    currentHash = hash;
  });
  //7. 客户端收到`ok`的消息
  socket.on("ok", () => {
     
    hotCheck();
  });
  socket.on("disconnect", () => {
     
     lastHash = currentHash = null;
  });
};
//8.执行hotCheck方法进行更新
function hotCheck() {
     
  if (!lastHash || lastHash === currentHash) {
     
    return (lastHash = currentHash);
  }
  //9.向 server 端发送 Ajax 请求,服务端返回一个hot-update.json文件,该文件包含了所有要更新的模块的 `hash` 值和chunk名
  hotDownloadManifest().then((update) => {
     
    let chunkIds = Object.keys(update.c);//['main']
    chunkIds.forEach((chunkId) => {
     
      //10. 通过JSONP请求获取到最新的模块代码
      hotDownloadUpdateChunk(chunkId);
    });
  });
}

function hotDownloadUpdateChunk(chunkId) {
     
  var script = document.createElement("script");
  script.charset = "utf-8";
  script.src = "/" + chunkId + "." + lastHash+ ".hot-update.js";
  document.head.appendChild(script);
}
function hotDownloadManifest() {
     
  var url = "/" + lastHash + ".hot-update.json";
  return fetch(url).then(res => res.json()).catch(error=>{
     console.log(error);});
}
//11. 补丁JS取回来后会调用`webpackHotUpdate`方法
window.webpackHotUpdate = (chunkId, moreModules) => {
     
  for (let moduleId in moreModules) {
     
    let oldModule = __webpack_require__.c[moduleId];//获取老模块
    let {
      parents } = oldModule;//父亲们 儿子们
    var module = (__webpack_require__.c[moduleId] = {
     
      i: moduleId,
      exports: {
     },
      parents,
      children,
      hot: window.hotCreateModule(),
    });
    moreModules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    parents.forEach((parent) => {
     
      let parentModule = __webpack_require__.c[parent];
      parentModule.hot &&
        parentModule.hot._acceptedDependencies[moduleId] &&
        parentModule.hot._acceptedDependencies[moduleId]();
    });
    lastHash = currentHash;
  }
};
socket.on("connect", onConnected);
window.hotCreateModule = () => {
     
  var hot = {
     
    _acceptedDependencies: {
     }, //接收的依赖
    accept: function (dep, callback) {
     
      for (var i = 0; i < dep.length; i++) {
     
        hot._acceptedDependencies[dep[i]] = callback;
        //hot._acceptedDependencies['./title']=callback
      }
    },
  };
  return hot;
}

6、总结

其实热更新本质就是服务器和客户端通过socket.io通信,然后代码修改后:
1、本地开发服务器就会emit hash和ok事件,传递文件当前hash值给currentHash
2、浏览器端监听ok事件后就会用上一次的lastHash去获取对应改变的模块的描述文件,根据描述文件里面的依赖模块chunkId和lastHash,jsonp去获取新的模块js(xxx.hot-update.js)
3、新的模块获取后会执行webpackHotUpdate, 初始化改变的模块和修改模块的父模块的_acceptedDependencies里面的对应callback方法。
4、父模块相当于更新了数据。
5、记录下lastHash = currentHash; 下次更新又用lastHash去获取描述文件和更新后的文件,即可。

* 集成 Socket.IO介绍 (只是普及和上面无关)

Socket.IO 由两部分组成:

一个服务端用于集成 (或挂载) 到 Node.JS HTTP 服务器: socket.io
一个加载到浏览器中的客户端: socket.io-client
开发环境下, socket.io 会自动提供客户端。正如我们所见,到目前为止,我们只需要安装一个模块:

npm install --save socket.io

这会安装模块并添加依赖到 package.json。在 index.js 文件中添加该模块:

var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
 
app.get('/', function(req, res){
     
  res.sendFile(__dirname + '/index.html');
});
 
io.on('connection', function(socket){
     
  console.log('a user connected');
});
 
http.listen(3000, function(){
     
  console.log('listening on *:3000');
});

我们通过传入 http (HTTP 服务器) 对象初始化了 socket.io 的一个实例。 然后监听 connection 事件来接收 sockets, 并将连接信息打印到控制台。

在 index.html 的 标签中添加如下内容:

// require('socket.io')(http)后就会有提供个js   /socket.io/socket.io.js加载socket.io-client, 客户端就能监听消息了
<script src="/socket.io/socket.io.js">script>
<script>
  var socket = io();
script>

这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。

请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。

重新加载服务器和网站,你将看到控制台打印出 “a user connected”。
尝试打开多个标签页,可以看到多条信息:

socket.io 快速入门教程——聊天应用4

每个 socket 还会触发一个特殊的 disconnect 事件:

io.on('connection', function(socket){
     
  console.log('a user connected');
  socket.on('disconnect', function(){
     
    console.log('user disconnected');
  });
});

你可以多次刷新标签页来查看效果:

socket.io 快速入门教程——聊天应用5

触发事件
Socket.IO 的核心理念就是允许发送、接收任意事件和任意数据。任意能被编码为 JSON 的对象都可以用于传输。二进制数据 也是支持的。

这里的实现方案是,当用户输入消息时,服务器接收一个 chat message 事件。index.html 文件中的 script 部分现在应该内容如下:

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
  $(function () {
     
    var socket = io();
    $('form').submit(function(){
     
      socket.emit('chat message', $('#m').val());
      $('#m').val('');
      return false;
    });
  });
</script>

在 index.js 中打印出 chat message 事件:

io.on('connection', function(socket){
     
  socket.on('chat message', function(msg){
     
    console.log('message: ' + msg);
  });
});

你可能感兴趣的:(前端干货,webpack,node.js)