热更新,主要就是把前端工程 文件变更,即时编译,然后通知到浏览器端,刷新代码。

服务单与客户端通信方式有:ajax 轮询,EventSource、websockt。

客户端刷新一般分为两种:

  • 整体页面刷新,不保留页面状态,就是简单粗暴,直接window.location.reload()。

  • 基于WDS (Webpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

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

HMR作为一个Webpack内置的功能,可以通过HotModuleReplacementPlugin或--hot开启。

模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。

当代码文件修改并保存之后,webapck通过watch监听到文件发生变化,会对代码文件重新打包生成两个模块补丁文件manifest(js)和一个(或多个)updated chunk(js),将结果存储在内存文件系统中,通过websocket通信机制将重新打包的模块发送到浏览器端,浏览器动态的获取新的模块补丁替换旧的模块,浏览器不需要刷新页面就可以实现应用的更新。

webpack基本概念复习

webpack中的module,chunk 和 bundle

  • module 就是一个js模块,就是被require或export的模块,例如 ES模块,commonjs模块,AMD模块

  • chunk 是 代码块,是进行依赖分析的时候,代码分割出来的代码块,包括一个或多个module,是被分组了的module集合,code spliting之后的就是chunk

  • bundle 是 文件,最终打包出来的文件,通常一个bundle对应一个chunk

webpack中loader和plugin在作用

  • loader是文件转换器,将webpack不能处理的模块转换为webpack能处理的模块,就是js模块

  • plugin是功能扩展,干预webpack的打包过程,修改编译结果或打包结果

Webpack插件机制之Tapable

Webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。tapable的核心思路有点类似于node.js中的events,最基本的发布/订阅模式。回顾grunt gulp  任务队列,省去一般的操作。

具体可阅读《webpack4.0源码分析之Tapable

webpack-dev-server热更新分析

内置了webpack-dev-middleware和express服务器,利用webpack-dev-middleware提供文件的监听和编译,利用express提供http服务,底层利用websocket代替EventSource实现了webpack-hot-middleware提供的客户端和服务器之间的通信机制。

webpack-dev-server代码分析

// webpack.config.server.js
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const config = require('./webpack.config');
config.plugins.push(
    new webpack.HotModuleReplacementPlugin(),//热替换
    new webpack.NoEmitOnErrorsPlugin(),//去除系统抛出的错误消息
);
// 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;
    });
}; // 通过websoket给客户端发消息
_sendStats() {
    this.sockWrite(sockets, 'hash', stats.hash);
    this.sockWrite(sockets, 'ok');
}

每次修改代码保存后(gulp grunt 通过watch 监听),控制台都会出现 Compiling…字样,触发新的编译中...可以在控制台中观察到:

  • 新的Hash值:a61bdd6e82294ed06fa3

  • 新的json文件: a93fd735d02d98633356.hot-update.json

  • 新的js文件:index.a93fd735d02d98633356.hot-update.js

每次修改代码,就会触发编译。说明我们还需要监听本地代码的变化,主要是通过setupDevMiddleware方法实现的。这个方法主要执行了webpack-dev-middleware库。

webpack-dev-middleware

webpack-dev-server只负责启动服务和前置准备工作,所有文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译和输出以及监听,无非就是职责的划分更清晰了。

webpack-dev-middleware 是一个 express 中间件,核心实现两个功能:

  • 第一通过 file-loader 内部集成了node的 monery-fs/memfs 内部文件系统,,直接将资源存储在内存;

  • 第二是通过watch监听文件的变化,动态编译。

webpack-dev-middleware源码里做了什么事:

// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
    if (err) { /*错误处理*/ }
});
// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, compiler);
    • 调用了compiler.watch方法,在第 1 步中也提到过,compiler的强大。这个方法主要就做了 2 件事:

      • 首先对本地文件代码进行编译打包,也就是webpack的一系列编译流程。

      • 其次编译结束后,开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听。

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

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

webpack --watch

webpack --watch 启动监听模式之后,webpack第一次编译项目,并将结果存储在内存文件系统,相比较磁盘文件读写方式内存文件管理速度更快,内存webpack服务器通知浏览器加载资源,浏览器获取的静态资源除了JS code内容之外,还有一部分通过 webpack-dev-server 注入的的 HMR runtime代码,作为浏览器和webpack服务器通信的客户端( webpack-hot-middleware 提供类似的功能)。

  • 文件系统中一个文件(或者模块)发生变化,webpack监听到文件变化对文件重新编译打包,每次编译生成唯一的hash值,根据变化的内容生成两个补丁文件

    • 说明变化内容的manifest(文件格式是hash.hot-update.json,包含了hash和chundId用来说明变化的内容)

    • chunk js(hash.hot-update.js)模块。

  • hrm-server通过websocket将manifest推送给浏览器

  • 浏览器接受到最新的 hotCurrentHash,触发 hotDownloadManifest 函数,获取manifest json 文件。

  • 浏览器端hmr runtime根据manifest的hash和chunkId使用ajax拉取最新的更新模块chun

  • HMR runtime 调用window["webpackHotUpdate"] 方法,调用hotAddUpdateChunk

