在前端應用框架中不論是react仍是vue,官方都提供了相應的腳手架方便開發者快速入手,當咱們在開發時修改某個js或者css文件時,webpack會自動編譯咱們的文件,咱們刷新瀏覽器就能夠看到編譯後的文件。爲此咱們會想,若是咱們修改保存以後,文件被編譯、瀏覽器自動刷新、或者瀏覽器局部刷新(不刷新整個瀏覽器),這樣的話多好。固然,基於webpack打包工具的相關庫已經實現了。下面對此部分流程作簡單的分析 javascript
相關版本選擇:
css
說明: 這裏選擇webpack的版本爲V2,是由於以前debug webpack的打包流程時恰爲v2的版本,那可能會問webpack-dev-server版本爲何是這個呢? 這裏解釋下,經過package.json中的字段peerDependencies能夠選定版本,爲此我選擇了對應的最新版本 v2.9.7。一樣webpack-dev-middleware版本選擇也是同樣的,主要看依賴關係。 附上webpack-dev-server庫的package.json文件描述html
"name": "webpack-dev-server",
"version": "2.9.7",
"peerDependencies": {
"webpack": "^2.2.0 || ^3.0.0" // 這裏說明須要的版本號
}
複製代碼
進入主題 demo是 webpack-dev-server 目錄下面的examples/api/simple列子,只粘貼出關鍵代碼,建議clone代碼比對一下前端
server.js 入口文件vue
'use strict';
const Webpack = require('webpack');
const WebpackDevServer = require('../../../lib/Server');
const webpackConfig = require('./webpack.config');
const compiler = Webpack(webpackConfig);
const devServerOptions = Object.assign({}, webpackConfig.devServer, {
stats: {
colors: true
}
});
const server = new WebpackDevServer(compiler, devServerOptions);
server.listen(8080, '127.0.0.1', () => {
console.log('Starting server on http://localhost:8080');
});
複製代碼
const webpackConfig = require('./webpack.config'); 文件以下java
'use strict';
var path = require("path");
// our setup function adds behind-the-scenes bits to the config that all of our
// examples need
const { setup } = require('../../util');
module.exports = setup({
context: __dirname,
entry: [
'./app.js',
'../../../client/index.js?http://localhost:8080/',
'webpack/hot/dev-server'
],
devServer: { // 這裏配置hot值決定當開發時文件被修改並保存後 更新模式爲熱更新HMR
hot: true
}
});
複製代碼
入口entry 包含'../../../client/index.js?http://localhost:8080/' 以及 'webpack/hot/dev-server' 做用分別是:前者是WebpackDevServer的客戶端瀏覽器代碼,經過sockjs-client來連接Server端進行通訊,好比開發時代碼修改後保存,WebpackDevServer會經過 webpack-dev-middleware 拿到webpack編譯後的結果,經過websockets 發送消息類型給客戶端瀏覽器。 後者是webpack熱更新HMR的客戶端瀏覽器代碼,打包時會insert進去,做用是當瀏覽器收到websockets發過來消息後,若是webpackConfig配置了webpack.HotModuleReplacementPlugin插件,就會走熱更新HMR模式 node
../../../client/index.js 文件以下react
'use strict';
const socket = require('./socket');
let urlParts;
let hotReload = true;
// __resourceQuery 也就是../../../client/index.js後面的參數 http://localhost:8080/ 經過webpack 打包時候替換
if (typeof __resourceQuery === 'string' && __resourceQuery) {
// If this bundle is inlined, use the resource query to get the correct url.
urlParts = url.parse(__resourceQuery.substr(1));
} else {
// ...
}
let hot = false;
let currentHash = '';
const onSocketMsg = {
hot: function msgHot() {
hot = true;
},
hash: function msgHash(hash) {
currentHash = hash;
},
ok: function msgOk() {
reloadApp();
}
};
// 創建websockets 連接
socket(socketUrl, onSocketMsg);
function reloadApp() {
if (isUnloading || !hotReload) {
return;
}
// 若是webpackConfig 中配置devServer.hot 爲true,就走熱更新HMR的模式,結論能夠經過webpack-dev-server 的lib/Server.js 文件邏輯得出
if (hot) {
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
} else { // 不然走熱重載live reload 直接刷新瀏覽器
applyReload(rootWindow, intervalId);
}
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
log.info('[WDS] App updated. Reloading...');
rootWindow.location.reload();
}
}
複製代碼
const socket = require('./socket'); 文件以下webpack
'use strict';
const SockJS = require('sockjs-client');
let sock = null;
function socket(url, handlers) {
sock = new SockJS(url);
sock.onclose = function onclose() {
// 此處是重連的邏輯 省略...
};
sock.onmessage = function onmessage(e) { // 當收到Server端的websockets 消息後執行對應的消息類型邏輯
// This assumes that all data sent via the websocket is JSON.
const msg = JSON.parse(e.data);
if (handlers[msg.type]) { handlers[msg.type](msg.data); }
};
}
module.exports = socket;
複製代碼
'webpack/hot/dev-server' 文件以下git
// => module.hot 被替換成true:在前期ast語法樹分析過程當中標識代碼位置,而後在webpack assets階段被替換
// => module.hot 被替換成true:在前期ast語法樹分析過程當中標識代碼位置,而後在webpack assets階段被替換
if(module.hot) {
var lastHash;
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
};
var check = function check() {
module.hot.check(true).then(function(updatedModules) {
if(!updatedModules) {
console.warn("[HMR] Cannot find update. Need to do a full reload!");
console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
window.location.reload();
return;
}
if(!upToDate()) {
check();
}
require("./log-apply-result")(updatedModules, updatedModules);
if(upToDate()) {
console.log("[HMR] App is up to date.");
}
}).catch(function(err) {
var status = module.hot.status();
if(["abort", "fail"].indexOf(status) >= 0) {
console.warn("[HMR] Cannot apply update. Need to do a full reload!");
console.warn("[HMR] " + err.stack || err.message);
// window.location.reload();
} else {
console.warn("[HMR] Update failed: " + err.stack || err.message);
}
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
if(!upToDate() && module.hot.status() === "idle") {
console.log("[HMR] Checking for updates on the server...");
check();
}
});
console.log("[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}
複製代碼
結論:被insert到客戶端瀏覽器中的這段代碼決定了 webpack熱更新HMR 的開始,當熱更新HMR模式失敗時,就直接刷新瀏覽器了
const { setup } = require('../../util'); 文件以下
module.exports = {
setup(config) {
const defaults = { plugins: [], devServer: {} };
const result = Object.assign(defaults, config);
result.plugins.push(new webpack.HotModuleReplacementPlugin());
result.plugins.push(new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, '.assets/layout.html'),
title: exampleTitle
}));
return result;
}
};
複製代碼
webpack.HotModuleReplacementPlugin 插件的做用就是:在webpack打包生成的代碼中添加功能代碼,當咱們開發時,修改某個文件並保存後,瀏覽器會拿到修改的模塊代碼,而後執行並更新依賴, 固然瀏覽器如何拿到代碼以及如何執行更新,下面會講到,這裏先提一下這個插件的做用
webpack entry 入口文件app.js
'use strict';
require('./example');
if (module.hot) {
module.hot.accept((err) => {
if (err) {
console.error('Cannot apply HMR update.', err);
}
});
}
複製代碼
webpack entry 入口文件example.js
'use strict';
const target = document.querySelector('#target');
target.innerHTML = 'Modify to update this element without reloading the page.';
複製代碼
html Template 模板文件
<!doctype html>
<html>
<head>
<title>WDS ▻ API: Simple Server</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/.assets/favicon.ico"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600|Source+Sans+Pro:400,400i,500,600"/>
<link rel="stylesheet" href="/.assets/style.css"/>
</head>
<body>
<main>
<header>
<h1>
<img src="/.assets/icon-square.svg" style="width: 35px; height: 35px;"/>
webpack-dev-server
</h1>
</header>
<section>
<h2>API: Simple Server</h2>
<div id="target"></div>
</section>
<section>
<div id="targetmodule"></div>
</section>
</main>
<script type="text/javascript" src="main.js"></script></body>
</html>
複製代碼
以上是涉及到的一些文件...
下面來看具體的效果: 運行 node --inspect-brk server.js 文件, 訪問http://localhost:8080
上圖左側是 webpack-dev-server 中 websockets server端的代碼,藉助webpack-dev-middleware註冊webapck打包生命週期事件回調函數,將打包過程關鍵生命點同步到客戶端瀏覽器(右側) ,從console處能夠知道收到了消息類型type:hot、hash、ok。其中hot類型是告訴客戶端瀏覽器更新代碼的方式採用 熱更新HMR 的方式, 而不是採用熱重載live reload 直接刷新瀏覽器的方式,hash是本次webpack打包後的hash值, ok標識webpack打包生命週期已經完成,能夠進行客戶端瀏覽器代碼的更新操做了,也就是 webpack熱更新HMR的過程。
下面當修改 example.js 文件 也就是瀏覽器如何更新代碼流程 關鍵時刻到了
//target.innerHTML = 'Modify to update this element without reloading the page.';
target.innerHTML = '熱更新HMR的模式';
複製代碼
文件變化後,webpack.HotModuleReplacementPlugin 插件 中關鍵的 webpack Compilation 對象事件回調函數以下
compilation.plugin("record", function(compilation, records) {
// 生成的 records 用於當文件變化後找出變話的模塊
debugger
if(records.hash === this.hash) return;
records.hash = compilation.hash;
records.moduleHashs = {};
// 循環每一個module, webpack中一個文件就是一個module,且經過hash值判斷文件是否有更改
this.modules.forEach(function(module) {
var identifier = module.identifier();
var hash = require("crypto").createHash("md5");
module.updateHash(hash);
records.moduleHashs[identifier] = hash.digest("hex");
});
records.chunkHashs = {};
// this webpack compilation 對象
this.chunks.forEach(function(chunk) {
records.chunkHashs[chunk.id] = chunk.hash;
});
records.chunkModuleIds = {};
this.chunks.forEach(function(chunk) {
records.chunkModuleIds[chunk.id] = chunk.modules.map(function(m) {
return m.id;
});
});
});
var initialPass = false;
var recompilation = false;
compilation.plugin("after-hash", function() {
// records 相應的hash 決定模塊變化以後的標識
debugger
var records = this.records;
if(!records) {
initialPass = true;
return;
}
if(!records.hash)
initialPass = true;
var preHash = records.preHash || "x";
var prepreHash = records.prepreHash || "x";
if(preHash === this.hash) {
recompilation = true;
this.modifyHash(prepreHash);
return;
}
records.prepreHash = records.hash || "x";
records.preHash = this.hash;
// complain 對象的hash值
this.modifyHash(records.prepreHash);
});
compilation.plugin("additional-chunk-assets", function() {
// 這裏當modul變化以後,找出變化的module 並並生成json 和對應的module Template模板信息
debugger
var records = this.records;
if(records.hash === this.hash) return;
if(!records.moduleHashs || !records.chunkHashs || !records.chunkModuleIds) return;
// 循環遍歷module 經過hash值標識module是否變化了
this.modules.forEach(function(module) {
var identifier = module.identifier();
var hash = require("crypto").createHash("md5");
module.updateHash(hash);
hash = hash.digest("hex");
module.hotUpdate = records.moduleHashs[identifier] !== hash;
});
// this.hash webpack Compilation 對象的hash值
var hotUpdateMainContent = {
h: this.hash,
c: {}
};
// records.chunkHashs 包含了 全部chunk的hash值信息
Object.keys(records.chunkHashs).forEach(function(chunkId) {
chunkId = isNaN(+chunkId) ? chunkId : +chunkId;
// 修改文件致使module 變化 => 找到對應的chunk
var currentChunk = this.chunks.find(chunk => chunk.id === chunkId);
if(currentChunk) {
// 經過chunk 來肯定是哪一個module變化了
var newModules = currentChunk.modules.filter(function(module) {
return module.hotUpdate;
});
var allModules = {};
currentChunk.modules.forEach(function(module) {
allModules[module.id] = true;
});
// 若是項目中有某個模塊沒有引用了 就會找出改模塊
var removedModules = records.chunkModuleIds[chunkId].filter(function(id) {
return !allModules[id];
});
// 若是發生了模塊module的變化
if(newModules.length > 0 || removedModules.length > 0) {
// 根據變化的module 獲得 module字符串模板
var source = hotUpdateChunkTemplate.render(chunkId, newModules, removedModules, this.hash, this.moduleTemplate, this.dependencyTemplates);
var filename = this.getPath(hotUpdateChunkFilename, {
hash: records.hash,
chunk: currentChunk
});
this.additionalChunkAssets.push(filename);
// filename 就是: `${currentChunk}.${records.hash}.hot-update.js}` => 0.9236d98784cee1af7a96.hot-update.js文件
this.assets[filename] = source;
// 標識module變化了
hotUpdateMainContent.c[chunkId] = true;
currentChunk.files.push(filename);
this.applyPlugins("chunk-asset", currentChunk, filename);
}
} else {
hotUpdateMainContent.c[chunkId] = false;
}
}, this);
// 下面是 `${records.hash}.hot-update.json` => 9236d98784cee1af7a96.hot-update.json 文件內容
var source = new RawSource(JSON.stringify(hotUpdateMainContent));
var filename = this.getPath(hotUpdateMainFilename, {
hash: records.hash
});
this.assets[filename] = source;
// 注: 以上添加到this.assets 的內容在 Compiler.emitAssets 階段 生成文件內容
});
複製代碼
結論: 當文件變化後,webpack 就會編譯生成 hot-update.json、以及對應的文件模塊hot-update.js信息 用於在Compiler.emitAssets 階段生成js文件
webpack 打包完過後,如何通知瀏覽器呢?以下webpack-dev-server Server.js文件
function Server(compiler, options) {
// debugger
// Default options
if (!options) options = {};
// webpack 配置中的屬性,決定經過熱更新的方式
this.hot = options.hot || options.hotOnly;
compiler.plugin('done', (stats) => {
// 這裏註冊 webpack compiler 對象的事件, 經過websockets 通知客戶端瀏覽器
debugger
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});
// Init express server
const app = this.app = new express(); // eslint-disable-line
app.all('*', (req, res, next) => { // eslint-disable-line
if (this.checkHost(req.headers)) { return next(); }
res.send('Invalid Host header');
});
// webpackDevMiddleware 監聽文件的變換 watch -> build
// middleware for serving webpack bundle
this.middleware = webpackDevMiddleware(compiler, options);
// ...
this.listeningApp = http.createServer(app);
// ...
}
// delegate listen call and init sockjs
Server.prototype.listen = function (port, hostname, fn) {
this.listenHostname = hostname;
// eslint-disable-next-line
const returnValue = this.listeningApp.listen(port, hostname, (err) => {
const sockServer = sockjs.createServer({
// Use provided up-to-date sockjs-client
sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
// Limit useless logs
log(severity, line) {
if (severity === 'error') {
log(line);
}
}
});
sockServer.on('connection', (conn) => {
if (!conn) return;
if (!this.checkHost(conn.headers)) {
this.sockWrite([conn], 'error', 'Invalid Host header');
conn.close();
return;
}
this.sockets.push(conn);
conn.on('close', () => {
const connIndex = this.sockets.indexOf(conn);
if (connIndex >= 0) {
this.sockets.splice(connIndex, 1);
}
});
// 這裏根據webpackConfig 中的配置 devServer.hot= true 通知客戶端瀏覽 更新代碼的方式
if (this.hot) this.sockWrite([conn], 'hot');
if (!this._stats) return;
this._sendStats([conn], this._stats.toJson(clientStats), true);
});
if (fn) {
fn.call(this.listeningApp, err);
}
});
return returnValue;
};
Server.prototype.sockWrite = function (sockets, type, data) {
sockets.forEach((sock) => {
sock.write(JSON.stringify({
type,
data
}));
});
};
// send stats to a socket or multiple sockets
Server.prototype._sendStats = function (sockets, stats, force) {
if (!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
stats.assets &&
stats.assets.every(asset => !asset.emitted)
) { return this.sockWrite(sockets, 'still-ok'); }
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
};
module.exports = Server;
複製代碼
當客戶端瀏覽器收到消息後 type: ok 消息類型發生時,流程以下: webpack打包後的部分代碼
//webpack/hot/dev-server.js 也就是webpack 入口添加的文件
module.hot.check(true).then(function(updatedModules) {}).catch(function(updatedModules) {})
// 進入
function hotCheck(apply) {
if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
hotApplyOnUpdate = apply;
hotSetStatus("check");
return hotDownloadManifest().then(function(update) {
// update.c標識對應的chunk是否發生了變化
hotAvailableFilesMap = update.c;
hotUpdateNewHash = update.h;
hotSetStatus("prepare");
var promise = new Promise(function(resolve, reject) {
});
// 開始請求 hot-update.json 文件
hotEnsureUpdateChunk(chunkId);
return promise;
});
}
// 請求以前webpack 生成的hot-update.json文件
function hotDownloadManifest() { // eslint-disable-line no-unused-vars
return new Promise(function(resolve, reject) {
if(typeof XMLHttpRequest === "undefined")
return reject(new Error("No browser support"));
try {
var request = new XMLHttpRequest();
var requestPath = __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json";
request.open("GET", requestPath, true);
request.timeout = 10000;
request.send(null);
} catch(err) {
return reject(err);
}
request.onreadystatechange = function() {
if(request.readyState !== 4) return;
// ...
resolve(update);
}
};
});
}
// 請求以前webpack 生成的hot-update.js 文件
function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.type = "text/javascript";
script.charset = "utf-8";
script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
head.appendChild(script);
}
// 請求的js文件執行以下代碼
function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
hotAddUpdateChunk(chunkId, moreModules);
if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
} ;
// 後續部分邏輯...
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
if(!module) continue;
var data = {};
// Call dispose handlers
var disposeHandlers = module.hot._disposeHandlers;
for(j = 0; j < disposeHandlers.length; j++) {
cb = disposeHandlers[j];
cb(data);
}
hotCurrentModuleData[moduleId] = data;
// disable module (this disables requires from this module)
module.hot.active = false;
// 刪除緩存
// remove module from cache
delete installedModules[moduleId];
// remove "parents" references from all children
for(j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(!child) continue;
idx = child.parents.indexOf(moduleId);
if(idx >= 0) {
child.parents.splice(idx, 1);
}
}
}
// 插入變化的模塊
// insert new code
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
// 插入模塊後, 從新執行js文件,這個過程瀏覽器是沒有刷新的,能夠經過瀏覽器Network看出
// Load self accepted modules
for(i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i];
moduleId = item.module;
hotCurrentParents = [moduleId];
try {
__webpack_require__(moduleId);
} catch(err) {}
}
// __webpack_require__(moduleId); 再次進入 app.js 文件執行 =>
/* 37 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
__webpack_require__(71);
if (true) {
module.hot.accept((err) => {
if (err) {
console.error('Cannot apply HMR update.', err);
}
});
}
複製代碼
最後再總結一下整個熱更新HMR流程吧:
當咱們修改文件並保存時,webpack-dev-server 經過 webpack-dev-middleware 可以拿到webpack打包過程的各個生命週期點, webpack打包過程經過HotModuleReplacementPlugin插件生成hot-update.js和hot-update.json文件,前者是變化的模塊字符串信息,後者是本次打包以後module模塊所對應的chunk信息以及打包後的hash值,決定客戶端瀏覽器是否更新。 而後webpack-dev-server 經過 websockets把消息發送給客戶端瀏覽器,瀏覽器收到消息後,分別請求這兩個文件,後續爲刪除installedModules全局緩存對象,並從新賦值,再次執行對應的文件,這樣就達到了在無刷新瀏覽器的條件下,更新變化的模塊了,webpack更新模塊的代碼比較複雜,有的細節沒有debug到,到此從Server 到 Client流程以及從Client 到 Server流程也就說清楚了
內容有點多,筆誤請諒解!涉及到的相關技術點有的沒有提到,好比webpack的打包流程、webpack中檢測文件變化的模塊、webpack-dev-middleware相關、webpack-dev-server模塊還有請求轉發等功能沒有說到,這個也不在討論範圍內,有興趣的能夠本身clone 代碼查看,若是你對webpack打包流程 debug 過 相信再來了解這些東西會好不少 不少...
可能會有同窗會說:看這些有什麼做用,固然對我來講是當時的好奇心,經過了解大牛的代碼實現,能學習到相關優秀的lib庫、加強本身對代碼的閱讀能力。再有就是了解了一些底層再對其使用時,也能遊刃有餘。
參考:
一、zhuanlan.zhihu.com/p/30669007
二、fed.taobao.org/blog/taofed…
三、github.com/webpack/tap… webpack 如何管理生命週期的核心庫
四、astexplorer.net 對了解webpack 如何對代碼進行ast分析對照頗有用