likes
comments
collection
share

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

作者站长头像
站长
· 阅读数 19

一. 热更新概念与优势

  1. 什么是热更新?

    1. Hot Module Replacement 是指当我们的代码修改并保存之后,webpack会对代码进行打包,打包后会通知浏览器进行热更新,让浏览器端单独拉去更新的模块代码。之后浏览器端会从模块缓存中删除原有模块,然后调用accept回调,并执行新的模块代码,这样新的模块就又被缓存到模块缓存中了。
    2. 浏览器不刷新,也得到了页面更新。
    3. 相对于监听到修改后直接刷新整个页面(live reload),HMR的优点就是可以保存应用状态,提高开发效率。比如:某个操作在很多后续弹窗中深藏,现在更新的逻辑只和当前深藏部分有关,那么直接刷新页面,就会让页面回到初始状态,需要重新将弹窗打开,并进入深藏的部分。
  2. 热更新示例——定制js文件的HMR能力。

content.js

let content = "hello world"
console.log("welcome");
export default content;

index.js

// 创建一个input,可以在里面输入一些东西,方便我们观察热更新的效果
let inputEl = document.createElement("input");
document.body.appendChild(inputEl);

let divEl = document.createElement("div")
document.body.appendChild(divEl);

let render = () => {
    let content = require("./content").default;
    divEl.innerText = content;
}
render();

// 要实现热更新,这段代码并不可少,描述当模块被更新后做什么
// 为什么vue-cli中.vue不用写额外的逻辑,也可以实现热更新呢?那是因为有vue-loader帮我们做了,很多loader都实现了热更新
if (module.hot) {
    module.hot.accept(["./content.js"], render);
}

HMR还没有神奇到在某个模块发生改变后,需要怎么热更新,具体热更新的行为需要我们自己定制。这就是accept回调的意义。另外Vue项目之所以有热更能力,且不需要单独编写热更新逻辑,是因为vue相关的热更新plugin帮我们对每一个编译后的组件文件都自动注入了热更新逻辑。

二. HotModuleReplacementPlugin做出浏览器运行时的准备

热更新时基于websocket实现服务端通知客户端更新。所以客户端代码需要植入websockt相关逻辑。同时删除模块缓存,新模块的重新执行,执行accept回调等等的一切浏览器端逻辑都需要注入。这个代码注入者是——HotModuleReplacementPlugin。

下面流程图描述的是由HotModuleReplacementPlugin注入后的webpack运行时的bootstrap代码。 阐述了在热更新环境下,module加载的流程。但还还没有涉及到websocket,删除模块,替换模块,调用accept等热更新核心逻辑。

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

三. 服务端实现

1. 4个关键实现

  1. 通过webpack创建compiler实例,webpack在watch模式下编译

    1. compiler实例实现监听本地文件变化,并做到变化后自动编译后输出。
    2. 更改config中的entry,做到偷偷将lib/client/index.js、lib/client/hot/dev-server.js作为额外的打包入口,使它们的代码被注入到打包输出的chunk文件中。这两个文件的代码就是浏览器端涉及到websocket,删除模块,替换模块,调用accept等热更新核心逻辑。
    3. 在compiler.hooks.done(webpack编译完成后触发)时,向客户端发送hash和ok两个websocket事件,表示首次编译完成。
  2. webpack-dev-middleware,实现编译、设置文件为内存系统,并返回编译后的文件

  3. 创建webserver静态服务器,让浏览器可以请求编译生成的静态资源

  4. 创建websocket服务,建立本地服务和浏览器的双向通信;每当有新的编译,立马告知浏览器执行热更新逻辑

2. 实现入口

  1. 先看webpack的入口文件,实际上就是在 new Compiler 之后执行了 new Server 的过程去创建devServer。devServer为了能拿到webpack的各种状态和方法,会将compiler实例作为参数引入。
// 创建webpack实例 
 const compiler = webpack(config);
 // 创建Server类,这个类里面包含了webpack-dev-server服务端的主要逻辑
 const server = new Server(compiler);
 
 // 启动webserver服务器
 server.listen(8000, "localhost", () => {
     console.log(`Project is running at http://localhost:8000/`);
 })

所以HMR在服务端的实现,只要知道new Server都执行了哪些逻辑就可以了。

3. new Server(compiler)流程

每一步都很清晰,主要就

  • 像entry中插入额外的浏览器热更新逻辑文件、

  • 创建基于内存的静态服务器、

  • 创建hmr用的websocket服务,

    • 浏览器首次连接ws后,发送hash和ok事件,告知浏览器本次编译最新文件的hash是什么。

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

四. 浏览器端实现

浏览器端的实现无非就是webpackHotModuleReplacementPlugin插件对webpack bootstrap的代码修改,以及在打包时,updateCompiler()向entry中混入的两个文件 /src/lib/client/index.js 和 webpack/hot/dev-server.js。

1. 两个核心实现

  1. 创建websocket客户端,连接websocket服务端,监听hash和ok两个事件。
  2. hash和ok对应事件的回调:发起http请求拉取新模块,删除老模块,执行新模块,执行accept回调,实现局部刷新页面。

2. /src/lib/client/index.js核心流程图

index.js只是做了一些很基本的初始化操作:建立socket连接,注册hash、on回调。创建reloadApp方法。

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

3. hot/dev-server.js 核心流程图

dev-server.js 主要实现了对 webpackHotUpdate 事件监听的回调。然后通过JSONP的方式去请求模块文件,拉取到最新的模块文件后,会执行这些新的模块文件,并将 parent 模块中 accept 收集的回调函数执行。

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

4. webpackHotUpdate承载新模块替换和accept回调执行的任务。

<chunkId>.<lashHash>.hot-update.js 文件一经JSONP请求,就会立刻被执行,其中的代码是打包好的可执行文件。类似于:

webpackHotUpdate("index", {
  "./src/lib/content.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      eval("");
    })
})

第一个参数 “index” 代表热更新涉及到的chunkName是index。第二个参数是一个map,里面是更新后重新打包的模块。

承载接下来新模块替换和accept回调执行的任务还是在经过webpackHotModuleReplacementPlugin改造后的webpack bootstrap里面。

window.webpackHotUpdate = (chunkID, moreModules) => {
    // 【9】热更新
    // 循环新拉来的模块
    Object.keys(moreModules).forEach(moduleID => {
        // 1、通过__webpack_require__.c 模块缓存可以找到旧模块
        let oldModule = __webpack_require__.c[moduleID];

        // 2、更新__webpack_require__.c,利用moduleID将新的拉来的模块覆盖原来的模块
        let newModule = __webpack_require__.c[moduleID] = {
            i: moduleID,
            l: false,
            exports: {},
            hot: hotCreateModule(moduleID),
            parents: oldModule.parents,
            children: oldModule.children
        };

        // 3、执行最新编译生成的模块代码
        moreModules[moduleID].call(newModule.exports, newModule, newModule.exports, __webpack_require__);
        newModule.l = true;

        // 这块请回顾下accept的原理
        // 4、让父模块中存储的_acceptedDependencies执行
        newModule.parents && newModule.parents.forEach(parentID => {
            let parentModule = __webpack_require__.c[parentID];
            parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]()
        });
    })
}

抽象成流程图非常简单:

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

五. 最后脑图总结一下

下面不详细阐述,只说明思路索引,具体可以结合文中所讲:

webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产

转载自:https://juejin.cn/post/7370379022285815819
评论
请登录