本文以剖析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 |
不知道是否全面,若有不足,歡迎指正。
第一篇文章,若是對你有幫助和啓發,還望給個小小的贊喲❤️~給我充充電🔋