文章首發於我的blog,歡迎關注~javascript
在使用 webpack-dev-server 的過程當中,若是指定了 hot 配置的話(使用 inline mode 的前提下), wds 會在內部更新 webpack 的相關配置,即將 HotModuleReplacementPlugin 加入到 webpack 的 plugins 當中。vue
在 HotModuleReplacementPlugin 執行的過程當中主要是完成了如下幾個工做:java
normalModuleFactory.hooks.parser .for("javascript/auto") .tap("HotModuleReplacementPlugin", addParserPlugins); normalModuleFactory.hooks.parser .for("javascript/dynamic") .tap("HotModuleReplacementPlugin", addParserPlugins);
其中在 addParserPlugins 方法當中添加了具體有關 parser hook 的回調,有幾個比較關鍵的 hook 單獨拿出來講下:webpack
parser.hooks.call .for("module.hot.accept") .tap("HotModuleReplacementPlugin")
這個 hook 主要是在 parser 編譯代碼過程當中遇到module.hot.accept
的調用的時候會觸發,主要的工做就是處理當前模塊部署依賴模塊的依賴分析,在編譯階段處理好依賴的路徑替換等內容。git
parser.hooks.call .for("module.hot.decline") .tap("HotModuleReplacementPlugin")
這個 hook 一樣是在 parser 編譯代碼過程當中遇到module.hot.decline
的調用的時候觸發,所作的工做和上面的 hook 相似。github
const mainTemplate = compilation.mainTemplate mainTemplate.hooks.moduleRequire.tap( "HotModuleReplacementPlugin", (_, chunk, hash, varModuleId) => { return `hotCreateRequire(${varModuleId})`; })
這個 hook 主要完成的工做是在生成 webpack bootstrap runtime 代碼當中對加載 module 的 require function
進行替換,變爲hotCreateRequire(${varModuleId})
的形式,這樣作的目的其實就是對於 module 的加載作了一層代理,在加載 module 的過程中創建起相關的依賴關係(須要注意的是這裏的依賴關係並不是是 webpack 在編譯打包構建過程當中的那個依賴關係,而是在 hmr 模式下代碼執行階段,一個 module 加載其餘 module 時在 hotCreateRequire 內部會創建起相關的加載依賴關係,方便以後的修改代碼以後進行的熱更新操做),具體這塊的分析能夠參見下面的章節。web
mainTemplate.hooks.bootstrap.tap( "HotModuleReplacementPlugin", (source, chunk, hash) => { // 在生成 runtime 最終的代碼前先經過 hooks.hotBootstrap 鉤子生成相關的 hmr 代碼而後再完成代碼的拼接 source = mainTemplate.hooks.hotBootstrap.call(source, chunk, hash); return Template.asString([ source, "", hotInitCode .replace(/\$require\$/g, mainTemplate.requireFn) .replace(/\$hash\$/g, JSON.stringify(hash)) .replace(/\$requestTimeout\$/g, requestTimeout) .replace( /\/\*foreachInstalledChunks\*\//g, // 經過一系列的佔位字符串,在生成代碼的階段完成代碼的替換工做 needChunkLoadingCode(chunk) ? "for(var chunkId in installedChunks)" : `var chunkId = ${JSON.stringify(chunk.id)};` ) ]); } )
在這個 hooks.bootstrap 當中所作的工做是在 mainTemplate 渲染 bootstrap runtime 的代碼的過程當中,對於hotInitCode
代碼進行字符串的匹配和替換工做。hotInitCode
這部分的代碼其實就是下面章節所要講的HotModuleReplacement.runtime
向 bootstrap runtime 代碼裏面注入的 hmr 運行時代碼。ajax
mainTemplate.hooks.moduleObj.tap( "HotModuleReplacementPlugin", (source, chunk, hash, varModuleId) => { return Template.asString([ `${source},`, `hot: hotCreateModule(${varModuleId}),`, // 這部分的內容即這個 hook 對相關內容的拓展 "parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),", "children: []" ]); } )
在這個 hooks.moduleObj 當中所作的工做是對__webpack_require__
這個函數體內部的 installedModules 緩存模塊變量進行拓展。幾個很是關鍵的點就是:json
hot: hotCreateModule(${varModuleId})
配置。這個 module.hot api 即對應這個 module 有關熱更新的 api,能夠看到這個部署 hot api 的工做是由 hotCreateModule 這個方法來完成的(這個方法是由 hmr runtime 代碼提供的,下面的章節會講)。最終和這個 module 全部有關熱更新相關的接口都經過module.hot.*
去訪問。Webpack 內部提供了 HotModuleReplacement.runtime 即熱更新運行時部分的代碼。這部分的代碼並非經過經過添加 webpack.entry 入口文件的方式來注入這部分的代碼,而是經過 mainTemplate 在渲染 boostrap runtime 代碼的階段完成代碼的注入工做的(對應上面的 mainTemplate.hooks.boostrap 所作的工做)。bootstrap
在這部分熱更新運行時的代碼當中所作的工做主要包含了如下幾個點:
hotCreateRequire
方法,用以對__webpack_require__
模塊引入方法進行代理,當一個模塊依賴其餘模塊,並將其引入的時候,會創建起宿主模塊和依賴模塊之間的相互依賴關係,這個依賴關係也是做爲以後某個模塊發生更新後,尋找與其有依賴關係的模塊的憑證。function hotCreateRequire(moduleId) { var me = installedModules[moduleId]; if (!me) return $require$; var fn = function(request) { // 這個是 hmr 模式下,對原來的 __webpack_require__ 引入模塊的函數作的一層代理 // 經過 depModule.parents 和 module.children 來雙向創建起 module 之間的依賴關係 if (me.hot.active) { if (installedModules[request]) { if (installedModules[request].parents.indexOf(moduleId) === -1) { installedModules[request].parents.push(moduleId); // 創建 module 之間的依賴關係,在被引入的 module 的 module.parents 當中添加當前這個 moduleId } } else { hotCurrentParents = [moduleId]; hotCurrentChildModule = request; } if (me.children.indexOf(request) === -1) { me.children.push(request); // 在當前 module 的 module.children 屬性當中添加被引入的 moduleId } } else { console.warn( "[HMR] unexpected require(" + request + ") from disposed module " + moduleId ); hotCurrentParents = []; } return $require$(request); // 引入模塊 }; ... return fn }
hotCreateModule
方法,用以給每一個 module 都部署熱更新相關的 api:function hotCreateModule(moduleId) { var hot = { // private stuff _acceptedDependencies: {}, _declinedDependencies: {}, _selfAccepted: false, _selfDeclined: false, _disposeHandlers: [], _main: hotCurrentChildModule !== moduleId, // Module API active: true, accept: function(dep, callback) { if (dep === undefined) hot._selfAccepted = true; // 表示這個 module 能夠進行 hmr else if (typeof dep === "function") hot._selfAccepted = dep; else if (typeof dep === "object") // 和其餘 module 創建起熱更新之間的關係 for (var i = 0; i < dep.length; i++) hot._acceptedDependencies[dep[i]] = callback || function() {}; else hot._acceptedDependencies[dep] = callback || function() {}; }, decline: function(dep) { if (dep === undefined) hot._selfDeclined = true; // 當前 module 不須要進行熱更新 else if (typeof dep === "object") // 當其依賴的 module 發生更新後,並不會觸發這個 module 的熱更新 for (var i = 0; i < dep.length; i++) hot._declinedDependencies[dep[i]] = true; else hot._declinedDependencies[dep] = true; }, dispose: function(callback) { hot._disposeHandlers.push(callback); }, addDisposeHandler: function(callback) { hot._disposeHandlers.push(callback); }, removeDisposeHandler: function(callback) { var idx = hot._disposeHandlers.indexOf(callback); if (idx >= 0) hot._disposeHandlers.splice(idx, 1); }, // Management API check: hotCheck, apply: hotApply, status: function(l) { if (!l) return hotStatus; hotStatusHandlers.push(l); }, addStatusHandler: function(l) { hotStatusHandlers.push(l); }, removeStatusHandler: function(l) { var idx = hotStatusHandlers.indexOf(l); if (idx >= 0) hotStatusHandlers.splice(idx, 1); }, //inherit from previous dispose call data: hotCurrentModuleData[moduleId] }; hotCurrentChildModule = undefined; return hot; }
在 hotCreateModule 方法當中完成 module.hot.* 和熱更新相關接口的定義。這些 api 也是暴露給用戶部署熱更新代碼的接口。
其中hot.accept
和hot.decline
方法主要是用戶來定義發生熱更新的模塊及其依賴是否須要熱更新的相關策略。例如hot.accept
方法用來決定當前模塊所依賴的哪些模塊發生更新的話,自身也須要完成一些更新相關的動做。而hot.decline
方法用來決定當前模塊依賴的模塊發生更新後,來決定自身是否須要進行更新。
而hot.check
和hot.apply
兩個方法實際上是 webpack 內部使用的2個方法,其中hot.check
方法:首先調用hotDownloadManifest
方法,經過發送一個 Get 請求去 server 獲取本次發生變動的相關內容。// TODO: 相關內容的具體格式和字段?
{ c: { // 發生更新的 chunk 集合 app: true }, h: 'xxxxx' // 服務端本次生成的編譯hash值,用來做爲下次瀏覽器獲取發生變動的 hash 值(至關於服務端下發的一個 token,瀏覽器拿着這個 token 去後端獲取對應的內容) }
function hotCheck(apply) { if (hotStatus !== "idle") { throw new Error("check() is only allowed in idle status"); } hotApplyOnUpdate = apply; hotSetStatus("check"); // 更新 熱更新 流程的內部狀態 return hotDownloadManifest(hotRequestTimeout).then(function(update) { if (!update) { hotSetStatus("idle"); return null; } hotRequestedFilesMap = {}; hotWaitingFilesMap = {}; hotAvailableFilesMap = update.c; // 發生更新的 chunk 集合 hotUpdateNewHash = update.h; // server 下發的本次生成的編譯 hash 值,做爲下次瀏覽器獲取發生變動的 hash 值 hotSetStatus("prepare"); var promise = new Promise(function(resolve, reject) { hotDeferred = { resolve: resolve, reject: reject }; }); hotUpdate = {}; /*foreachInstalledChunks*/ // 這段註釋在渲染 bootstrap runtime 部分的代碼的時候會經過字符串匹配給替換掉,最終替換後的代碼執行就是對已經下載的 chunk 進行循環 hotEnsureUpdateChunk(chunkId) // eslint-disable-next-line no-lone-blocks { /*globals chunkId */ hotEnsureUpdateChunk(chunkId); // hotEnsureUpdateChunk(lib/web/JsonpMainTemplate.runtime.js) 方法內部其實就是經過建立 script 標籤,而後傳入到文檔當中完成發生更新的 chunk 的下載 } if ( hotStatus === "prepare" && hotChunksLoading === 0 && hotWaitingFiles === 0 ) { hotUpdateDownloaded(); } return promise; }); }
// TODO: 補一個 hot.check 執行的流程圖
總結下hot.check
方法執行的流程其實就是:
接下來看下被下載的更新的 chunk 具體內容:
webpackHotUpdate('app', { 'compiled/module1/path': (function() { eval('...script...') }), 'compiled/module2/path': (function() { eval('...script...') }) })
能夠看到的是返回的 chunk 內容是能夠當即執行的函數:
function hotAddUpdateChunk(chunkId, moreModules) { if (!hotAvailableFilesMap[chunkId] || !hotRequestedFilesMap[chunkId]) return; hotRequestedFilesMap[chunkId] = false; for (var moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { hotUpdate[moduleId] = moreModules[moduleId]; } } if (--hotWaitingFiles === 0 && hotChunksLoading === 0) { hotUpdateDownloaded(); } }
對應所作的工做就是將須要更新的模塊緩存至hotUpdate
上,同時判斷須要更新的 chunk 是否已經下載完了,若是所有下載完成那麼執行hotUpdateDownloaded
方法,其內部實際就是調用hotApply
進行接下來進行細粒度的模塊更新和替換的工做。
首先先講下hotApply
內部的執行流程:
hotUpdate
須要更新的模塊,找出和須要更新的模塊有依賴關係的模塊;function hotApply(options) { function getAffectedStuff(updateModuleId) { var outdatedModules = [updateModuleId] var outdatedDependencies = {} var queue = outdatedModules.slice().map(function (id) { return { chain: [id], id: id } }) while (queue.length > 0) { var queueItem = queue.pop() var moduleId = queueItem.id var chain = queueItem.chain module = installedModules[moduleId] // installedModules 爲在 bootstrap runtime 裏面定義的已經被加載過的 module 集合,這裏其實就是爲了取到這個 module 本身定義部署的有關熱更新的相關策略 if (!module || module.hot._selfAccepted) continue // 若是這個 module 不存在或者只接受自更新,那麼直接略過接下來的代碼處理 if (module.hot._selfDeclined) { return { type: 'self-declined', chain: chain, moduleId: moduleId } } if (module.hot._main) { return { type: 'unaccepted', chain: chain, moduleId: moduleId } } for (var i = 0; i < module.parents.length; i++) { // 遍歷全部依賴這個模塊的 module var parentId = module.parents[i] var parent = installedModules[parentId] if (!parent) continue if (parent.hot._declinedDependencies[moduleId]) { // 若是這個 parentModule 的 module.hot._declinedDependencies 裏面設置了不受更新影響的 moduleId return { type: 'declined', chain: chain.concat([parentId]), moduleId: moduleId, parentId: parentId } } if (outdatedModules.indexOf(parentId) !== -1) continue if (parent.hot._acceptedDependencies[moduleId]) { // 若是這個 parentModule 的 module.hot._acceptedDependencies 裏面設置了其受更新影響的 moduleId if (!outdatedDependencies[parentId]) outdatedDependencies[parentId] = [] addAllToSet(outdatedDependencies[parentId], [moduleId]) continue } // 若是這個 parentModule 沒有部署任何相關熱更新的**模塊間依賴的更新策略**(不算_selfAccepted 和 _selfDeclined 狀態),那麼須要將這個 parentModule 加入到 outdatedModules 隊列裏面,同時更新 queue 來進行下一輪的遍歷找出全部須要進行更新的 module delete outdatedDependencies[parentId] outdatedModules.push(parentId) queue.push({ chain: chain.concat([parentId]), id: parentId }) } } return { type: 'accepted', moduleId: updateModuleId, outdatedModules: outdatedModules, // 本次更新當中全部過時的 modules outdatedDependencies: outdatedDependencies // 全部過時的依賴 modules } } for (var id in hotUpdate) { if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) { moduleId = toModuleId(id) /** @type {TODO} */ var result if (hotUpdate[id]) { result = getAffectedStuff(moduleId) } else { result = { type: 'disposed', moduleId: id } } /** @type {Error|false} */ var abortError = false var doApply = false var doDispose = false var chainInfo = '' if (result.chain) { chainInfo = '\nUpdate propagation: ' + result.chain.join(' -> ') } switch (result.type) { case 'self-declined': if (options.onDeclined) options.onDeclined(result) if (!options.ignoreDeclined) abortError = new Error( 'Aborted because of self decline: ' + result.moduleId + chainInfo ) break case 'declined': if (options.onDeclined) options.onDeclined(result) if (!options.ignoreDeclined) abortError = new Error( 'Aborted because of declined dependency: ' + result.moduleId + ' in ' + result.parentId + chainInfo ) break case 'unaccepted': if (options.onUnaccepted) options.onUnaccepted(result) if (!options.ignoreUnaccepted) abortError = new Error( 'Aborted because ' + moduleId + ' is not accepted' + chainInfo ) break case 'accepted': if (options.onAccepted) options.onAccepted(result) doApply = true break case 'disposed': if (options.onDisposed) options.onDisposed(result) doDispose = true break default: throw new Error('Unexception type ' + result.type) } if (abortError) { hotSetStatus('abort') return Promise.reject(abortError) } if (doApply) { appliedUpdate[moduleId] = hotUpdate[moduleId] // 須要更新的模塊 addAllToSet(outdatedModules, result.outdatedModules) // 使用單獨一個 outdatedModules 數組變量存放全部過時須要更新的 moduleId,其中 result.outdatedModules 是經過 getAffectedStuff 方法找到的當前遍歷的 module 所依賴的過時的須要更新的模塊 for (moduleId in result.outdatedDependencies) { // 使用單獨的 outdatedDependencies 集合去存放相關依賴更新模塊 if ( Object.prototype.hasOwnProperty.call( result.outdatedDependencies, moduleId ) ) { if (!outdatedDependencies[moduleId]) outdatedDependencies[moduleId] = [] addAllToSet( outdatedDependencies[moduleId], result.outdatedDependencies[moduleId] ) } } } if (doDispose) { addAllToSet(outdatedModules, [result.moduleId]) appliedUpdate[moduleId] = warnUnexpectedRequire } } // Store self accepted outdated modules to require them later by the module system // 在全部 outdatedModules 裏面找到部署了 module.hot._selfAccepted 屬性的模塊。(部署了這個屬性的模塊會經過 webpack 的模塊系統從新加載一次這個模塊的新的內容來完成熱更新) var outdatedSelfAcceptedModules = [] for (i = 0; i < outdatedModules.length; i++) { moduleId = outdatedModules[i] if ( installedModules[moduleId] && installedModules[moduleId].hot._selfAccepted ) outdatedSelfAcceptedModules.push({ module: moduleId, errorHandler: installedModules[moduleId].hot._selfAccepted }) } // dispose phase TODO: 各個熱更新階段 hooks? var idx var queue = outdatedModules.slice() 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 // 從 installedModules 集合當中剔除掉過時的 module,即其餘 module 引入這個被剔除掉的 module 的時候,實際上是會從新執行這個 module,這也是爲何要從 installedModules 上剔除這個須要被更新的模塊的緣由 // remove module from cache delete installedModules[moduleId] // when disposing there is no need to call dispose handler delete outdatedDependencies[moduleId] // 將這個 module 所依賴的模塊(module.children)當中剔除掉 module.children.parentModule,即解除模塊之間的依賴關係 // 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) } } } // 這裏一樣是經過遍歷 outdatedDependencies 裏面須要更新的模塊,須要注意的是 outdateDependencies 裏面的 key 爲被依賴的 module,這個 key 所對應的 value 數組裏面存放的是發生了更新的 module。因此這是須要解除被依賴的 module 和這些發生更新了的 module 之間的引用依賴關係。 // remove outdated dependency from module children var dependency var moduleOutdatedDependencies for (moduleId in outdatedDependencies) { if ( Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId) ) { module = installedModules[moduleId] if (module) { moduleOutdatedDependencies = outdatedDependencies[moduleId] for (j = 0; j < moduleOutdatedDependencies.length; j++) { dependency = moduleOutdatedDependencies[j] idx = module.children.indexOf(dependency) if (idx >= 0) module.children.splice(idx, 1) } } } } // Not in "apply" phase hotSetStatus('apply') // 更新當前的熱更新 hash 值(即經過 get 請求獲取 server 下發的 hash 值) hotCurrentHash = hotUpdateNewHash // 遍歷 appliedUpdate 發生更新的 module // insert new code for (moduleId in appliedUpdate) { if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId] // HIGHLIGHT: 這裏的 modules 變量爲 bootstrap 代碼裏面接收到的全部的 modules 的集合,即在這裏完成新老 module 的替換 } } // 執行那些在 module.hot.accept 上部署了依賴模塊發生更新後的回調函數 // call accept handlers var error = null for (moduleId in outdatedDependencies) { if ( Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId) ) { module = installedModules[moduleId] if (module) { moduleOutdatedDependencies = outdatedDependencies[moduleId] var callbacks = [] for (i = 0; i < moduleOutdatedDependencies.length; i++) { dependency = moduleOutdatedDependencies[i] cb = module.hot._acceptedDependencies[dependency] if (cb) { if (callbacks.indexOf(cb) !== -1) continue callbacks.push(cb) } } for (i = 0; i < callbacks.length; i++) { cb = callbacks[i] try { cb(moduleOutdatedDependencies) } catch (err) { ... } } } } } // 從新加載那些部署了 module.hot._selfAccepted 爲 true 的 module,即這個 module 會被從新加載並執行一次,這樣也就在 installedModules 上緩存了這個新的 module // Load self accepted modules for (i = 0; i < outdatedSelfAcceptedModules.length; i++) { var item = outdatedSelfAcceptedModules[i] moduleId = item.module hotCurrentParents = [moduleId] try { $require$(moduleId) // $require$ 會在被最終渲染到 bootstrap runtime 當中被替換爲 webpack require 加載模塊的方法 } catch (err) { if (typeof item.errorHandler === 'function') { try { item.errorHandler(err) } catch (err2) { ... } } else { ... } } hotSetStatus('idle') return new Promise(function (resolve) { resolve(outdatedModules) }) } } }
因此當一個模塊發生變化後,依賴這個模塊的 parentModule 有以下幾種熱更新執行的策略:
module.hot.accept()
當依賴的模塊發生更新後,這個模塊須要經過從新加載去完成本模塊的全量更新。
module.hot.accept(['xxx'], callback)
當依賴的模塊且爲 xxx
模塊發生更新後,這個模塊會執行 callback 來完成相關的更新的動做。而不須要經過從新加載的方式去完成更新。
module.hot.decline()
這個模塊無論其依賴的模塊是否發生了變化。這個模塊都不會發生更新。
module.hot.decline(['xxx'])
當依賴的模塊爲xxx
發生更新的狀況下,這個模塊不會發生更新。當依賴的其餘模塊(除了xxx
模塊外)發生更新的話,那麼最終仍是會將本模塊從緩存中刪除。
這些熱更新的 api 也是須要用戶本身在代碼當中進行部署的。就拿平時咱們使用的 vue 來講,在本地開發階段, vue sfc 通過 vue-loader 的編譯處理後,會自動幫咱們在組件代碼當中當中注入和熱更新相關的代碼。
// vue-loader/lib/codegen/hotReload.js const hotReloadAPIPath = JSON.stringify(require.resolve('vue-hot-reload-api')) const genTemplateHotReloadCode = (id, request) => { return ` module.hot.accept(${request}, function () { api.rerender('${id}', { render: render, staticRenderFns: staticRenderFns }) }) `.trim() } exports.genHotReloadCode = (id, functional, templateRequest) => { return ` /* hot reload */ if (module.hot) { var api = require(${hotReloadAPIPath}) api.install(require('vue')) if (api.compatible) { // 判斷使用的 vue 的版本是否支持熱更新 module.hot.accept() if (!api.isRecorded('${id}')) { api.createRecord('${id}', component.options) } else { api.${functional ? 'rerender' : 'reload'}('${id}', component.options) } ${templateRequest ? genTemplateHotReloadCode(id, templateRequest) : ''} } } `.trim() }
vue-loader
經過 genHotReloadCode 方法在處理 vue sfc 代碼的時候完成熱更新 api 的部署功能。這裏大體講下 vue component 進行熱更新的流程:
$forceUpdate
);<template>
,<script>
中的內容外會進行熱更新外,在咱們修改<style>
樣式內容的時候也有熱更新的效果。這也是 vue component 在編譯階段在 vue style block 的代碼當中部署了熱更新代碼的緣由。具體更新策略可參見vue-style-loader相關資料: