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

          徹底搞懂并實(shí)現(xiàn) webpack 熱更新原理

          共 24056字,需瀏覽 49分鐘

           ·

          2021-10-28 18:09

          點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

          回復(fù)算法,加入前端編程面試算法每日一題群

          目錄

          • HMR是什么

            • 使用場景
          • 配置使用HMR

            • 配置webpack
            • 解析webpack打包后的文件內(nèi)容
            • 配置HMR
          • HMR原理

          • debug服務(wù)端源碼

            • 服務(wù)端簡易實(shí)現(xiàn)
            • 服務(wù)端調(diào)試階段
          • debug客戶端源碼

            • 客戶端簡易實(shí)現(xiàn)
            • 客戶端調(diào)試階段
          • 問題

          • 總結(jié)

          • 招聘

          HMR是什么

          HMRHot Module Replacement是指當(dāng)你對代碼修改并保存后,webpack將會(huì)對代碼進(jìn)行重新打包,并將改動(dòng)的模塊發(fā)送到瀏覽器端,瀏覽器用新的模塊替換掉舊的模塊,去實(shí)現(xiàn)局部更新頁面而非整體刷新頁面。接下來將從使用到實(shí)現(xiàn)一版簡易功能帶領(lǐng)大家深入淺出HMR

          文章首發(fā)于@careteen/webpack-hmr,轉(zhuǎn)載請注明來源即可。

          使用場景

          scenario

          如上圖所示,一個(gè)注冊頁面包含用戶名密碼郵箱三個(gè)必填輸入框,以及一個(gè)提交按鈕,當(dāng)你在調(diào)試郵箱模塊改動(dòng)了代碼時(shí),沒做任何處理情況下是會(huì)刷新整個(gè)頁面,頻繁的改動(dòng)代碼會(huì)浪費(fèi)你大量時(shí)間去重新填寫內(nèi)容。預(yù)期是保留用戶名密碼的輸入內(nèi)容,而只替換郵箱這一模塊。這一訴求就需要借助webpack-dev-server的熱模塊更新功能。

          相對于live reload整體刷新頁面的方案,HMR的優(yōu)點(diǎn)在于可以保存應(yīng)用的狀態(tài),提高開發(fā)效率。

          配置使用HMR

          配置webpack

          首先借助webpack搭建項(xiàng)目

          • 初識(shí)化項(xiàng)目并導(dǎo)入依賴
          mkdir webpack-hmr && cd webpack-hmr
          npm i -y
          npm i -S webpack webpack-cli webpack-dev-server html-webpack-plugin
          • 配置文件webpack.config.js
          const path = require('path')
          const webpack = require('webpack')
          const htmlWebpackPlugin = require('html-webpack-plugin')

          module.exports = {
            mode'development'// 開發(fā)模式不壓縮代碼,方便調(diào)試
            entry'./src/index.js'// 入口文件
            output: {
              path: path.join(__dirname, 'dist'),
              filename'main.js'
            },
            devServer: {
              contentBase: path.join(__dirname, 'dist')
            },
            plugins: [
              new htmlWebpackPlugin({
                template'./src/index.html',
                filename'index.html'
              })
            ]
          }
          • 新建src/index.html模板文件
          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>Webpack Hot Module Replacement</title>
          </head>
          <body>
            <div id="root"></div>
          </body>
          </html>
          • 新建src/index.js入口文件編寫簡單邏輯
          var root = document.getElementById('root')
          function render () {
            root.innerHTML = require('./content.js')
          }
          render()
          • 新建依賴文件src/content.js導(dǎo)出字符供index渲染頁面
          var ret = 'Hello Webpack Hot Module Replacement'
          module.exports = ret
          // export default ret
          • 配置package.json
           "scripts": {
              "dev""webpack-dev-server",
              "build""webpack"
            }
          • 然后npm run dev即可啟動(dòng)項(xiàng)目
          • 通過npm run build打包生成靜態(tài)資源到dist目錄

          接下來先分析下dist目錄中的文件

          解析webpack打包后的文件內(nèi)容

          • webpack自己實(shí)現(xiàn)的一套commonjs規(guī)范講解
          • 區(qū)分commonjs和esmodule

          dist目錄結(jié)構(gòu)

          .
          ├── index.html
          └── main.js

          其中index.html內(nèi)容如下

          <!-- ... -->
          <div id="root"></div>
          <script type="text/javascript" src="main.js"></script></body>
          <!-- ... -->

          使用html-webpack-plugin插件將入口文件及其依賴通過script標(biāo)簽引入

          先對main.js內(nèi)容去掉注釋和無關(guān)內(nèi)容進(jìn)行分析

          (function (modules// webpackBootstrap
            // ...
          })
          ({
            "./src/content.js":
              (function (module, exports{
                eval("var ret = 'Hello Webpack Hot Module Replacement'\n\nmodule.exports = ret\n// export default ret\n\n");
              }),
            "./src/index.js": (function (module, exports, __webpack_require__{
              eval("var root = document.getElementById('root')\nfunction render () {\n  root.innerHTML = __webpack_require__(/*! ./content.js */ \"./src/content.js\")\n}\nrender()\n\n\n");
            })
          });

          可見webpack打包后會(huì)產(chǎn)出一個(gè)自執(zhí)行函數(shù),其參數(shù)為一個(gè)對象

          "./src/content.js": (function (module, exports{
            eval("...")
          }

          鍵為入口文件或依賴文件相對于根目錄的相對路徑,值則是一個(gè)函數(shù),其中使用eval執(zhí)行文件的內(nèi)容字符。

          • 再進(jìn)入自執(zhí)行函數(shù)體內(nèi),可見webpack自己實(shí)現(xiàn)了一套commonjs規(guī)范
          (function (modules{
            // 模塊緩存
            var installedModules = {};
            function __webpack_require__(moduleId{
              // 判斷是否有緩存
              if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
              }
              // 沒有緩存則創(chuàng)建一個(gè)模塊對象并將其放入緩存
              var module = installedModules[moduleId] = {
                i: moduleId,
                lfalse// 是否已加載
                exports: {}
              };
              // 執(zhí)行模塊函數(shù)
              modules[moduleId].call(module.exports, modulemodule.exports, __webpack_require__);
              // 將狀態(tài)置為已加載
              module.l = true;
              // 返回模塊對象
              return module.exports;
            }
            // ...
            // 加載入口文件
            return __webpack_require__(__webpack_require__.s = "./src/index.js");
          })

          如果對上面commonjs規(guī)范感興趣可以前往我的另一篇文章手摸手帶你實(shí)現(xiàn)commonjs規(guī)范

          給出上面代碼主要是先對webpack的產(chǎn)出文件混個(gè)眼熟,不要懼怕。其實(shí)任何一個(gè)不管多復(fù)雜的事物都是由更小更簡單的東西組成,剖開它認(rèn)識(shí)它愛上它。

          配置HMR

          接下來配置并感受一下熱更新帶來的便捷開發(fā)

          webpack.config.js配置

           // ...
            devServer: {
              hottrue
            }
            // ...

          ./src/index.js配置

          // ...
          if (module.hot) {
            module.hot.accept(['./content.js'], () => {
              render()
            })
          }

          當(dāng)更改./content.js的內(nèi)容并保存時(shí),可以看到頁面沒有刷新,但是內(nèi)容已經(jīng)被替換了。

          這對提高開發(fā)效率意義重大。接下來將一層層剖開它,認(rèn)識(shí)它的實(shí)現(xiàn)原理。

          HMR原理

          core

          如上圖所示,右側(cè)Server端使用webpack-dev-server去啟動(dòng)本地服務(wù),內(nèi)部實(shí)現(xiàn)主要使用了webpackexpresswebsocket

          • 使用express啟動(dòng)本地服務(wù),當(dāng)瀏覽器訪問資源時(shí)對此做響應(yīng)。

          • 服務(wù)端和客戶端使用websocket實(shí)現(xiàn)長連接

          • webpack監(jiān)聽源文件的變化,即當(dāng)開發(fā)者保存文件時(shí)觸發(fā)webpack的重新編譯。

            • 每次編譯都會(huì)生成hash值已改動(dòng)模塊的json文件已改動(dòng)模塊代碼的js文件
            • 編譯完成后通過socket向客戶端推送當(dāng)前編譯的hash戳
          • 客戶端的websocket監(jiān)聽到有文件改動(dòng)推送過來的hash戳,會(huì)和上一次對比

            • 一致則走緩存
            • 不一致則通過ajaxjsonp向服務(wù)端獲取最新資源
          • 使用內(nèi)存文件系統(tǒng)去替換有修改的內(nèi)容實(shí)現(xiàn)局部刷新

          上圖先只看個(gè)大概,下面將從服務(wù)端和客戶端兩個(gè)方面進(jìn)行詳細(xì)分析

          debug服務(wù)端源碼

          core

          現(xiàn)在也只需要關(guān)注上圖的右側(cè)服務(wù)端部分,左側(cè)可以暫時(shí)忽略。下面步驟主要是debug服務(wù)端源碼分析其詳細(xì)思路,也給出了代碼所處的具體位置,感興趣的可以先行定位到下面的代碼處設(shè)置斷點(diǎn),然后觀察數(shù)據(jù)的變化情況。也可以先跳過閱讀此步驟。

          1. 啟動(dòng)webpack-dev-server服務(wù)器,源代碼地址@webpack-dev-server/webpack-dev-server.js#L173

          2. 創(chuàng)建webpack實(shí)例,源代碼地址@webpack-dev-server/webpack-dev-server.js#L89

          3. 創(chuàng)建Server服務(wù)器,源代碼地址@webpack-dev-server/webpack-dev-server.js#L107

          4. 添加webpack的done事件回調(diào),源代碼地址@webpack-dev-server/Server.js#L122

            1. 編譯完成向客戶端發(fā)送消息,源代碼地址@webpack-dev-server/Server.js#L184
          5. 創(chuàng)建express應(yīng)用app,源代碼地址@webpack-dev-server/Server.js#L123

          6. 設(shè)置文件系統(tǒng)為內(nèi)存文件系統(tǒng),源代碼地址@webpack-dev-middleware/fs.js#L115

          7. 添加webpack-dev-middleware中間件,源代碼地址@webpack-dev-server/Server.js#L125

            1. 中間件負(fù)責(zé)返回生成的文件,源代碼地址@webpack-dev-middleware/middleware.js#L20
          8. 啟動(dòng)webpack編譯,源代碼地址@webpack-dev-middleware/index.js#L51

          9. 創(chuàng)建http服務(wù)器并啟動(dòng)服務(wù),源代碼地址@webpack-dev-server/Server.js#L135

          10. 使用sockjs在瀏覽器端和服務(wù)端之間建立一個(gè) websocket 長連接,源代碼地址@webpack-dev-server/Server.js#L745

          11. 創(chuàng)建socket服務(wù)器,源代碼地址@webpack-dev-server/SockJSServer.js#L34

          服務(wù)端簡易實(shí)現(xiàn)

          上面是我通過debug得出dev-server運(yùn)行流程比較核心的幾個(gè)點(diǎn),下面將其抽象整合到一個(gè)文件中。

          啟動(dòng)webpack-dev-server服務(wù)器

          先導(dǎo)入所有依賴

          const path = require('path'// 解析文件路徑
          const express = require('express'// 啟動(dòng)本地服務(wù)
          const mime = require('mime'// 獲取文件類型 實(shí)現(xiàn)一個(gè)靜態(tài)服務(wù)器
          const webpack = require('webpack'// 讀取配置文件進(jìn)行打包
          const MemoryFileSystem = require('memory-fs'// 使用內(nèi)存文件系統(tǒng)更快,文件生成在內(nèi)存中而非真實(shí)文件
          const config = require('./webpack.config'// 獲取webpack配置文件

          創(chuàng)建webpack實(shí)例

          const compiler = webpack(config)

          compiler代表整個(gè)webpack編譯任務(wù),全局只有一個(gè)

          創(chuàng)建Server服務(wù)器

          class Server {
            constructor(compiler) {
              this.compiler = compiler
            }
            listen(port) {
              this.server.listen(port, () => {
                console.log(`服務(wù)器已經(jīng)在${port}端口上啟動(dòng)了`)
              })
            }
          }
          let server = new Server(compiler)
          server.listen(8000)

          在后面是通過express來當(dāng)啟動(dòng)服務(wù)的

          添加webpack的done事件回調(diào)

           constructor(compiler) {
              let sockets = []
              let lasthash
              compiler.hooks.done.tap('webpack-dev-server', (stats) => {
                lasthash = stats.hash
                // 每當(dāng)新一個(gè)編譯完成后都會(huì)向客戶端發(fā)送消息
                sockets.forEach(socket => {
                  socket.emit('hash', stats.hash) // 先向客戶端發(fā)送最新的hash值
                  socket.emit('ok'// 再向客戶端發(fā)送一個(gè)ok
                })
              })
            }

          webpack編譯后提供提供了一系列鉤子函數(shù),以供插件能訪問到它的各個(gè)生命周期節(jié)點(diǎn),并對其打包內(nèi)容做修改。compiler.hooks.done則是插件能修改其內(nèi)容的最后一個(gè)節(jié)點(diǎn)。

          編譯完成通過socket向客戶端發(fā)送消息,推送每次編譯產(chǎn)生的hash。另外如果是熱更新的話,還會(huì)產(chǎn)出二個(gè)補(bǔ)丁文件,里面描述了從上一次結(jié)果到這一次結(jié)果都有哪些chunk和模塊發(fā)生了變化。

          使用let sockets = []數(shù)組去存放當(dāng)打開了多個(gè)Tab時(shí)每個(gè)Tab的socket實(shí)例

          創(chuàng)建express應(yīng)用app

          let app = new express()

          設(shè)置文件系統(tǒng)為內(nèi)存文件系統(tǒng)

          let fs = new MemoryFileSystem()

          使用MemoryFileSystemcompiler的產(chǎn)出文件打包到內(nèi)存中。

          添加webpack-dev-middleware中間件

           function middleware(req, res, next{
              if (req.url === '/favicon.ico') {
                return res.sendStatus(404)
              }
              // /index.html   dist/index.html
              let filename = path.join(config.output.path, req.url.slice(1))
              let stat = fs.statSync(filename)
              if (stat.isFile()) { // 判斷是否存在這個(gè)文件,如果在的話直接把這個(gè)讀出來發(fā)給瀏覽器
                let content = fs.readFileSync(filename)
                let contentType = mime.getType(filename)
                res.setHeader('Content-Type', contentType)
                res.statusCode = res.statusCode || 200
                res.send(content)
              } else {
                return res.sendStatus(404)
              }
            }
            app.use(middleware)

          使用expres啟動(dòng)了本地開發(fā)服務(wù)后,使用中間件去為其構(gòu)造一個(gè)靜態(tài)服務(wù)器,并使用了內(nèi)存文件系統(tǒng),使讀取文件后存放到內(nèi)存中,提高讀寫效率,最終返回生成的文件。

          啟動(dòng)webpack編譯

           compiler.watch({}, err => {
              console.log('又一次編譯任務(wù)成功完成了')
            })

          以監(jiān)控的模式啟動(dòng)一次webpack編譯,當(dāng)編譯成功之后執(zhí)行回調(diào)

          創(chuàng)建http服務(wù)器并啟動(dòng)服務(wù)

           constructor(compiler) {
              // ...
              this.server = require('http').createServer(app)
              // ...
            }
            listen(port) {
              this.server.listen(port, () => {
                console.log(`服務(wù)器已經(jīng)在${port}端口上啟動(dòng)了`)
              })
            }

          使用sockjs在瀏覽器端和服務(wù)端之間建立一個(gè) websocket 長連接

           constructor(compiler) {
              // ...
              this.server = require('http').createServer(app)
              let io = require('socket.io')(this.server)
              io.on('connection', (socket) => {
                sockets.push(socket)
                socket.emit('hash', lastHash)
                socket.emit('ok')
              })
            }

          啟動(dòng)一個(gè) websocket服務(wù)器,然后等待連接來到,連接到來之后存進(jìn)sockets池

          當(dāng)有文件改動(dòng),webpack重新編譯時(shí),向客戶端推送hashok兩個(gè)事件

          服務(wù)端調(diào)試階段

          感興趣的可以根據(jù)上面debug服務(wù)端源碼所帶的源碼位置,并在瀏覽器的調(diào)試模式下設(shè)置斷點(diǎn)查看每個(gè)階段的值。

          node dev-server.js

          使用我們自己編譯的dev-server.js啟動(dòng)服務(wù),可看到頁面可以正常展示,但還沒有實(shí)現(xiàn)熱更新。

          下面將調(diào)式客戶端的源代碼分析其實(shí)現(xiàn)流程。

          debug客戶端源碼

          core

          現(xiàn)在也只需要關(guān)注上圖的左側(cè)客戶端部分,右側(cè)可以暫時(shí)忽略。下面步驟主要是debug客戶端源碼分析其詳細(xì)思路,也給出了代碼所處的具體位置,感興趣的可以先行定位到下面的代碼處設(shè)置斷點(diǎn),然后觀察數(shù)據(jù)的變化情況。也可以先跳過閱讀此步驟。

          debug客戶端源碼分析其詳細(xì)思路

          1. webpack-dev-server/client端會(huì)監(jiān)聽到此hash消息,源代碼地址@webpack-dev-server/index.js#L54
          2. 客戶端收到ok的消息后會(huì)執(zhí)行reloadApp方法進(jìn)行更新,源代碼地址index.js#L101
          3. 在reloadApp中會(huì)進(jìn)行判斷,是否支持熱更新,如果支持的話發(fā)射webpackHotUpdate事件,如果不支持則直接刷新瀏覽器,源代碼地址reloadApp.js#L7
          4. 在webpack/hot/dev-server.js會(huì)監(jiān)聽webpackHotUpdate事件,源代碼地址dev-server.js#L55
          5. 在check方法里會(huì)調(diào)用module.hot.check方法,源代碼地址dev-server.js#L13
          6. HotModuleReplacement.runtime請求Manifest,源代碼地址HotModuleReplacement.runtime.js#L180
          7. 它通過調(diào)用 JsonpMainTemplate.runtime的hotDownloadManifest方法,源代碼地址JsonpMainTemplate.runtime.js#L23
          8. 調(diào)用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通過JSONP請求獲取到最新的模塊代碼,源代碼地址JsonpMainTemplate.runtime.js#L14
          9. 補(bǔ)丁JS取回來后會(huì)調(diào)用JsonpMainTemplate.runtime.js的webpackHotUpdate方法,源代碼地址JsonpMainTemplate.runtime.js#L8
          10. 然后會(huì)調(diào)用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法動(dòng)態(tài)更新模塊代碼,源代碼地址HotModuleReplacement.runtime.js#L222
          11. 然后調(diào)用hotApply方法進(jìn)行熱更新,源代碼地址HotModuleReplacement.runtime.js#L257、HotModuleReplacement.runtime.js#L278

          客戶端簡易實(shí)現(xiàn)

          上面是我通過debug得出dev-server運(yùn)行流程比較核心的幾個(gè)點(diǎn),下面將其抽象整合成一個(gè)文件。

          webpack-dev-server/client端會(huì)監(jiān)聽到此hash消息

          在開發(fā)客戶端功能之前,需要在src/index.html中引入socket.io

          <script src="/socket.io/socket.io.js"></script>

          下面連接socket并接受消息

          let socket = io('/')
          socket.on('connect', onConnected)
          const onConnected = () => {
            console.log('客戶端連接成功')
          }
          let hotCurrentHash // lastHash 上一次 hash值 
          let currentHash // 這一次的hash值
          socket.on('hash', (hash) => {
            currentHash = hash
          })

          將服務(wù)端webpack每次編譯所產(chǎn)生hash進(jìn)行緩存

          客戶端收到ok的消息后會(huì)執(zhí)行reloadApp方法進(jìn)行更新

          socket.on('ok', () => {
            reloadApp(true)
          })

          reloadApp中判斷是否支持熱更新

          // 當(dāng)收到ok事件后,會(huì)重新刷新app
          function reloadApp(hot{
            if (hot) { // 如果hot為true 走熱更新的邏輯
              hotEmitter.emit('webpackHotUpdate')
            } else { // 如果不支持熱更新,則直接重新加載
              window.location.reload()
            }
          }

          在reloadApp中會(huì)進(jìn)行判斷,是否支持熱更新,如果支持的話發(fā)射webpackHotUpdate事件,如果不支持則直接刷新瀏覽器。

          在webpack/hot/dev-server.js會(huì)監(jiān)聽webpackHotUpdate事件

          首先需要一個(gè)發(fā)布訂閱去綁定事件并在合適的時(shí)機(jī)觸發(fā)。

          class Emitter {
            constructor() {
              this.listeners = {}
            }
            on(type, listener) {
              this.listeners[type] = listener
            }
            emit(type) {
              this.listeners[type] && this.listeners[type]()
            }
          }
          let hotEmitter = new Emitter()
          hotEmitter.on('webpackHotUpdate', () => {
            if (!hotCurrentHash || hotCurrentHash == currentHash) {
              return hotCurrentHash = currentHash
            }
            hotCheck()
          })

          會(huì)判斷是否為第一次進(jìn)入頁面和代碼是否有更新。

          上面的發(fā)布訂閱較為簡單,且只支持先發(fā)布后訂閱功能。對于一些較為復(fù)雜的場景可能需要先訂閱后發(fā)布,此時(shí)可以移步@careteen/event-emitter。其實(shí)現(xiàn)原理也挺簡單,需要維護(hù)一個(gè)離線事件棧存放還沒發(fā)布就訂閱的事件,等到訂閱時(shí)可以取出所有事件執(zhí)行。

          在check方法里會(huì)調(diào)用module.hot.check方法

          function hotCheck() {
            hotDownloadManifest().then(update => {
              let chunkIds = Object.keys(update.c)
              chunkIds.forEach(chunkId => {
                hotDownloadUpdateChunk(chunkId)
              })
            })
          }

          上面也提到過webpack每次編譯都會(huì)產(chǎn)生hash值已改動(dòng)模塊的json文件已改動(dòng)模塊代碼的js文件

          此時(shí)先使用ajax請求Manifest即服務(wù)器這一次編譯相對于上一次編譯改變了哪些module和chunk。

          然后再通過jsonp獲取這些已改動(dòng)的module和chunk的代碼。

          調(diào)用hotDownloadManifest方法

          function hotDownloadManifest() {
            return new Promise(function (resolve{
              let request = new XMLHttpRequest()
              //hot-update.json文件里存放著從上一次編譯到這一次編譯 取到差異
              let requestPath = '/' + hotCurrentHash + ".hot-update.json"
              request.open('GET', requestPath, true)
              request.onreadystatechange = function () {
                if (request.readyState === 4) {
                  let update = JSON.parse(request.responseText)
                  resolve(update)
                }
              }
              request.send()
            })
          }

          調(diào)用hotDownloadUpdateChunk方法通過JSONP請求獲取到最新的模塊代碼

          function hotDownloadUpdateChunk(chunkId{
            let script = document.createElement('script')
            script.charset = 'utf-8'
            // /main.xxxx.hot-update.js
            script.src = '/' + chunkId + "." + hotCurrentHash + ".hot-update.js"
            document.head.appendChild(script)
          }

          這里解釋下為什么使用JSONP獲取而不直接利用socket獲取最新代碼?主要是因?yàn)?code style="font-size: 14px;border-radius: 4px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(155, 110, 35);background-color: rgb(255, 245, 227);padding: 3px;margin: 3px;">JSONP獲取的代碼可以直接執(zhí)行。

          調(diào)用webpackHotUpdate方法

          當(dāng)客戶端把最新的代碼拉到瀏覽之后

          window.webpackHotUpdate = function (chunkId, moreModules{
            // 循環(huán)新拉來的模塊
            for (let moduleId in moreModules) {
              // 從模塊緩存中取到老的模塊定義
              let oldModule = __webpack_require__.c[moduleId]
              // parents哪些模塊引用這個(gè)模塊 children這個(gè)模塊引用了哪些模塊
              // parents=['./src/index.js']
              let {
                parents,
                children
              } = oldModule
              // 更新緩存為最新代碼 緩存進(jìn)行更新
              let module = __webpack_require__.c[moduleId] = {
                i: moduleId,
                lfalse,
                exports: {},
                parents,
                children,
                hotwindow.hotCreateModule(moduleId)
              }
              moreModules[moduleId].call(module.exports, modulemodule.exports, __webpack_require__)
              module.l = true // 狀態(tài)變?yōu)榧虞d就是給module.exports 賦值了
              parents.forEach(parent => {
                // parents=['./src/index.js']
                let parentModule = __webpack_require__.c[parent]
                // _acceptedDependencies={'./src/title.js',render}
                parentModule && parentModule.hot && parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]()
              })
              hotCurrentHash = currentHash
            }
          }

          hotCreateModule的實(shí)現(xiàn)

          實(shí)現(xiàn)我們可以在業(yè)務(wù)代碼中定義需要熱更新的模塊以及回調(diào)函數(shù),將其存放在hot._acceptedDependencies中。

          window.hotCreateModule = function () {
            let hot = {
              _acceptedDependencies: {},
              dispose() {
                // 銷毀老的元素
              },
              acceptfunction (deps, callback{
                for (let i = 0; i < deps.length; i++) {
                  // hot._acceptedDependencies={'./title': render}
                  hot._acceptedDependencies[deps[i]] = callback
                }
              }
            }
            return hot
          }

          然后在webpackHotUpdate中進(jìn)行調(diào)用

           parents.forEach(parent => {
                // parents=['./src/index.js']
                let parentModule = __webpack_require__.c[parent]
                // _acceptedDependencies={'./src/title.js',render}
                parentModule && parentModule.hot && parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]()
              })

          最后調(diào)用hotApply方法進(jìn)行熱更新

          客戶端調(diào)試階段

          經(jīng)過上述實(shí)現(xiàn)了一個(gè)基本版的HMR,可更改代碼保存的同時(shí)查看瀏覽器并非整體刷新,而是局部更新代碼進(jìn)而更新視圖。在涉及到大量表單的需求時(shí)大大提高了開發(fā)效率。

          問題

          • 如何實(shí)現(xiàn)commonjs規(guī)范?

          感興趣的可前往debug CommonJs規(guī)范了解其實(shí)現(xiàn)原理。

          • webpack實(shí)現(xiàn)流程以及各個(gè)生命周期的作用是什么?

          webpack主要借助了tapable這個(gè)庫所提供的一系列同步/異步鉤子函數(shù)貫穿整個(gè)生命周期。

          基于此我實(shí)現(xiàn)了一版簡易的webpack,源碼100+行,食用時(shí)伴著注釋很容易消化,感興趣的可前往看個(gè)思路。


          • 發(fā)布訂閱的使用和實(shí)現(xiàn),并且如何實(shí)現(xiàn)一個(gè)可先訂閱后發(fā)布的機(jī)制?

          上面也提到需要使用到發(fā)布訂閱模式,且只支持先發(fā)布后訂閱功能。對于一些較為復(fù)雜的場景可能需要先訂閱后發(fā)布,此時(shí)可以移步@careteen/event-emitter。其實(shí)現(xiàn)原理也挺簡單,需要維護(hù)一個(gè)離線事件棧存放還沒發(fā)布就訂閱的事件,等到訂閱時(shí)可以取出所有事件執(zhí)行。

          • 為什么使用JSONP而不用socke通信獲取更新過的代碼?

          因?yàn)橥ㄟ^socket通信獲取的是一串字符串需要再做處理。而通過JSONP獲取的代碼可以直接執(zhí)行。

          引用

          • 模塊熱替換 - webpack官網(wǎng)

          招聘

          急缺前端,對搜狐焦點(diǎn)感興趣的直接簡歷發(fā)我郵件[email protected]或加我vx: Careteen

          關(guān)于本文

          來源:careteenL

          https://segmentfault.com/a/1190000020310371


          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


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

          手機(jī)掃一掃分享

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

          手機(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>
                  狼友视频首页入口 | 久久熟妇 | 日批视频网站 | 九九免费观看视频 | 久久三级片成年人 |