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

          【W(wǎng)ebpack】1047- 輕松理解webpack熱更新原理

          共 16045字,需瀏覽 33分鐘

           ·

          2021-08-15 23:56

          一、前言 - webpack熱更新

          Hot Module Replacement,簡(jiǎn)稱HMR,無(wú)需完全刷新整個(gè)頁(yè)面的同時(shí),更新模塊。HMR的好處,在日常開(kāi)發(fā)工作中體會(huì)頗深:節(jié)省寶貴的開(kāi)發(fā)時(shí)間、提升開(kāi)發(fā)體驗(yàn)

          刷新我們一般分為兩種:

          • 一種是頁(yè)面刷新,不保留頁(yè)面狀態(tài),就是簡(jiǎn)單粗暴,直接window.location.reload()
          • 另一種是基于WDS (Webpack-dev-server)的模塊熱替換,只需要局部刷新頁(yè)面上發(fā)生變化的模塊,同時(shí)可以保留當(dāng)前的頁(yè)面狀態(tài),比如復(fù)選框的選中狀態(tài)、輸入框的輸入等。

          HMR作為一個(gè)Webpack內(nèi)置的功能,可以通過(guò)HotModuleReplacementPlugin--hot開(kāi)啟。那么,HMR到底是怎么實(shí)現(xiàn)熱更新的呢?下面讓我們來(lái)了解一下吧!

          二、webpack的編譯構(gòu)建過(guò)程

          項(xiàng)目啟動(dòng)后,進(jìn)行構(gòu)建打包,控制臺(tái)會(huì)輸出構(gòu)建過(guò)程,我們可以觀察到生成了一個(gè) Hash值a93fd735d02d98633356

          首次構(gòu)建控制臺(tái)輸出日志

          然后,在我們每次修改代碼保存后,控制臺(tái)都會(huì)出現(xiàn) Compiling…字樣,觸發(fā)新的編譯中...可以在控制臺(tái)中觀察到:

          • 新的Hash值a61bdd6e82294ed06fa3
          • 新的json文件a93fd735d02d98633356.hot-update.json
          • 新的js文件index.a93fd735d02d98633356.hot-update.js
          修改代碼的編譯日志

          首先,我們知道Hash值代表每一次編譯的標(biāo)識(shí)。其次,根據(jù)新生成文件名可以發(fā)現(xiàn),上次輸出的Hash值會(huì)作為本次編譯新生成的文件標(biāo)識(shí)。依次類推,本次輸出的Hash值會(huì)被作為下次熱更新的標(biāo)識(shí)。

          然后看一下,新生成的文件是什么?每次修改代碼,緊接著觸發(fā)重新編譯,然后瀏覽器就會(huì)發(fā)出 2 次請(qǐng)求。請(qǐng)求的便是本次新生成的 2 個(gè)文件。如下:

          瀏覽器請(qǐng)求

          首先看json文件,返回的結(jié)果中,h代表本次新生成的Hash值,用于下次文件熱更新請(qǐng)求的前綴。c表示當(dāng)前要熱更新的文件對(duì)應(yīng)的是index模塊。

          再看下生成的js文件,那就是本次修改的代碼,重新編譯打包后的。

          img

          還有一種情況是,如果沒(méi)有任何代碼改動(dòng),直接保存文件,控制臺(tái)也會(huì)輸出編譯打包信息的。

          • 新的Hash值d2e4208eca62aa1c5389
          • 新的json文件a61bdd6e82294ed06fa3.hot-update.json
          img

          但是我們發(fā)現(xiàn),并沒(méi)有生成新的js文件,因?yàn)闆](méi)有改動(dòng)任何代碼,同時(shí)瀏覽器發(fā)出的請(qǐng)求,可以看到c值為空,代表本次沒(méi)有需要更新的代碼。

          img

          小聲說(shuō)下,webapck以前的版本這種情況hash值是不會(huì)變的,后面可能出于什么原因改版了。細(xì)節(jié)不用在意,了解原理才是真諦!!!

          最后思考下??,瀏覽器是如何知道本地代碼重新編譯了,并迅速請(qǐng)求了新生成的文件?是誰(shuí)告知了瀏覽器?瀏覽器獲得這些文件又是如何熱更新成功的?那讓我們帶著疑問(wèn)看下熱更新的過(guò)程,從源碼的角度看原理。

          三、熱更新實(shí)現(xiàn)原理

          相信大家都會(huì)配置webpack-dev-server熱更新,我就不示意例子了。自己網(wǎng)上查下即可。接下來(lái)我們就來(lái)看下webpack-dev-server是如何實(shí)現(xiàn)熱更新的?(源碼都是精簡(jiǎn)過(guò)的,第一行會(huì)注明代碼路徑,看完最好結(jié)合源碼食用一次)。

          1. webpack-dev-server啟動(dòng)本地服務(wù)

          我們根據(jù)webpack-dev-serverpackage.json中的bin命令,可以找到命令的入口文件bin/webpack-dev-server.js

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

          // 生成webpack編譯主引擎 compiler
          let compiler = webpack(config);

          // 啟動(dòng)本地服務(wù)
          let server = new Server(compiler, options, log);
          server.listen(options.port, options.host, (err) => {
              if (err) {throw err};
          });
          復(fù)制代碼

          本地服務(wù)代碼:

          // node_modules/webpack-dev-server/lib/Server.js
          class Server {
              constructor() {
                  this.setupApp();
                  this.createServer();
              }
              
              setupApp() {
                  // 依賴了express
               this.app = new express();
              }
              
              createServer() {
                  this.listeningApp = http.createServer(this.app);
              }
              listen(port, hostname, fn) {
                  return this.listeningApp.listen(port, hostname, (err) => {
                      // 啟動(dòng)express服務(wù)后,啟動(dòng)websocket服務(wù)
                      this.createSocketServer();
                  }
              }                                   
          }
          復(fù)制代碼

          這一小節(jié)代碼主要做了三件事:

          • 啟動(dòng)webpack,生成compiler實(shí)例。compiler上有很多方法,比如可以啟動(dòng) webpack 所有編譯工作,以及監(jiān)聽(tīng)本地文件的變化。
          • 使用express框架啟動(dòng)本地server,讓瀏覽器可以請(qǐng)求本地的靜態(tài)資源
          • 本地server啟動(dòng)之后,再去啟動(dòng)websocket服務(wù),如果不了解websocket,建議簡(jiǎn)單了解一下websocket速成。通過(guò)websocket,可以建立本地服務(wù)和瀏覽器的雙向通信。這樣就可以實(shí)現(xiàn)當(dāng)本地文件發(fā)生變化,立馬告知瀏覽器可以熱更新代碼啦!

          上述代碼主要干了三件事,但是源碼在啟動(dòng)服務(wù)前又做了很多事,接下來(lái)便看看webpack-dev-server/lib/Server.js還做了哪些事?

          2. 修改webpack.config.js的entry配置

          啟動(dòng)本地服務(wù)前,調(diào)用了updateCompiler(this.compiler)方法。這個(gè)方法中有 2 段關(guān)鍵性代碼。一個(gè)是獲取websocket客戶端代碼路徑,另一個(gè)是根據(jù)配置獲取webpack熱更新代碼路徑。

          // 獲取websocket客戶端代碼
          const clientEntry = `${require.resolve(
              '../../client/'
          )}?${domain}${sockHost}${sockPath}${sockPort}`;

          // 根據(jù)配置獲取熱更新代碼
          let hotEntry;
          if (options.hotOnly) {
              hotEntry = require.resolve('webpack/hot/only-dev-server');
          else if (options.hot) {
              hotEntry = require.resolve('webpack/hot/dev-server');
          }
          復(fù)制代碼

          修改后的webpack入口配置如下:

          // 修改后的entry入口
          { entry:
              { index: 
                  [
                      // 上面獲取的clientEntry
                      'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
                      // 上面獲取的hotEntry
                      'xxx/node_modules/webpack/hot/dev-server.js',
                      // 開(kāi)發(fā)配置的入口
                      './src/index.js'
               ],
              },
          }      
          復(fù)制代碼

          為什么要新增了 2 個(gè)文件?在入口默默增加了 2 個(gè)文件,那就意味會(huì)一同打包到bundle文件中去,也就是線上運(yùn)行時(shí)。

          (1)webpack-dev-server/client/index.js

          首先這個(gè)文件用于websocket的,因?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;">websoket是雙向通信,如果不了解websocket,建議簡(jiǎn)單了解一下websocket速成。我們?cè)诘?1 步 webpack-dev-server初始化 的過(guò)程中,啟動(dòng)的是本地服務(wù)端的websocket。那客戶端也就是我們的瀏覽器,瀏覽器還沒(méi)有和服務(wù)端通信的代碼呢?總不能讓開(kāi)發(fā)者去寫吧hhhhhh。因此我們需要把websocket客戶端通信代碼偷偷塞到我們的代碼中。客戶端具體的代碼后面會(huì)在合適的時(shí)機(jī)細(xì)講哦。

          (2)webpack/hot/dev-server.js

          這個(gè)文件主要是用于檢查更新邏輯的,這里大家知道就好,代碼后面會(huì)在合適的時(shí)機(jī)(第5步)細(xì)講。

          3. 監(jiān)聽(tīng)webpack編譯結(jié)束

          修改好入口配置后,又調(diào)用了setupHooks方法。這個(gè)方法是用來(lái)注冊(cè)監(jiān)聽(tīng)事件的,監(jiān)聽(tīng)每次webpack編譯完成。

          // node_modules/webpack-dev-server/lib/Server.js
          // 綁定監(jiān)聽(tīng)事件
          setupHooks() {
              const {done} = compiler.hooks;
              // 監(jiān)聽(tīng)webpack的done鉤子,tapable提供的監(jiān)聽(tīng)方法
              done.tap('webpack-dev-server', (stats) => {
                  this._sendStats(this.sockets, this.getStats(stats));
                  this._stats = stats;
              });
          };
          復(fù)制代碼

          當(dāng)監(jiān)聽(tīng)到一次webpack編譯結(jié)束,就會(huì)調(diào)用_sendStats方法通過(guò)websocket給瀏覽器發(fā)送通知,okhash事件,這樣瀏覽器就可以拿到最新的hash值了,做檢查更新邏輯。

          // 通過(guò)websoket給客戶端發(fā)消息
          _sendStats() {
              this.sockWrite(sockets, 'hash', stats.hash);
              this.sockWrite(sockets, 'ok');
          }
          復(fù)制代碼

          4. webpack監(jiān)聽(tīng)文件變化

          每次修改代碼,就會(huì)觸發(fā)編譯。說(shuō)明我們還需要監(jiān)聽(tīng)本地代碼的變化,主要是通過(guò)setupDevMiddleware方法實(shí)現(xiàn)的。

          這個(gè)方法主要執(zhí)行了webpack-dev-middleware庫(kù)。很多人分不清webpack-dev-middlewarewebpack-dev-server的區(qū)別。其實(shí)就是因?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;">webpack-dev-server只負(fù)責(zé)啟動(dòng)服務(wù)和前置準(zhǔn)備工作,所有文件相關(guān)的操作都抽離到webpack-dev-middleware庫(kù)了,主要是本地文件的編譯輸出以及監(jiān)聽(tīng),無(wú)非就是職責(zé)的劃分更清晰了。

          那我們來(lái)看下webpack-dev-middleware源碼里做了什么事:

          // node_modules/webpack-dev-middleware/index.js
          compiler.watch(options.watchOptions, (err) => {
              if (err) { /*錯(cuò)誤處理*/ }
          });

          // 通過(guò)“memory-fs”庫(kù)將打包后的文件寫入內(nèi)存
          setFs(context, compiler); 
          復(fù)制代碼

          (1)調(diào)用了compiler.watch方法,在第 1 步中也提到過(guò),compiler的強(qiáng)大。這個(gè)方法主要就做了 2 件事:

          • 首先對(duì)本地文件代碼進(jìn)行編譯打包,也就是webpack的一系列編譯流程。
          • 其次編譯結(jié)束后,開(kāi)啟對(duì)本地文件的監(jiān)聽(tīng),當(dāng)文件發(fā)生變化,重新編譯,編譯完成之后繼續(xù)監(jiān)聽(tīng)。

          為什么代碼的改動(dòng)保存會(huì)自動(dòng)編譯,重新打包?這一系列的重新檢測(cè)編譯就歸功于compiler.watch這個(gè)方法了。監(jiān)聽(tīng)本地文件的變化主要是通過(guò)文件的生成時(shí)間是否有變化,這里就不細(xì)講了。

          (2)執(zhí)行setFs方法,這個(gè)方法主要目的就是將編譯后的文件打包到內(nèi)存。這就是為什么在開(kāi)發(fā)的過(guò)程中,你會(huì)發(fā)現(xiàn)dist目錄沒(méi)有打包后的代碼,因?yàn)槎荚趦?nèi)存中。原因就在于訪問(wèn)內(nèi)存中的代碼比訪問(wèn)文件系統(tǒng)中的文件更快,而且也減少了代碼寫入文件的開(kāi)銷,這一切都?xì)w功于memory-fs

          5. 瀏覽器接收到熱更新的通知

          我們已經(jīng)可以監(jiān)聽(tīng)到文件的變化了,當(dāng)文件發(fā)生變化,就觸發(fā)重新編譯。同時(shí)還監(jiān)聽(tīng)了每次編譯結(jié)束的事件。當(dāng)監(jiān)聽(tīng)到一次webpack編譯結(jié)束,_sendStats方法就通過(guò)websoket給瀏覽器發(fā)送通知,檢查下是否需要熱更新。下面重點(diǎn)講的就是_sendStats方法中的okhash事件都做了什么。

          那瀏覽器是如何接收到websocket的消息呢?回憶下第 2 步驟增加的入口文件,也就是websocket客戶端代碼。

          'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080'
          復(fù)制代碼

          這個(gè)文件的代碼會(huì)被打包到bundle.js中,運(yùn)行在瀏覽器中。來(lái)看下這個(gè)文件的核心代碼吧。

          // webpack-dev-server/client/index.js
          var socket = require('./socket');
          var onSocketMessage = {
              hashfunction hash(_hash) {
                  // 更新currentHash值
                  status.currentHash = _hash;
              },
              ok: function ok() {
                  sendMessage('Ok');
                  // 進(jìn)行更新檢查等操作
                  reloadApp(options, status);
              },
          };
          // 連接服務(wù)地址socketUrl,?http://localhost:8080,本地服務(wù)地址
          socket(socketUrl, onSocketMessage);

          function reloadApp() {
           if (hot) {
                  log.info('[WDS] App hot update...');
                  
                  // hotEmitter其實(shí)就是EventEmitter的實(shí)例
                  var hotEmitter = require('webpack/hot/emitter');
                  hotEmitter.emit('webpackHotUpdate', currentHash);
              } 
          }
          復(fù)制代碼

          socket方法建立了websocket和服務(wù)端的連接,并注冊(cè)了 2 個(gè)監(jiān)聽(tīng)事件。

          • hash事件,更新最新一次打包后的hash值。
          • ok事件,進(jìn)行熱更新檢查。

          熱更新檢查事件是調(diào)用reloadApp方法。比較奇怪的是,這個(gè)方法又利用node.jsEventEmitter,發(fā)出webpackHotUpdate消息。這是為什么?為什么不直接進(jìn)行檢查更新呢?

          個(gè)人理解就是為了更好的維護(hù)代碼,以及職責(zé)劃分的更明確。websocket僅僅用于客戶端(瀏覽器)和服務(wù)端進(jìn)行通信。而真正做事情的活還是交回給了webpack

          webpack怎么做的呢?再來(lái)回憶下第 2 步。入口文件還有一個(gè)文件沒(méi)有講到,就是:

          'xxx/node_modules/webpack/hot/dev-server.js'
          復(fù)制代碼

          這個(gè)文件的代碼同樣會(huì)被打包到bundle.js中,運(yùn)行在瀏覽器中。這個(gè)文件做了什么就顯而易見(jiàn)了吧!先瞄一眼代碼:

          // node_modules/webpack/hot/dev-server.js
          var check = function check() {
              module.hot.check(true)
                  .then(function(updatedModules) {
                      // 容錯(cuò),直接刷新頁(yè)面
                      if (!updatedModules) {
                          window.location.reload();
                          return;
                      }
                      
                      // 熱更新結(jié)束,打印信息
                      if (upToDate()) {
                          log("info""[HMR] App is up to date.");
                      }
              })
                  .catch(function(err) {
                      window.location.reload();
                  });
          };

          var hotEmitter = require("./emitter");
          hotEmitter.on("webpackHotUpdate"function(currentHash) {
              lastHash = currentHash;
              check();
          });
          復(fù)制代碼

          這里webpack監(jiān)聽(tīng)到了webpackHotUpdate事件,并獲取最新了最新的hash值,然后終于進(jìn)行檢查更新了。檢查更新呢調(diào)用的是module.hot.check方法。那么問(wèn)題又來(lái)了,module.hot.check又是哪里冒出來(lái)了的!答案是HotModuleReplacementPlugin搞得鬼。這里留個(gè)疑問(wèn),繼續(xù)往下看。

          6. HotModuleReplacementPlugin

          前面好像一直是webpack-dev-server做的事,那HotModuleReplacementPlugin在熱更新過(guò)程中又做了什么偉大的事業(yè)呢?

          首先你可以對(duì)比下,配置熱更新和不配置時(shí)bundle.js的區(qū)別。內(nèi)存中看不到?直接執(zhí)行webpack命令就可以看到生成的bundle.js文件啦。不要用webpack-dev-server啟動(dòng)就好了。

          (1)沒(méi)有配置的。

          img

          (2)配置了HotModuleReplacementPlugin--hot的。

          img

          哦~ 我們發(fā)現(xiàn)moudle新增了一個(gè)屬性為hot,再看hotCreateModule方法。這不就找到module.hot.check是哪里冒出來(lái)的。

          img

          經(jīng)過(guò)對(duì)比打包后的文件,__webpack_require__中的moudle以及代碼行數(shù)的不同。我們都可以發(fā)現(xiàn)HotModuleReplacementPlugin原來(lái)也是默默的塞了很多代碼到bundle.js中呀。這和第 2 步驟很是相似哦!為什么,因?yàn)闄z查更新是在瀏覽器中操作呀。這些代碼必須在運(yùn)行時(shí)的環(huán)境。

          你也可以直接看瀏覽器Sources下的代碼,會(huì)發(fā)現(xiàn)webpackplugin偷偷加的代碼都在哦。在這里調(diào)試也很方便。

          img

          HotModuleReplacementPlugin如何做到的?這里我就不講了,因?yàn)檫@需要你對(duì)tapable以及plugin機(jī)制有一定了解

          7. moudle.hot.check 開(kāi)始熱更新

          通過(guò)第 6 步,我們就可以知道moudle.hot.check方法是如何來(lái)的啦。那都做了什么?之后的源碼都是HotModuleReplacementPlugin塞入到bundle.js中的哦,我就不寫文件路徑了。

          • 利用上一次保存的hash值,調(diào)用hotDownloadManifest發(fā)送xxx/hash.hot-update.jsonajax請(qǐng)求;
          • 請(qǐng)求結(jié)果獲取熱更新模塊,以及下次熱更新的Hash 標(biāo)識(shí),并進(jìn)入熱更新準(zhǔn)備階段。
          hotAvailableFilesMap = update.c; // 需要更新的文件
          hotUpdateNewHash = update.h; // 更新下次熱更新hash
          hotSetStatus("prepare"); // 進(jìn)入熱更新準(zhǔn)備狀態(tài)
          復(fù)制代碼
          • 調(diào)用hotDownloadUpdateChunk發(fā)送xxx/hash.hot-update.js 請(qǐng)求,通過(guò)JSONP方式。
          function hotDownloadUpdateChunk(chunkId) {
              var script = document.createElement("script");
              script.charset = "utf-8";
              script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
              if (null) script.crossOrigin = null;
              document.head.appendChild(script);
           }
          復(fù)制代碼

          這個(gè)函數(shù)體為什么要單獨(dú)拿出來(lái),因?yàn)檫@里要解釋下為什么使用JSONP獲取最新代碼?主要是因?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í)行。為什么要直接執(zhí)行?我們來(lái)回憶下/hash.hot-update.js的代碼格式是怎么樣的。

          img

          可以發(fā)現(xiàn),新編譯后的代碼是在一個(gè)webpackHotUpdate函數(shù)體內(nèi)部的。也就是要立即執(zhí)行webpackHotUpdate這個(gè)方法。

          再看下webpackHotUpdate這個(gè)方法。

          window["webpackHotUpdate"] = function (chunkId, moreModules) {
              hotAddUpdateChunk(chunkId, moreModules);
          } ;
          復(fù)制代碼
          • hotAddUpdateChunk方法會(huì)把更新的模塊moreModules賦值給全局全量hotUpdate
          • hotUpdateDownloaded方法會(huì)調(diào)用hotApply進(jìn)行代碼的替換。
          function hotAddUpdateChunk(chunkId, moreModules) {
              // 更新的模塊moreModules賦值給全局全量hotUpdate
              for (var moduleId in moreModules) {
                  if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
               hotUpdate[moduleId] = moreModules[moduleId];
                  }
              }
              // 調(diào)用hotApply進(jìn)行模塊的替換
              hotUpdateDownloaded();
          }
          復(fù)制代碼

          8. hotApply 熱更新模塊替換

          熱更新的核心邏輯就在hotApply方法了。hotApply代碼有將近 400 行,還是挑重點(diǎn)講了,看哭??

          ①刪除過(guò)期的模塊,就是需要替換的模塊

          通過(guò)hotUpdate可以找到舊模塊

          var queue = outdatedModules.slice();
          while (queue.length > 0) {
              moduleId = queue.pop();
              // 從緩存中刪除過(guò)期的模塊
              module = installedModules[moduleId];
              // 刪除過(guò)期的依賴
              delete outdatedDependencies[moduleId];
              
              // 存儲(chǔ)了被刪掉的模塊id,便于更新代碼
              outdatedSelfAcceptedModules.push({
                  module: moduleId
              });
          }
          復(fù)制代碼

          ②將新的模塊添加到 modules 中

          appliedUpdate[moduleId] = hotUpdate[moduleId];
          for (moduleId in appliedUpdate) {
              if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
                  modules[moduleId] = appliedUpdate[moduleId];
              }
          }
          復(fù)制代碼

          ③通過(guò)__webpack_require__執(zhí)行相關(guān)模塊的代碼

          for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
              var item = outdatedSelfAcceptedModules[i];
              moduleId = item.module;
              try {
                  // 執(zhí)行最新的代碼
                  __webpack_require__(moduleId);
              } catch (err) {
                  // ...容錯(cuò)處理
              }
          }

          復(fù)制代碼

          hotApply的確比較復(fù)雜,知道大概流程就好了,這一小節(jié),要求你對(duì)webpack打包后的文件如何執(zhí)行的有一些了解,大家可以自去看下。

          四、總結(jié)

          還是以閱讀源碼的形式畫的圖,①-④的小標(biāo)記,是文件發(fā)生變化的一個(gè)流程。

          img

          鏈接:https://juejin.cn/post/6844904008432222215

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 120+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

          瀏覽 70
          點(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>
                  国产三级电影网站 | 无码对白 | 国产精品吴梦梦一区二区 | 亚洲AV激情无码专区在线播放 | 中文字幕精品久久久 |