服务端流程

  1. 启动webpack,生成compiler实例。compiler上有很多方法,比如可以启动 webpack 所有编译工作,以及监听本地文件的变化。

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

  3. 本地server启动之后,再去启动websocket服务。通过websocket,可以建立本地服务和浏览器的双向通信。这样就可以实现当本地文件发生变化,立马告知浏览器可以热更新代码啦!

  4. 浏览器接收到热更新的通知,当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。

客服端更新流程

当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。下面重点讲的就是_sendStats方法中的ok和hash事件都做了什么。

那浏览器是如何接收到websocket的消息呢?

也就是websocket客户端代码。

// 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 个监听事件。

  1. hash事件,更新最新一次打包后的hash值。

  2. ok事件,进行热更新检查。

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

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

webpack如何刷新资源

// 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搞得鬼。

HotModuleReplacementPlugin

moudle.hot.check 之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中

利用上一次保存的hash值,调用hotDownloadManifest发送xxx/hash.hot-update.json的ajax请求;

请求结果获取热更新模块,以及下次热更新的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);
 }

再看下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();
}

hotApply 热更新模块替换

热更新的核心逻辑就在hotApply方法了。

删除过期的模块,就是需要替换的模块,通过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打包后的文件如何执行的有一些了解,大家可以自去看下。

WebSocket

WebSocket是基于TCP的全双工通讯的协议,它与EventSource有着本质上的不同.(前者基于TCP,后者依然基于HTTP)。

可以通过scockt.io 库来实现更新

webpack-hot-middleware

webpack-hot-middleware中间件是webpack的一个plugin,通常结合webpack-dev-middleware一起使用。借助它可以实现浏览器的无刷新更新(热更新),即webpack里的HMR(Hot Module Replacement)。

核心是给webpack提高服务端和客户端之间的通信机制,内部使用windoe.EventSocurce实现。

如何配置请参考 webpack-hot-middleware,如何理解其相关插件请参考手把手深入理解 webpack dev middleware 原理与相关 plugins

HotModuleReplacementPlugin

EventSource

EventSource 不是一个新鲜的技术,它早就随着H5规范提出了,正式一点应该叫Server-sent events,即SSE。鉴于传统的通过ajax轮训获取服务器信息的技术方案已经过时,我们迫切需要一个高效的节省资源的方式去获取服务器信息,一旦服务器资源有更新,能够及时地通知到客户端,从而实时地反馈到用户界面上。EventSource就是这样的技术,它本质上还是HTTP,通过response流实时推送服务器信息到客户端

客户端

const es = new EventSource('/message');// /message是服务端支持EventSource的接口
es.onmessage = function(e){
    console.log(e.data); // 打印服务器推送的信息
}

使用EventSource技术实时更新网页信息十分高效。实际使用中,我们几乎不用担心兼容性问题,主流浏览器都了支持EventSource,当然,除了掉队的IE系。对于不支持的浏览器,其PolyFill方案请参考HTML5 Cross Browser Polyfills

服务端

服务端实现/message接口,需要返回类型为 text/event-stream的响应头。

