Use webpack with a development server that provides live reloading. This should be used for development only.
It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.
將webpack與提供實時從新加載的開發服務器一塊兒使用。 這應該僅用於開發。
它使用了引擎蓋下的webpack-dev-middleware,它提供了對webpack資產的快速內存訪問。
_sendStats
發放向client發送ok
或warning
消息,並同時發送向client發送hash值,在client保存下來。ok
或warning
消息後調用reloadApp
發佈客戶端檢查更新事件(webpackHotUpdate
)webpackHotUpdate
事件,調用check
方法進行hash值對比以及檢查各modules是否須要更新。如需更新會調用hotDownloadManifest
方法下載json(manifest)文件。hotDownloadManifest
完成後調用hotDownloadUpdateChunk
方法,經過jsonp的方式加載最新的chunk,以後分析對比文件進行文件的更新替換,完成整個熱更新流程。注:如下源碼分析採用倒敘分析方式node
在webpack構建項目時,webpack-dev-server會在編譯後js文件加上兩個依賴文件:webpack
/***/ (function(module, exports, __webpack_require__) { // 創建socket鏈接,保持先後端實時通信 __webpack_require__("./node_modules/webpack-dev-server/client/index.js?http://localhost:8080"); // dev-server client熱更新的方法 __webpack_require__("./node_modules/webpack/hot/dev-server.js"); module.exports = __webpack_require__("./src/index.js"); /***/ })
webpack/hot/dev-server.js的文件內容:git
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ /*globals window __webpack_hash__ */ if (module.hot) { var lastHash; //__webpack_hash__是每次編譯的hash值是全局的,就是放在window上 var upToDate = function upToDate() { return lastHash.indexOf(__webpack_hash__) >= 0; }; var log = require("./log"); var check = function check() { module.hot .check(true) // 這裏的check方法最終進入到webpack\lib\HotModuleReplacement.runtime.js文件中 .then(function(updatedModules) { //檢查全部要更新的module,若是沒有module要更新那麼返回null if (!updatedModules) { log("warning", "[HMR] Cannot find update. Need to do a full reload!"); log( "warning", "[HMR] (Probably because of restarting the webpack-dev-server)" ); window.location.reload(); return; } //檢測時候還有module須要更新,若是有=>check() if (!upToDate()) { check(); } //打印更新結果,全部須要更新的module和已經被更新的module都是updatedModules require("./log-apply-result")(updatedModules, updatedModules); if (upToDate()) { log("info", "[HMR] App is up to date."); } }) .catch(function(err) { //若是報錯直接全局reload var status = module.hot.status(); if (["abort", "fail"].indexOf(status) >= 0) { log( "warning", "[HMR] Cannot apply update. Need to do a full reload!" ); log("warning", "[HMR] " + (err.stack || err.message)); window.location.reload(); } else { log("warning", "[HMR] Update failed: " + (err.stack || err.message)); } }); }; //獲取MyEmitter對象 var hotEmitter = require("./emitter"); //監聽‘webpackHotUpdate’事件 hotEmitter.on("webpackHotUpdate", function(currentHash) { lastHash = currentHash; //根據兩點判斷是否須要檢查modules以及更新 // 1 對比服務端傳過來的最新hash值和客戶端的__webpack_hash__是否一致 // 2 調用module.hot.status方法獲取狀態 是否爲 ‘idle’ if (!upToDate() && module.hot.status() === "idle") { log("info", "[HMR] Checking for updates on the server..."); check(); } }); log("info", "[HMR] Waiting for update signal from WDS..."); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); }
加載最新chunk的源碼分析:github
module.hot.check
=>HotModuleReplacement
=>hotDownloadManifest
=>hotEnsureUpdateChunk
=>hotDownloadUpdateChunk
HotModuleReplacement模塊內容:web
... function hotCheck(apply) { ... return hotDownloadManifest(hotRequestTimeout).then(function(update) { ... // 獲取到manifest後經過jsonp加載最新的chunk /*foreachInstalledChunks*/ // eslint-disable-next-line no-lone-blocks { /*globals chunkId */ hotEnsureUpdateChunk(chunkId); } ... }); } ... function hotEnsureUpdateChunk(chunkId) { if (!hotAvailableFilesMap[chunkId]) { hotWaitingFilesMap[chunkId] = true; } else { hotRequestedFilesMap[chunkId] = true; hotWaitingFiles++; hotDownloadUpdateChunk(chunkId); } }
webpack會根據不一樣運行環境(node / webworker / web)調用對應的方法(hotDownloadManifest & hotDownloadUpdateChunk)來加載chunk:,咱們主要考慮web環境下的方法json
// eslint-disable-next-line no-unused-vars function webpackHotUpdateCallback(chunkId, moreModules) { //... } //$semicolon // eslint-disable-next-line no-unused-vars //jsonp方法加載chunk function hotDownloadUpdateChunk(chunkId) { var script = document.createElement("script"); script.charset = "utf-8"; script.src = $require$.p + $hotChunkFilename$; if ($crossOriginLoading$) script.crossOrigin = $crossOriginLoading$; document.head.appendChild(script); } // eslint-disable-next-line no-unused-vars function hotDownloadManifest(requestTimeout) { //... }
log-apply-result模塊內容:後端
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ module.exports = function(updatedModules, renewedModules) { //renewedModules表示哪些模塊被更新了,剩餘的模塊表示,哪些模塊因爲 ignoreDeclined,ignoreUnaccepted配置沒有更新 var unacceptedModules = updatedModules.filter(function(moduleId) { return renewedModules && renewedModules.indexOf(moduleId) < 0; }); var log = require("./log"); //哪些模塊沒法HMR,打印log //哪些模塊因爲某種緣由沒有更新成功。其中沒有更新的緣由多是以下的: // ignoreUnaccepted // ignoreDecline // ignoreErrored if (unacceptedModules.length > 0) { log( "warning", "[HMR] The following modules couldn't be hot updated: (They would need a full reload!)" ); unacceptedModules.forEach(function(moduleId) { log("warning", "[HMR] - " + moduleId); }); } //沒有模塊更新,表示模塊是最新的 if (!renewedModules || renewedModules.length === 0) { log("info", "[HMR] Nothing hot updated."); } else { log("info", "[HMR] Updated modules:"); //更新的模塊 renewedModules.forEach(function(moduleId) { if (typeof moduleId === "string" && moduleId.indexOf("!") !== -1) { var parts = moduleId.split("!"); log.groupCollapsed("info", "[HMR] - " + parts.pop()); log("info", "[HMR] - " + moduleId); log.groupEnd("info"); } else { log("info", "[HMR] - " + moduleId); } }); //每個moduleId都是數字那麼建議使用NamedModulesPlugin var numberIds = renewedModules.every(function(moduleId) { return typeof moduleId === "number"; }); if (numberIds) log( "info", "[HMR] Consider using the NamedModulesPlugin for module names." ); } };
webpack構建過程觸發module更新的時機瀏覽器
webpack-dev-server客戶端源碼的關鍵部分服務器
... ... const onSocketMsg = { ... ok() { sendMsg('Ok'); if (useWarningOverlay || useErrorOverlay) overlay.clear(); if (initial) return (initial = false); // eslint-disable-line no-return-assign reloadApp(); }, warnings(warnings) { log.warn('[WDS] Warnings while compiling.'); const strippedWarnings = warnings.map((warning) => stripAnsi(warning)); sendMsg('Warnings', strippedWarnings); for (let i = 0; i < strippedWarnings.length; i++) { log.warn(strippedWarnings[i]); } if (useWarningOverlay) overlay.showMessage(warnings); if (initial) return (initial = false); // eslint-disable-line no-return-assign reloadApp(); }, ... }; ... function reloadApp() { if (isUnloading || !hotReload) { return; } if (hot) { log.info('[WDS] App hot update...'); // eslint-disable-next-line global-require const hotEmitter = require('webpack/hot/emitter'); hotEmitter.emit('webpackHotUpdate', currentHash); //從新啓動webpack/hot/emitter,同時設置當前hash if (typeof self !== 'undefined' && self.window) { // broadcast update to window self.postMessage(`webpackHotUpdate${currentHash}`, '*'); } } else { //若是不是Hotupdate那麼咱們直接reload咱們的window就能夠了 let rootWindow = self; // use parent window for reload (in case we're in an iframe with no valid src) const intervalId = self.setInterval(() => { if (rootWindow.location.protocol !== 'about:') { // reload immediately if protocol is valid applyReload(rootWindow, intervalId); } else { rootWindow = rootWindow.parent; if (rootWindow.parent === rootWindow) { // if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways applyReload(rootWindow, intervalId); } } }); } function applyReload(rootWindow, intervalId) { clearInterval(intervalId); log.info('[WDS] App updated. Reloading...'); rootWindow.location.reload(); }
根據以上代碼 能夠看出:當客戶端接收到服務器端發送的ok和warning信息的時候,同時支持HMR的狀況下就會要求檢查更新,同時還收到服務器端本次編譯的hash值。咱們再看一下服務端是在什麼時機發送’ok’和’warning’消息。
class Server { ... _sendStats(sockets, stats, force) { if ( !force && stats && (!stats.errors || stats.errors.length === 0) && stats.assets && //每個asset都是沒有emitted屬性,表示沒有發生變化。若是發生變化那麼這個assets確定有emitted屬性 stats.assets.every((asset) => !asset.emitted) ) { return this.sockWrite(sockets, 'still-ok'); } //設置hash 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'); } } ... }
上面的代碼是發送‘ok’和‘warning’消息的方法,那具體在什麼時機調用此方法呢?
class Server { constructor(compiler, options = {}, _log) { ... const addHooks = (compiler) => { const { compile, invalid, done } = compiler.hooks; compile.tap('webpack-dev-server', invalidPlugin); invalid.tap('webpack-dev-server', invalidPlugin); done.tap('webpack-dev-server', (stats) => { this._sendStats(this.sockets, stats.toJson(STATS)); this._stats = stats; }); }; if (compiler.compilers) { compiler.compilers.forEach(addHooks); } else { addHooks(compiler); } ... } ... }
再看這部分代碼,就能夠理解了,每次在compiler的’done’鉤子函數(生命週期)被調用的時候就會經過socket向客戶端發送消息,要求客戶端去檢查模塊更新完成HMR工做。
總結:最近正處於換工做階段,有些閒暇時間,正好拔草,以前工做的過程就很想看一下webpack-dev-server 的實現原理,趁此機會粗略學習了一下它的源碼,有什麼不對的地方還望指教,互相學習,加深理解。