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

          手寫HRM熱更新核心原理,再也不怕面試官問(wèn)我HRM

          共 19348字,需瀏覽 39分鐘

           ·

          2021-08-22 08:56

          目錄

          • HMR是什么
            • 使用場(chǎng)景
          • 配置使用HMR
            • 配置webpack
            • 解析webpack打包后的文件內(nèi)容
            • 配置HMR
          • HMR原理
          • debug服務(wù)端源碼
            • 服務(wù)端簡(jiǎn)易實(shí)現(xiàn)
            • 服務(wù)端調(diào)試階段
          • debug客戶端源碼
            • 客戶端簡(jiǎn)易實(shí)現(xiàn)
            • 客戶端調(diào)試階段
          • 問(wèn)題
          • 總結(jié)

          HMR是什么

          完整代碼已經(jīng)放到github:https://github.com/Sunny-lucking/webpack-hmr

          歡迎關(guān)注《前端陽(yáng)光》,加入技術(shù)交流群,加入內(nèi)推群

          HMRHot Module Replacement是指當(dāng)你對(duì)代碼修改并保存后,webpack將會(huì)對(duì)代碼進(jìn)行重新打包,并將改動(dòng)的模塊發(fā)送到瀏覽器端,瀏覽器用新的模塊替換掉舊的模塊,去實(shí)現(xiàn)局部更新頁(yè)面而非整體刷新頁(yè)面。

          使用場(chǎng)景

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

          相對(duì)于live reload整體刷新頁(yè)面的方案,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入口文件編寫簡(jiǎn)單邏輯
          var root = document.getElementById('root')
          function render ({
            root.innerHTML = require('./content.js')
          }
          render()
          • 新建依賴文件src/content.js導(dǎo)出字符供index渲染頁(yè)面
          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)目

          • 通過(guò)npm run build打包生成靜態(tài)資源到dist目錄

          接下來(lái)先分析下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插件將入口文件及其依賴通過(guò)script標(biāo)簽引入

          先對(duì)main.js內(nèi)容去掉注釋和無(wú)關(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è)對(duì)象

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

          鍵為入口文件或依賴文件相對(duì)于根目錄的相對(duì)路徑,值則是一個(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è)模塊對(duì)象并將其放入緩存
              var module = installedModules[moduleId] = {
                i: moduleId,
                lfalse// 是否已加載
                exports: {}
              };
              // 執(zhí)行模塊函數(shù)
              modules[moduleId].call(module.exports, modulemodule.exports, __webpack_require__);
              // 將狀態(tài)置為已加載
              module.l = true;
              // 返回模塊對(duì)象
              return module.exports;
            }
            // ...
            // 加載入口文件
            return __webpack_require__(__webpack_require__.s = "./src/index.js");
          })

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

          配置HMR

          接下來(lái)配置并感受一下熱更新帶來(lái)的便捷開發(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í),可以看到頁(yè)面沒有刷新,但是內(nèi)容已經(jīng)被替換了。

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

          HMR原理

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

          • 使用express啟動(dòng)本地服務(wù),當(dāng)瀏覽器訪問(wèn)資源時(shí)對(duì)此做響應(yīng)。
          • 服務(wù)端和客戶端使用websocket實(shí)現(xiàn)長(zhǎng)連接
          • webpack監(jiān)聽源文件的變化,即當(dāng)開發(fā)者保存文件時(shí)觸發(fā)webpack的重新編譯。
            • 每次編譯都會(huì)生成hash值已改動(dòng)模塊的json文件、已改動(dòng)模塊代碼的js文件
            • 編譯完成后通過(guò)socket向客戶端推送當(dāng)前編譯的hash戳
          • 客戶端的websocket監(jiān)聽到有文件改動(dòng)推送過(guò)來(lái)的hash戳,會(huì)和上一次對(duì)比
            • 一致則走緩存
            • 不一致則通過(guò)ajaxjsonp向服務(wù)端獲取最新資源
          • 使用內(nèi)存文件系統(tǒng)去替換有修改的內(nèi)容實(shí)現(xiàn)局部刷新

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

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

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

          完整代碼已經(jīng)放到github:https://github.com/Sunny-lucking/webpack-hmr

          歡迎關(guān)注《前端陽(yáng)光》,加入技術(shù)交流群,加入內(nèi)推群

          啟動(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)

          在后面是通過(guò)express來(lái)當(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ù),以供插件能訪問(wèn)到它的各個(gè)生命周期節(jié)點(diǎn),并對(duì)其打包內(nèi)容做修改。compiler.hooks.done則是插件能修改其內(nèi)容的最后一個(gè)節(jié)點(diǎn)。

          編譯完成通過(guò)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è)讀出來(lái)發(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 長(zhǎng)連接

            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ù)器,然后等待連接來(lái)到,連接到來(lái)之后存進(jìn)sockets池

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

          node dev-server.js

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

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

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

          上面是我通過(guò)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)入頁(yè)面和代碼是否有更新。

          上面的發(fā)布訂閱較為簡(jiǎn)單,且只支持先發(fā)布后訂閱功能。對(duì)于一些較為復(fù)雜的場(chǎng)景可能需要先訂閱后發(fā)布,此時(shí)可以移步@careteen/event-emitter。其實(shí)現(xiàn)原理也挺簡(jiǎ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)
              })
            })
          }

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

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

          然后再通過(guò)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方法通過(guò)JSONP請(qǐng)求獲取到最新的模塊代碼

          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;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">JSONP獲取的代碼可以直接執(zhí)行。

          調(diào)用webpackHotUpdate方法

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

          window.webpackHotUpdate = function (chunkId, moreModules{
            // 循環(huán)新拉來(lái)的模塊
            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)過(guò)上述實(shí)現(xiàn)了一個(gè)基本版的HMR,可更改代碼保存的同時(shí)查看瀏覽器并非整體刷新,而是局部更新代碼進(jìn)而更新視圖。在涉及到大量表單的需求時(shí)大大提高了開發(fā)效率。

          問(wèn)題

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

          webpack主要借助了tapable這個(gè)庫(kù)所提供的一系列同步/異步鉤子函數(shù)貫穿整個(gè)生命周期。基于此我實(shí)現(xiàn)了一版簡(jiǎn)易的webpack,源碼100+行,食用時(shí)伴著注釋很容易消化,感興趣的可前往看個(gè)思路。

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

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

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

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

          總結(jié)

          TODO: add

          • 可能存在的坑
          • 你可能不需要它!
          • 什么場(chǎng)景下才真正的需要它?
          • 上面代碼量較多,主要目的是學(xué)習(xí)實(shí)現(xiàn)思路。細(xì)節(jié)自行debug。

          完整代碼已經(jīng)放到github:https://github.com/Sunny-lucking/webpack-hmr

          歡迎關(guān)注《前端陽(yáng)光》,加入技術(shù)交流群,加入內(nèi)推群

          原文鏈接:https://github.com/careteenL/webpack-hmr

          瀏覽 131
          點(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>
                  国产精品盗摄!偷窥盗摄 | 国产乱伦影音先锋 | 99操逼网站 | 精品无码免费一区二区 | 波动野结衣网站 |