<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模塊熱更新原理

          共 9127字,需瀏覽 19分鐘

           ·

          2022-03-03 17:16

          什么是模塊熱更新?

          模塊熱替換(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允許在運(yùn)行時(shí)更新所有類型的模塊,而無(wú)需完全刷新。

          下面我們運(yùn)行一個(gè)例子來(lái)更直觀的感受什么是模塊熱更新。

          視頻中,我修改了字體顏色,頁(yè)面會(huì)立即更新,但輸入框中的內(nèi)容依然保留著。HMR就是幫助我們實(shí)現(xiàn)了這樣一個(gè)效果,不然我們?cè)诿看涡薷拇a時(shí),還?需要手動(dòng)刷新頁(yè)面,且頁(yè)面的內(nèi)容不會(huì)保留。模塊熱更新的好處顯而易見(jiàn),它可以幫助我們節(jié)省開(kāi)發(fā)時(shí)間,提升開(kāi)發(fā)體驗(yàn)。

          細(xì)心的同學(xué)可能會(huì)發(fā)現(xiàn),webpack自動(dòng)進(jìn)行重新編譯同時(shí)又多生成了兩個(gè)文件。

          • HMR 是怎樣實(shí)現(xiàn)自動(dòng)編譯的?
          • 模塊內(nèi)容的變更瀏覽器又是如何感知的?
          • 以及新產(chǎn)生的兩個(gè)文件又是干嘛的?
          • 局部更新又是如何做到的?

          下面讓我們帶著這些疑問(wèn),一起來(lái)探索模塊熱更新的原理。

          模塊熱更新的配置

          在學(xué)習(xí)原理前,我們需要對(duì)模塊熱更新的配置有一個(gè)清晰的認(rèn)識(shí)。因?yàn)槠綍r(shí)的工作中很少需要我們自己手動(dòng)去配置,所以會(huì)導(dǎo)致我們忽略一些細(xì)節(jié)的問(wèn)題。現(xiàn)在我們來(lái)回顧一下配置流程,這樣更有助于對(duì)源碼的理解。

          第一步:安裝webpack-dev-server

          npm?install?--save-dev.?webpack-dev-server

          第二步:在父模塊中注冊(cè)module.hot.accept事件

          //src/index.js



          let?div?=?document.createElement('div');

          document.body.appendChild(div);



          let?input?=?document.createElement('input');

          document.body.appendChild(input);



          let?render?=?()?=>?{

          ????let?title?=?require('./title.js')

          ????div.innerHTML?=?title;

          }



          render()



          //添加如下內(nèi)容

          +?if?(module.hot)?{

          +?????module.hot.accept(['./title.js'],?render)

          +?}
          //?子模塊?src/title.js

          module.exports?=?'Hello?webpack'

          第三步:在webpack.config.js中配置hot:true

          const?path?=?require('path');

          const?HtmlWebpackPlugin?=?require('html-webpack-plugin')

          module.exports?=?{

          ????mode:?'development',

          ????devtool:?'source-map',

          ????entry:?'./src/index.js',

          ????output:?{

          ????????filename:?'main.js',

          ????????path:?path.resolve(__dirname,?'dist')

          ????},

          ?+???devServer:?{

          ?+???????hot:?true

          ?+???},

          ????plugins:?[

          ????????new?HtmlWebpackPlugin(),

          ????],

          }

          現(xiàn)在你可能會(huì)有一些疑問(wèn),為什么平時(shí)修改代碼的時(shí)候不用監(jiān)聽(tīng)module.hot.accept也能實(shí)現(xiàn)熱更新?那是因?yàn)槲覀兪褂玫?loader 已經(jīng)在幕后幫我們實(shí)現(xiàn)了。

          webpack-dev-server 提供了實(shí)時(shí)重加載的功能,但是不能局部刷新。必須配合后兩步的配置才能實(shí)現(xiàn)局部刷新,這兩步的背后其實(shí)是借助了HotModuleReplacementPlugin

          可以說(shuō)HMR是webpack-dev-serverHotModuleReplacementPlugin 共同的功勞。

          熱更新原理

          下面就正式進(jìn)入我們今天的主題。先來(lái)介紹第一位主角:webpack-dev-server。

          Webpack-dev-server

          通過(guò)node_modules/webpack-dev-server下的package.json文件,根據(jù) bin 的值可以找到命令實(shí)際運(yùn)行的文件。./node_modules/webpack-dev-server/bin/webpack-dev-server.js

          下面我們就順著入口文件,來(lái)看一看webpack-dev-server都做了哪些事。為了減少篇幅,提高閱讀質(zhì)量,以下示例均為簡(jiǎn)易版的實(shí)現(xiàn),感興趣的可以參照源碼一起來(lái)看。

          1、開(kāi)啟本地服務(wù)

          首先通過(guò)webpack創(chuàng)建了一個(gè)compiler實(shí)例,然后通過(guò)創(chuàng)建自定義server實(shí)例,開(kāi)啟了一個(gè)本地服務(wù)。

          //?node_modules/webpack-dev-server/bin/webpack-dev-server.js

          const?webpack?=?require('webpack');

          const?config?=?require('../../webpack.config');

          const?Server?=?require('../lib/Server')



          const?compiler?=?webpack(config);

          const?server?=?new?Server(compiler);

          server.listen(8080,?'localhost',?()?=>?{})

          這個(gè)自定義Server 不僅是創(chuàng)建了一個(gè)http服務(wù),它還基于http服務(wù)創(chuàng)建了一個(gè)websocket服務(wù),同時(shí)監(jiān)聽(tīng)瀏覽器的接入,當(dāng)瀏覽器成功接入時(shí)向它發(fā)送hash值,從而實(shí)現(xiàn)服務(wù)端和瀏覽器間的雙向通信。

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

          class?Server?{

          ????constructor()?{

          ????????this.setupApp();

          ????????this.createServer();

          ????}

          ????//創(chuàng)建http應(yīng)用

          ????setupApp()?{

          ????????this.app?=?express();

          ????}

          ????//創(chuàng)建http服務(wù)

          ????createServer()?{

          ????????this.server?=?http.createServer(this.app);

          ????}

          ????//監(jiān)聽(tīng)端口號(hào)?

          ????listen(port,?host,?callback)?{

          ????????this.server.listen(port,?host,?callback)

          ????????this.createSocketServer();

          ????}

          ????//基于http服務(wù)創(chuàng)建websocket服務(wù),并注冊(cè)監(jiān)聽(tīng)事件connection

          ????createSocketServer()?{

          ????????const?io?=?socketIO(this.server);

          ????????io.on('connection',?(socket)?=>?{

          ????????????this.clientSocketList.push(socket);

          ????????????socket.emit('hash',?this.currentHash);

          ????????????socket.emit('ok');

          ????????????socket.on('disconnect',?()?=>?{

          ????????????????let?index?=?this.clientSocketList.indexOf(socket);

          ????????????????this.clientSocketList.splice(index,?1)

          ????????????})

          ????????})

          ????}

          }



          module.exports?=?Server;

          2、監(jiān)聽(tīng)編譯完成

          僅僅在建立websocket連接時(shí),服務(wù)端向?yàn)g覽器發(fā)送hash和拉取代碼的通知還不夠,我們還希望當(dāng)代碼改變時(shí),瀏覽器也可以接到這樣的通知。于是,在開(kāi)啟服務(wù)前,還需要對(duì)編譯完成事件進(jìn)行監(jiān)聽(tīng)。

          //監(jiān)聽(tīng)編譯完成,當(dāng)編譯完成后通過(guò)websocket向?yàn)g覽器發(fā)送廣播

          setupHooks()?{

          ????let?{?compiler?}?=?this;

          ????compiler.hooks.done.tap('webpack-dev-server',?(stats)?=>?{

          ????????this.currentHash?=?stats.hash;

          ????????this.clientSocketList.forEach((socket)?=>?{

          ????????????socket.emit('hash',?this.currentHash);

          ????????????socket.emit('ok');

          ????????})

          ????})

          }

          3、監(jiān)聽(tīng)文件修改

          要想在代碼修改的時(shí)候,觸發(fā)重新編譯,那么就需要對(duì)代碼的變動(dòng)進(jìn)行監(jiān)聽(tīng)。這一步,源碼是通過(guò)webpackDevMiddleware庫(kù)實(shí)現(xiàn)的。庫(kù)中使用了compiler.watch對(duì)文件的修改進(jìn)行了監(jiān)聽(tīng),并且通過(guò)memory-fs實(shí)現(xiàn)了將編譯的產(chǎn)物存放到內(nèi)存中,這也是為什么我們?cè)赿ist目錄下看不到變化的內(nèi)容,放到內(nèi)存的好處就是為了更快的讀寫(xiě)從而提高開(kāi)發(fā)效率。

          //?node_modules/webpack-dev-middleware/index.js

          const?MemoryFs?=?require('memory-fs')

          compiler.watch({},?()?=>?{})

          let?fs?=?new?MemoryFs();

          this.fs?=?compiler.outputFileSystem?=?fs;

          4、向?yàn)g覽器中插入客戶端代碼

          前面提到要想實(shí)現(xiàn)瀏覽器和本地服務(wù)的通信,那么就需要瀏覽器接入到本地開(kāi)啟的websocket服務(wù),然而瀏覽器本身并不具備這樣的能力,這就需要我們自己提供這樣的客戶端代碼將它運(yùn)行在瀏覽器。因此自定Server在開(kāi)啟http服務(wù)之前,就調(diào)用了updateCompiler()方法,它修改了webpack配置中的entry,使得插入的兩個(gè)文件的代碼可以一同被打包到 main.js 中,運(yùn)行在瀏覽器。

          //node_modules/webpack-dev-server/lib/utils/updateCompiler.js

          const?path?=?require('path');

          function?updateCompiler(compiler)?{

          ????compiler.options.entry?=?{

          ????????main:?[

          ????????????path.resolve(__dirname,?'../../client/index.js'),

          ????????????path.resolve(__dirname,?'../../../webpack/hot/dev-server.js'),

          ????????????config.entry,

          ????????]

          ????}

          }

          module.exports?=?updateCompiler

          node_modules /webpack-dev-server/client/index.js

          這段代碼會(huì)放在瀏覽器作為客戶端代碼,它用來(lái)建立 websocket 連接,當(dāng)服務(wù)端發(fā)送hash廣播時(shí)就保存hash,當(dāng)服務(wù)端發(fā)送ok廣播時(shí)就調(diào)用reloadApp()。

          let?currentHash;

          let?hotEmitter?=?new?EventEmitter();

          const?socket?=?window.io('/');



          socket.on('hash',?(hash)?=>?{

          ????currentHash?=?hash;

          })

          socket.on('ok',?()?=>?{

          ????reloadApp();

          })



          function?reloadApp()?{

          ????hotEmitter.emit('webpackHotUpdate',?currentHash)

          }

          webpack/hot/dev-server.js

          reloadApp()繼續(xù)調(diào)用module.hot.check(),當(dāng)然第一次加載頁(yè)面時(shí)是不會(huì)被調(diào)用的。至于這里為啥會(huì)分成兩個(gè)文件,個(gè)人理解是為了解藕,每個(gè)模塊負(fù)責(zé)不同的分工。

          let?lastHash;

          hotEmitter.on('webpackHotUpdate',?(currentHash)?=>?{

          ????if?(!lastHash)?{

          ????????lastHash?=?currentHash;

          ????????return;

          ????}

          ????module.hot.check();

          })

          module.hot.check()是哪來(lái)的?答案是HotModuleReplacementPlugin。我們可以在瀏覽器的sources下看到,main.js被插入很多代碼,這些代碼就是被HotModuleReplacementPlugin 插入進(jìn)來(lái)的。

          它不僅在main.js中插入了代碼,前面提到過(guò)的編譯后生成的兩個(gè)補(bǔ)丁包也是它生成的 。

          HotModuleReplacementPlugin

          現(xiàn)在,我們來(lái)看一下今天的第二位主角HotModuleReplacementPlugin 在main.js都悄悄插了哪些代碼,從而實(shí)現(xiàn)的熱更新。

          1、為模塊添加hot屬性

          前面提到過(guò),當(dāng)代碼發(fā)生改動(dòng)時(shí),服務(wù)端會(huì)向?yàn)g覽器發(fā)送ok消息,瀏覽器會(huì)執(zhí)行module.hot.check進(jìn)行模塊熱檢查。check方法就是來(lái)源于這里了。

          function?hotCreateModule()?{

          ????let?hot?=?{

          ????????_acceptedDependencies:?{},

          ????????accept(deps,?callback)?{

          ????????????deps.forEach(dep?=>?hot._acceptedDependencies[dep]?=?callback);

          ????????},

          ????????check:?hotCheck

          ????}

          ????return?hot

          }

          2、請(qǐng)求補(bǔ)丁文件

          module.hot.check()就是調(diào)用hotCheck,此時(shí)瀏覽器會(huì)向服務(wù)端獲取兩個(gè)補(bǔ)丁文件。

          function?hotCheck()?{

          ????hotDownloadManifest().then(update?=>?{

          ????????//{"h":"eb861ba9f6408c42f1fd","c":{"main":true}}

          ????????let?chunkIds?=?Object.keys(update.c)?//['main']

          ????????chunkIds.forEach(chunkId?=>?{

          ????????????hotDownloadUpdateChunk(chunkId)

          ????????})

          ????????lastHash?=?currentHash;

          ????}).catch(()?=>?{

          ????????window.location.reload();

          ????})

          }

          先看一眼這兩個(gè)文件長(zhǎng)什么樣

          • d04feccfa446b174bc10.hot-update.json

          告知瀏覽器新的hash值,并且是哪個(gè)chunk發(fā)生了改變

          • main.d04feccfa446b174bc10.hot-update.js

          告知瀏覽器,main 代碼塊中的/src/title.js模塊變更的內(nèi)容

          首先是通過(guò)XMLHttpRequest的方式,利用上一次保存的hash值請(qǐng)求hot-update.json文件。這個(gè)描述文件的作用就是提供了修改的文件所在的chunkId。

          ????function?hotDownloadManifest()?{

          ????????return?new?Promise(function?(resolve,?reject)?{

          ????????????let?xhr?=?new?XMLHttpRequest();

          ????????????let?url?=?`${lastHash}.hot-update.json`

          ????????????xhr.open('get',?url);

          ????????????xhr.responseType?=?'json'

          ????????????xhr.onload?=?function?()?{

          ????????????????resolve(xhr.response)

          ????????????}

          ????????????xhr.send()

          ????????})

          ????}

          然后通過(guò)JSONP的方式,利用hot-update.json返回的chunkId 及 上一次保存的hash 拼接文件名進(jìn)而獲取文件內(nèi)容。

          function?hotDownloadUpdateChunk(chunkId)?{

          ????let?script?=?document.createElement('script');

          ????script.src?=?`${chunkId}.${lastHash}.hot-update.js`;

          ????document.head.appendChild(script);

          }

          window.webpackHotUpdate?=?function?(chunkId,?moreModules)?{

          ????hotAddUpdateChunk(chunkId,?moreModules);

          }

          3、模塊內(nèi)容替換

          當(dāng)hot-update.js文件加載好后,就會(huì)執(zhí)行window.webpackHotUpdate,進(jìn)而調(diào)用了hotApply。hotApply根據(jù)模塊ID找到舊模塊然后將它刪除,然后執(zhí)行父模塊中注冊(cè)的accept回調(diào),從而實(shí)現(xiàn)模塊內(nèi)容的局部更新。

          ????window.webpackHotUpdate?=?function?(chunkId,?moreModules)?{

          ????????hotAddUpdateChunk(chunkId,?moreModules);

          ????}

          ????let?hotUpdate?=?{}

          ????function?hotAddUpdateChunk(chunkId,?moreModules)?{

          ????????for?(let?moduleId?in?moreModules)?{

          ????????????modules[moduleId]?=?hotUpdate[moduleId]?=?moreModules[moduleId];

          ????????}

          ????????hotApply();

          ????}

          ????function?hotApply()?{

          ????????for?(let?moduleId?in?hotUpdate)?{

          ????????????let?oldModule?=?installedModules[moduleId]

          ????????????delete?installedModules[moduleId]

          ????????????oldModule.parents.forEach((parentModule)?=>?{

          ????????????????let?cb?=?parentModule.hot._acceptedDependencies[moduleId]

          ????????????????cb?&&?cb()

          ????????????})

          ????????}

          ????}

          總結(jié)

          模塊熱更新原理總結(jié):

          在執(zhí)行npm run dev 后,首先會(huì)通過(guò)updateCompiler方法去修改compiler的entry,將兩個(gè)文件的代碼一起打包到main.js,這兩個(gè)文件一個(gè)是用來(lái)與服務(wù)端進(jìn)行通信的,一個(gè)是用來(lái)調(diào)用module.hot.check的。接著通過(guò)compiler.hooks.done.tap來(lái)監(jiān)聽(tīng)編譯完成,通過(guò)compiler.watch 監(jiān)聽(tīng)代碼的改動(dòng),通過(guò)createSocketServer()開(kāi)啟http服務(wù)和websocekt服務(wù)。

          當(dāng)用戶訪問(wèn)http://localhost:8080時(shí),瀏覽器會(huì)與服務(wù)端建立websocket連接。隨后服務(wù)端向?yàn)g覽器發(fā)送hash 和 ok ,用來(lái)通知瀏覽器當(dāng)前最新編譯版本的hash值和告訴瀏覽器拉取代碼。同時(shí)服務(wù)端,會(huì)根據(jù)路由,將內(nèi)存中的文件返回,此時(shí)瀏覽器保存hash,頁(yè)面內(nèi)容出現(xiàn)。

          當(dāng)修改本地代碼時(shí),會(huì)觸發(fā)重新編譯,此時(shí)webpackDevMiddleWare會(huì)將編譯的產(chǎn)物保存到內(nèi)存中,這得益于內(nèi)置模塊memory-fs的功勞。同時(shí)HotModuleReplacementPlugin 會(huì)生成兩個(gè)補(bǔ)丁包,這兩個(gè)補(bǔ)丁包一個(gè)是用來(lái)告訴瀏覽器哪個(gè)chunk變更了,一個(gè)是用來(lái)告訴瀏覽器變更模塊及內(nèi)容。當(dāng)重新編譯完成,瀏覽器會(huì)保存當(dāng)前hash,然后通上一次的hash 值拼接出要請(qǐng)求的描述文件路徑,再根據(jù)描述文件返回的內(nèi)容,拼接出要另一個(gè)要請(qǐng)求的補(bǔ)丁包文件。請(qǐng)求成功就開(kāi)始執(zhí)行webpckHotUdate了,會(huì)繼續(xù)調(diào)用 hotApply,實(shí)質(zhì)就是執(zhí)行了我們當(dāng)初在配置模塊熱更新第二步中的回調(diào)事件,從而實(shí)現(xiàn)了頁(yè)面內(nèi)容的局部刷新。

          參考文檔:

          模塊熱替換 | webpack 中文文檔[1]

          輕松理解webpack熱更新原理 - 掘金[2]

          參考資料


          [1] 模塊熱替換 | webpack 中文文檔: https://webpack.docschina.org/guides/hot-module-replacement/

          [2] 輕松理解webpack熱更新原理 - 掘金:https://juejin.cn/post/6844904008432222215

          ???

          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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级片网站 | 精品毛片一区二区免费看 |