var http = require('http');
http.createServer(function(req,res){
  if(req.url === '/message'){
    res.writeHead(200,{
      'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });
    setInterval(function(){
      res.write('data: ' + +new Date() + '\n\n');
    }, 1000);
  }
}).listen(8888);

为了避免缓存,Cache-Control 特别设置成了 no-cache,为了能够发送多个response, Connection被设置成了keep-alive.。发送数据时,请务必保证服务器推送的数据以 data:开始,以\n\n结束,否则推送将会失败(原因就不说了,这是约定的)。

参看文章:

webpack 热更新(HMR)实现原理 https://segmentfault.com/a/1190000022485386

轻松理解webpack热更新原理 https://juejin.im/post/5de0cfe46fb9a071665d3df0

Webpack 热更新实现原理分析 https://zhuanlan.zhihu.com/p/30623057

Webpack插件机制之Tapable-源码解析 https://juejin.im/post/5dc169b0f265da4d542092c6

转载本站文章《webpack原理(1):Webpack热更新实现原理代码分析》,
请注明出处:https://www.zhoulujun.cn/html/tools/Bundler/webpackTheory/8503.html

webpack原理(1):Webpack热更新实现原理代码分析的更多相关文章

  1. Android热更新实现原理

    最近Android社区的氛围很不错嘛,连续放出一系列的android动态加载插件和热更新库,这篇文章就来介绍一下Android中实现热更新的原理. ClassLoader 我们知道Java在运行时加载 ...

  2. 另类Unity热更新大法:代码注入式补丁热更新

    对老项目进行热更新 项目用纯C#开发的? 眼看Unity引擎热火朝天,无数程序猿加入到了Unity开发的大本营. 一些老项目,在当时ulua/slua还不如今天那样的成熟,因此他们选择了全c#开发:也 ...

  3. React Native超简单完整示例-tabs、页面导航、热更新、用户行为分析

    初学React Native,如果没有人指引,会发现好多东西无从下手,但当有人指引后,会发现其实很简单.这也是本人写这篇博客的主要原因,希望能帮到初学者. 本文不会介绍如何搭建开发环境,如果你还没有搭 ...

  4. webpack 环境搭建+实现热更新

    让我们一起构建一个小的app 为了便于你更好的了解Webpack带来的好处,我们将会构建一个非常小的app并将资源文件打包.在这个教程中我推荐基于Node4或Node5和NPM3来进行开发,这样就避免 ...

  5. Nand ECC校验和纠错原理及2.6.27内核ECC代码分析

    ECC的全称是Error Checking and Correction,是一种用于Nand的差错检测和修正算法.如果操作时序和电路稳定性不存在问题的话,NAND Flash出错的时候一般不会造成整个 ...

  6. Python实例---抽屉热搜榜前端代码分析

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. 轻松理解webpack热更新原理

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

  8. 优化单页面开发环境:webpack与react的运行时打包与热更新

    前面两篇文章介绍初步搭建单页面应用的开发环境: 第一篇:使用webpack.babel.react.antdesign配置单页面应用开发环境 第二篇:使用react-router实现单页面应用路由 这 ...

  9. webpack热更新问题和antd design字体图标库扩展

    标题也不知道怎么写好,真是尴尬.不过话说回来,距离上一次写文快两个月了,最近有点忙,一直在开发新项目, 今天刚刚闲下来,项目准备提测.借这个功夫写点东西,把新项目上学到的一些好的干活分享一下,以便之后 ...

  10. Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建

    前言 前面铺垫了这么久,终于要开始写热更新了. Unity游戏热更新包含两个方面,一个是资源的更新,一个是脚本的更新. 资源更新是Unity本来就支持的,在各大平台也都能用.而脚本的热更新在iOS平台 ...

随机推荐

  1. ssr next 学习记录

    预加载页面   只有生产环境才有 当页面初始化加载时,getInitialProps只会加载在服务端.只有当路由跳转(Link组件跳转或 API 方法跳转)时,客户端才会执行getInitialPro ...

  2. python机器学习——朴素贝叶斯算法

    背景与原理: 朴素贝叶斯算法是机器学习领域最经典的算法之一,仍然是用来解决分类问题的. 那么对于分类问题,我们的模型始终是:用$m$组数据,每条数据形如$(x_{1},...,x_{n},y)$,表示 ...

  3. 医学分割 不确定性 2019 MICCAI

    z今天分享一篇发表在MICCAI 2019上的论文: Uncertainty-aware Self-ensembling Model for Semi-supervised 3D Left Atriu ...

  4. docker安装常用软件

    linux安装docker 1.安装gcc相关 yum install gcc -y yum install gcc-c++ -y 2.安装工具包 #安装工具包 yum -y install yum- ...

  5. ant design upload组件的beforeUpload阻止默认上传行为

    const onImportExcel = (file) => { return new Promise(async (resolve, reject) => { ... //要执行的语句 ...

  6. css穿透

    https://www.cnblogs.com/linjiangxian/p/13183412.html

  7. springBoot 这货特别火

    现在 Spring Boot 非常火,各种技术文章,各种付费教程,多如牛毛,可能还有些不知道 Spring Boot 的,那它到底是什么呢?有什么用?今天给大家详细介绍一下. Spring Boot ...

  8. mmdetection RPNHead--_init_layers()

    RPNHead类包含的函数: (1)_init_():初始化函数 (2)_init_layers():设置Head中的卷积层 (3)forward_single():单尺度特征图的前向传播 (4)lo ...

  9. 磊磊零基础打卡算法:day17 c++堆排序

    5.20 前言吐槽: 今天是5.20啦,但是作为单身修狗的我只能和代码过啦...继续加油算法打卡!!! 堆排序: 堆就是一棵完全二叉树 二叉堆是一种支持插入,删除,查询最值的数据结构.他其实是一棵满足 ...

  10. nhrhrhr

    每名学生按规定时间进行答辩,答辩总时间控制在12分钟,其中包括学生报告7分钟.提问以及回答问题5分钟. 1.答辩开始前由答辩委员会组长宣布答辩程序:学生的答辩顺序由教师确定,前一名学生答辩时,下一名答 ...