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");

你可能感兴趣的