Webpack的HMR原理分析

Webpack的HMR原理分析

module.exports = {
    entry : {
        main : './src/main.js',
        home : './src/home.js',
        common : ['jquery'],
        common2 : ['react']
    },
    output : {
        path: path.join(__dirname, 'build'),
        filename: '[name].js'
    },
     plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new CommonsChunkPlugin({
            name: "chunk",
            minChunks: function(module, count) {
               return module.resource && (/common/).test(module.resource) && count === 2;
            },
        }),
        new SetVersion()
    ],
    devServer: {
      contentBase: './build',
      hot: true
      //支持 HMR
    }
}

在 Webpack 的 devServer 配置中设置了 hot 为 true,而且在 Webpack 的 plugin 中添加了 new webpack.HotModuleReplacementPlugin() 这个插件。
Webpack 的 HMR 的实现原理

compiler.plugin("done", function(stats) {
         //clientStats 表示需要保存 stats 中的那些属性,可以允许配置,参见 Webpack 官网
        this._sendStats(this.sockets, stats.toJson(clientStats));
        this._stats = stats;
    }.bind(this));

每次 compiler 的 'done' 钩子函数被调用的时候就会要求客户端去检查模块更新,如果客户端不支持 HMR,那么就会全局加载。

Server.prototype._sendStats = function(sockets, stats, force) {
    if(!force &&
        stats &&
        (!stats.errors || stats.errors.length === 0) &&
        stats.assets &&
        stats.assets.every(function(asset) {
            return !asset.emitted;
            //(1)每一个 asset 都是没有 emitted 属性,表示没有发生变化。如果发生变化那么这个 assets 肯定有 emitted 属性
        })
    )
    return this.sockWrite(sockets, "still-ok");
    //(1)将 stats 的 hash 写给 socket 客户端
    this.sockWrite(sockets, "hash", stats.hash);
    //设置 hash
    if(stats.errors.length > 0)
        this.sockWrite(sockets, "errors", stats.errors);
    else if(stats.warnings.length > 0)
        this.sockWrite(sockets, "warnings", stats.warnings);
    else
        this.sockWrite(sockets, "ok");
}

通过 webpack-dev-server 提供的 websocket 服务端代码通知 websocket 客户端)发送的 ok 和 warning 信息的时候会要求更新。如果支持 HMR 的情况下就会要求检查更新,同时发送过来的还有服务器端本次编译的 compilation 的 hash 值。如果不支持 HMR,那么要求刷新页面。

ok: function() {
        sendMsg("Ok");
        if(useWarningOverlay || useErrorOverlay) overlay.clear();
        if(initial) return initial = false;
        reloadApp();
    },
    warnings: function(warnings) {
        log("info", "[WDS] Warnings while compiling.");
        var strippedWarnings = warnings.map(function(warning) {
            return stripAnsi(warning);
        });
        sendMsg("Warnings", strippedWarnings);
        for(var i = 0; i < strippedWarnings.length; i++)
            console.warn(strippedWarnings[i]);
        if(useWarningOverlay) overlay.showMessage(warnings);

        if(initial) return initial = false;
        reloadApp();
    },
   function reloadApp() {
    //(1)如果开启了 HMR 模式
    if(hot) {
        log("info", "[WDS] App hot update...");
        var hotEmitter = require("webpack/hot/emitter");
        hotEmitter.emit("webpackHotUpdate", currentHash);
        //重新启动 webpack/hot/emitter,同时设置当前 hash,通知上面的 webpack-dev-server 的 webpackHotUpdate 事件,告诉它打印哪些模块的更新信息
        if(typeof self !== "undefined" && self.window) {
            // broadcast update to window
            self.postMessage("webpackHotUpdate" + currentHash, "*");
        }
    } else {
       //(2)如果不是 Hotupdate 那么直接 reload 我们的 window 就可以了
        log("info", "[WDS] App updated. Reloading...");
        self.location.reload();
    }
}

如果 ok 则调用reloadApp方法,而 reloadApp 方法 判断如果开启了 HMR 模式, 通过hotEmitter 执行webpackHotUpdate方法,如果不是 Hotupdate 那么直接 reload刷新网页。

