原創不易,但願能關注下咱們,再順手點個贊~~ |
本文首發於政採雲前端團隊博客: 看完這篇,面試不再怕被問 Webpack 熱更新javascript
Webpack熱更新( Hot Module Replacement,簡稱 HMR,後續均以 HMR 替代),無需徹底刷新整個頁面的同時,更新全部類型的模塊,是 Webpack 提供的最有用的功能之一。前端
HMR 做爲一個 Webpack 內置的功能,能夠經過 --hot 或者 HotModuleReplacementPlugin 開啓。java
刷新分爲兩種:一種是頁面刷新,不保留頁面狀態,就是簡單粗暴,直接window.location.reload();另外一種是基於 WDS(Webpack-dev-server)的模塊熱替換,只須要局部刷新頁面上發生變化的模塊,同時能夠保留當前的頁面狀態,好比複選框的選中狀態、輸入框的輸入等。webpack
HMR 的好處,在平常開發工做中體會頗深:節省寶貴的開發時間、提高開發體驗。引用官網的描述來概述一下:web
模塊熱替換(HMR - hot module replacement)功能會在應用程序運行過程當中,替換、添加或刪除 模塊,而無需從新加載整個頁面。主要是經過如下幾種方式,來顯著加快開發速度:面試
- 保留在徹底從新加載頁面期間丟失的應用程序狀態。
- 只更新變動內容,以節省寶貴的開發時間。
- 在源代碼中對 CSS / JS 進行修改,會馬上在瀏覽器中進行更新,這幾乎至關於在瀏覽器 devtools 直接更改樣式。
在開發環境,能夠將 HMR 做爲 LiveReload 的替代。那麼,HMR究竟是怎麼實現熱更新的呢?下面經過觀察編譯構建過程,以及分析源代碼來了解一下。json
本次探索依賴公司前端 Vue 項目開發腳手架配置:Webpack + Webpack-Dev-Middleware + Webpack-Hot-Middleware + Express。瀏覽器
項目啓動以後,會進行首次構建打包,控制檯中會輸出整個的構建過程,能夠觀察到一個 Hash 值 3606e1ab1ddcf6626797。性能優化
在每次代碼的修改後,保存時都會在控制檯上出現 compiling…字樣,能夠在控制檯中觀察到:服務器
Hash 值更新:4f8c0eff7ac051c13277;
新生成文件:3606e1ab1ddcf6626797.hot-update.json
main1.3606e1ab1ddcf6626797.hot-update.js。
若是沒有任何改動,直接保存,控制檯輸出編譯打包信息,Hash 值沒有發生變化,仍舊是 4f8c0eff7ac051c13277。
再次修改代碼保存,控制檯輸出編譯打包信息。根據文件名能夠發現,上次輸出的 Hash 值被做爲本次編譯新生成的 Hmr 文件標識。一樣,本次輸出的 Hash 值會被做爲下次熱更新的標識。
爲何代碼的改動保存會自動編譯,從新打包?這一系列的從新檢測編譯依賴於 Webpack 的文件監聽:在項目啓動以後,Webpack 會經過 Compiler 類的 Run 方法開啓編譯構建過程,編譯完成後,調用 Watch 方法監聽文件變動,當文件發生變化,從新編譯,編譯完成以後繼續監聽。
頁面的訪問須要依賴 Web 服務器,那要如何將 Webpack 編譯打包以後的文件傳遞給 Web 服務器呢?這就要看 Webpack-dev-middleware了。 Webpack-dev-middleware 是一個封裝器( wrapper ),它能夠把 Webpack 處理過的文件發送到一個 Server(其中 Webpack-Dev-Server 就是內置了 Webpack-dev-middleware 和 Express 服務器)。上面有說到編譯以後的文件會被寫入到內存,而 Webpack-dev-middleware 插件經過 memory-fs
實現靜態資源請求直接訪問內存文件。
const webpack = require('webpack');
const webpackConfig = require('./webpack.dev.conf');
const compiler = webpack(webpackConfig);
debug('webpack編譯完成');
debug('將編譯流經過webpack-dev-middleware');
const devMiddleware = require('webpack-dev-middleware')(compiler, {
// self-define options
});
複製代碼
上面代碼能夠看到,Webpack 編譯打包以後獲得一個 Compilation ,並將 Compilation 傳遞到 Webpack-dev-middleware 插件中,Webpack-dev-middleware 能夠經過 Compilation 調用 Webpack中 的 Watch 方法實時監控文件變化,並從新編譯打包寫入內存。
留意一下瀏覽器端,在 Network 中能夠看到幾個請求:
/__Webpack_hmr 請求返回的消息包含了首次 Hash 值,每次代碼變更從新編譯後,瀏覽器會發出 hash.hot-update.json、fileChunk.hash.hot-update.js 資源請求。
點開查看 hash.hot-update.json 請求,返回的結果中,h 是一個 hash 值,用於下次文件熱更新請求的前綴,c 表示當前要熱更新的文件是 main1 。
繼續查看 fileChunk.hash.hot-update.js,返回的內容是使用 webpackHotUpdate 標識的 fileChunk 內容。
那麼 Webpack 服務器和瀏覽器端是怎麼創建起通訊的呢?這就是 Webpack-hot-middleware 插件的功勞了。 Webpack-hot-middleware 的 README.md 文檔中有這樣一段描述:
This module is only concerned with the mechanisms to connect a browser client to a Webpack server & receive updates.
Webpack-hot-middleware 插件的做用就是提供瀏覽器和 Webpack 服務器之間的通訊機制、且在瀏覽器端接收 Webpack 服務器端的更新變化。
爲了更好的理解這一段話,打開瀏覽器開發者調試工具,能夠看到在 Webpack 打包好的 Js 中主要包含了如下幾部分。下面截取關鍵部分進行說明:
源碼中有這麼一段配置,看到這裏瞬間想到了在開發時瀏覽器的 Network 中老是有一個 __Webpack_hmr 的請求,點開查看會看到EventStream 事件流(服務器端事件流,服務器向瀏覽器推送消息,除了 websocket 全雙工通道雙向通訊方式還有一種 Server-Sent Events 單向通道的通訊方法,只能服務器端向瀏覽器端經過流信息的方式推送消息;頁面能夠經過 EventSource 實例接收服務器發送事件通知並觸發 onmessage 事件),而且以 2s 的頻率不停的更新消息內容,每行消息內容都有 ❤️ 的圖標,沒錯這就是一個心跳請求。
var options = {
path: '/__Webpack_hmr',
timeout: 20 * 1000,
overlay: true,
reload: false,
log: true,
warn: true,
name: '',
autoConnect: true,
overlayStyles: {},
overlayWarnings: false,
ansiColors: {},
};
複製代碼
繼續向下查看 Client.js 代碼,發現這徹底就是一個只要瀏覽器支持就能夠自發創建通訊通道的客戶端。
if (typeof window === 'undefined') {
// do nothing
} else if (typeof window.EventSource === 'undefined') {
// warning
} else {
if (options.autoConnect) {
// 創建通訊鏈接
connect();
}
}
複製代碼
在創建通訊的過程當中,瀏覽器端會初始化一個 EventSource 實例並經過 onmessage 事件監聽消息。瀏覽器端在收到服務器發來的數據時,就會觸發 onmessage 事件,能夠經過定義 onmessage 的回調函數處理接收到的消息。
// 瀏覽器端創建鏈接
function EventSourceWrapper() {
var source;
var listeners = [];
// 初始化EventSource實例
source = new window.EventSource(options.path);
// 定義onmessage事件監聽服務器端消息返回
source.onmessage = handleMessage;
function handleMessage(event) {
for (var i = 0; i < listeners.length; i++) {
listeners[i](event);
}
}
return {
addMessageListener: function(fn) {
listeners.push(fn);
}
};
}
// 瀏覽器端創建通訊通道,監聽處理服務器端推送的消息
function connect() {
EventSourceWrapper().addMessageListener(handleMessage);
function handleMessage(event) {
try {
processMessage(JSON.parse(event.data));
} catch (ex) {
// handler exception
}
}
}
複製代碼
Client.js 監聽的消息有:
building/built:構建中,不會觸發熱更新;
sync:開始更新的流程。
在 processUpdate 方法中,處理一切異常/錯誤的方法都是直接更新整個頁面即調用 window.location.reload(),首先調用 module.hot.check 方法檢測是否有更新,而後進入 HotModuleReplacement.runtime 的 Check 階段。
function processMessage(obj) {
switch (obj.action) {
case 'building':
// tell you rebuilding
break;
case 'built':
// tell you rebuilt in n ms
// fall through
case 'sync':
// 省略...
var applyUpdate = true;
if (applyUpdate) {
processUpdate(obj.hash, obj.modules, options);
}
break;
default:
// do something
}
}
複製代碼
改動頁面代碼保存以後,Webpack 會從新編譯文件併發消息通知瀏覽器,瀏覽器在 Check 以後觸發 WebpackHotUpdateCallback,具體 HotModuleReplacement.runtime.js 會作如下幾個操做:
進入 HotCheck,調用 hotDownloadManifest 發送 /hash.hot-update.json 請求;
經過 Json 請求結果獲取熱更新文件,以及下次熱更新的 Hash 標識,並進入熱更新準備階段;
hotAvailableFilesMap = update.c;// 須要更新的文件
hotUpdateNewHash = update.h;// 下次熱更新hash值
hotSetStatus("prepare");// 進入熱更新準備狀態
複製代碼
HotCheck 確認須要熱更新以後進入 hotAddUpdateChunk 方法,該方法先檢查 Hash 標識的模塊是否已更新,若是沒更新,則經過在 DOM 中添加 Script 標籤的方式,動態請求js: /fileChunk.hash.hot-update.js,獲取最新打包的 js 內容;
最新打包的js內容如何更新的呢?HotModuleReplacement.runtime.js 在 window 對象上定義了 WebpackHotUpdate方法;在這裏定義瞭如何解析前面 fileChunk.hash.hot-update.js 請求返回的js內容 webpackHotUpdate(main1, { moreModules }),直接遍歷 moreModules,而且執行 hotUpdate 方法更新;
至此頁面已經完成熱更新,Webpack 如何實現熱更新的呢?首先是創建起瀏覽器端和服務器端之間的通訊,瀏覽器會接收服務器端推送的消息,若是須要熱更新,瀏覽器發起http請求去服務器端獲取打包好的資源解析並局部刷新頁面。
招人,前端,隸屬政採雲前端大團隊(ZooTeam),50 餘個小夥伴正等你加入一塊兒浪~ 若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變「5年工做時間3年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手參與一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com