原文地址:https://juejin.im/post/5df36ffd518825124d6c1765
前言
本文以剖析webpack-dev-server源碼,從零開始實現一個webpack熱更新HMR,深刻了解webpack-dev-server、webpack-dev-middleware、webpack-hot-middleware的實現機制,完全搞懂他們的原理,在面試過程當中這個知識點能答的很是出彩,在搭建腳手架過程當中這塊能駕輕就熟。知其然並知其因此然,更上一層樓。javascript
一年前做爲一個小白的我去面試,小姐姐問webpack熱更新原理,我跟她說了一小時,而後我被掛了,這也是我人生中惟一一次面試沒有經過,她跟我說:其實你不用說這麼詳細的html
舒適提示❤️~篇幅較長,建議收藏到電腦端食用更佳。前端
源碼連接
原理圖連接vue
零、什麼是HMR
1. 概念
Hot Module Replacement是指當咱們對代碼修改並保存後,webpack將會對代碼進行得新打包,並將新的模塊發送到瀏覽器端,瀏覽器用新的模塊替換掉舊的模塊,以實如今不刷新瀏覽器的前提下更新頁面。java
2. 優勢
相對於live reload
刷新頁面的方案,HMR的優勢在於能夠保存應用的狀態,提升了開發效率node
3. 那就來用用吧
./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
web
let content = "hello world"
console.log("welcome");
export default content;
複製代碼
cd 項目根目錄
面試
npm run dev
vue-cli
4. 效果看圖
當咱們在輸入框中輸入了123,這個時候更新content.js中的代碼,會發現hello world!!!!變成了hello world,可是 輸入框的值 還保留着,這正是HMR的意義,頁面刷新期間保留狀態
5. 理解chunk和module的概念
chunk 就是若干 module 打成的包,一個 chunk 應該包括多個 module,通常來講最終會造成一個 file。而 js 之外的資源,webpack 會經過各類 loader 轉化成一個 module,這個模塊會被打包到某個 chunk 中,並不會造成一個單獨的 chunk。
1、webpack編譯
Webpack watch:使用監控模式開始啓動webpack編譯,在 webpack 的 watch 模式下,文件系統中某一個文件發生修改,webpack 監聽到文件變化,根據配置文件對模塊從新編譯打包,每次編譯都會產生一個惟一的hash值,
1. HotModuleReplacementPlugin作了哪些事
生成兩個補丁文件
manifest(JSON)
上一次編譯生成的hash.hot-update.json
(如:b1f49e2fc76aae861d9f.hot-update.json)updated chunk (JavaScript)
chunk名字.上一次編譯生成的hash.hot-update.js
(如main.b1f49e2fc76aae861d9f.hot-update.js)這裏調用了一個全局的
webpackHotUpdate
函數,留心一下這個js的結構是的這兩個文件不是webpack生成的,而是這個插件生成的,你可在配置文件把HotModuleReplacementPlugin去掉試一試
在chunk文件中注入HMR runtime運行時代碼:咱們的熱更新客戶端主要邏輯(
拉取新模塊代碼
、執行新模塊代碼
、執行accept的回調實現局部更新
)都是這個插件 把函數 注入到咱們的chunk文件中,而非webpack-dev-server,webpack-dev-server只是調用了這些函數
2. 看懂打包文件
下面這段代碼就是使用的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;
}
複製代碼
3. 聊聊module.hot和module.hot.accept
1. accept使用
若是要實現熱更新,下面這段代碼是必不可少的,accept傳入的回調函數就是局部刷新邏輯,當./content.js模塊改變時執行
if (module.hot) {
module.hot.accept(["./content.js"], render);
}
複製代碼
2. accept原理
爲何咱們只有寫了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蒐集的回調)
3. 再看accept
// 再看下面這段代碼是否是有點明白了
if (module.hot) {
module.hot.accept(["./content.js"], render);
// 等價於module.hot._acceptedDependencies["./content.js"] = render
// 沒錯,他就是將模塊改變時,要作的事進行了蒐集,蒐集到_acceptedDependencies中
// 以便當content.js模塊改變時,他的父模塊index.js經過_acceptedDependencies知道要幹什麼
}
複製代碼
2、整體流程
1. 整個流程分爲客戶端和服務端
2. 經過 websocket
創建起 瀏覽器端 和 服務器端 之間的通訊
3. 服務端主要分爲四個關鍵點
經過webpack建立compiler實例,webpack在watch模式下編譯
compiler實例:監聽本地文件的變化、文件改變自動編譯、編譯輸出
更改config中的entry屬性:將lib/client/index.js、lib/client/hot/dev-server.js注入到打包輸出的chunk文件中
往compiler.hooks.done鉤子(webpack編譯完成後觸發)註冊事件:裏面會向客戶端發射
hash
和ok
事件調用webpack-dev-middleware:啓動編譯、設置文件爲內存文件系統、裏面有一箇中間件負責返回編譯的文件
建立webserver靜態服務器:讓瀏覽器能夠請求編譯生成的靜態資源
建立websocket服務:創建本地服務和瀏覽器的雙向通訊;每當有新的編譯,立馬告知瀏覽器執行熱更新邏輯
4. 客戶端主要分爲兩個關鍵點
建立一個 websocket客戶端 鏈接 websocket服務端,websocket客戶端監聽
hash
和ok
事件主要的熱更新客戶端實現邏輯,瀏覽器會接收服務器端推送的消息,若是須要熱更新,瀏覽器發起http請求去服務器端獲取新的模塊資源解析並局部刷新頁面(這本是HotModuleReplacementPlugin幫咱們作了,他將HMR 運行時代碼注入到chunk中了,可是我會帶你們實現這個
HMR runtime
)
5. 原理圖
3、源碼實現
1、結構
.
├── 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配置文件
複製代碼
2、看看webpack.config.js
// /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代碼
]
}
複製代碼
3、依賴的模塊
"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",
}
複製代碼
4、服務端實現
/src/myHMR-webpack-dev-server.js 熱更新服務端入口
/src/lib/server/Server.js Server類是熱更新服務端的主要邏輯
/src/lib/server/updateCompiler.js 更改entry,增長/src/lib/client/index.js和/src/lib/client/hot/dev-server.js
1. myHMR-webpack-dev-server.js總體一覽
// /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/`);
})
複製代碼
2. Server總體一覽
// /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;
複製代碼
3. 更改webpack的entry屬性,增長 websocket客戶端文件,讓其編譯到chunk中
在進行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'
],
},
}
複製代碼
4. 添加webpack的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");
});
});
}
複製代碼
5.建立express實例app
setupApp() {
this.app = new express();
}
複製代碼
6. 添加webpack-dev-middleware中間件
1. 關於webpack-dev-server和webpack-dev-middleware
webpack-dev-server核心是作準備工做(更改entry、監聽webpack done事件等)、建立webserver服務器和websocket服務器讓瀏覽器和服務端創建通訊
編譯和編譯文件相關的操做都抽離到webpack-dev-middleware
2. Webpack-dev-middleware主要乾了三件事(這裏咱們本身實現他的邏輯)
本地文件的監聽、啓動webpack編譯;使用監控模式開始啓動webpack編譯在 webpack 的 watch 模式下,文件系統中某一個文件發生修改,webpack 監聽到文件變化,根據配置文件對模塊從新編譯打包;
設置文件系統爲內存文件系統(讓編譯輸出到內存中)
實現了一個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使用
}
複製代碼
7. app中使用webpack-dev-middlerware返回的中間件
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導出的中間件
}
複製代碼
8. 建立webserver服務器
讓瀏覽器能夠請求webpack編譯後的靜態資源
這裏使用了express和原生的http,你可能會有個疑問?爲何不直接使用express和http中的任意一個?
不直接使用express,是由於咱們拿不到server,能夠看下express的源碼,爲何要這個server,由於咱們要在socket中使用;
不直接使用http,想必你們也知道,原生http寫邏輯簡直傷不起;咱們這裏只是寫了一個簡單的static處理邏輯,因此看不出什麼,可是源碼中還有不少的邏輯,這裏只是將核心邏輯挑了出來實現
那既然二者都有缺陷,就結合一下唄,咱們用原生http建立一個服務,不就拿到了server嘛,這個server的請求邏輯,仍是交給express處理就行了唄,
this.server = http.createServer(app);
一行代碼完美搞定
// /src/lib/server/Server.js
createServer() {
this.server = http.createServer(this.app);
}
複製代碼
9. 建立websocket服務器
使用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');
});
}
複製代碼
10. 啓動webserver服務,開始監聽
// /src/lib/server/Server.js
listen(port, host = "localhost", cb = new Function()) {
this.server.listen(port, host, cb);
}
複製代碼
5、客戶端實現
/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實例
0. emitter紐帶
// /src/lib/client/emiitter.js
const { EventEmitter } = require("events");
module.exports = new EventEmitter();
// 使用events 發佈訂閱的模式,主要仍是爲了解耦
複製代碼
1. index.js實現
// /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();
}
}
複製代碼
2. 聊聊源碼中的webpack/hot/dev-server.js
咱們說了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等代碼
架起一座橋樑
3. hot/dev-server.js總體概覽
和源碼的出入:源碼中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】熱更新的重點代碼實現
}
複製代碼
4. 監聽webpackHotUpdate事件
和源碼的出入:源碼中調用的是check方法,在check方法裏調用module.hot.check方法——也就是hotCheck方法,check裏面還會進行一些日誌輸出。這裏直接寫check裏面的核心hotCheck方法
hotEmitter.on("webpackHotUpdate", (hash) => {
currentHash = hash;
if (!lastHash) {// 說明是第一次請求
return lastHash = currentHash
}
hotCheck();
})
複製代碼
5. 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();
});
}
複製代碼
6. 拉補丁代碼——lasthash.hot-update.json
// 六、向 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();
})
}
複製代碼
7. 拉補丁代碼——更新的模塊代碼lasthash.hot-update.json
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);
}
複製代碼
8.0 hotCreateModule
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;
}
複製代碼
8. webpackHotUpdate實現熱更新
回顧下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]()
});
})
}
複製代碼
6、webpack-dev-server,webpack-hot-middleware,webpack-dev-middleware
利用webpack-dev-middleware、webpack-hot-middleware、express實現HMR Demo
1.Webpack-dev-middleware
讓webpack以watch模式編譯;
並將文件系統改成內存文件系統,不會把打包後的資源寫入磁盤而是在內存中處理;
中間件負責將編譯的文件返回;
2. Webpack-hot-middleware:
提供瀏覽器和 Webpack 服務器之間的通訊機制、且在瀏覽器端訂閱並接收 Webpack 服務器端的更新變化,而後使用webpack的HMR API執行這些更改
1. 服務端
服務端監聽compiler.hooks.done事件;
經過SSE,服務端編譯完成向客戶端發送building、built、sync事件;
webpack-dev-middleware是經過
EventSource
也叫做server-sent-event(SSE)
來實現服務器發客戶端單向推送消息。經過心跳檢測,來檢測客戶端是否還活着,這個💓就是SSE心跳檢測,在服務端設置了一個 setInterval 每一個10s向客戶端發送一次
2. 客戶端
一樣客戶端代碼須要添加到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運行時代碼進行更新;
3. 總結
其實咱們在實現webpack-dev-server熱更新的時候,已經把webpack-hot-middleware的功能都實現了。
他們的最大區別就是瀏覽器和服務器之間的通訊方式,
webpack-dev-server
使用的是websocket
,webpack-hot-middleware
使用的是eventSource
;以及通訊過程的事件名不同了,webpack-dev-server是利用hash和ok,webpack-dev-middleware是build(構建中,不會觸發熱更新)和sync(判斷是否開始熱更新流程)
3. webpack-dev-server
Webpack-Dev-Server 就是內置了 Webpack-dev-middleware 和 Express 服務器,以及利用websocket
替代eventSource
實現webpack-hot-middleware的邏輯
4. 區別
Q: 爲何有了webpack-dev-server
,還有有webpack-dev-middleware
搭配webpack-hot-middleware
的方式呢?
A: webpack-dev-server
是封裝好的,除了webpack.config
和命令行參數以外,很難定製型開發。在搭建腳手架時,利用 webpack-dev-middleware
和webpack-hot-middleware
,以及後端服務,讓開發更靈活。
7、源碼位置
1. 服務端
步驟 | 功能 | 源碼連接 |
---|---|---|
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 |
2. 客戶端
步驟 | 功能 | 源碼連接 |
---|---|---|
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 |
8、流程圖
這是剖析webpack-dev-server源碼的流程圖

本文分享自微信公衆號 - 前端巔峯(Java-Script-)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。