本文以剖析webpack-dev-server源码,从零开始实现一个webpack热更新HMR,深刻了解webpack-dev-server、webpack-dev-middleware、webpack-hot-middleware的实现机制,完全搞懂他们的原理,在面试过程当中这个知识点能答的很是出彩,在搭建脚手架过程当中这块能驾轻就熟。知其然并知其因此然,更上一层楼。javascript
舒适提示❤️~篇幅较长,建议收藏到电脑端食用更佳。html
Hot Module Replacement是指当咱们对代码修改并保存后,webpack将会对代码进行得新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实如今不刷新浏览器的前提下更新页面。java
相对于live reload
刷新页面的方案,HMR的优势在于能够保存应用的状态,提升了开发效率node
./src/index.js
webpack
// 建立一个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); } 复制代码
./src/content.js
git
let content = "hello world" console.log("welcome"); export default content; 复制代码
cd 项目根目录
github
npm run dev
web
当咱们在输入框中输入了123,这个时候更新content.js中的代码,会发现hello world!!!!变成了hello world,可是 输入框的值 还保留着,这正是HMR的意义,页面刷新期间保留状态 面试
chunk 就是若干 module 打成的包,一个 chunk 应该包括多个 module,通常来讲最终会造成一个 file。而 js 之外的资源,webpack 会经过各类 loader 转化成一个 module,这个模块会被打包到某个 chunk 中,并不会造成一个单独的 chunk。
Webpack watch:使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包,每次编译都会产生一个惟一的hash值,
上一次编译生成的hash.hot-update.json
(如:b1f49e2fc76aae861d9f.hot-update.json)
chunk名字.上一次编译生成的hash.hot-update.js
(如main.b1f49e2fc76aae861d9f.hot-update.js)
webpackHotUpdate
函数,留心一下这个js的结构拉取新模块代码
、执行新模块代码
、执行accept的回调实现局部更新
)都是这个插件 把函数 注入到咱们的chunk文件中,而非webpack-dev-server,webpack-dev-server只是调用了这些函数下面这段代码就是使用的HotModuleReplacementPlugin编译生成的chunk,注入了HMR runtime的代码,启动服务npm run dev,输入http://localhost:8000/main.js,截取主要的逻辑,细节处理省了(先细看,有个大概印象)
(function (modules) { //(HMR runtime代码) module.hot属性就是hotCreateModule函数的执行结果,全部hot属性有accept、check等属性 function hotCreateModule() { var hot = { accept: function (dep, callback) { for (var i = 0; i < dep.length; i++) hot._acceptedDependencies[dep[i]] = callback; }, check: hotCheck,//【在webpack/hot/dev-server.js中调用module.hot.accept就是hotCheck函数】 }; return hot; } //(HMR runtime代码) 如下几个方法是 拉取更新模块的代码 function hotCheck(apply) {} function hotDownloadUpdateChunk(chunkId) {} function hotDownloadManifest(requestTimeout) {} //(HMR runtime代码) 如下几个方法是 执行新代码 并 执行accept回调 window["webpackHotUpdate"] = function webpackHotUpdateCallback(chunkId, moreModules) { hotAddUpdateChunk(chunkId, moreModules); }; function hotAddUpdateChunk(chunkId, moreModules) {hotUpdateDownloaded();} function hotUpdateDownloaded() {hotApply()} function hotApply(options) {} //(HMR runtime代码) hotCreateRequire给模块parents、children赋值了 function hotCreateRequire(moduleId) { var fn = function(request) { return __webpack_require__(request); }; return fn; } // 模块缓存对象 var installedModules = {}; // 实现了一个 require 方法 function __webpack_require__(moduleId) { // 判断这个模块是否在 installedModules缓存 中 if (installedModules[moduleId]) { // 在缓存中,直接返回 installedModules缓存 中该 模块的导出对象 return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, // 模块是否加载 exports: {}, // 模块的导出对象 hot: hotCreateModule(moduleId), // module.hot === hotCreateModule导出的对象 parents: [], // 这个模块 被 哪些模块引用了 children: [] // 这个模块 引用了 哪些模块 }; // (HMR runtime代码) 执行模块的代码,传入参数 modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId)); // 设置模块已加载 module.l = true; // 返回模块的导出对象 return module.exports; } // 暴露 模块的缓存 __webpack_require__.c = installedModules; // 加载入口模块 而且 返回导出对象 return hotCreateRequire(0)(__webpack_require__.s = 0); })( { "./src/content.js": (function (module, __webpack_exports__, __webpack_require__) {}), "./src/index.js": (function (module, exports, __webpack_require__) {}),// 在模块中使用的require都编译成了__webpack_require__ "./src/lib/client/emitter.js": (function (module, exports, __webpack_require__) {}), "./src/lib/client/hot/dev-server.js": (function (module, exports, __webpack_require__) {}), "./src/lib/client/index.js": (function (module, exports, __webpack_require__) {}), 0:// 主入口 (function (module, exports, __webpack_require__) { eval(` __webpack_require__("./src/lib/client/index.js"); __webpack_require__("./src/lib/client/hot/dev-server.js"); module.exports = __webpack_require__("./src/index.js"); `); }) } ); 复制代码
梳理下大概的流程:
hotCreateRequire(0)(__webpack_require__.s = 0)
主入口
当浏览器执行这个chunk时,在执行每一个模块的时候,会给每一个模块传入一个module对象,结构以下,并把这个module对象放到缓存installedModules中;咱们能够经过__webpack_require__.c拿到这个模块缓存对象
var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {}, hot: hotCreateModule(moduleId), parents: [], children: [] }; 复制代码
hotCreateRequire会帮咱们给模块 module的parents、children赋值
接下来看看hot属性,hotCreateModule(moduleId)返回了啥?没错hot是一个对象有accept、check两个主要属性,接下来咱们就详细的解剖下module.hot和module.hot.accept
function hotCreateModule() { var hot = { accept: function (dep, callback) { for (var i = 0; i < dep.length; i++) hot._acceptedDependencies[dep[i]] = callback; }, check: hotCheck, }; return hot; } 复制代码
若是要实现热更新,下面这段代码是必不可少的,accept传入的回调函数就是局部刷新逻辑,当./content.js模块改变时执行
if (module.hot) { module.hot.accept(["./content.js"], render); } 复制代码
为何咱们只有写了module.hot.accept(["./content.js"], render);
才能实现热更新,这得从accept这个函数的原理开始提及,咱们再来看看 module.hot 和 module.hot.accept
function hotCreateModule() { var hot = { accept: function (dep, callback) { for (var i = 0; i < dep.length; i++) hot._acceptedDependencies[dep[i]] = callback; }, }; return hot; } var module = installedModules[moduleId] = { // ... hot: hotCreateModule(moduleId), }; 复制代码
没错accept就是往hot._acceptedDependencies
对象存入 局部更新回调函数,_acceptedDependencies何时会用到呢?(当模块文件改变的时候,咱们会调用acceptedDependencies搜集的回调)
// 再看下面这段代码是否是有点明白了 if (module.hot) { module.hot.accept(["./content.js"], render); // 等价于module.hot._acceptedDependencies["./content.js"] = render // 没错,他就是将模块改变时,要作的事进行了搜集,搜集到_acceptedDependencies中 // 以便当content.js模块改变时,他的父模块index.js经过_acceptedDependencies知道要干什么 } 复制代码
websocket
创建起 浏览器端 和 服务器端 之间的通讯hash
和ok
事件hash
和 ok
事件HMR runtime
).
├── package-lock.json
├── package.json
├── src
│ ├── content.js 测试代码
│ ├── index.js 测试代码入口
│ ├── lib
│ │ ├── client 热更新客户端实现逻辑
│ │ │ ├── index.js 等价于源码中的webpack-dev-server/client/index.js
│ │ │ ├── emitter.js
│ │ │ └── hot
│ │ │ └── dev-server.js 等价于源码中的webpack/hot/dev-server.js 和 HMR runtime
│ │ └── server 热更新服务端实现逻辑
│ │ ├── Server.js
│ │ └── updateCompiler.js
│ └── myHMR-webpack-dev-server.js 热更新服务端主入口
└── webpack.config.js webpack配置文件
复制代码
// /webpack.config.js let webpack = require("webpack"); let HtmlWebpackPlugin = require("html-webpack-plugin") let path = require("path"); module.exports = { mode: "development", entry:"./src/index.js",// 这里咱们尚未将客户端代码配置,而是经过updateCompiler方法更改entry属性 output: { filename: "[name].js", path: path.resolve(__dirname, "dist") }, plugins: [ new HtmlWebpackPlugin(),// 输出一个html,并将打包的chunk引入 new webpack.HotModuleReplacementPlugin()// 注入HMR runtime代码 ] } 复制代码
"dependencies": { "express": "^4.17.1", "mime": "^2.4.4", "socket.io": "^2.3.0", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", "memory-fs": "^0.5.0", "html-webpack-plugin": "^3.2.0", } 复制代码
// /src/myHMR-webpack-dev-server.js const webpack = require("webpack"); const Server = require("./lib/server/Server"); const config = require("../../webpack.config"); // 【1】建立webpack实例 const compiler = webpack(config); // 【2】建立Server类,这个类里面包含了webpack-dev-server服务端的主要逻辑(在 2.Server总体 中会梳理他的逻辑) const server = new Server(compiler); // 最后一步【10】启动webserver服务器 server.listen(8000, "localhost", () => { console.log(`Project is running at http://localhost:8000/`); }) 复制代码
// /src/lib/server/Server.js const express = require("express"); const http = require("http"); const mime = require("mime");// 能够根据文件后缀,生成相应的Content-Type类型 const path = require("path"); const socket = require("socket.io");// 经过它和http实现websocket服务端 const MemoryFileSystem = require("memory-fs");// 内存文件系统,主要目的就是将编译后的文件打包到内存 const updateCompiler = require("./updateCompiler"); class Server { constructor(compiler) { this.compiler = compiler;// 将webpack实例挂载到this上 updateCompiler(compiler);// 【3】entry增长 websocket客户端的两个文件,让其一同打包到chunk中 this.currentHash;// 每次编译的hash this.clientSocketList = [];// 全部的websocket客户端 this.fs;// 会指向内存文件系统 this.server;// webserver服务器 this.app;// express实例 this.middleware;// webpack-dev-middleware返回的express中间件,用于返回编译的文件 this.setupHooks();// 【4】添加webpack的done事件回调,编译完成时会触发;编译完成时向客户端发送消息,经过websocket向全部的websocket客户端发送两个事件,告知浏览器来拉取新的代码了 this.setupApp();//【5】建立express实例app this.setupDevMiddleware();// 【6】里面就是webpack-dev-middlerware完成的工做,主要是本地文件的监听、启动webpack编译、设置文件系统为内存文件系统(让编译输出到内存中)、里面有一个中间件负责返回编译的文件 this.routes();// 【7】app中使用webpack-dev-middlerware返回的中间件 this.createServer();// 【8】建立webserver服务器,让浏览器能够访问编译的文件 this.createSocketServer();// 【9】建立websocket服务器,监听connection事件,将全部的websocket客户端存起来,同时经过发送hash事件,将最新一次的编译hash传给客户端 } setupHooks() {} setupApp() {} setupDevMiddleware() {} routes() {} createServer() {} createSocketServer() {} listen() {}// 启动服务器 } module.exports = Server; 复制代码
在进行webpack编译前,调用了updateCompiler(compiler)
方法,这个方法很关键,他会往咱们的chunk中偷偷塞入两个文件,lib/client/client.js
和lib/client/hot-dev-server.js
这两个文件是干什么的呢?咱们说利用websocket实现双向通讯的,咱们服务端会建立一个websocket服务器(第9步会讲),每当代码改动时会从新进行编译,生成新的编译文件,这时咱们websocket服务端将通知浏览器,你快来拉取新的代码啦
那么一个websocket客户端,实现和服务端通讯的逻辑,是否是也的有呢?因而webpack-dev-server给咱们提供了客户端的代码,也就是上面的两个文件,为咱们安插了一个间谍,悄悄地去拉新的代码、实现热更新
为啥要分红两个文件呢?固然是模块划分啦,balabala写在一坨总很差吧,在客户端实现部分我会细说这两个文件干了什么
// /src/lib/server/updateCompiler.js const path = require("path"); let updateCompiler = (compiler) => { const config = compiler.options; config.entry = { main: [ path.resolve(__dirname, "../client/index.js"), path.resolve(__dirname, "../client/hot-dev-server.js"), config.entry ] } compiler.hooks.entryOption.call(config.context, config.entry); } module.exports = updateCompiler; 复制代码
修改后的webpack
入口配置以下:
{ entry:{ main: [ 'xxx/src/lib/client/index.js', 'xxx/src/lib/client/hot/dev-server.js', './src/index.js' ], }, } 复制代码
done
事件回调咱们要在compiler编译完成的钩子上注册一个事件,这个事件主要干了一件事情,每当新一次编译完成后都会向全部的websocket客户端发送消息,发射两个事件,通知浏览器来拉代码啦
浏览器会监听这两个事件,浏览器会去拉取上次编译生成的hash.hot-update.json
,具体的逻辑咱们会在下面的客户端章节详细讲解
// /src/lib/server/Server.js setupHooks() { let { compiler } = this; compiler.hooks.done.tap("webpack-dev-server", (stats) => { //每次编译都会产生一个惟一的hash值 this.currentHash = stats.hash; //每当新一个编译完成后都会向全部的websocket客户端发送消息 this.clientSocketList.forEach(socket => { //先向客户端发送最新的hash值 socket.emit("hash", this.currentHash); //再向客户端发送一个ok socket.emit("ok"); }); }); } 复制代码
setupApp() { this.app = new express(); } 复制代码
// /src/lib/server/Server.js setupDevMiddleware() { let { compiler } = this; // 会监控文件的变化,每当有文件改变(ctrl+s)的时候都会从新编译打包 // 在编译输出的过程当中,会生成两个补丁文件 hash.hot-update.json 和 chunk名.hash.hot-update.js compiler.watch({}, () => { console.log("Compiled successfully!"); }); //设置文件系统为内存文件系统,同时挂载到this上,以方便webserver中使用 let fs = new MemoryFileSystem(); this.fs = compiler.outputFileSystem = fs; // express中间件,将编译的文件返回 // 为何不直接使用express的static中间件,由于咱们要读取的文件在内存中,因此本身实现一款简易版的static中间件 let staticMiddleWare = (fileDir) => { return (req, res, next) => { let { url } = req; if (url === "/favicon.ico") { return res.sendStatus(404); } url === "/" ? url = "/index.html" : null; let filePath = path.join(fileDir, url); try { let statObj = this.fs.statSync(filePath); if (statObj.isFile()) {// 判断是不是文件,不是文件直接返回404(简单粗暴) // 路径和原来写到磁盘的同样,只是这是写到内存中了 let content = this.fs.readFileSync(filePath); res.setHeader("Content-Type", mime.getType(filePath)); res.send(content); } else { res.sendStatus(404); } } catch (error) { res.sendStatus(404); } } } this.middleware = staticMiddleWare;// 将中间件挂载在this实例上,以便app使用 } 复制代码
routes() { let { compiler } = this; let config = compiler.options;// 通过webpack(config),会将 webpack.config.js导出的对象 挂在compiler.options上 this.app.use(this.middleware(config.output.path));// 使用webpack-dev-middleware导出的中间件 } 复制代码
让浏览器能够请求webpack编译后的静态资源
这里使用了express和原生的http,你可能会有个疑问?为何不直接使用express和http中的任意一个?
this.server = http.createServer(app);
一行代码完美搞定// /src/lib/server/Server.js createServer() { this.server = http.createServer(this.app); } 复制代码
使用socket.js在浏览器端和服务端之间创建一个 websocket 长链接
// /src/lib/server/Server.js createSocketServer() { // socket.io+http服务 实现一个websocket const io = socket(this.server); io.on("connection", (socket) => { console.log("a new client connect server"); // 把全部的websocket客户端存起来,以便编译完成后向这个websocket客户端发送消息(实现双向通讯的关键) this.clientSocketList.push(socket); // 每当有客户端断开时,移除这个websocket客户端 socket.on("disconnect", () => { let num = this.clientSocketList.indexOf(socket); this.clientSocketList = this.clientSocketList.splice(num, 1); }); // 向客户端发送最新的一个编译hash socket.emit('hash', this.currentHash); // 再向客户端发送一个ok socket.emit('ok'); }); } 复制代码
// /src/lib/server/Server.js listen(port, host = "localhost", cb = new Function()) { this.server.listen(port, host, cb); } 复制代码
/src/lib/client/index.js负责websocket客户端hash和ok事件的监听,ok事件的回调只干了一件事发射webpackHotUpdate事件
/src/lib/client/hot/dev-server.js负责监听webpackHotUpdate
,调用hotCheck
开始拉取代码,实现局部更新
他们经过/src/lib/client/emitter.js的共用了一个EventEmitter实例
// /src/lib/client/emiitter.js const { EventEmitter } = require("events"); module.exports = new EventEmitter(); // 使用events 发布订阅的模式,主要仍是为了解耦 复制代码
// /src/lib/client/index.js const io = require("socket.io-client/dist/socket.io");// websocket客户端 const hotEmitter = require("./emitter");// 和hot/dev-server.js共用一个EventEmitter实例,这里用于发射事件 let currentHash;// 最新的编译hash //【1】链接websocket服务器 const URL = "/"; const socket = io(URL); //【2】websocket客户端监听事件 const onSocketMessage = { //【2.1】注册hash事件回调,这个回调主要干了一件事,获取最新的编译hash值 hash(hash) { console.log("hash",hash); currentHash = hash; }, //【2.2】注册ok事件回调,调用reloadApp进行热更新 ok() { console.log("ok"); reloadApp(); }, connect() { console.log("client connect successfully!"); } }; // 将onSocketMessage进行循环,给websocket注册事件 Object.keys(onSocketMessage).forEach(eventName => { let handler = onSocketMessage[eventName]; socket.on(eventName, handler); }); //【3】reloadApp中 发射webpackHotUpdate事件 let reloadApp = () => { let hot = true; // 会进行判断,是否支持热更新;咱们自己就是为了实现热更新,因此简单粗暴设置为true if (hot) { // 事件通知:若是支持的话发射webpackHotUpdate事件 hotEmitter.emit("webpackHotUpdate", currentHash); } else { // 直接刷新:若是不支持则直接刷新浏览器 window.location.reload(); } } 复制代码
咱们说了webpack-dev-server.js会在updateCompiler(compiler)
更改entry配置,将webpack-dev-server/client/index.js?http://localhost:8080
和webpack/hot/dev-server.js
一块儿打包到chunk中,那咱们就来揭开源码中的hot/devserver.js的真面目吧,没错下面就是主要代码
// 源码中webpack/hot/dev-server.js if (module.hot) {// 是否支持热更新 var check = function check() { module.hot .check(true)// 没错module.hot.check就是hotCheck函数,看是否是绕到了HRMPlugin在打包的chunk中注入的HMR runtime代码啦 .then( /*日志输出*/) .catch( /*日志输出*/) }; // 和client/index.js共用一个EventEmitter实例,这里用于监听事件 var hotEmitter = require("./emitter"); // 监听webpackHotUpdate事件 hotEmitter.on("webpackHotUpdate", function(currentHash) { check(); }); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); } 复制代码
明白了吧,真正的客户端热更新的逻辑都是HotModuleReplacementPlugin.runtime运行时代码干的,经过module.hot.check=hotCheck把 webpack/hot/dev-server.js
和 HotModuleReplacementPlugin在chunk文件中注入的hotCheck等代码
架起一座桥梁
和源码的出入:源码中hot/dev-server.js很简单,就是调用了module.hot.check(即HMR runtime运行时的hotCheck)。HotModuleReplacementPlugin插入的代码是热更新客户端的核心
接下来看看咱们本身要实现的hot/dev-server.js的总体,咱们不使用HotModuleReplacementPlugin插入的运行时代码,而是在hot/dev-server.js咱们本身实现一遍
let hotEmitter = require("../emitter");// 和client.js公用一个EventEmitter实例 let currentHash;// 最新编译生成的hash let lastHash;// 表示上一次编译生成的hash,源码中是hotCurrentHash,为了直接表达他的字面意思换了个名字 //【4】监听webpackHotUpdate事件,而后执行hotCheck()方法进行检查 hotEmitter.on("webpackHotUpdate", (hash) => { hotCheck(); }) //【5】调用hotCheck拉取两个补丁文件 let hotCheck = () => { hotDownloadManifest().then(hotUpdate => { hotDownloadUpdateChunk(chunkID); }) } // 【6】拉取lashhash.hot-update.json,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lasthash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名 let hotDownloadManifest = () => {} // 【7】拉取更新的模块chunkName.lashhash.hot-update.json,经过JSONP请求获取到更新的模块代码 let hotDownloadUpdateChunk = (chunkID) => {} // 【8.0】这个hotCreateModule很重要,module.hot的值 就是这个函数执行的结果 let hotCreateModule = (moduleID) => { let hot = { accept() {}, check: hotCheck } return hot; } //【8】补丁JS取回来后会调用webpackHotUpdate方法(请看update chunk的格式),里面会实现模块的热更新 window.webpackHotUpdate = (chunkID, moreModules) => { //【9】热更新的重点代码实现 } 复制代码
和源码的出入:源码中调用的是check方法,在check方法里调用module.hot.check方法——也就是hotCheck方法,check里面还会进行一些日志输出。这里直接写check里面的核心hotCheck方法
hotEmitter.on("webpackHotUpdate", (hash) => { currentHash = hash; if (!lastHash) {// 说明是第一次请求 return lastHash = currentHash } hotCheck(); }) 复制代码
let hotCheck = () => { //【6】hotDownloadManifest用来拉取lasthash.hot-update.json hotDownloadManifest().then(hotUpdate => {// {"h":"58ddd9a7794ab6f4e750","c":{"main":true}} let chunkIdList = Object.keys(hotUpdate.c); //【7】调用hotDownloadUpdateChunk方法经过JSONP请求获取到最新的模块代码 chunkIdList.forEach(chunkID => { hotDownloadUpdateChunk(chunkID); }); lastHash = currentHash; }).catch(err => { window.location.reload(); }); } 复制代码
// 六、向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(xxxlasthash.hot-update.json),该 Manifest 包含了全部要更新的模块的 hash 值和chunk名 let hotDownloadManifest = () => { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); let hotUpdatePath = `${lastHash}.hot-update.json` xhr.open("get", hotUpdatePath); xhr.onload = () => { let hotUpdate = JSON.parse(xhr.responseText); resolve(hotUpdate);// {"h":"58ddd9a7794ab6f4e750","c":{"main":true}} }; xhr.onerror = (error) => { reject(error); } xhr.send(); }) } 复制代码
hotDownloadUpdateChunk方法经过JSONP
请求获取到最新的模块代码
为何是JSONP?由于chunkName.lasthash.hot-update.js是一个js文件,咱们为了让他从服务端获取后能够立马执行js脚本
let hotDownloadUpdateChunk = (chunkID) => { let script = document.createElement("script") script.charset = "utf-8"; script.src = `${chunkID}.${lastHash}.hot-update.js`//chunkID.xxxlasthash.hot-update.js document.head.appendChild(script); } 复制代码
module.hot
module.hot.accept
module.hot.check
let hotCreateModule = (moduleID) => { let hot = {// module.hot属性值 accept(deps = [], callback) { deps.forEach(dep => { // 调用accept将回调函数 保存在module.hot._acceptedDependencies中 hot._acceptedDependencies[dep] = callback || function () { }; }) }, check: hotCheck// module.hot.check === hotCheck } return hot; } 复制代码
回顾下hotDownloadUpdateChunk来取的代码长什么样
webpackHotUpdate("index", { "./src/lib/content.js": (function (module, __webpack_exports__, __webpack_require__) { eval(""); }) }) 复制代码
调用了一个webpackHotUpdate
方法,说明咱们得在全局上有一个webpackHotUpdate
方法
和源码的出入:源码webpackHotUpdate里面会调用hotAddUpdateChunk方法动态更新模块代码(用新的模块替换掉旧的模块),而后调用hotApply方法进行热更新,这里将这几个方法核心直接写在webpackHotUpdate中
window.webpackHotUpdate = (chunkID, moreModules) => { // 【9】热更新 // 循环新拉来的模块 Object.keys(moreModules).forEach(moduleID => { // 一、经过__webpack_require__.c 模块缓存能够找到旧模块 let oldModule = __webpack_require__.c[moduleID]; // 二、更新__webpack_require__.c,利用moduleID将新的拉来的模块覆盖原来的模块 let newModule = __webpack_require__.c[moduleID] = { i: moduleID, l: false, exports: {}, hot: hotCreateModule(moduleID), parents: oldModule.parents, children: oldModule.children }; // 三、执行最新编译生成的模块代码 moreModules[moduleID].call(newModule.exports, newModule, newModule.exports, __webpack_require__); newModule.l = true; // 这块请回顾下accept的原理 // 四、让父模块中存储的_acceptedDependencies执行 newModule.parents && newModule.parents.forEach(parentID => { let parentModule = __webpack_require__.c[parentID]; parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]() }); }) } 复制代码
利用webpack-dev-middleware、webpack-hot-middleware、express实现HMR Demo
让webpack以watch模式编译;
并将文件系统改成内存文件系统,不会把打包后的资源写入磁盘而是在内存中处理;
中间件负责将编译的文件返回;
提供浏览器和 Webpack 服务器之间的通讯机制、且在浏览器端订阅并接收 Webpack 服务器端的更新变化,而后使用webpack的HMR API执行这些更改
服务端监听compiler.hooks.done事件;
经过SSE,服务端编译完成向客户端发送building、built、sync事件;
webpack-dev-middleware是经过EventSource
也叫做server-sent-event(SSE)
来实现服务器发客户端单向推送消息。经过心跳检测,来检测客户端是否还活着,这个💓就是SSE心跳检测,在服务端设置了一个 setInterval 每一个10s向客户端发送一次
一样客户端代码须要添加到config的entry属性中,
// /dev-hot-middleware demo/webpack.config.js entry: { index: [ // 主动引入client.js "./node_modules/webpack-hot-middleware/client.js", // 无需引入webpack/hot/dev-server,webpack/hot/dev-server 经过 require('./process-update') 已经集成到 client.js模块 "./src/index.js", ] }, 复制代码
客户端 建立EventSource
实例 请求 /__webpack_hmr,监听building、built、sync事件,回调函数会利用HotModuleReplacementPlugin运行时代码进行更新;
webpack-dev-server
使用的是websocket
,webpack-hot-middleware
使用的是eventSource
;以及通讯过程的事件名不同了,webpack-dev-server是利用hash和ok,webpack-dev-middleware是build(构建中,不会触发热更新)和sync(判断是否开始热更新流程)Webpack-Dev-Server 就是内置了 Webpack-dev-middleware 和 Express 服务器,以及利用websocket
替代eventSource
实现webpack-hot-middleware的逻辑
Q: 为何有了webpack-dev-server
,还有有webpack-dev-middleware
搭配webpack-hot-middleware
的方式呢?
A: webpack-dev-server
是封装好的,除了webpack.config
和命令行参数以外,很难定制型开发。在搭建脚手架时,利用 webpack-dev-middleware
和webpack-hot-middleware
,以及后端服务,让开发更灵活。
步骤 | 功能 | 源码连接 |
---|---|---|
1 | 建立webpack实例 | webpack-dev-server |
2 | 建立Server实例 | webpack-dev-server |
3 | 更改config的entry属性 | Server updateCompiler |
entry添加dev-server/client/index.js | addEntries | |
entry添加webpack/hot/dev-server.js | addEntries | |
4 | 监听webpack的done事件 | Server |
编译完成向websocket客户端推送消息,最主要信息仍是新模块的hash 值,后面的步骤根据这一hash 值来进行模块热替换 |
Server | |
5 | 建立express实例app | Server |
6 | 使用webpack-dev-middlerware | Server |
以watch模式启动webpack编译,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包 | webpack-dev-middleware | |
设置文件系统为内存文件系统 | webpack-dev-middleware | |
返回一个中间件,负责返回生成的文件 | webpack-dev-middleware | |
7 | app中使用webpack-dev-middlerware返回的中间件 | Server |
8 | 建立webserver服务器并启动服务 | Server |
9 | 使用sockjs在浏览器端和服务端之间创建一个 websocket 长链接 | Server |
建立socket服务器并监听connection事件 | SockJSServer |
步骤 | 功能 | 源码连接 |
---|---|---|
1 | 链接websocket服务器 | client/index.js |
2 | websocket客户端监听事件 | client/index.js |
监听hash事件,保存此hash值 | client/index.js | |
监听ok事件,执行reloadApp 方法进行更新 |
client/index.js | |
3 | 调用reloadApp,在reloadApp中会进行判断,是否支持热更新,若是支持的话发射webpackHotUpdate 事件,若是不支持则直接刷新浏览器 |
client/index.js |
reloadApp中发射webpackHotUpdate事件 | reloadApp | |
4 | 在webpack/hot/dev-server.js 会监听webpackHotUpdate事件, |
webpack/hot/dev-server.js |
而后执行check()方法进行检查 | webpack/hot/dev-server.js | |
在check方法里会调用module.hot.check 方法 |
webpack/hot/dev-server.js | |
5 | module.hot.check也就是hotCheck | HotModuleReplacement.runtime |
6 | 调用hotDownloadManifest`,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lasthash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名 | HotModuleReplacement.runtime JsonpMainTemplate.runtime |
7 | 调用hotDownloadUpdateChunk``方法经过JSONP请求获取到最新的模块代码 | HotModuleReplacement.runtime HotModuleReplacement.runtime JsonpMainTemplate.runtime |
8 | 补丁JS取回来后会调用的webpackHotUpdate 方法,里面会调用hotAddUpdateChunk 方法,用新的模块替换掉旧的模块 |
JsonpMainTemplate.runtime |
9 | 调用hotAddUpdateChunk 方法动态更新模块代码 |
JsonpMainTemplate.runtime JsonpMainTemplate.runtime |
10 | 调用hotApply 方法进行热更新 |
HotModuleReplacement.runtime HotModuleReplacement.runtime |
从缓存中删除旧模块 | HotModuleReplacement.runtime | |
执行accept的回调 | HotModuleReplacement.runtime | |
执行新模块 | HotModuleReplacement.runtime |
不知道是否全面,若有不足,欢迎指正。
第一篇文章,若是对你有帮助和启发,还望给个小小的赞哟❤️~给我充充电🔋