<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          webpack-dev-server 運(yùn)行原理分析

          共 15158字,需瀏覽 31分鐘

           ·

          2021-01-25 06:46

          前言

          現(xiàn)代 web 開發(fā)者們對于 webpack 想必已經(jīng)很熟悉了,webpack-dev-server 幾乎都是標(biāo)配。但是 webpack-dev-server 背后的運(yùn)行原理是怎樣的呢?想了解 how 我們先看看 what。

          webpack 將我們的項(xiàng)目源代碼進(jìn)行編譯打包成可分發(fā)上線的靜態(tài)資源,在開發(fā)階段我們想要預(yù)覽頁面效果的話就需要啟動一個服務(wù)器伺服 webpack 編譯出來的靜態(tài)資源。webpack-dev-server 就是用來啟動 webpack 編譯、伺服這些靜態(tài)資源,

          除此之外,它還默認(rèn)提供了liveReload的功能,就是在一次 webpack 編譯完成后瀏覽器端就能自動刷新頁面讀取最新的編譯后資源。為了提升開發(fā)體驗(yàn)和效率,它還提供了 hot 選項(xiàng)開啟 hotReload,相對于 liveReload, hotReload 不刷新整個頁面,只更新被更改過的模塊。

          alt

          上圖是我對 webpack-dev-server 的一個簡單的整理。具體的實(shí)現(xiàn)原理是怎樣的我們接著往下看。

          版本

          本文基于以下版本進(jìn)行分析:

          入口

          如果作為命令行啟動,webpack-dev-server/bin/webpack-dev-server.js 就是整個命令行的入口。貼出來的代碼進(jìn)行了一些精簡,忽略了一些非核心的分支處理,只關(guān)心 webpack-dev-server 的核心邏輯??梢园凑兆⑨寴?biāo)注的順序簡單閱讀下代碼。

          //?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.?對參數(shù)進(jìn)行處理后啟動
          processOptions(config,?argv,?(config,?options)?=>?{
          ??startDevServer(config,?options);
          });

          webpack-dev-server 作為命令行啟動,首先是調(diào)用了 webpack-cli 模塊下的兩個文件,分別配置了命令行提示選項(xiàng)、和從命令行和配置文件收集了 webpack 的 config,這樣復(fù)用了webpack-cli 的代碼,保持行為一致,上面貼出來的代碼省略了這部分代碼,有興趣的可以自己翻閱源碼。

          之后調(diào)用 processOptions 對收集的參數(shù)進(jìn)行一些默認(rèn)處理后得到需要傳給 webpack 的 config 和需要傳給 wepack-dev-server 的 options。傳入這兩個配置參數(shù)調(diào)用 startDevServer,startDevServer 這個函數(shù)主要是先調(diào)用 webpack 函數(shù)實(shí)例化了 compiler,注意這里沒有給 webpack 函數(shù)傳入回調(diào)函數(shù),根據(jù) webpack 源碼實(shí)現(xiàn),不傳入回調(diào)函數(shù)就不會直接運(yùn)行 webpack 而是返回 webpack compiler 的實(shí)例,供調(diào)用方自行啟動 webpack 運(yùn)行。拿到 webpack compiler 實(shí)例和先前的 webpack-dev-server 的 options 就去實(shí)例化 Server,這個 Server 類就是實(shí)現(xiàn) webpack-dev-server 的核心邏輯。

          最后調(diào)用 Server 類的 listen 方法,就正式開啟監(jiān)聽請求,listen 方法后面會再解析具體邏輯。這就是 webapck-dev-server 大致的啟動過程,后面來看下 Server 類具體做了什么。

          核心框架

          //?webpack-dev-server/lib/Server.js

          class?Server?{
          ??constructor(compiler,?options?=?{},?_log)?{
          ????//?0.?校驗(yàn)參數(shù)是否符合?schema,?不符合會拋出錯誤
          ????validateOptions(schema,?options,?'webpack?Dev?Server');
          ????this.compiler?=?compiler;
          ????this.options?=?options;
          ????//?1.?為一些選項(xiàng)提供默認(rèn)參數(shù)
          ????normalizeOptions(this.compiler,?this.options);
          ????//?2.?對?webpack?compiler?進(jìn)行一些修改??webpack-dev-server/lib/utils/updateCompiler.js
          ????//????-?如果設(shè)置了?hot?選項(xiàng),自動給?webpack?配置?HotModuleReplacementPlugin
          ????//????-?注入一些客戶端代碼:webpack 的 websocket 客戶端依賴 sockJS/websocket + websocket 客戶端業(yè)務(wù)代碼?+ hot 模式下的 webpack/hot/dev-server
          ????updateCompiler(this.compiler,?this.options);
          ????//?3.?添加一些?hooks?插件,這里主要關(guān)注?webpack?compiler?的?done?鉤子,即每次編譯完成后的鉤子?(編譯完成觸發(fā)?_sendStats?方法給客戶端廣播消息?)
          ????this.setupHooks();
          ????//?4.?實(shí)例化?express?服務(wù)器
          ????this.setupApp();
          ????//?5.?設(shè)置?webpack-dev-middleware,用于處理對靜態(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)?=>?{
          ??????//?連接后保存客戶端連接
          ??????this.sockets.push(connection);

          ??????if?(this.hot)?{
          ????????//?hot?選項(xiàng)先廣播一個?hot?類型的消息
          ????????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');
          ????}
          ??}
          }

          這部分代碼稍長,主邏輯都在構(gòu)造函數(shù)里。

          在構(gòu)造函數(shù)中進(jìn)行參數(shù)校驗(yàn),參數(shù)缺省值處理,注入客戶端代碼,綁定 webpack compiler 鉤子,這里主要關(guān)注是 done 鉤子,(在 webpack compiler 實(shí)例每次觸發(fā)編譯完成后就會進(jìn)行 webscoket 廣播 webpack 的編譯信息)。實(shí)例化 express 服務(wù)器,添加 webpack-dev-middleware 中間件用于處理靜態(tài)資源的請求,然后初始化 HTTP 服務(wù)器。

          我們在上面的 webpack-dev-server.js 中調(diào)用的 listen 方法就是開始監(jiān)聽配置的端口,監(jiān)聽回調(diào)里再初始化 websocket 的服務(wù)端。代碼執(zhí)行到這已經(jīng)完成了服務(wù)器端所有的邏輯,但是 webpack 還沒有啟動編譯,用戶打開瀏覽器后請求設(shè)置的IP和端口服務(wù)端又是怎么處理的呢?這部分暫時被我們略過了,這部分就是 webpack-dev-middleware 處理的內(nèi)容了。

          webapck-dev-middleware 初始化

          webapck-dev-middleware 作為一個獨(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.?啟動?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?模式是請求過來一次才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)資源的請求。執(zhí)行過程中顯示初始化了一個 context 對象,默認(rèn)非 lazy 模式,開啟了 webpack 的 watch 模式開始啟動編譯。

          然后將 compiler 的原來基于 fs 模塊的 outputFileSystem 替換成 memory-fs模塊的實(shí)例。memory-fs 是實(shí)現(xiàn)了 node fs api 的基于內(nèi)存的 fileSystem,這意味著 webpack 編譯后的資源不會被輸出到硬盤而是內(nèi)存。最后將真正處理請求的 middleware 返回裝載在 express 上。

          webapck-dev-middleware 處理請求

          當(dāng)用戶在瀏覽器打開配置的IP和端口,如 https://localhost:8080 ,請求就會被 middleware 處理。middleware 使用 memory-fs 從內(nèi)存中讀到請求的資源返回給客戶端。

          //?webpack-dev-middleware/lib/middleware.js
          module.exports?=?function?wrapper(context)?{
          ??return?function?middleware(req,?res,?next)?{
          ????//?1.?根據(jù)請求的?URL?地址,得到絕對路徑的?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.?返回給客戶端
          ????????if?(res.send)?{
          ??????????res.send(content);
          ????????}?else?{
          ??????????res.end(content);
          ????????}
          ????????resolve();
          ??????}
          ????});
          ??};
          };

          webscoket 通信

          當(dāng)我們編輯了源代碼,觸發(fā) webpack 重新編譯,編譯完成后執(zhí)行 done 鉤子上的回調(diào)。具體可參考上面 Server.js 中 setupHooks 方法。_sendStats 方法會先廣播一個類型為 hash 的消息,然后再根據(jù)編譯信息廣播 warnings/errors/ok 消息。這里我們只關(guān)注正常流程 ok 消息。

          我們已經(jīng)很熟悉客戶端接收到更新后都會對應(yīng)用進(jìn)行 Reload 來獲取更好的開發(fā)體驗(yàn)。具體是 liveReload(刷新整個頁面)還是 hotReload(更新改動過的模塊)就取決于我們傳入的 hot 選項(xiàng)。

          以下代碼就是我們在上面就講到的在 webpack 編譯的時候注入到 bundle.js 進(jìn)去的。當(dāng)用戶打開頁面預(yù)覽時,這些代碼就會自動執(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 客戶端,然后為不同的消息類型設(shè)置了相應(yīng)的回調(diào)函數(shù)。

          在前面 Server.js 中我們看到如果 hot 選項(xiàng)為 true 時,當(dāng) websocket 客戶端連接到服務(wù)端,服務(wù)端會先廣播一個 hot 類型的消息,客戶端接收到后會把 options 對象的 hot 設(shè)置為 true。

          服務(wù)端在每次編譯后都會廣播 hash 消息,客戶端接收到后就會將這個webpack 編譯產(chǎn)生的 hash 值暫存起來。編譯成功如果沒有 warning 也沒有 error 就會廣播 ok 消息,客戶端接收到 ok 消息就會執(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 客戶端就會引入 webpack/hot/emitter,觸發(fā)一個 webpackHotUpdate 事件,將 hash 值傳遞過去。這個 webpack/hot/emitter 我們查閱 webpack 源碼看到其實(shí)就是 node 的 events 模塊。我們暫時不關(guān)注這個事件會觸發(fā)什么回調(diào)后面再具體再看。如果沒有設(shè)置 hot: true。那么就是使用 liveReload 模式,liveReload 就比較無腦,直接刷新整個頁面。

          再回到上一個問題,到底是在哪里接收 webpackHotUpdate 事件并處理的呢?就是 webpack/hot/dev-server.js 中處理的。在這里會去檢查是否可以更新,如果更新失敗就會刷新整個頁面來降級實(shí)現(xiàn)代碼更新的功能。其實(shí)我們回過頭來看看這樣降級也是必須的,如果更新失敗,源碼更新了,而客戶端的代碼卻沒更新,這樣顯然是不合理的。


          ?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?更新失敗,降級為重新刷新整個應(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.?注冊事件回調(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();
          ??}
          ?});

          模塊更新依賴判斷

          module.hot.check 方法位于 webpack/lib/HotModuleReplacement.runtime.js 中,是 webpack 內(nèi)置的 HotModuleReplacementPlugin 注入在 webpack bootstrap runtime 中的。

          所以 check 方法主要做了什么呢,這里提前總結(jié)一下。在 webpack 使用了 HotModuleReplacementPlugin 編譯時,每次增量編譯就會多產(chǎn)出兩個文件,形如c390bbe0037a0dd079a6.hot-update.json,main.c390bbe0037a0dd079a6.hot-update.js,分別是描述 chunk 更新的 manifest文件和更新過后的 chunk 文件。那么瀏覽器端調(diào)用 hotDownloadManifest 方法去下載模塊更新的 manifest.json 文件,然后調(diào)用 hotDownloadUpdateChunk 方法使用 jsonp 的方式下載需要更新的 chunk。

          hotDownloadUpdateChunk 下載完成后調(diào)用 webpackHotUpdate 回調(diào)。回調(diào)內(nèi)拿到更新的模塊,然后從模塊自身開始進(jìn)行冒泡,如果發(fā)現(xiàn)只要有一條祖先路徑?jīng)]有 accept 這次改動就直接刷新頁面實(shí)行降級強(qiáng)制更新, 如果有被 accept, 就會替換掉原來 webpack runtime 里 module 里舊的模塊,然后再執(zhí)行 accept 的 callback 進(jìn)行更新。為什么要執(zhí)行這樣的判斷呢?

          假設(shè)給定這樣的依賴路徑:

          componentA.js?->?componentB.js?->?app.js?->?index.js
          componentA.js?->?componentC.js?->?app.js?->?index.js

          參考以下的代碼示例,accept 指該 module 的祖先模塊調(diào)用了 module.hot.accept, 處理了該 module 更新過后的業(yè)務(wù)邏輯,一般都是 rerender。

          //?index.js
          if(module.hot)?{
          ????module.hot.accept('./app',?function()?{
          ????????rerender()
          ????})
          }

          如果我們對 componentA.js 進(jìn)行了更改,但是如果僅僅 componentB accept 了更改,componentC 卻沒 accept,那么這樣是沒有到達(dá)更新的目的的。所以在祖先路徑回溯的時候,要保證每一條路徑都被 accept。

          function?hotCheck(apply)?{
          ??//?1.?拿這次編譯后的?hash?請求服務(wù)器,拿到結(jié)構(gòu)為?{c:?{main:?true}?h:?"ac69ee760bb48d5db5f5"}?的數(shù)據(jù)
          ??return?hotDownloadManifest(hotRequestTimeout).then(function(update)?{
          ????hotAvailableFilesMap?=?update.c;
          ????hotUpdateNewHash?=?update.h;

          ????//?2.?生成一個?defered?promise,供上面提到的?promise?鏈消費(fèi)
          ????var?promise?=?new?Promise(function(resolve,?reject)?{
          ??????hotDeferred?=?{
          ????????resolve:?resolve,
          ????????reject:?reject
          ??????};
          ????});

          ????hotUpdate?=?{};
          ????//?3.?這個方法里面調(diào)用的就是?hotDownloadUpdateChunk,就是發(fā)起一個?jsonp?請求更新過后的?chunk,jsonp的回調(diào)是?HMR?runtime?里的?webpackHotUpdate
          ????{
          ??????hotEnsureUpdateChunk(chunkId);
          ????}

          ????return?promise;
          ??});
          }

          hotCheck 方法就是和服務(wù)器進(jìn)行通信拿到更新過后的 chunk,下載好 chunk 后就開始執(zhí)行 HMR runtime 里的 webpackHotUpdate 回調(diào)。

          window["webpackHotUpdate"]?=?function?webpackHotUpdateCallback(chunkId,?moreModules)?{
          ?hotAddUpdateChunk(chunkId,?moreModules);
          ?if?(parentHotUpdateCallback)?parentHotUpdateCallback(chunkId,?moreModules);
          }?;

          經(jīng)過一系列方法調(diào)用然后來到 hotApplyInternal 方法,這個方法把更新過后的模塊 apply 到業(yè)務(wù)中,整個方法比較長,就不完整貼出來了。這里拿出核心的部分,

          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);
          ????????}
          ????}
          }

          把更新過的模塊進(jìn)行遍歷,找到被該模塊影響到的祖先模塊,返回一個結(jié)果,如果結(jié)果標(biāo)識為 unaccepted 就會被拋出錯誤,然后走到 webpack/hot/dev-server.js 里的 catch 進(jìn)行頁面級刷新。如果被 accept 的話就會執(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.?如果父模塊處理了模塊變更的話就跳過,繼續(xù)檢查
          ??????if?(parent.hot._acceptedDependencies[moduleId])?{
          ????????continue;
          ??????}
          ??????outdatedModules.push(parentId);
          ??????//?6.?沒跳過的話推入隊(duì)列,繼續(xù)檢查
          ??????queue.push({
          ????????chain:?chain.concat([parentId]),
          ????????id:?parentId
          ??????});
          ????}
          ??}

          ??//?7.如果所有依賴路徑都有被?accept?就返回?accepted
          ??return?{
          ????type:?"accepted",
          ????moduleId:?updateModuleId,
          ????outdatedModules:?outdatedModules,
          ????outdatedDependencies:?outdatedDependencies
          ??};
          }

          module apply

          看過 webpack runtime 代碼之后我們知道 runtime 里聲明了 installedModules 這個變量,里面緩存了所有被 __webpack_require__ 調(diào)用后加載過的模塊,還有 modules 這個變量存儲了所有模塊。(如果不了解 webpack runtime 可以先了解 webpack runtime 的執(zhí)行機(jī)制)。如果模塊有被 accept 的話,那么就會從 installedModules 里刪掉舊的模塊,把模塊從父子依賴中刪除,然后把 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];
          ????}
          }

          這樣僅僅完成了模塊的替換,還沒有執(zhí)行過新模塊代碼,也就是沒被 __webpack_require__ 調(diào)用過。對于 ES Module,新模塊代碼的執(zhí)行是在 accept 函數(shù)的 callback 里被 webpack 自動插入代碼執(zhí)行的。使用 require() 引入的模塊不會被自動執(zhí)行。

          if(module.hot)?{
          ????module.hot.accept('./App',?function()?{
          ????????console.log('accepted')
          ????})
          }

          會被 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 可以作為命令行工具使用,核心模塊依賴是 webpack 和 webpack-dev-middleware。webapck-dev-server 負(fù)責(zé)啟動一個 express 服務(wù)器監(jiān)聽客戶端請求;實(shí)例化 webpack compiler;啟動負(fù)責(zé)推送 webpack 編譯信息的 webscoket 服務(wù)器;負(fù)責(zé)向 bundle.js 注入和服務(wù)端通信用的 webscoket 客戶端代碼和處理邏輯。webapck-dev-middleware 把 webpack compiler 的 outputFileSystem 改為 in-memory fileSystem;啟動 webpack watch 編譯;處理瀏覽器發(fā)出的靜態(tài)資源的請求,把 webpack 輸出到內(nèi)存的文件響應(yīng)給瀏覽器。

          每次 webpack 編譯完成后向客戶端廣播 ok 消息,客戶端收到信息后根據(jù)是否開啟 hot 模式使用 liveReload 頁面級刷新模式或者 hotReload 模塊熱替換。hotReload 存在失敗的情況,失敗的情況下會降級使用頁面級刷新。

          開啟 hot 模式,即啟用 HMR 插件。hot 模式會向服務(wù)器請求更新過后的模塊,然后對模塊的父模塊進(jìn)行回溯,對依賴路徑進(jìn)行判斷,如果每條依賴路徑都配置了模塊更新后所需的業(yè)務(wù)處理回調(diào)函數(shù)則是 accepted 狀態(tài),否則就降級刷新頁面。判斷 accepted 狀態(tài)后對舊的緩存模塊和父子依賴模塊進(jìn)行替換和刪除,然后執(zhí)行 accept 方法的回調(diào)函數(shù),執(zhí)行新模塊代碼,引入新模塊,執(zhí)行業(yè)務(wù)處理代碼。

          為了更加熟悉完整的編譯流程可以初始化一個 webpack-dev-server 項(xiàng)目,使用 vscode 的 debug 功能進(jìn)行斷點(diǎn)調(diào)試的方式去閱讀源碼。


          ??愛心三連擊

          1.看到這里了就點(diǎn)個在看支持下吧,你的點(diǎn)贊,在看是我創(chuàng)作的動力。

          2.關(guān)注公眾號程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端?開發(fā)者,會討論?前端 Node 知識,互相學(xué)習(xí)」!

          3.也可添加微信【ikoala520】,一起成長。

          “在看轉(zhuǎn)發(fā)”是最大的支持

          瀏覽 55
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产婬乱片A片AAA毛片下载 | 欧美性生交大片免费看A片免费 | 大鷄巴嫲嫲亂伦 | 性感美女操逼视频 | 一级性爱视频中文字幕 |