webpack热更新原理伪代码+流程图让你了解webpack的hmr实现原理。hmr是devServer和bundle产
一. 热更新概念与优势
-
什么是热更新?
- Hot Module Replacement 是指当我们的代码修改并保存之后,webpack会对代码进行打包,打包后会通知浏览器进行热更新,让浏览器端单独拉去更新的模块代码。之后浏览器端会从模块缓存中删除原有模块,然后调用accept回调,并执行新的模块代码,这样新的模块就又被缓存到模块缓存中了。
- 浏览器不刷新,也得到了页面更新。
- 相对于监听到修改后直接刷新整个页面(live reload),HMR的优点就是可以保存应用状态,提高开发效率。比如:某个操作在很多后续弹窗中深藏,现在更新的逻辑只和当前深藏部分有关,那么直接刷新页面,就会让页面回到初始状态,需要重新将弹窗打开,并进入深藏的部分。
-
热更新示例——定制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等热更新核心逻辑。
三. 服务端实现
1. 4个关键实现
-
通过webpack创建compiler实例,webpack在watch模式下编译
- compiler实例实现监听本地文件变化,并做到变化后自动编译后输出。
- 更改config中的entry,做到偷偷将lib/client/index.js、lib/client/hot/dev-server.js作为额外的打包入口,使它们的代码被注入到打包输出的chunk文件中。这两个文件的代码就是浏览器端涉及到websocket,删除模块,替换模块,调用accept等热更新核心逻辑。
- 在compiler.hooks.done(webpack编译完成后触发)时,向客户端发送hash和ok两个websocket事件,表示首次编译完成。
-
webpack-dev-middleware,实现编译、设置文件为内存系统,并返回编译后的文件
-
创建webserver静态服务器,让浏览器可以请求编译生成的静态资源
-
创建websocket服务,建立本地服务和浏览器的双向通信;每当有新的编译,立马告知浏览器执行热更新逻辑
2. 实现入口
- 先看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是什么。
四. 浏览器端实现
浏览器端的实现无非就是webpackHotModuleReplacementPlugin插件对webpack bootstrap的代码修改,以及在打包时,updateCompiler()向entry中混入的两个文件 /src/lib/client/index.js 和 webpack/hot/dev-server.js。
1. 两个核心实现
- 创建websocket客户端,连接websocket服务端,监听hash和ok两个事件。
- hash和ok对应事件的回调:发起http请求拉取新模块,删除老模块,执行新模块,执行accept回调,实现局部刷新页面。
2. /src/lib/client/index.js核心流程图
index.js只是做了一些很基本的初始化操作:建立socket连接,注册hash、on回调。创建reloadApp方法。
3. hot/dev-server.js 核心流程图
dev-server.js 主要实现了对 webpackHotUpdate 事件监听的回调。然后通过JSONP的方式去请求模块文件,拉取到最新的模块文件后,会执行这些新的模块文件,并将 parent 模块中 accept 收集的回调函数执行。
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]()
});
})
}
抽象成流程图非常简单:
五. 最后脑图总结一下
下面不详细阐述,只说明思路索引,具体可以结合文中所讲:
转载自:https://juejin.cn/post/7370379022285815819