wepack-hot-middleware 深入解讀
webpack-hot-middleware 做什么的?
webpack-hot-middleware 是和webpack-dev-middleware配套使用的。從上一篇文章中知道,webpack-dev-middleware是一個express中間件,實現(xiàn)的效果兩個,一個是在fs基于內(nèi)存,提高了編譯讀取速度;第二點是,通過watch文件的變化,動態(tài)編譯。但是,這兩點并沒有實現(xiàn)瀏覽器端的加載,也就是,只是在我們的命令行可以看到效果,但是并不能在瀏覽器動態(tài)刷新。那么webpack-hot-middleware就是完成這件小事的。沒錯,這就是一件小事,代碼不多,后面我們就深入解讀。-
webpack-hot-middleware 怎么使用的?
官方文檔已經(jīng)很詳細的介紹了,那么我再重復一遍。- 在plugins中增加HotModuleReplacementPlugin().
- 在entry中新增webpack-hot-middleware/client
- 在express中加入中間件webpack-hot-middleware.
- 在入口文件添加
//灰常重要,知會 webpack 允許此模塊的熱更新 if (module.hot) { module.hot.accept(); }詳細的配置文件也可以看我的github
// webpack.config.js const path = require('path'); const webpack = require('webpack'); module.exports = { entry: ['webpack-hot-middleware/client.js', './app.js'], output: { publicPath: "/assets/", filename: 'bundle.js', }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin() ] }; // express.config.js app.use(require("webpack-hot-middleware")(compiler, { path: '/__webpack_hmr', })); app.get("/", function(req, res) { res.render("index"); }); app.listen(3333); -
重點,重點,重點,源代碼分析。
通過上面的配置,我們可以發(fā)現(xiàn)webpack實現(xiàn)了熱加載,那么是如何實現(xiàn)的呢?進入正題。- webpack-hot-middleware中的入口文件middleware.js,哇,只有100多行。
整個文件的結構如下:
文件結構文件結構能夠看到line6-36是核心。那么,我們深入分析,
line7-10定義了基本的option,分別定義了option的log方法,path路徑以及server的socket響應時間。(哈哈,log方法怎么在插件都進行了定義?。。。?/p>
line 12創(chuàng)建了eventStream對象,這是個什么呢?line38-76,
好簡單,就是執(zhí)行了這兩行,
setInterval(function heartbeatTick() { everyClient(function(client) { client.write("data: \uD83D\uDC93\n\n"); }); }, heartbeat).unref();每間隔heartbeat秒,遍歷clients,每個client socket eventStream 寫一個心(紅心)。
紅心
最后返回了一個handler以及publish方法的對象,也就是eventStream。
下一步就是line 15,在compiler編譯時,加入一個回調(diào)處理函數(shù)。
compiler.plugin("compile", function() { latestStats = null; if (opts.log) opts.log("webpack building..."); eventStream.publish({action: "building"}); });上述這種通過node定義webpack插件的方式很常見(其實可以理解為加入了一個事件處理函數(shù))。它做了什么呢?對你的代碼(如果參考我的代碼的話,改變index.js即可),會發(fā)生什么呢?
也就是通過客戶端EventStream,向瀏覽器發(fā)送消息("action: building").
ok!還有另一個(一共只有兩個,竊喜,好簡單)。
compiler.plugin("done", function(statsResult) { // Keep hold of latest stats so they can be propagated to new clients latestStats = statsResult; publishStats("built", latestStats, eventStream, opts.log); });另一個函數(shù)啊,publishStats().函數(shù)內(nèi)部,又調(diào)用了extractBundles(),以及buildModuleMap
function extractBundles(stats) { if (stats.modules) return [stats]; if (stats.children && stats.children.length) return stats.children; return [stats]; }很簡單的幾行,就是將stats包裝為數(shù)組,有子元素children,直接用,沒有,就[stats]。
buildModuleMap就簡單了,建立了一個key,value的map映射。
這就簡單了,回到compiler的done回調(diào)函數(shù),整個流程就是執(zhí)行了抽取bundle,每個bundle執(zhí)行一次eventStream的publish回調(diào)。
實例效果,也就是上圖展示的那樣了,那個built,看清楚了吧。
那么,接著繼續(xù)!line25-35.
var middleware = function(req, res, next) { if (!pathMatch(req.url, opts.path)) return next(); eventStream.handler(req, res); if (latestStats) { // Explicitly not passing in `log` fn as we don't want to log again on // the server publishStats("sync", latestStats, eventStream); } }; middleware.publish = eventStream.publish; return middleware;重點來了。返回的中間件middleware,流程是這樣滴:判斷是不是__webpack_hmr(默認,可配置),不是的話跳過,執(zhí)行next()。是請求__webpack_hmr的話呢,執(zhí)行eventStream的handler方法,處理請求。
handler: function(req, res) { req.socket.setKeepAlive(true); res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/event-stream;charset=utf-8', 'Cache-Control': 'no-cache, no-transform', 'Connection': 'keep-alive', // While behind nginx, event stream should not be buffered: // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering 'X-Accel-Buffering': 'no' }); res.write('\n'); var id = clientId++; clients[id] = res; req.on("close", function(){ delete clients[id]; }); },其實就是,建立了一個eventStream。關鍵點:Content-Type: 'text/event-stream'。并且記錄了請求的clients.
line29-32,就是判斷如果已經(jīng)編譯完成,就向瀏覽器publish一個sync的消息。
至此,這個hot-middleware的服務器端的整個執(zhí)行過程就分析完了。我們上文一直提到eventStream,作為EventStream,如果少了客戶端怎么行呢?哈哈,別漏了這么重要的角色。
-
client.js
你應該還記得,上文中的配置webpack.config.js中,在entry中引入的“webpack-hot
-middleware/client.js”,對,這個就是client.js登上舞臺的入口。line 4-33,做了一件事,就是配置,根據(jù)__resourceQuery,轉化查詢字符串中的配置項?不明白。__resourceQuery是webpack中的默認API,表示require某個模塊時的查詢字符串。例如,我們在entry的client.js這樣寫。
entry: ['webpack-hot-middleware/client.js?name="zzf"', './app.js'],那么,在client.js中取到__resourceQuery的值就是?name="zzf"
line 35-45,一次判斷是否為客戶端瀏覽器,是否支持EventSource。如果都支持的話,connect()連接server端。connect()方法就是這個client.js的全部了。
function connect() { getEventSourceWrapper().addMessageListener(handleMessage); function handleMessage(event) { if (event.data == "\uD83D\uDC93") { return; } try { processMessage(JSON.parse(event.data)); } catch (ex) { if (options.warn) { console.warn("Invalid HMR message: " + event.data + "\n" + ex); } } } }line104執(zhí)行了哪些呢?
function getEventSourceWrapper() { if (!window.__whmEventSourceWrapper) { window.__whmEventSourceWrapper = {}; } if (!window.__whmEventSourceWrapper[options.path]) { // cache the wrapper for other entries loaded on // the same page with the same options.path window.__whmEventSourceWrapper[options.path] = EventSourceWrapper(); } return window.__whmEventSourceWrapper[options.path]; }上述主要執(zhí)行了就是創(chuàng)建了EventSourceWrapper()的對象。那么EventSourceWrapper()執(zhí)行了什么呢?
function EventSourceWrapper() { var source; var lastActivity = new Date(); var listeners = []; init(); var timer = setInterval(function() { if ((new Date() - lastActivity) > options.timeout) { handleDisconnect(); } }, options.timeout / 2); function init() { source = new window.EventSource(options.path); source.onopen = handleOnline; source.onerror = handleDisconnect; source.onmessage = handleMessage; } function handleOnline() { if (options.log) console.log("[HMR] connected"); lastActivity = new Date(); } function handleMessage(event) { lastActivity = new Date(); for (var i = 0; i < listeners.length; i++) { listeners[i](event); } } function handleDisconnect() { clearInterval(timer); source.close(); setTimeout(init, options.timeout); } return { addMessageListener: function(fn) { listeners.push(fn); } }; }主要執(zhí)行邏輯就是在init()方法中,就是新建一個window.EventSource(options.path)對象。然后通過每隔10s輪詢判斷是否,已經(jīng)20s(兩次)連接失敗了,就斷開本次連接,然后在timeout20s后,重新嘗試建立連接。最后返回一個對象,也就是對外拋出一個可以添加listener的口子。
line106-117,添加了這個事件監(jiān)聽處理函數(shù),handleMessage(event)方法。這個方法,首先根據(jù)服務端返回的數(shù)據(jù),是不是(心形 沒錯 "\uD83D\uDC93"就是紅心),如果是,表示正常的輪詢,直接return就可以。如果不是,調(diào)用processMessage處理,根據(jù)不同action,有不同的行為。這也就是middleware.js中的action行為。
我們再深入processMessage()方法,前兩個action:"building","built"就很簡單了,就是一個console.log()提示。如果是sync就分多種情況了,warn,error。通過reporter(下文創(chuàng)建的)去處理。最后調(diào)用了processUpdate(),下文再詳解。
還有兩部分沒有分析,就是createReporter().line134-190分析得到如下結論,reporter簡單的區(qū)分了warn,error,并以不同的style(console控制臺樣式,不知道的自行惡補)提示信息。并且,如果是編譯錯誤信息,通過overlay.js展示錯誤信息(創(chuàng)建一個遮罩層,打印出錯誤信息)。
現(xiàn)在,還遺漏了一個文件的分析,也就是processUpdate()方法。這其實是核心的玩意兒啊,不容忽視。分析后,就會發(fā)現(xiàn),至此為止,還少了至關重要的一部,EventStream只是將變化,通知給了client端,但是client端怎么實現(xiàn)hmr的呢?核心邏輯就在processUpdate()方法中。
// Based heavily on https://github.com/webpack/webpack/blob/ // c0afdf9c6abc1dd70707c594e473802a566f7b6e/hot/only-dev-server.js依賴這個玩意兒啊,hot/only-dev-server.js,這個玩意兒的分析在下一篇webpack-dev-server會深入分析。
返回正題,這里,line9-11判斷了module.hot如果不支持,直接拋出error,這也就是webpack-hot-middleware必須配合HotModuleReplacementPlugin使用的原因,它是給webpack添加了module.hot能力的啊。
line24-132,也就是整個方法了,這個方法嵌套的還是蠻深的。
var reload = options.reload; if (!upToDate(hash) && module.hot.status() == "idle") { if (options.log) console.log("[HMR] Checking for updates on the server..."); check(); }首先獲取options中的reload配置,還記得怎么配置的不?client.js?reload=true. 然后判斷hash是否已經(jīng)過期,也就是webpack進行了重新打包,manifest有變化,是的話,就check()檢查變化的資源。
function upToDate(hash) { if (hash) lastHash = hash; return lastHash == __webpack_hash__; }檢查hash是否過期的方法,使用了這樣一個__webpack_hash,這個是webpack給出的一個常量(可以通過webpack官網(wǎng)查詢),它表示資源的hash值,也就是已經(jīng)在瀏覽器端加載的資源的hash值。而hash,從上文中,我們還記得,這個玩意兒是EventStream傳過來的新的hash值。對的,沒有看錯,判斷文件是否變化,就是這么簡單。
繼續(xù),check()方法。
check()執(zhí)行,從64行開始,module.hot.check(false, cb);源文檔
module.hot.check(autoApply, (error, outdatedModules) => { // Catch errors and outdated modules... });這就明白了這里module.hot.check檢查webpack打包資源是否變化,將會沿著依賴樹往上遍歷。
再看一下回調(diào)函數(shù)cb
line33, 如果error,handleError()處理。handleError在line112-125很簡單,就根據(jù)module.hot.status()獲取hot module Replacement 進程的狀態(tài)。(官方原話:Retrieve the current status of the hot module replacement process.),如果狀態(tài)是abort或者fail,表示檢查失敗。。那么執(zhí)行performReload().也就是刷新瀏覽器(window.location.reload()).
回歸正題,line35-42可以看到,雖然不是error,但是updatedModules為undefined,也就是同樣沒有找到,執(zhí)行了一樣的邏輯,performReload().
line 52行,module.hot.apply()方法。為什么呢,還記得module.hot.check(false, cb)么,這里第一個參數(shù)為false表示需要手動調(diào)用module.hot.apply()。繼續(xù)。
module.hot.apply(applyOptions, applyCallback);
var applyOptions = { ignoreUnaccepted: true };這個option的意思就很清晰了。還記得第一步中的第四條么,這里就是它的用武之地了。
var applyCallback = function(applyErr, renewedModules) { if (applyErr) return handleError(applyErr); if (!upToDate()) check(); logUpdates(updatedModules, renewedModules); };這里代碼就好玩了。首先檢查applyErr是否出現(xiàn),出現(xiàn)的話,handleError處理。然后再次檢查了hash是否最新,如果不是的話,重新執(zhí)行了check().不知道會不會有死循環(huán)的風險。。。最后,就是logUpdate().沒什么技術了就,簡單區(qū)分了下,然后打印log信息。
line52-60是promise的then處理。line64-71同樣是如此。
That's all!
- webpack-hot-middleware中的入口文件middleware.js,哇,只有100多行。
-
擴展。
unref()方法。
還記得在client.js中有這么一處代碼么?
setInterval(function heartbeatTick() { everyClient(function(client) { client.write("data: \uD83D\uDC93\n\n"); }); }, heartbeat).unref();這里,我們就介紹一下這個unref()方法,看看官網(wǎng)是怎么介紹的。
翻譯成中文就是,當只有這一個timer處于active態(tài)時,并不需要事件循環(huán)去維持這個玩意兒,也就是process進程會退出。當然,如果有其他timer或者活動需要事件循環(huán)時,它也是可以跑著的。還不理解,這就是俗話:哎呀,我隨便,你們要是沒有吃的,我也不要了,要是有要的,就也給我一份。哈哈。
測試一下!var timer1 = setInterval(function() { console.log('timer1'); }, 1000).unref();沒有任何輸出。
var timer1 = setInterval(function() { console.log('timer1'); }, 1000).unref(); var timer2 = setInterval(function() { console.log('timer2'); }, 1000);




