webpack-dev-server 運(yùn)行原理
前言
現(xiàn)代 web 開(kāi)發(fā)者們對(duì)于 webpack 想必已經(jīng)很熟悉了,webpack-dev-server 幾乎都是標(biāo)配。但是 webpack-dev-server 背后的運(yùn)行原理是怎樣的呢?想了解 how 我們先看看 what。
webpack 將我們的項(xiàng)目源代碼進(jìn)行編譯打包成可分發(fā)上線(xiàn)的靜態(tài)資源,在開(kāi)發(fā)階段我們想要預(yù)覽頁(yè)面效果的話(huà)就需要啟動(dòng)一個(gè)服務(wù)器伺服 webpack 編譯出來(lái)的靜態(tài)資源。webpack-dev-server 就是用來(lái)啟動(dòng) webpack 編譯、伺服這些靜態(tài)資源,
除此之外,它還默認(rèn)提供了liveReload的功能,就是在一次 webpack 編譯完成后瀏覽器端就能自動(dòng)刷新頁(yè)面讀取最新的編譯后資源。為了提升開(kāi)發(fā)體驗(yàn)和效率,它還提供了 hot 選項(xiàng)開(kāi)啟 hotReload,相對(duì)于 liveReload, hotReload 不刷新整個(gè)頁(yè)面,只更新被更改過(guò)的模塊。