if(module.hot) {
    var lastHash;
    var upToDate = function upToDate() {
        return lastHash.indexOf(__webpack_hash__) >= 0;
      //(1)如果两个 hash 相同那么表示没有更新,其中 lastHash 表示上一次编译的 hash,记住是 compilation 的 hash
      //只有在 HotModuleReplacementPlugin 开启的时候存在。任意文件变化后 compilation 都会发生变化
    };
    //(2)下面是检查更新的模块
    var check = function check() {
        module.hot.check().then(function(updatedModules) {
            //(2.1)没有更新的模块直接返回,通知用户无需 HMR
            if(!updatedModules) {
                console.warn("[HMR] Cannot find update. Need to do a full reload!");
                console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
                return;
            }
            //(2.2)开始更新
            return module.hot.apply({
                ignoreUnaccepted: true,
                //和 accept 函数指定热加载那些模块
                ignoreDeclined: true,
                //decline 表示不支持这个模块热加载
                ignoreErrored: true,
                //error 表示出错的模块
                onUnaccepted: function(data) {
                    console.warn("Ignored an update to unaccepted module " + data.chain.join(" -> "));
                },
                onDeclined: function(data) {
                    console.warn("Ignored an update to declined module " + data.chain.join(" -> "));
                },
                onErrored: function(data) {
                    console.warn("Ignored an error while updating module " + data.moduleId + " (" + data.type + ")");
                }
             //(2.2.1)renewedModules 表示哪些模块已经更新了
            }).then(function(renewedModules) {
                //(2.2.2)如果有模块没有更新完成,那么继续检查
                if(!upToDate()) {
                    check();
                }
                //(2.2.3)更新的模块 updatedModules,renewedModules 表示哪些模块已经更新了
                require("./log-apply-result")(updatedModules, renewedModules);
                //通知已经热加载完成
                if(upToDate()) {
                    console.log("[HMR] App is up to date.");
                }
            });
        }).catch(function(err) {
        //(2.3)更新异常,输出 HMR 信息
            var status = module.hot.status();
            if(["abort", "fail"].indexOf(status) >= 0) {
                console.warn("[HMR] Cannot check for update. Need to do a full reload!");
                console.warn("[HMR] " + err.stack || err.message);
            } else {
                console.warn("[HMR] Update check failed: " + err.stack || err.message);
            }
        });
    };
    var hotEmitter = require("./emitter");
    //(3)emitter 模块内容,也就是导出一个 events 实例
    /*
    var EventEmitter = require("events");
    module.exports = new EventEmitter();
     */
    hotEmitter.on("webpackHotUpdate", function(currentHash) {
        lastHash = currentHash;
        //(3.1)表示本次更新后得到的 hash 值
        if(!upToDate()) {
            //(3.1.1)有更新
            var status = module.hot.status();
            if(status === "idle") {
                console.log("[HMR] Checking for updates on the server...");
                check();
            } else if(["abort", "fail"].indexOf(status) >= 0) {
                console.warn("[HMR] Cannot apply update as a previous update " + status + "ed. Need to do a full reload!");
            }
        }
    });
    console.log("[HMR] Waiting for update signal from WDS...");
} else {
    throw new Error("[HMR] Hot Module Replacement is disabled.");
}

上面看到了 log-apply-result 模块,该模块是在所有的内容已经更新完成后调用的,下面继续看一下它到底做了什么事情:

module.exports = function(updatedModules, renewedModules) {
    //(1)renewedModules 表示哪些模块需要更新,剩余的模块 unacceptedModules 表示,哪些模块由于 ignoreDeclined,ignoreUnaccepted 配置没有更新
    var unacceptedModules = updatedModules.filter(function(moduleId) {
        return renewedModules && renewedModules.indexOf(moduleId) < 0;
    });
    //(2)unacceptedModules 表示该模块无法 HMR,打印 log
    if(unacceptedModules.length > 0) {
        console.warn("[HMR] The following modules couldn't be hot updated: (They would need a full reload!)");
        unacceptedModules.forEach(function(moduleId) {
            console.warn("[HMR]  - " + moduleId);
        });
    }
    //(2)没有模块更新,表示模块是最新的
    if(!renewedModules || renewedModules.length === 0) {
        console.log("[HMR] Nothing hot updated.");
    } else {
        console.log("[HMR] Updated modules:");
        //(3)打印那些模块被热更新。每一个 moduleId 都是数字,那么建议使用 NamedModulesPlugin(webpack 2 建议)
        renewedModules.forEach(function(moduleId) {
            console.log("[HMR]  - " + moduleId);
        });
        var numberIds = renewedModules.every(function(moduleId) {
            return typeof moduleId === "number";
        });
        if(numberIds)
            console.log("[HMR] Consider using the NamedModulesPlugin for module names.");
    }
};

