精华内容
下载资源
问答
  • 理解webpack热更新原理
    千次阅读
    2020-05-17 13:50:24

    本文参考链接:https://mp.weixin.qq.com/s/2L9Y0pdwTTmd8U2kXHFlPA

    一、开启:HotModuleReplacementPlugin/--hot

    二、webpack编译构建后,控制台出现:

           1、新的hash值 // 作为下次热更新的标识

           2、新的json文件 // h代表新hash,c代表要热更新的模块

           3、新的js文件 // 本次修改后的代码

    三、热更新实现原理:

            1、启动webpack生成compiler实例,compiler上有很多方法,例如监听本地文件的变化

            2、使用express框架启动本地server,让浏览器可以请求本地的静态资源

            3、启动websocket服务,当本地文件发生变化,可以立即告知浏览器可以热更新代码

    四、启动本地服务前在入口增加两个文件

            1、获取websocket客户端代码

            2、根据配置获取webpack热更新代码路径

    五、过程:

            1、监听webpack编译结束,通过websocket给浏览器发送通知,ok和hash事件,浏览器拿到最新的hash值做更新检查逻辑

            2、webpack-deb-server主要职责:启动服务和前置准备工作

            3、webpack-dev-middleware主要职责:本地文件的编译和输出以及监听

            4、浏览器接收到通知,hash事件:更新最新一次打包后的hash值

                                                  ok事件:进行热更新检查

            5、利用上一次保存的hash值,发送xxx/hash.hot-update.json的ajax请求,获取热更新模块

            6、通过jsonp方式发送xxx/hash.hot-update.js请求,使用jsonp可以直接执行代码,更新代码,而不用刷新页面更新

    更多相关内容
  • webpack热更新原理

    2020-08-19 11:02:15
    HMR Server: 将热更新的文件输出给 HMR Runtime Bundle Server: 提供文件在浏览器访问 HMR Runtime:会被注入到浏览器,更新文件变化 bundle.js : 构建输出的文件 资源内联 代码层面: 页面框架的初始化...

    webpack Compile: 将js编译成Bundle

    HMR Server: 将热更新的文件输出给 HMR Runtime

    Bundle Server: 提供文件在浏览器访问

    HMR Runtime:会被注入到浏览器,更新文件变化

    bundle.js : 构建输出的文件

    资源内联

    代码层面:

    页面框架的初始化脚本
    上报相关打点
    css内联避免页面闪动
    请求层面:
    减少http网络请求数 (小图片或者字体内联(url-loader))

    html和 js内联
    `
    raw-loader 内联html

    raw-loader 内联js

    `

    多页面打包通用方案

    1.动态获取 entry 和设置 html-webpack-plugin数量

    2.利用 glob.sync

      entry: glob.sync(path.join(__dirname,'./src/*/index.js'))
    

    使用source map

    1.五个关键字

    eval: 使用eval包裹代码块
    source map: 产生.map文件
    cheap : 不包含列信息
    inline 将.map作为DataURI嵌入,不单独生成 .map文件
    module : 包含loader的sourcemap

    webpack ssr打包存在的问题

    1.浏览器的全局变量(nodejs中没有 document window)
    组件适配: 将不兼容的组件根据打包环境进行适配。
    请求适配: 将fetch或者 ajax发送请求的写法改成 isomorphic-fetch或者 axios。
    2.样式问题(nodejs无法解析css)
    方案一: 服务端打包通过ignore-loader忽略掉css的解析
    方案二: 将style-loader 替换成 isomorphic-style-loader。

    优化构建时命令行的显示日志

    |preset|alternative|description|
    |"errors-only"|none|只在发生错误时输出|
    |"minimal"|none|只在发生错误或有新的编译时输出|
    |"none"|false|没有输出|
    |"normal"|true|标准输出|
    |"verbose"|none|全部输出|

    如何优化?
    1.插件 friendly-errors-webpack-plugin:

    success: 构建成功的日志提示
    warning:构建警告的日志提示
    error : 构建报错的日志提示
    2.stats设置成 errors-only

    如何判断构建是否成功?

    在CI/CD的 pipline 或者发布系统需要知道当前的构建状态。

    每次构建完成后输入 echo $? 获取错误码

    webpack4 之前的版本构建失败不会抛出错误码
    nodejs中的 process.exit规范:

    0表示成功完成,回调函数中err为null。
    非0表示执行失败,回调函数中,err不为null, err.code就是传给exit的数字。

    如何主动捕获并处理构建错误?

    compiler 在每次构建结束后会触发done这个hook
    process.exit主动处理构建报错

    展开全文
  • webpack热更新原理介绍

    2020-09-17 13:50:25
    webpack热更新原理1、 什么是HMR2、搭建HMR项目3、热更新流程图3、项目文件4.手写实现webpack-dev-server.js5、编写客户端更新模块webpackHotDevClient6、总结* 集成 Socket.IO介绍 (只是普及和上面无关) 1、 什么...

    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"
      }
    }
    
    

    项目结构
    在这里插入图片描述

    3、热更新流程图

    在这里插入图片描述

    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()
      ],
    };
    
    
    <!DOCTYPE html>
    <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我
    在这里插入图片描述
    h:当前hash值 留到下次才用到更新 每次请求描述文件都是传上一个的hash值
    c:里面的main代表修改的模块

    在这里插入图片描述

    4、通过script标签加载 “/” + chunkId + “.” + lastHash+ “.hot-update.js”
    即 /main.f147xxxxxx.hot-update.js js内容如下,调用webpackHotUpdate 传入 main 和 修改的js对象
    在这里插入图片描述
    调用下面的方法

    //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热更新原理

    千次阅读 多人点赞 2020-06-21 23:26:16
    一、前言 - webpack热更新 Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验。 刷新我们一般分为两种: 一种...

    一、前言 - webpack热更新

    Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验

    刷新我们一般分为两种:

    • 一种是页面刷新,不保留页面状态,就是简单粗暴,直接window.location.reload()
    • 另一种是基于WDS (Webpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

    HMR作为一个Webpack内置的功能,可以通过HotModuleReplacementPlugin--hot开启。那么,HMR到底是怎么实现热更新的呢?下面让我们来了解一下吧!

    二、webpack的编译构建过程

    项目启动后,进行构建打包,控制台会输出构建过程,我们可以观察到生成了一个 Hash值a93fd735d02d98633356

    首次构建控制台输出日志

     

    然后,在我们每次修改代码保存后,控制台都会出现 Compiling…字样,触发新的编译中...可以在控制台中观察到:

     

    • 新的Hash值a61bdd6e82294ed06fa3
    • 新的json文件a93fd735d02d98633356.hot-update.json
    • 新的js文件index.a93fd735d02d98633356.hot-update.js

     

    修改代码的编译日志

     

     

    首先,我们知道Hash值代表每一次编译的标识。其次,根据新生成文件名可以发现,上次输出的Hash值会作为本次编译新生成的文件标识。依次类推,本次输出的Hash值会被作为下次热更新的标识。

    然后看一下,新生成的文件是什么?每次修改代码,紧接着触发重新编译,然后浏览器就会发出 2 次请求。请求的便是本次新生成的 2 个文件。如下:

    浏览器请求

     

    首先看json文件,返回的结果中,h代表本次新生成的Hash值,用于下次文件热更新请求的前缀。c表示当前要热更新的文件对应的是index模块。

     

    再看下生成的js文件,那就是本次修改的代码,重新编译打包后的。

     

    还有一种情况是,如果没有任何代码改动,直接保存文件,控制台也会输出编译打包信息的。

     

    • 新的Hash值d2e4208eca62aa1c5389
    • 新的json文件a61bdd6e82294ed06fa3.hot-update.json

     

     

     

    但是我们发现,并没有生成新的js文件,因为没有改动任何代码,同时浏览器发出的请求,可以看到c值为空,代表本次没有需要更新的代码。

     

     

    小声说下,webapck以前的版本这种情况hash值是不会变的,后面可能出于什么原因改版了。细节不用在意,了解原理才是真谛!!!

    最后思考下🤔,浏览器是如何知道本地代码重新编译了,并迅速请求了新生成的文件?是谁告知了浏览器?浏览器获得这些文件又是如何热更新成功的?那让我们带着疑问看下热更新的过程,从源码的角度看原理。

    三、热更新实现原理

    相信大家都会配置webpack-dev-server热更新,我就不示意例子了。自己网上查下即可。接下来我们就来看下webpack-dev-server是如何实现热更新的?(源码都是精简过的,第一行会注明代码路径,看完最好结合源码食用一次)。

    1. webpack-dev-server启动本地服务

    我们根据webpack-dev-serverpackage.json中的bin命令,可以找到命令的入口文件bin/webpack-dev-server.js

    // node_modules/webpack-dev-server/bin/webpack-dev-server.js
    
    // 生成webpack编译主引擎 compiler
    let compiler = webpack(config);
    
    // 启动本地服务
    let server = new Server(compiler, options, log);
    server.listen(options.port, options.host, (err) => {
        if (err) {throw err};
    });
    复制代码

    本地服务代码:

    // node_modules/webpack-dev-server/lib/Server.js
    class Server {
        constructor() {
            this.setupApp();
            this.createServer();
        }
        
        setupApp() {
            // 依赖了express
        	this.app = new express();
        }
        
        createServer() {
            this.listeningApp = http.createServer(this.app);
        }
        listen(port, hostname, fn) {
            return this.listeningApp.listen(port, hostname, (err) => {
                // 启动express服务后,启动websocket服务
                this.createSocketServer();
            }
        }                                   
    }
    复制代码

    这一小节代码主要做了三件事:

    • 启动webpack,生成compiler实例。compiler上有很多方法,比如可以启动 webpack 所有编译工作,以及监听本地文件的变化。
    • 使用express框架启动本地server,让浏览器可以请求本地的静态资源
    • 本地server启动之后,再去启动websocket服务,如果不了解websocket,建议简单了解一下websocket速成。通过websocket,可以建立本地服务和浏览器的双向通信。这样就可以实现当本地文件发生变化,立马告知浏览器可以热更新代码啦!

    上述代码主要干了三件事,但是源码在启动服务前又做了很多事,接下来便看看webpack-dev-server/lib/Server.js还做了哪些事?

    2. 修改webpack.config.js的entry配置

    启动本地服务前,调用了updateCompiler(this.compiler)方法。这个方法中有 2 段关键性代码。一个是获取websocket客户端代码路径,另一个是根据配置获取webpack热更新代码路径。

    // 获取websocket客户端代码
    const clientEntry = `${require.resolve(
        '../../client/'
    )}?${domain}${sockHost}${sockPath}${sockPort}`;
    
    // 根据配置获取热更新代码
    let hotEntry;
    if (options.hotOnly) {
        hotEntry = require.resolve('webpack/hot/only-dev-server');
    } else if (options.hot) {
        hotEntry = require.resolve('webpack/hot/dev-server');
    }
    复制代码

    修改后的webpack入口配置如下:

    // 修改后的entry入口
    { entry:
        { index: 
            [
                // 上面获取的clientEntry
                'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
                // 上面获取的hotEntry
                'xxx/node_modules/webpack/hot/dev-server.js',
                // 开发配置的入口
                './src/index.js'
        	],
        },
    }      
    复制代码

    为什么要新增了 2 个文件?在入口默默增加了 2 个文件,那就意味会一同打包到bundle文件中去,也就是线上运行时。

    (1)webpack-dev-server/client/index.js

    首先这个文件用于websocket的,因为websoket是双向通信,如果不了解websocket,建议简单了解一下websocket速成。我们在第 1 步 webpack-dev-server初始化 的过程中,启动的是本地服务端的websocket。那客户端也就是我们的浏览器,浏览器还没有和服务端通信的代码呢?总不能让开发者去写吧hhhhhh。因此我们需要把websocket客户端通信代码偷偷塞到我们的代码中。客户端具体的代码后面会在合适的时机细讲哦。

    (2)webpack/hot/dev-server.js

    这个文件主要是用于检查更新逻辑的,这里大家知道就好,代码后面会在合适的时机(第5步)细讲。

    3. 监听webpack编译结束

    修改好入口配置后,又调用了setupHooks方法。这个方法是用来注册监听事件的,监听每次webpack编译完成。

    // node_modules/webpack-dev-server/lib/Server.js
    // 绑定监听事件
    setupHooks() {
        const {done} = compiler.hooks;
        // 监听webpack的done钩子,tapable提供的监听方法
        done.tap('webpack-dev-server', (stats) => {
            this._sendStats(this.sockets, this.getStats(stats));
            this._stats = stats;
        });
    };
    复制代码

    当监听到一次webpack编译结束,就会调用_sendStats方法通过websocket给浏览器发送通知,okhash事件,这样浏览器就可以拿到最新的hash值了,做检查更新逻辑。

    // 通过websoket给客户端发消息
    _sendStats() {
        this.sockWrite(sockets, 'hash', stats.hash);
        this.sockWrite(sockets, 'ok');
    }
    复制代码

    4. webpack监听文件变化

    每次修改代码,就会触发编译。说明我们还需要监听本地代码的变化,主要是通过setupDevMiddleware方法实现的。

    这个方法主要执行了webpack-dev-middleware库。很多人分不清webpack-dev-middlewarewebpack-dev-server的区别。其实就是因为webpack-dev-server只负责启动服务和前置准备工作,所有文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译输出以及监听,无非就是职责的划分更清晰了。

    那我们来看下webpack-dev-middleware源码里做了什么事:

    // node_modules/webpack-dev-middleware/index.js
    compiler.watch(options.watchOptions, (err) => {
        if (err) { /*错误处理*/ }
    });
    
    // 通过“memory-fs”库将打包后的文件写入内存
    setFs(context, compiler); 
    复制代码

    (1)调用了compiler.watch方法,在第 1 步中也提到过,compiler的强大。这个方法主要就做了 2 件事:

    • 首先对本地文件代码进行编译打包,也就是webpack的一系列编译流程。
    • 其次编译结束后,开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听。

    为什么代码的改动保存会自动编译,重新打包?这一系列的重新检测编译就归功于compiler.watch这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。

    (2)执行setFs方法,这个方法主要目的就是将编译后的文件打包到内存。这就是为什么在开发的过程中,你会发现dist目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs

    5. 浏览器接收到热更新的通知

    我们已经可以监听到文件的变化了,当文件发生变化,就触发重新编译。同时还监听了每次编译结束的事件。当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。下面重点讲的就是_sendStats方法中的okhash事件都做了什么。

    那浏览器是如何接收到websocket的消息呢?回忆下第 2 步骤增加的入口文件,也就是websocket客户端代码。

    'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080'
    复制代码

    这个文件的代码会被打包到bundle.js中,运行在浏览器中。来看下这个文件的核心代码吧。

    // webpack-dev-server/client/index.js
    var socket = require('./socket');
    var onSocketMessage = {
        hash: function hash(_hash) {
            // 更新currentHash值
            status.currentHash = _hash;
        },
        ok: function ok() {
            sendMessage('Ok');
            // 进行更新检查等操作
            reloadApp(options, status);
        },
    };
    // 连接服务地址socketUrl,?http://localhost:8080,本地服务地址
    socket(socketUrl, onSocketMessage);
    
    function reloadApp() {
    	if (hot) {
            log.info('[WDS] App hot update...');
            
            // hotEmitter其实就是EventEmitter的实例
            var hotEmitter = require('webpack/hot/emitter');
            hotEmitter.emit('webpackHotUpdate', currentHash);
        } 
    }
    复制代码

    socket方法建立了websocket和服务端的连接,并注册了 2 个监听事件。

    • hash事件,更新最新一次打包后的hash值。
    • ok事件,进行热更新检查。

    热更新检查事件是调用reloadApp方法。比较奇怪的是,这个方法又利用node.jsEventEmitter,发出webpackHotUpdate消息。这是为什么?为什么不直接进行检查更新呢?

    个人理解就是为了更好的维护代码,以及职责划分的更明确。websocket仅仅用于客户端(浏览器)和服务端进行通信。而真正做事情的活还是交回给了webpack

    webpack怎么做的呢?再来回忆下第 2 步。入口文件还有一个文件没有讲到,就是:

    'xxx/node_modules/webpack/hot/dev-server.js'
    复制代码

    这个文件的代码同样会被打包到bundle.js中,运行在浏览器中。这个文件做了什么就显而易见了吧!先瞄一眼代码:

    // node_modules/webpack/hot/dev-server.js
    var check = function check() {
        module.hot.check(true)
            .then(function(updatedModules) {
                // 容错,直接刷新页面
                if (!updatedModules) {
                    window.location.reload();
                    return;
                }
                
                // 热更新结束,打印信息
                if (upToDate()) {
                    log("info", "[HMR] App is up to date.");
                }
        })
            .catch(function(err) {
                window.location.reload();
            });
    };
    
    var hotEmitter = require("./emitter");
    hotEmitter.on("webpackHotUpdate", function(currentHash) {
        lastHash = currentHash;
        check();
    });
    复制代码

    这里webpack监听到了webpackHotUpdate事件,并获取最新了最新的hash值,然后终于进行检查更新了。检查更新呢调用的是module.hot.check方法。那么问题又来了,module.hot.check又是哪里冒出来了的!答案是HotModuleReplacementPlugin搞得鬼。这里留个疑问,继续往下看。

    6. HotModuleReplacementPlugin

    前面好像一直是webpack-dev-server做的事,那HotModuleReplacementPlugin在热更新过程中又做了什么伟大的事业呢?

    首先你可以对比下,配置热更新和不配置时bundle.js的区别。内存中看不到?直接执行webpack命令就可以看到生成的bundle.js文件啦。不要用webpack-dev-server启动就好了。

    (1)没有配置的。

     

    (2)配置了HotModuleReplacementPlugin--hot的。

     

    哦~ 我们发现moudle新增了一个属性为hot,再看hotCreateModule方法。 这不就找到module.hot.check是哪里冒出来的。

     

     

    经过对比打包后的文件,__webpack_require__中的moudle以及代码行数的不同。我们都可以发现HotModuleReplacementPlugin原来也是默默的塞了很多代码到bundle.js中呀。这和第 2 步骤很是相似哦!为什么,因为检查更新是在浏览器中操作呀。这些代码必须在运行时的环境。

    你也可以直接看浏览器Sources下的代码,会发现webpackplugin偷偷加的代码都在哦。在这里调试也很方便。

     

    HotModuleReplacementPlugin如何做到的?这里我就不讲了,因为这需要你对tapable以及plugin机制有一定了解,可以看下我写的文章Webpack插件机制之Tapable-源码解析。当然你也可以选择跳过,只关心热更新机制即可,毕竟信息量太大。

     

    7. moudle.hot.check 开始热更新

    通过第 6 步,我们就可以知道moudle.hot.check方法是如何来的啦。那都做了什么?之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中的哦,我就不写文件路径了。

    • 利用上一次保存的hash值,调用hotDownloadManifest发送xxx/hash.hot-update.jsonajax请求;
    • 请求结果获取热更新模块,以及下次热更新的Hash 标识,并进入热更新准备阶段。
    hotAvailableFilesMap = update.c; // 需要更新的文件
    hotUpdateNewHash = update.h; // 更新下次热更新hash值
    hotSetStatus("prepare"); // 进入热更新准备状态
    复制代码
    • 调用hotDownloadUpdateChunk发送xxx/hash.hot-update.js 请求,通过JSONP方式。
    function hotDownloadUpdateChunk(chunkId) {
        var script = document.createElement("script");
        script.charset = "utf-8";
        script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
        if (null) script.crossOrigin = null;
        document.head.appendChild(script);
     }
    复制代码

    这个函数体为什么要单独拿出来,因为这里要解释下为什么使用JSONP获取最新代码?主要是因为JSONP获取的代码可以直接执行。为什么要直接执行?我们来回忆下/hash.hot-update.js的代码格式是怎么样的。

     

     

    可以发现,新编译后的代码是在一个webpackHotUpdate函数体内部的。也就是要立即执行webpackHotUpdate这个方法。

    再看下webpackHotUpdate这个方法。

    window["webpackHotUpdate"] = function (chunkId, moreModules) {
        hotAddUpdateChunk(chunkId, moreModules);
    } ;
    复制代码
    • hotAddUpdateChunk方法会把更新的模块moreModules赋值给全局全量hotUpdate
    • hotUpdateDownloaded方法会调用hotApply进行代码的替换。
    function hotAddUpdateChunk(chunkId, moreModules) {
        // 更新的模块moreModules赋值给全局全量hotUpdate
        for (var moduleId in moreModules) {
            if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
    	    hotUpdate[moduleId] = moreModules[moduleId];
            }
        }
        // 调用hotApply进行模块的替换
        hotUpdateDownloaded();
    }
    复制代码

    8. hotApply 热更新模块替换

    热更新的核心逻辑就在hotApply方法了。 hotApply代码有将近 400 行,还是挑重点讲了,看哭😭

    ①删除过期的模块,就是需要替换的模块

    通过hotUpdate可以找到旧模块

    var queue = outdatedModules.slice();
    while (queue.length > 0) {
        moduleId = queue.pop();
        // 从缓存中删除过期的模块
        module = installedModules[moduleId];
        // 删除过期的依赖
        delete outdatedDependencies[moduleId];
        
        // 存储了被删掉的模块id,便于更新代码
        outdatedSelfAcceptedModules.push({
            module: moduleId
        });
    }
    复制代码

    ②将新的模块添加到 modules 中

    appliedUpdate[moduleId] = hotUpdate[moduleId];
    for (moduleId in appliedUpdate) {
        if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
            modules[moduleId] = appliedUpdate[moduleId];
        }
    }
    复制代码

    ③通过__webpack_require__执行相关模块的代码

    for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
        var item = outdatedSelfAcceptedModules[i];
        moduleId = item.module;
        try {
            // 执行最新的代码
            __webpack_require__(moduleId);
        } catch (err) {
            // ...容错处理
        }
    }
    
    复制代码

    hotApply的确比较复杂,知道大概流程就好了,这一小节,要求你对webpack打包后的文件如何执行的有一些了解,大家可以自去看下。

    四、总结

    还是以阅读源码的形式画的图,①-④的小标记,是文件发生变化的一个流程。

     

     

    写在最后

    本次是以阅读源码的方式讲解原理,是因为觉得热更新这块涉及的知识量比较多。所以知识把关键性代码拿出来,因为每一个块细节说起来都能写一篇文章了,大家可以自己对着源码再理解下。

    还是建议提前了解以下知识会更好理解热更新:

    参考链接

    参考的文章大家也可以看下,但是由于源码版本不同,所以不要太纠结与细节

     

    展开全文
  • Webpack热更新原理

    2021-01-14 23:23:54
    WDS:webpack-dev-server(WDS)来实现自动刷新
  • 彻底搞懂并实现webpack热更新原理

    千次阅读 2019-10-22 11:37:41
    HMR原理 debug服务端源码 服务端简易实现 服务端调试阶段 debug客户端源码 客户端简易实现 客户端调试阶段 问题 总结 HMR是什么 HMR即Hot Module Replacement是指当你...
  • 之前遇见几次问webpack热更新原理,我只是知道和怎么使用,也没真的去了解一下,这次了解了一下,发现没有对webpack深层次研究,根本不懂,所以今天只是简单了解一下,具体还是得去研究源码才能真正掌握。...
  • webpack 热更新原理

    千次阅读 2019-08-14 22:15:58
    ,当你对代码进行修改并保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就能够对应用进行更新。例如,在开发 Web 页面过程中,当你...
  • 点击上方前端瓶子君,关注公众号回复算法,加入前端编程面试算法每日一题群目录HMR是什么使用场景配置使用HMR配置webpack解析webpack打包后的文件内容配置HMRHMR原理deb...
  • 一、前言 - webpack热更新Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间...
  • 作者:悟空来也来源:https://juejin.im/post/5df36ffd518825124d6c1765前言本文以剖析webpack-dev-server源码,从零开始实现一个...
  • HMR 全称 Hot Module Replacement,中文语境通常翻译为模块热更新,它能够在保持页面状态的情况下动态替换资源模块,提供丝滑顺畅的 Web 页面开发体验。 1.1 HMR 之前 在 HMR 之前,应用的加载、更新是一种页面...
  • 主要介绍了webpack 模块替换原理,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • webpack热更新实现原理

    2021-01-09 18:37:59
    参考:https://juejin.cn/post/6844904008432222215 我还没太看懂,待会回来看懂在自己写一写。
  • webpack热更新及其原理

    2020-09-12 10:02:37
    1、热更新WDS (webpack-dev-server) npm i webpack-dev-server -D 不刷新浏览器 不输出文件,而是放在内存中 使用 webpack内置的plugin插件 HMR – HotModuleReplacementPlugin hot 如果替换失败就会自动回...
  • webpack模块热更新原理

    2022-03-03 00:30:19
    什么是模块热更新?模块替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。下面我...
  • 一、是什么 HMR全称Hot Module Replacement,可以理解为模块替换,指在应用程序运行过程中,替换、添加、删除...在webpack中配置开启模块也非常的简单,如下代码: constwebpack=require('webpack') module...
  • 文章目录为什么需要热更新使用热更新以及遇到的问题构建过程热更新原理 为什么需要热更新 在文件更新时, 希望能保持页面当前的状态值, 而不是直接刷新页面, 可以大大节省宝贵的开发调试时间 使用热更新以及遇到的...
  • 在使用 Webpack 构建开发期时,Webpack 提供热更新功能为开发带来良好的体验和开发效率,那热更新机制是怎么实现的呢? 代码实现 Webpack 配置添加 HotModuleReplacementPlugin 插件 new webpack....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,643
精华内容 1,857
关键字:

webpack热更新原理