上圖是我對(duì) webpack-dev-server 的一個(gè)簡(jiǎn)單的整理。具體的實(shí)現(xiàn)原理是怎樣的我們接著往下看。
版本
本文基于以下版本進(jìn)行分析:
入口
如果作為命令行啟動(dòng),webpack-dev-server/bin/webpack-dev-server.js 就是整個(gè)命令行的入口。貼出來(lái)的代碼進(jìn)行了一些精簡(jiǎn),忽略了一些非核心的分支處理,只關(guān)心 webpack-dev-server 的核心邏輯??梢园凑兆⑨寴?biāo)注的順序簡(jiǎn)單閱讀下代碼。
//?webpack-dev-server/bin/webpack-dev-server.js
function?startDevServer(config,?options)?{
??let?compiler;
??try?{
????//?2.?調(diào)用webpack函數(shù)返回的是?webpack?compiler?實(shí)例
????compiler?=?webpack(config);
??}?catch?(err)?{
??}
??try?{
????//?3.?實(shí)例化?webpack-dev-server
????server?=?new?Server(compiler,?options,?log);
??}?catch?(err)?{
??}
??if?(options.socket)?{
??}?else?{
????//?4.?調(diào)用?server?實(shí)例的?listen?方法
????server.listen(options.port,?options.host,?(err)?=>?{
??????if?(err)?{
????????throw?err;
??????}
????});
??}
}
//?1.?對(duì)參數(shù)進(jìn)行處理后啟動(dòng)
processOptions(config,?argv,?(config,?options)?=>?{
??startDevServer(config,?options);
});
webpack-dev-server 作為命令行啟動(dòng),首先是調(diào)用了 webpack-cli 模塊下的兩個(gè)文件,分別配置了命令行提示選項(xiàng)、和從命令行和配置文件收集了 webpack 的 config,這樣復(fù)用了webpack-cli 的代碼,保持行為一致,上面貼出來(lái)的代碼省略了這部分代碼,有興趣的可以自己翻閱源碼。
之后調(diào)用 processOptions 對(duì)收集的參數(shù)進(jìn)行一些默認(rèn)處理后得到需要傳給 webpack 的 config 和需要傳給 wepack-dev-server 的 options。傳入這兩個(gè)配置參數(shù)調(diào)用 startDevServer,startDevServer 這個(gè)函數(shù)主要是先調(diào)用 webpack 函數(shù)實(shí)例化了 compiler,注意這里沒(méi)有給 webpack 函數(shù)傳入回調(diào)函數(shù),根據(jù) webpack 源碼實(shí)現(xiàn),不傳入回調(diào)函數(shù)就不會(huì)直接運(yùn)行 webpack 而是返回 webpack compiler 的實(shí)例,供調(diào)用方自行啟動(dòng) webpack 運(yùn)行。拿到 webpack compiler 實(shí)例和先前的 webpack-dev-server 的 options 就去實(shí)例化 Server,這個(gè) Server 類(lèi)就是實(shí)現(xiàn) webpack-dev-server 的核心邏輯。
最后調(diào)用 Server 類(lèi)的 listen 方法,就正式開(kāi)啟監(jiān)聽(tīng)請(qǐng)求,listen 方法后面會(huì)再解析具體邏輯。這就是 webapck-dev-server 大致的啟動(dòng)過(guò)程,后面來(lái)看下 Server 類(lèi)具體做了什么。
核心框架
//?webpack-dev-server/lib/Server.js
class?Server?{
??constructor(compiler,?options?=?{},?_log)?{
????//?0.?校驗(yàn)參數(shù)是否符合?schema,?不符合會(huì)拋出錯(cuò)誤
????validateOptions(schema,?options,?'webpack?Dev?Server');
????this.compiler?=?compiler;
????this.options?=?options;
????//?1.?為一些選項(xiàng)提供默認(rèn)參數(shù)
????normalizeOptions(this.compiler,?this.options);
????//?2.?對(duì)?webpack?compiler?進(jìn)行一些修改??webpack-dev-server/lib/utils/updateCompiler.js
????//????-?如果設(shè)置了?hot?選項(xiàng),自動(dòng)給?webpack?配置?HotModuleReplacementPlugin
????//????-?注入一些客戶(hù)端代碼:webpack 的 websocket 客戶(hù)端依賴(lài) sockJS/websocket + websocket 客戶(hù)端業(yè)務(wù)代碼?+ hot 模式下的 webpack/hot/dev-server
????updateCompiler(this.compiler,?this.options);
????//?3.?添加一些?hooks?插件,這里主要關(guān)注?webpack?compiler?的?done?鉤子,即每次編譯完成后的鉤子?(編譯完成觸發(fā)?_sendStats?方法給客戶(hù)端廣播消息?)
????this.setupHooks();
????//?4.?實(shí)例化?express?服務(wù)器
????this.setupApp();
????//?5.?設(shè)置?webpack-dev-middleware,用于處理對(duì)靜態(tài)資源的處理,后面解析
????this.setupDevMiddleware();
????//?6.?創(chuàng)建?HTTP?服務(wù)器
????this.createServer();
??}
??setupApp()?{
????//?Init?express?server
????//?eslint-disable-next-line?new-cap
????this.app?=?new?express();
??}
??setupHooks()?{
????const?addHooks?=?(compiler)?=>?{
??????const?{?compile??}?=?compiler.hooks;
??????done.tap('webpack-dev-server',?(stats)?=>?{
????????this._sendStats(this.sockets,?this.getStats(stats));
????????this._stats?=?stats;
??????});
????};
????addHooks(this.compiler);
??}
??setupDevMiddleware()?{
????//?middleware?for?serving?webpack?bundle
????this.middleware?=?webpackDevMiddleware(
??????this.compiler,
??????Object.assign({},?this.options,?{?logLevel:?this.log.options.level?})
????);
????this.app.use(this.middleware);
??}
??createServer()?{
????this.listeningApp?=?http.createServer(this.app);
????this.listeningApp.on('error',?(err)?=>?{
??????this.log.error(err);
????});
??}
??listen(port,?hostname,?fn)?{
????this.hostname?=?hostname;
????return?this.listeningApp.listen(port,?hostname,?(err)?=>?{
??????this.createSocketServer();
????});
??}
??createSocketServer()?{
????const?SocketServerImplementation?=?this.socketServerImplementation;
????this.socketServer?=?new?SocketServerImplementation(this);
????this.socketServer.onConnection((connection,?headers)?=>?{
??????//?連接后保存客戶(hù)端連接
??????this.sockets.push(connection);
??????if?(this.hot)?{
????????//?hot?選項(xiàng)先廣播一個(gè)?hot?類(lèi)型的消息
????????this.sockWrite([connection],?'hot');
??????}
??????this._sendStats([connection],?this.getStats(this._stats),?true);
????});
??}
??//?eslint-disable-next-line
??sockWrite(sockets,?type,?data)?{
????sockets.forEach((socket)?=>?{
??????this.socketServer.send(socket,?JSON.stringify({?type,?data?}));
????});
??}
??//?send?stats?to?a?socket?or?multiple?sockets
??_sendStats(sockets,?stats,?force)?{
????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');
????}
??}
}
這部分代碼稍長(zhǎng),主邏輯都在構(gòu)造函數(shù)里。
在構(gòu)造函數(shù)中進(jìn)行參數(shù)校驗(yàn),參數(shù)缺省值處理,注入客戶(hù)端代碼,綁定 webpack compiler 鉤子,這里主要關(guān)注是 done 鉤子,(在 webpack compiler 實(shí)例每次觸發(fā)編譯完成后就會(huì)進(jìn)行 webscoket 廣播 webpack 的編譯信息)。實(shí)例化 express 服務(wù)器,添加 webpack-dev-middleware 中間件用于處理靜態(tài)資源的請(qǐng)求,然后初始化 HTTP 服務(wù)器。
我們?cè)谏厦娴?webpack-dev-server.js 中調(diào)用的 listen 方法就是開(kāi)始監(jiān)聽(tīng)配置的端口,監(jiān)聽(tīng)回調(diào)里再初始化 websocket 的服務(wù)端。代碼執(zhí)行到這已經(jīng)完成了服務(wù)器端所有的邏輯,但是 webpack 還沒(méi)有啟動(dòng)編譯,用戶(hù)打開(kāi)瀏覽器后請(qǐng)求設(shè)置的IP和端口服務(wù)端又是怎么處理的呢?這部分暫時(shí)被我們略過(guò)了,這部分就是 webpack-dev-middleware 處理的內(nèi)容了。
webapck-dev-middleware 初始化
webapck-dev-middleware 作為一個(gè)獨(dú)立的模塊,以下是它的目錄結(jié)構(gòu):
.
├──?README.md
├──?index.js
├──?lib
│???├──?DevMiddlewareError.js
│???├──?context.js
│???├──?fs.js
│???├──?middleware.js
│???├──?reporter.js
│???└──?util.js
└──?package.json
webapck-dev-middleware 初始化執(zhí)行:
//?webpack-dev-middleware/index.js
module.exports?=?function?wdm(compiler,?opts)?{
??const?options?=?Object.assign({},?defaults,?opts);
??//?1.?初始化?context
??const?context?=?createContext(compiler,?options);
??//?start?watching
??if?(!options.lazy)?{
????//?2.?啟動(dòng)?webpack?編譯
????context.watching?=?compiler.watch(options.watchOptions,?(err)?=>?{
??????if?(err)?{
????????context.log.error(err.stack?||?err);
????????if?(err.details)?{
??????????context.log.error(err.details);
????????}
??????}
????});
??}?else?{
????//?lazy?模式是請(qǐng)求過(guò)來(lái)一次才webpack編譯一次,?這里不關(guān)注
??}
??//?3.?替換?webpack?默認(rèn)的?outputFileSystem?為?memory-fs,?存取都在內(nèi)存上操作
??//?fileSystem?=?new?MemoryFileSystem();
??//?compiler.outputFileSystem?=?fileSystem;
??setFs(context,?compiler);
??//?3.?執(zhí)行?middleware?函數(shù)返回真正的?middleware
??return?middleware(context);
};
wdm 函數(shù)返回結(jié)果是 express 標(biāo)準(zhǔn)的 middleware 用于處理瀏覽器靜態(tài)資源的請(qǐng)求。執(zhí)行過(guò)程中顯示初始化了一個(gè) context 對(duì)象,默認(rèn)非 lazy 模式,開(kāi)啟了 webpack 的 watch 模式開(kāi)始啟動(dòng)編譯。
然后將 compiler 的原來(lái)基于 fs 模塊的 outputFileSystem 替換成 memory-fs模塊的實(shí)例。memory-fs 是實(shí)現(xiàn)了 node fs api 的基于內(nèi)存的 fileSystem,這意味著 webpack 編譯后的資源不會(huì)被輸出到硬盤(pán)而是內(nèi)存。最后將真正處理請(qǐng)求的 middleware 返回裝載在 express 上。
webapck-dev-middleware 處理請(qǐng)求
當(dāng)用戶(hù)在瀏覽器打開(kāi)配置的IP和端口,如 https://localhost:8080 ,請(qǐng)求就會(huì)被 middleware 處理。middleware 使用 memory-fs 從內(nèi)存中讀到請(qǐng)求的資源返回給客戶(hù)端。
//?webpack-dev-middleware/lib/middleware.js
module.exports?=?function?wrapper(context)?{
??return?function?middleware(req,?res,?next)?{
????//?1.?根據(jù)請(qǐng)求的?URL?地址,得到絕對(duì)路徑的?webpack?輸出的資源路徑地址
????let?filename?=?getFilenameFromUrl(
??????context.options.publicPath,
??????context.compiler,
??????req.url
????);
????return?new?Promise((resolve)?=>?{
??????handleRequest(context,?filename,?processRequest,?req);
??????//?eslint-disable-next-line?consistent-return
??????function?processRequest()?{
????????//?2.從內(nèi)存讀取到資源內(nèi)容
????????let?content?=?context.fs.readFileSync(filename);
????????//?3.?返回給客戶(hù)端
????????if?(res.send)?{
??????????res.send(content);
????????}?else?{
??????????res.end(content);
????????}
????????resolve();
??????}
????});
??};
};
webscoket 通信
當(dāng)我們編輯了源代碼,觸發(fā) webpack 重新編譯,編譯完成后執(zhí)行 done 鉤子上的回調(diào)。具體可參考上面 Server.js 中 setupHooks 方法。_sendStats 方法會(huì)先廣播一個(gè)類(lèi)型為 hash 的消息,然后再根據(jù)編譯信息廣播 warnings/errors/ok 消息。這里我們只關(guān)注正常流程 ok 消息。
我們已經(jīng)很熟悉客戶(hù)端接收到更新后都會(huì)對(duì)應(yīng)用進(jìn)行 Reload 來(lái)獲取更好的開(kāi)發(fā)體驗(yàn)。具體是 liveReload(刷新整個(gè)頁(yè)面)還是 hotReload(更新改動(dòng)過(guò)的模塊)就取決于我們傳入的 hot 選項(xiàng)。
以下代碼就是我們?cè)谏厦婢椭v到的在 webpack 編譯的時(shí)候注入到 bundle.js 進(jìn)去的。當(dāng)用戶(hù)打開(kāi)頁(yè)面預(yù)覽時(shí),這些代碼就會(huì)自動(dòng)執(zhí)行。
//?webpack-dev-server/client/index.js
var?onSocketMessage?=?{
??hot:?function?hot()?{
????options.hot?=?true;
????log.info('[WDS]?Hot?Module?Replacement?enabled.');
??},
??liveReload:?function?liveReload()?{
????options.liveReload?=?true;
????log.info('[WDS]?Live?Reloading?enabled.');
??},
??hash:?function?hash(_hash)?{
????status.currentHash?=?_hash;
??},
??ok:?function?ok()?{
????if?(options.initial)?{
??????return?options.initial?=?false;
????}?//?eslint-disable-line?no-return-assign
????reloadApp(options,?status);
??}
};
socket(socketUrl,?onSocketMessage);
client/index.js 主要就是初始化了 webscoket 客戶(hù)端,然后為不同的消息類(lèi)型設(shè)置了相應(yīng)的回調(diào)函數(shù)。
在前面 Server.js 中我們看到如果 hot 選項(xiàng)為 true 時(shí),當(dāng) websocket 客戶(hù)端連接到服務(wù)端,服務(wù)端會(huì)先廣播一個(gè) hot 類(lèi)型的消息,客戶(hù)端接收到后會(huì)把 options 對(duì)象的 hot 設(shè)置為 true。
服務(wù)端在每次編譯后都會(huì)廣播 hash 消息,客戶(hù)端接收到后就會(huì)將這個(gè)webpack 編譯產(chǎn)生的 hash 值暫存起來(lái)。編譯成功如果沒(méi)有 warning 也沒(méi)有 error 就會(huì)廣播 ok 消息,客戶(hù)端接收到 ok 消息就會(huì)執(zhí)行 ok 回調(diào)函數(shù)中的 reloadApp 刷新應(yīng)用。
webscoket 消息處理
//?webpack-dev-server/client/utils/reloadApp.js
function?reloadApp(_ref,?_ref2)?{
??var?hotReload?=?_ref.hotReload,
??????hot?=?_ref.hot,
??????liveReload?=?_ref.liveReload,
??????currentHash?=?_ref2.currentHash;
??if?(hot)?{
????log.info('[WDS]?App?hot?update...');
????var?hotEmitter?=?require('webpack/hot/emitter');
????hotEmitter.emit('webpackHotUpdate',?currentHash);
??}
??else?if?(liveReload)?{
??????var?rootWindow?=?self;?//?use?parent?window?for?reload?(in?case?we're?in?an?iframe?with?no?valid?src)
??????var?intervalId?=?self.setInterval(function?()?{
????????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();
??}
}
Hot Module Replacement
觸發(fā) hot check
如果設(shè)置了 hot: true 客戶(hù)端就會(huì)引入 webpack/hot/emitter,觸發(fā)一個(gè) webpackHotUpdate 事件,將 hash 值傳遞過(guò)去。這個(gè) webpack/hot/emitter 我們查閱 webpack 源碼看到其實(shí)就是 node 的 events 模塊。我們暫時(shí)不關(guān)注這個(gè)事件會(huì)觸發(fā)什么回調(diào)后面再具體再看。如果沒(méi)有設(shè)置 hot: true。那么就是使用 liveReload 模式,liveReload 就比較無(wú)腦,直接刷新整個(gè)頁(yè)面。
再回到上一個(gè)問(wèn)題,到底是在哪里接收 webpackHotUpdate 事件并處理的呢?就是 webpack/hot/dev-server.js 中處理的。在這里會(huì)去檢查是否可以更新,如果更新失敗就會(huì)刷新整個(gè)頁(yè)面來(lái)降級(jí)實(shí)現(xiàn)代碼更新的功能。其實(shí)我們回過(guò)頭來(lái)看看這樣降級(jí)也是必須的,如果更新失敗,源碼更新了,而客戶(hù)端的代碼卻沒(méi)更新,這樣顯然是不合理的。
?var?lastHash;
?var?upToDate?=?function?upToDate()?{
??return?lastHash.indexOf(__webpack_hash__)?>=?0;
?};
?var?log?=?require("./log");
????//?2.?檢查更新
?var?check?=?function?check()?{
????//?3.?具體的檢查邏輯
??module.hot
???.check(true)
???.then(function(updatedModules)?{
????????//?3.1?更新成功
???})
???.catch(function(err)?{
????var?status?=?module.hot.status();
????????//?3.2?更新失敗,降級(jí)為重新刷新整個(gè)應(yīng)用
????if?(["abort",?"fail"].indexOf(status)?>=?0)?{
?????log(
??????"warning",
??????"[HMR]?Cannot?apply?update.?Need?to?do?a?full?reload!"
?????);
?????window.location.reload();
????}?else?{
?????log("warning",?"[HMR]?Update?failed:?"?+?log.formatError(err));
????}
???});
?};
?var?hotEmitter?=?require("./emitter");
??//?1.?注冊(cè)事件回調(diào)
?hotEmitter.on("webpackHotUpdate",?function(currentHash)?{
??lastHash?=?currentHash;
??if?(!upToDate()?&&?module.hot.status()?===?"idle")?{
???log("info",?"[HMR]?Checking?for?updates?on?the?server...");
???check();
??}
?});
模塊更新依賴(lài)判斷
module.hot.check 方法位于 webpack/lib/HotModuleReplacement.runtime.js 中,是 webpack 內(nèi)置的 HotModuleReplacementPlugin 注入在 webpack bootstrap runtime 中的。
所以 check 方法主要做了什么呢,這里提前總結(jié)一下。在 webpack 使用了 HotModuleReplacementPlugin 編譯時(shí),每次增量編譯就會(huì)多產(chǎn)出兩個(gè)文件,形如c390bbe0037a0dd079a6.hot-update.json,main.c390bbe0037a0dd079a6.hot-update.js,分別是描述 chunk 更新的 manifest文件和更新過(guò)后的 chunk 文件。那么瀏覽器端調(diào)用 hotDownloadManifest 方法去下載模塊更新的 manifest.json 文件,然后調(diào)用 hotDownloadUpdateChunk 方法使用 jsonp 的方式下載需要更新的 chunk。
hotDownloadUpdateChunk 下載完成后調(diào)用 webpackHotUpdate 回調(diào)?;卣{(diào)內(nèi)拿到更新的模塊,然后從模塊自身開(kāi)始進(jìn)行冒泡,如果發(fā)現(xiàn)只要有一條祖先路徑?jīng)]有 accept 這次改動(dòng)就直接刷新頁(yè)面實(shí)行降級(jí)強(qiáng)制更新, 如果有被 accept, 就會(huì)替換掉原來(lái) webpack runtime 里 module 里舊的模塊,然后再執(zhí)行 accept 的 callback 進(jìn)行更新。為什么要執(zhí)行這樣的判斷呢?
假設(shè)給定這樣的依賴(lài)路徑:
componentA.js?->?componentB.js?->?app.js?->?index.js
componentA.js?->?componentC.js?->?app.js?->?index.js
參考以下的代碼示例,accept 指該 module 的祖先模塊調(diào)用了 module.hot.accept, 處理了該 module 更新過(guò)后的業(yè)務(wù)邏輯,一般都是 rerender。
//?index.js
if(module.hot)?{
????module.hot.accept('./app',?function()?{
????????rerender()
????})
}
如果我們對(duì) componentA.js 進(jìn)行了更改,但是如果僅僅 componentB accept 了更改,componentC 卻沒(méi) accept,那么這樣是沒(méi)有到達(dá)更新的目的的。所以在祖先路徑回溯的時(shí)候,要保證每一條路徑都被 accept。
function?hotCheck(apply)?{
??//?1.?拿這次編譯后的?hash?請(qǐng)求服務(wù)器,拿到結(jié)構(gòu)為?{c:?{main:?true}?h:?"ac69ee760bb48d5db5f5"}?的數(shù)據(jù)
??return?hotDownloadManifest(hotRequestTimeout).then(function(update)?{
????hotAvailableFilesMap?=?update.c;
????hotUpdateNewHash?=?update.h;
????//?2.?生成一個(gè)?defered?promise,供上面提到的?promise?鏈消費(fèi)
????var?promise?=?new?Promise(function(resolve,?reject)?{
??????hotDeferred?=?{
????????resolve:?resolve,
????????reject:?reject
??????};
????});
????hotUpdate?=?{};
????//?3.?這個(gè)方法里面調(diào)用的就是?hotDownloadUpdateChunk,就是發(fā)起一個(gè)?jsonp?請(qǐng)求更新過(guò)后的?chunk,jsonp的回調(diào)是?HMR?runtime?里的?webpackHotUpdate
????{
??????hotEnsureUpdateChunk(chunkId);
????}
????return?promise;
??});
}
hotCheck 方法就是和服務(wù)器進(jìn)行通信拿到更新過(guò)后的 chunk,下載好 chunk 后就開(kāi)始執(zhí)行 HMR runtime 里的 webpackHotUpdate 回調(diào)。
window["webpackHotUpdate"]?=?function?webpackHotUpdateCallback(chunkId,?moreModules)?{
?hotAddUpdateChunk(chunkId,?moreModules);
?if?(parentHotUpdateCallback)?parentHotUpdateCallback(chunkId,?moreModules);
}?;
經(jīng)過(guò)一系列方法調(diào)用然后來(lái)到 hotApplyInternal 方法,這個(gè)方法把更新過(guò)后的模塊 apply 到業(yè)務(wù)中,整個(gè)方法比較長(zhǎng),就不完整貼出來(lái)了。這里拿出核心的部分,
for?(var?id?in?hotUpdate)?{
????if?(Object.prototype.hasOwnProperty.call(hotUpdate,?id))?{
????????var?result;
????????if?(hotUpdate[id])?{
????????????result?=?getAffectedStuff(moduleId);
????????}?else?{
????????????result?=?{
????????????????type:?"disposed",
????????????????moduleId:?id
????????????};
????????}
????????switch?(result.type)?{
????????????case?"self-declined":
????????????case?"declined":
????????????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":
????????????????break;
????????????default:
????????????????throw?new?Error("Unexception?type?"?+?result.type);
????????}
????}
}
把更新過(guò)的模塊進(jìn)行遍歷,找到被該模塊影響到的祖先模塊,返回一個(gè)結(jié)果,如果結(jié)果標(biāo)識(shí)為 unaccepted 就會(huì)被拋出錯(cuò)誤,然后走到 webpack/hot/dev-server.js 里的 catch 進(jìn)行頁(yè)面級(jí)刷新。如果被 accept 的話(huà)就會(huì)執(zhí)行后面的 apply 的邏輯。
function?getAffectedStuff(updateModuleId)?{
??var?outdatedModules?=?[updateModuleId];
??var?outdatedDependencies?=?{};
??var?queue?=?outdatedModules.map(function(id)?{
????return?{
??????chain:?[id],
??????id:?id
????};
??});
??//?1.?遍歷?queue
??while?(queue.length?>?0)?{
????var?queueItem?=?queue.pop();
????var?moduleId?=?queueItem.id;
????var?chain?=?queueItem.chain;
????//?2.?找到改模塊的舊版本
????module?=?installedModules[moduleId];
????//?3.?如果到根模塊了,返回?unaccepted
????if?(module.hot._main)?{
??????return?{
????????type:?"unaccepted",
????????chain:?chain,
????????moduleId:?moduleId
??????};
????}
????//?4.?遍歷父模塊
????for?(var?i?=?0;?i?module.parents.length;?i++)?{
??????var?parentId?=?module.parents[i];
??????var?parent?=?installedModules[parentId];
??????//?5.?如果父模塊處理了模塊變更的話(huà)就跳過(guò),繼續(xù)檢查
??????if?(parent.hot._acceptedDependencies[moduleId])?{
????????continue;
??????}
??????outdatedModules.push(parentId);
??????//?6.?沒(méi)跳過(guò)的話(huà)推入隊(duì)列,繼續(xù)檢查
??????queue.push({
????????chain:?chain.concat([parentId]),
????????id:?parentId
??????});
????}
??}
??//?7.如果所有依賴(lài)路徑都有被?accept?就返回?accepted
??return?{
????type:?"accepted",
????moduleId:?updateModuleId,
????outdatedModules:?outdatedModules,
????outdatedDependencies:?outdatedDependencies
??};
}
module apply
看過(guò) webpack runtime 代碼之后我們知道 runtime 里聲明了 installedModules 這個(gè)變量,里面緩存了所有被 __webpack_require__ 調(diào)用后加載過(guò)的模塊,還有 modules 這個(gè)變量存儲(chǔ)了所有模塊。(如果不了解 webpack runtime 可以先了解 webpack runtime 的執(zhí)行機(jī)制)。如果模塊有被 accept 的話(huà),那么就會(huì)從 installedModules 里刪掉舊的模塊,把模塊從父子依賴(lài)中刪除,然后把 modules 里面的模塊替換成新的模塊。
//?remove?module?from?cache
delete?installedModules[moduleId];
//?insert?new?code
for?(moduleId?in?appliedUpdate)?{
????if?(Object.prototype.hasOwnProperty.call(appliedUpdate,?moduleId))?{
????????modules[moduleId]?=?appliedUpdate[moduleId];
????}
}
這樣僅僅完成了模塊的替換,還沒(méi)有執(zhí)行過(guò)新模塊代碼,也就是沒(méi)被 __webpack_require__ 調(diào)用過(guò)。對(duì)于 ES Module,新模塊代碼的執(zhí)行是在 accept 函數(shù)的 callback 里被 webpack 自動(dòng)插入代碼執(zhí)行的。使用 require() 引入的模塊不會(huì)被自動(dòng)執(zhí)行。
if(module.hot)?{
????module.hot.accept('./App',?function()?{
????????console.log('accepted')
????})
}
會(huì)被 webpack 改造為:
if(true)?{
????module.hot.accept("./src/App.js",?function(__WEBPACK_OUTDATED_DEPENDENCIES__)?{
??????_App__WEBPACK_IMPORTED_MODULE_0__?=?__webpack_require__("./src/App.js");
??????(function()?{
????????console.log('accepted')
??????})(__WEBPACK_OUTDATED_DEPENDENCIES__);
????}.bind(this))
}
所以新模塊的代碼是在 accept 方法回調(diào)執(zhí)行之前被執(zhí)行的。引入了新代碼后就可以執(zhí)行我們的業(yè)務(wù)代碼,這些業(yè)務(wù)代碼一般都和框架相關(guān),框架去處理模塊的熱更新邏輯。比如 react-hot-loader, vue-loader,想要了解更多可以參考 官方文檔。
總結(jié)
最后總結(jié)一下,webpack-dev-server 可以作為命令行工具使用,核心模塊依賴(lài)是 webpack 和 webpack-dev-middleware。webapck-dev-server 負(fù)責(zé)啟動(dòng)一個(gè) express 服務(wù)器監(jiān)聽(tīng)客戶(hù)端請(qǐng)求;實(shí)例化 webpack compiler;啟動(dòng)負(fù)責(zé)推送 webpack 編譯信息的 webscoket 服務(wù)器;負(fù)責(zé)向 bundle.js 注入和服務(wù)端通信用的 webscoket 客戶(hù)端代碼和處理邏輯。webapck-dev-middleware 把 webpack compiler 的 outputFileSystem 改為 in-memory fileSystem;啟動(dòng) webpack watch 編譯;處理瀏覽器發(fā)出的靜態(tài)資源的請(qǐng)求,把 webpack 輸出到內(nèi)存的文件響應(yīng)給瀏覽器。
每次 webpack 編譯完成后向客戶(hù)端廣播 ok 消息,客戶(hù)端收到信息后根據(jù)是否開(kāi)啟 hot 模式使用 liveReload 頁(yè)面級(jí)刷新模式或者 hotReload 模塊熱替換。hotReload 存在失敗的情況,失敗的情況下會(huì)降級(jí)使用頁(yè)面級(jí)刷新。
開(kāi)啟 hot 模式,即啟用 HMR 插件。hot 模式會(huì)向服務(wù)器請(qǐng)求更新過(guò)后的模塊,然后對(duì)模塊的父模塊進(jìn)行回溯,對(duì)依賴(lài)路徑進(jìn)行判斷,如果每條依賴(lài)路徑都配置了模塊更新后所需的業(yè)務(wù)處理回調(diào)函數(shù)則是 accepted 狀態(tài),否則就降級(jí)刷新頁(yè)面。判斷 accepted 狀態(tài)后對(duì)舊的緩存模塊和父子依賴(lài)模塊進(jìn)行替換和刪除,然后執(zhí)行 accept 方法的回調(diào)函數(shù),執(zhí)行新模塊代碼,引入新模塊,執(zhí)行業(yè)務(wù)處理代碼。
為了更加熟悉完整的編譯流程可以初始化一個(gè) webpack-dev-server 項(xiàng)目,使用 vscode 的 debug 功能進(jìn)行斷點(diǎn)調(diào)試的方式去閱讀源碼。