所以"webpack/hot/only-dev-server"的文件内容就是检查哪些模块更新了(通过 webpackHotUpdate 事件完成,而该事件依赖于compilation的 hash 值),其中哪些模块更新成功,而哪些模块由于某种原因没有更新成功。
接下来看看 "webpack/hot/dev-server":

f(module.hot) {
    var lastHash;
    //__webpack_hash__ 是每次编译的 hash 值是全局的
    var upToDate = function upToDate() {
        return lastHash.indexOf(__webpack_hash__) >= 0;
    };
    var check = function check() {
        module.hot.check(true).then(function(updatedModules) {
            //检查所有要更新的模块,如果没有模块要更新那么回调函数就是 null
            if(!updatedModules) {
                console.warn("[HMR] Cannot find update. Need to do a full reload!");
                console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
                window.location.reload();
                return;
            }
            //如果还有更新
            if(!upToDate()) {
                check();
            }
            require("./log-apply-result")(updatedModules, updatedModules);
            //已经被更新的模块都是 updatedModules
            if(upToDate()) {
                console.log("[HMR] App is up to date.");
            }

        }).catch(function(err) {
            var status = module.hot.status();
            //如果报错直接全局 reload
            if(["abort", "fail"].indexOf(status) >= 0) {
                console.warn("[HMR] Cannot apply update. Need to do a full reload!");
                console.warn("[HMR] " + err.stack || err.message);
                window.location.reload();
            } else {
                console.warn("[HMR] Update failed: " + err.stack || err.message);
            }
        });
    };
    var hotEmitter = require("./emitter");
    //获取 MyEmitter 对象
    hotEmitter.on("webpackHotUpdate", function(currentHash) {
        lastHash = currentHash;
        if(!upToDate() && module.hot.status() === "idle") {
            //调用 module.hot.status 方法获取状态
            console.log("[HMR] Checking for updates on the server...");
            check();
        }
    });
    console.log("[HMR] Waiting for update signal from WDS...");
} else {
    throw new Error("[HMR] Hot Module Replacement is disabled.");
}

两者的主要代码区别在于 check() 函数的调用方式:
如果 autoApply 设置为 true,那么回调函数传入的就是所有被自己 dispose 处理 过的模块,同时 apply 方法也会自动调用,如果 auApply 设置为 false,那么所有的模块更新都会通过手动调用 apply 来完成。而所说的被自己 dispose 处理就是通过如下的方式来完成的:

if (module.hot) {
    module.hot.accept();
    //支持热更新
    //当前模块代码更新后的回调,常用于移除持久化资源或者清除定时器等操作,如果想传递数据到更新后的模块,可以通过传入 data 参数,后续参数可以通过 module.hot.data 获取
    module.hot.dispose(() => {
        window.clearInterval(intervalId);
    });
}

而一般调用 webpack-dev-server 只会添加 --hot 而已,即内部不需要调用 apply,而传入的都是被 dispose 处理过的模块:

f(devServerOptions.hotOnly)
        devClient.push("webpack/hot/only-dev-server");
    else if(devServerOptions.hot)
        devClient.push("webpack/hot/dev-server");
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Webpack 概念 webpack 是一个现代的 JavaScript 应用程序的模块打包器(module bu...
    静默虚空阅读 485评论 0 0
  • webpack 介绍 webpack 是什么 为什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert阅读 6,325评论 2 71
  • 在现在的前端开发中,前后端分离、模块化开发、版本控制、文件合并与压缩、mock数据等等一些原本后端的思想开始...
    Charlot阅读 5,386评论 1 32
  • 构建一个小项目——FlyBird,学习webpack和react。(本文成文于2017/2/25) 从webpac...
    布蕾布蕾阅读 16,750评论 31 98
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,080评论 7 35