1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        webpack模塊熱更新原理

        共 9127字,需瀏覽 19分鐘

         ·

        2022-03-03 17:16

        什么是模塊熱更新?

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

        下面我們運行一個例子來更直觀的感受什么是模塊熱更新。

        視頻中,我修改了字體顏色,頁面會立即更新,但輸入框中的內(nèi)容依然保留著。HMR就是幫助我們實現(xiàn)了這樣一個效果,不然我們在每次修改代碼時,還?需要手動刷新頁面,且頁面的內(nèi)容不會保留。模塊熱更新的好處顯而易見,它可以幫助我們節(jié)省開發(fā)時間,提升開發(fā)體驗。

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

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

        下面讓我們帶著這些疑問,一起來探索模塊熱更新的原理。

        模塊熱更新的配置

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

        第一步:安裝webpack-dev-server

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

        第二步:在父模塊中注冊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)在你可能會有一些疑問,為什么平時修改代碼的時候不用監(jiān)聽module.hot.accept也能實現(xiàn)熱更新?那是因為我們使用的 loader 已經(jīng)在幕后幫我們實現(xiàn)了。

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

        可以說HMR是webpack-dev-serverHotModuleReplacementPlugin 共同的功勞。

        熱更新原理

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

        Webpack-dev-server

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

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

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

        首先通過webpack創(chuàng)建了一個compiler實例,然后通過創(chuàng)建自定義server實例,開啟了一個本地服務(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',?()?=>?{})

        這個自定義Server 不僅是創(chuàng)建了一個http服務(wù),它還基于http服務(wù)創(chuàng)建了一個websocket服務(wù),同時監(jiān)聽瀏覽器的接入,當(dāng)瀏覽器成功接入時向它發(fā)送hash值,從而實現(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)聽端口號?

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

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

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

        ????}

        ????//基于http服務(wù)創(chuàng)建websocket服務(wù),并注冊監(jiān)聽事件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)聽編譯完成

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

        //監(jiān)聽編譯完成,當(dāng)編譯完成后通過websocket向瀏覽器發(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)聽文件修改

        要想在代碼修改的時候,觸發(fā)重新編譯,那么就需要對代碼的變動進(jìn)行監(jiān)聽。這一步,源碼是通過webpackDevMiddleware庫實現(xiàn)的。庫中使用了compiler.watch對文件的修改進(jìn)行了監(jiān)聽,并且通過memory-fs實現(xiàn)了將編譯的產(chǎn)物存放到內(nèi)存中,這也是為什么我們在dist目錄下看不到變化的內(nèi)容,放到內(nè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、向瀏覽器中插入客戶端代碼

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

        //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

        這段代碼會放在瀏覽器作為客戶端代碼,它用來建立 websocket 連接,當(dāng)服務(wù)端發(fā)送hash廣播時就保存hash,當(dāng)服務(wù)端發(fā)送ok廣播時就調(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)然第一次加載頁面時是不會被調(diào)用的。至于這里為啥會分成兩個文件,個人理解是為了解藕,每個模塊負(fù)責(zé)不同的分工。

        let?lastHash;

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

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

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

        ????????return;

        ????}

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

        })

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

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

        HotModuleReplacementPlugin

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

        1、為模塊添加hot屬性

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

        function?hotCreateModule()?{

        ????let?hot?=?{

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

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

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

        ????????},

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

        ????}

        ????return?hot

        }

        2、請求補丁文件

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

        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();

        ????})

        }

        先看一眼這兩個文件長什么樣

        • d04feccfa446b174bc10.hot-update.json

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

        • main.d04feccfa446b174bc10.hot-update.js

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

        首先是通過XMLHttpRequest的方式,利用上一次保存的hash值請求hot-update.json文件。這個描述文件的作用就是提供了修改的文件所在的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()

        ????????})

        ????}

        然后通過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文件加載好后,就會執(zhí)行window.webpackHotUpdate,進(jìn)而調(diào)用了hotApply。hotApply根據(jù)模塊ID找到舊模塊然后將它刪除,然后執(zhí)行父模塊中注冊的accept回調(diào),從而實現(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 后,首先會通過updateCompiler方法去修改compiler的entry,將兩個文件的代碼一起打包到main.js,這兩個文件一個是用來與服務(wù)端進(jìn)行通信的,一個是用來調(diào)用module.hot.check的。接著通過compiler.hooks.done.tap來監(jiān)聽編譯完成,通過compiler.watch 監(jiān)聽代碼的改動,通過createSocketServer()開啟http服務(wù)和websocekt服務(wù)。

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

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

        參考文檔:

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

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

        參考資料


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

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

        ???

        瀏覽 53
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日本免费在线黄色视频 | 成人无码做爰www免费 | 丁香五月激情综合 | 一级A片AAAAAAAXXXX | 日韩在线播放视频 | 午夜天堂精品久久久久91 | 精品黑人XXXXXX在线观看 | 自拍毛片| 激情久久一区 | 女人被强行糟蹋过程h电影 |