<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 進階】Webpack 打包后的代碼是怎樣的?

          共 21838字,需瀏覽 44分鐘

           ·

          2021-03-10 15:42

          webpack 是我們現(xiàn)階段要掌握的重要的打包工具之一,我們知道 webpack 會遞歸的構建依賴關系圖,其中包含應用程序的每個模塊,然后將這些模塊打包成一個或者多個 bundle。

          那么webpack 打包后的代碼是怎樣的呢?是怎么將各個 bundle連接在一起的?模塊與模塊之間的關系是怎么處理的?動態(tài) import() 的時候又是怎樣的呢?

          本文讓我們一步步來揭開 webpack 打包后代碼的神秘面紗

          準備工作

          創(chuàng)建一個文件,并初始化

          mkdir learn-webpack-output
          cd learn-webpack-output
          npm init -y 
          yarn add webpack webpack-cli -D

          根目錄中新建一個文件 webpack.config.js,這個是 webpack 默認的配置文件

          const path = require('path');

          module.exports = {
            mode'development'// 可以設置為 production
            // 執(zhí)行的入口文件
            entry'./src/index.js',
            output: {
              // 輸出的文件名
              filename'bundle.js',
              // 輸出文件都放在 dist 
              path: path.resolve(__dirname, './dist')
            },
            // 為了更加方便查看輸出
            devtool'cheap-source-map'
          }

          然后我們回到 package.json 文件中,在 npm script 中添加啟動 webpack 配置的命令

          "scripts": {
            "test""echo \"Error: no test specified\" && exit 1",
            "build""webpack"
          }

          新建一個 src文件夾,新增 index.js 文件和 sayHello 文件

          // src/index.js
          import sayHello from './sayHello';

          console.log(sayHello, sayHello('Gopal'));
          // src/sayHello.js
          function sayHello(name{
            return `Hello ${name}`;
          }

          export default sayHello;

          一切準備完畢,執(zhí)行 yarn build

          分析主流程

          看輸出文件,這里不放具體的代碼,有點占篇幅,可以點擊這里查看[1]

          其實就是一個 IIFE

          莫慌,我們一點點拆分開看,其實總體的文件就是一個 IIFE——立即執(zhí)行函數(shù)。

          (function(modules// webpackBootstrap
           // The module cache
           var installedModules = {};
           function __webpack_require__(moduleId{
              // ...省略細節(jié)
           }
           // 入口文件
           return __webpack_require__(__webpack_require__.s = "./src/index.js");
          })
          ({

           "./src/index.js": (function(module, __webpack_exports__, __webpack_require__{}),
            "./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__{})
          });

          函數(shù)的入?yún)?modules 是一個對象,對象的 key 就是每個 js 模塊的相對路徑,value 就是一個函數(shù)(我們下面稱之為模塊函數(shù))。IIFE 會先 require 入口模塊。即上面就是 ./src/index.js

          // 入口文件
          return __webpack_require__(__webpack_require__.s = "./src/index.js");

          然后入口模塊會在執(zhí)行時 require 其他模塊例如 ./src/sayHello.js"以下為簡化后的代碼,從而不斷的加載所依賴的模塊,形成依賴樹,比如如下的模塊函數(shù)中就引用了其他的文件 sayHello.js

          {
          "./src/index.js": (function(module, __webpack_exports__, __webpack_require__
              __webpack_require__.r(__webpack_exports__);
             var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
              console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
              Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
            })
          }

          重要的實現(xiàn)機制——__webpack_require__

          這里去 require 其他模塊的函數(shù)主要是 __webpack_require__ 。接下來主要介紹一下 __webpack_require__ 這個函數(shù)

            // 緩存模塊使用
            var installedModules = {};
            // The require function
            // 模擬模塊的加載,webpack 實現(xiàn)的 require
            function __webpack_require__(moduleId{
              // Check if module is in cache
              // 檢查模塊是否在緩存中,有則直接從緩存中獲取
              if(installedModules[moduleId]) {
                return installedModules[moduleId].exports;
              }
              // Create a new module (and put it into the cache)
              // 沒有則創(chuàng)建并放入緩存中,其中 key 值就是模塊 Id,也就是上面所說的文件路徑
              var module = installedModules[moduleId] = {
                i: moduleId, // Module ID
                lfalse// 是否已經(jīng)執(zhí)行
                exports: {}
              };

              // Execute the module function
              // 執(zhí)行模塊函數(shù),掛載到 module.exports 上。this 指向 module.exports
              modules[moduleId].call(module.exports, modulemodule.exports, __webpack_require__);

              // Flag the module as loaded
              // 標記這個 module 已經(jīng)被加載
              module.l = true;

              // Return the exports of the module
              // module.exports通過在執(zhí)行module的時候,作為參數(shù)存進去,然后會保存module中暴露給外界的接口,如函數(shù)、變量等
              return module.exports;
            }

          第一步,webpack 這里做了一層優(yōu)化,通過對象 installedModules 進行緩存,檢查模塊是否在緩存中,有則直接從緩存中獲取,沒有則創(chuàng)建并放入緩存中,其中 key 值就是模塊 Id,也就是上面所說的文件路徑

          第二步,然后執(zhí)行模塊函數(shù),將 module, module.exports, __webpack_require__ 作為參數(shù)傳遞,并把模塊的函數(shù)調(diào)用對象指向 module.exports,保證模塊中的 this 指向永遠指向當前的模塊。

          第三步,最后返回加載的模塊,調(diào)用方直接調(diào)用即可。

          所以這個__webpack_require__就是來加載一個模塊,并在最后返回模塊 module.exports 變量

          webpack 是如何支持 ESM 的

          可能大家已經(jīng)發(fā)現(xiàn),我上面的寫法是 ESM 的寫法,對于模塊化的一些方案的了解,可以看看我的另外一篇文章【面試說】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?[2]

          我們重新看回模塊函數(shù)

          {
          "./src/index.js": (function(module, __webpack_exports__, __webpack_require__
              __webpack_require__.r(__webpack_exports__);
             var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
              console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
              Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
            })
          }

          我們看看 __webpack_require__.r 函數(shù)

          __webpack_require__.r = function(exports{
           object.defineProperty(exports, '__esModule', { valuetrue });
          };

          就是為 __webpack_exports__ 添加一個屬性 __esModule,值為 true

          再看一個 __webpack_require__.n 的實現(xiàn)

          // getDefaultExport function for compatibility with non-harmony modules
          __webpack_require__.n = function(module{
            var getter = module && module.__esModule ?
              function getDefault(return module['default']; } :
              function getModuleExports(return module; };
            __webpack_require__.d(getter, 'a', getter);
            return getter;
          };

          __webpack_require__.n會判斷module是否為es模塊,當__esModule為 true 的時候,標識 module 為es 模塊,默認返回module.default,否則返回module

          最后看 __webpack_require__.d,主要的工作就是將上面的 getter 函數(shù)綁定到 exports 中的屬性 a 的 getter

          // define getter function for harmony exports
          __webpack_require__.d = function(exports, name, getter{
           if(!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
             configurablefalse,
             enumerabletrue,
             get: getter
            });
           }
          };

          我們最后再看會 sayHello.js 打包后的模塊函數(shù),可以看到這里的導出是 __webpack_exports__["default"] ,實際上就是 __webpack_require__.n 做了一層包裝來實現(xiàn)的,其實也可以看出,實際上 webpack 是可以支持 CommonJSES Module 一起混用的

           "./src/sayHello.js":
            /*! exports provided: default */
           (function(module, __webpack_exports__, __webpack_require__{
            "use strict";
            __webpack_require__.r(__webpack_exports__);
            function sayHello(name{
              return `Hello ${name}`;
            }
            /* harmony default export */ __webpack_exports__["default"] = (sayHello);
           })

          目前為止,我們大致知道了 webpack 打包出來的文件是怎么作用的了,接下來我們分析下代碼分離的一種特殊場景——動態(tài)導入

          動態(tài)導入

          代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級,如果使用合理,會極大影響加載時間。

          常見的代碼分割有以下幾種方法:

          • 入口起點:使用 `entry`[3] 配置手動地分離代碼。
          • 防止重復:使用 Entry dependencies[4] 或者 `SplitChunksPlugin`[5] 去重和分離 chunk。
          • 動態(tài)導入:通過模塊的內(nèi)聯(lián)函數(shù)調(diào)用來分離代碼。

          本文我們主要看看動態(tài)導入,我們在 src 下面新建一個文件 another.js

          function Another({
            return 'Hi, I am Another Module';
          }

          export { Another };

          修改 index.js

          import sayHello from './sayHello';

          console.log(sayHello, sayHello('Gopal'));

          // 單純?yōu)榱搜菔?,就是有條件的時候才去動態(tài)加載
          if (true) {
            import('./Another.js').then(res => console.log(res))
          }

          我們來看下打包出來的內(nèi)容,忽略 .map 文件,可以看到多出一個 0.bundle.js 文件,這個我們稱它為動態(tài)加載的 chunk,bundle.js 我們稱為主 chunk

          輸出的代碼的話,主 chunk這里[6],動態(tài)加載的 chunk這里[7] ,下面是針對這兩份代碼的分析

          主 chunk 分析

          我們先來看看主 chunk

          內(nèi)容多了很多,我們來細看一下:

          首先我們注意到,我們動態(tài)導入的地方編譯后變成了以下,這是看起來就像是一個異步加載的函數(shù)

          if (true) {
            __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null/*! ./Another.js */ "./src/Another.js")).then(res => console.log(res))
          }

          所以我們來看 __webpack_require__.e 這個函數(shù)的實現(xiàn)

          __webpack_require__.e ——使用 JSONP 動態(tài)加載

          // 已加載的chunk緩存
          var installedChunks = {
            "main"0
          };
          // ...
          __webpack_require__.e = function requireEnsure(chunkId{
            // promises 隊列,等待多個異步 chunk 都加載完成才執(zhí)行回調(diào)
            var promises = [];

            // JSONP chunk loading for javascript
            var installedChunkData = installedChunks[chunkId];
            // 0 代表已經(jīng) installed
            if(installedChunkData !== 0) { // 0 means "already installed".

              // a Promise means "currently loading".
              // 目標chunk正在加載,則將 promise push到 promises 數(shù)組
              if(installedChunkData) {
                promises.push(installedChunkData[2]);
              } else {
                // setup Promise in chunk cache
                // 利用Promise去異步加載目標chunk
                var promise = new Promise(function(resolve, reject{
                  // 設置 installedChunks[chunkId]
                  installedChunkData = installedChunks[chunkId] = [resolve, reject];
                });
                // i設置chunk加載的三種狀態(tài)并緩存在 installedChunks 中,防止chunk重復加載
                // nstalledChunks[chunkId]  = [resolve, reject, promise]
                promises.push(installedChunkData[2] = promise);
                // start chunk loading
                // 使用 JSONP
                var head = document.getElementsByTagName('head')[0];
                var script = document.createElement('script');

                script.charset = 'utf-8';
                script.timeout = 120;

                if (__webpack_require__.nc) {
                  script.setAttribute("nonce", __webpack_require__.nc);
                }
                // 獲取目標chunk的地址,__webpack_require__.p 表示設置的publicPath,默認為空串
                script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
                // 請求超時的時候直接調(diào)用方法結束,時間為 120 s
                var timeout = setTimeout(function(){
                  onScriptComplete({ type'timeout'target: script });
                }, 120000);
                script.onerror = script.onload = onScriptComplete;
                // 設置加載完成或者錯誤的回調(diào)
                function onScriptComplete(event{
                  // avoid mem leaks in IE.
                  // 防止 IE 內(nèi)存泄露
                  script.onerror = script.onload = null;
                  clearTimeout(timeout);
                  var chunk = installedChunks[chunkId];
                  // 如果為 0 則表示已加載,主要邏輯看 webpackJsonpCallback 函數(shù)
                  if(chunk !== 0) {
                    if(chunk) {
                      var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                      var realSrc = event && event.target && event.target.src;
                      var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
                      error.type = errorType;
                      error.request = realSrc;
                      chunk[1](error "1");
                    }
                    installedChunks[chunkId] = undefined;
                  }
                };
                head.appendChild(script);
              }
            }
            return Promise.all(promises);
          };
          • 可以看出將 import() 轉換成模擬 JSONP 去加載動態(tài)加載的 chunk 文件

          • 設置 chunk 加載的三種狀態(tài)并緩存在installedChunks中,防止chunk重復加載。這些狀態(tài)的改變會在 webpackJsonpCallback 中提到

            // 設置 installedChunks[chunkId]
            installedChunkData = installedChunks[chunkId] = [resolve, reject];
            • installedChunks[chunkId]0,代表該 chunk 已經(jīng)加載完畢
            • installedChunks[chunkId]undefined,代表該 chunk 加載失敗、加載超時、從未加載過
            • installedChunks[chunkId]Promise對象,代表該 chunk 正在加載

          看完__webpack_require__.e,我們知道的是,我們通過 JSONP 去動態(tài)引入 chunk 文件,并根據(jù)引入的結果狀態(tài)進行處理,那么我們怎么知道引入之后的狀態(tài)呢?我們來看異步加載的 chunk 是怎樣的

          異步 Chunk

          // window["webpackJsonp"] 實際上是一個數(shù)組,向中添加一個元素。這個元素也是一個數(shù)組,其中數(shù)組的第一個元素是chunkId,第二個對象,跟傳入到 IIFE 中的參數(shù)一樣
          (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

            /***/ "./src/Another.js":
            /***/ (function(module, __webpack_exports__, __webpack_require__{
            
            "use strict";
            __webpack_require__.r(__webpack_exports__);
            /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Another"function(return Another; });
            function Another({
              return 'Hi, I am Another Module';
            }
            /***/ })
            
            }]);
            //# sourceMappingURL=0.bundle.js.map

          主要做的事情就是往一個數(shù)組 window['webpackJsonp'] 中塞入一個元素,這個元素也是一個數(shù)組,其中數(shù)組的第一個元素是 chunkId,第二個對象,跟主 chunk 中 IIFE 傳入的參數(shù)類似。關鍵是這個 window['webpackJsonp'] 在哪里會用到呢?我們回到主 chunk 中。在 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 進入入口之前還有一段

          var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
          // 保存原始的 Array.prototype.push 方法
          var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
          // 將 push 方法的實現(xiàn)修改為 webpackJsonpCallback
          // 這樣我們在異步 chunk 中執(zhí)行的 window['webpackJsonp'].push 其實是 webpackJsonpCallback 函數(shù)。
          jsonpArray.push = webpackJsonpCallback;
          jsonpArray = jsonpArray.slice();
          // 對已在數(shù)組中的元素依次執(zhí)行webpackJsonpCallback方法
          for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
          var parentJsonpFunction = oldJsonpFunction;

          jsonpArray 就是 window["webpackJsonp"] ,重點看下面這一句代碼,當執(zhí)行 push 方法的時候,就會執(zhí)行 webpackJsonpCallback,相當于做了一層劫持,也就是執(zhí)行完 push 操作的時候就會調(diào)用這個函數(shù)

          jsonpArray.push = webpackJsonpCallback;

          webpackJsonpCallback ——加載完動態(tài) chunk 之后的回調(diào)

          我們再來看看 webpackJsonpCallback 函數(shù),這里的入?yún)⒕褪莿討B(tài)加載的 chunkwindow['webpackJsonp'] push 進去的參數(shù)。

          var installedChunks = {
            "main"0
          }; 

          function webpackJsonpCallback(data{
            // window["webpackJsonp"] 中的第一個參數(shù)——即[0]
            var chunkIds = data[0];
            // 對應的模塊詳細信息,詳見打包出來的 chunk 模塊中的 push 進 window["webpackJsonp"] 中的第二個參數(shù)
            var moreModules = data[1];

            // add "moreModules" to the modules object,
            // then flag all "chunkIds" as loaded and fire callback
            var moduleId, chunkId, i = 0, resolves = [];
            for(;i < chunkIds.length; i++) {
              chunkId = chunkIds[i];
              // 所以此處是找到那些未加載完的chunk,他們的value還是[resolve, reject, promise]
              // 這個可以看 __webpack_require__.e 中設置的狀態(tài)
              // 表示正在執(zhí)行的chunk,加入到 resolves 數(shù)組中
              if(installedChunks[chunkId]) {
                resolves.push(installedChunks[chunkId][0]);
              }
              // 標記成已經(jīng)執(zhí)行完
              installedChunks[chunkId] = 0;
            }
            // 挨個將異步 chunk 中的 module 加入主 chunk 的 modules 數(shù)組中
            for(moduleId in moreModules) {
              if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
                modules[moduleId] = moreModules[moduleId];
              }
            }
            // parentJsonpFunction: 原始的數(shù)組 push 方法,將 data 加入 window["webpackJsonp"] 數(shù)組。
            if(parentJsonpFunction) parentJsonpFunction(data);
            // 等到 while 循環(huán)結束后,__webpack_require__.e 的返回值 Promise 得到 resolve
            // 執(zhí)行 resolove
            while(resolves.length) {
              resolves.shift()();
            }
          };

          當我們 JSONP 去加載異步 chunk 完成之后,就會去執(zhí)行 window["webpackJsonp"] || []).push,也就是 webpackJsonpCallback。主要有以下幾步

          • 遍歷要加載的 chunkIds,找到未執(zhí)行完的 chunk,并加入到 resolves 中
          for(;i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            // 所以此處是找到那些未加載完的chunk,他們的value還是[resolve, reject, promise]
            // 這個可以看 __webpack_require__.e 中設置的狀態(tài)
            // 表示正在執(zhí)行的chunk,加入到 resolves 數(shù)組中
            if(installedChunks[chunkId]) {
              resolves.push(installedChunks[chunkId][0]);
            }
            // 標記成已經(jīng)執(zhí)行完
            installedChunks[chunkId] = 0;
          }
          • 這里未執(zhí)行的是非 0 狀態(tài),執(zhí)行完就設置為0

          • installedChunks[chunkId][0] 實際上就是 Promise 構造函數(shù)中的 resolve

            // __webpack_require__.e 
            var promise = new Promise(function(resolve, reject{
             installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
          • 挨個將異步 chunk 中的 module 加入主 chunkmodules 數(shù)組中

          • 原始的數(shù)組 push 方法,將 data 加入 window["webpackJsonp"] 數(shù)組

          • 執(zhí)行各個 resolves 方法,告訴 __webpack_require__.e 中回調(diào)函數(shù)的狀態(tài)

          只有當這個方法執(zhí)行完成的時候,我們才知道 JSONP 成功與否,也就是script.onload/onerror 會在 webpackJsonpCallback 之后執(zhí)行。所以 onload/onerror 其實是用來檢查 webpackJsonpCallback 的完成度:有沒有將 installedChunks 中對應的 chunk 值設為 0

          動態(tài)導入小結

          大致的流程如下圖所示

          流程圖

          總結

          本篇文章分析了 webpack 打包主流程以及和動態(tài)加載情況下輸出代碼,總結如下

          • 總體的文件就是一個 IIFE——立即執(zhí)行函數(shù)
          • webpack 會對加載過的文件進行緩存,從而優(yōu)化性能
          • 主要是通過 __webpack_require__來模擬 import 一個模塊,并在最后返回模塊 export 的變量
          • webpack 是如何支持 ES Module
          • 動態(tài)加載 import() 的實現(xiàn)主要是使用 JSONP 動態(tài)加載模塊,并通過 webpackJsonpCallback 判斷加載的結果

          參考

          • 分析 webpack 打包后的文件[8]
          • webpack 打包產(chǎn)物代碼分析[9]
          • 『Webpack系列』—— 路由懶加載的原理[10]

          參考資料

          [1]

          這里查看: https://github.com/GpingFeng/learn-webpack/blob/main/output/main.js

          [2]

          【面試說】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?: https://juejin.cn/post/6935973925004247077?utm_source=gold_browser_extension#heading-0

          [3]

          entry: https://webpack.docschina.org/configuration/entry-context

          [4]

          Entry dependencies: https://webpack.docschina.org/configuration/entry-context/#dependencies

          [5]

          SplitChunksPlugin: https://webpack.docschina.org/plugins/split-chunks-plugin

          [6]

          這里: https://github.com/GpingFeng/learn-webpack/blob/main/output/bundle.js

          [7]

          這里: https://github.com/GpingFeng/learn-webpack/blob/main/output/0.bundle.js

          [8]

          分析 webpack 打包后的文件: https://juejin.cn/post/6844903492063068167

          [9]

          webpack 打包產(chǎn)物代碼分析: https://hellogithub2014.github.io/2019/01/02/webpack-bundle-code-analysis/

          [10]

          『Webpack系列』—— 路由懶加載的原理: https://juejin.cn/post/6844904180285456398


          瀏覽 57
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  青娱乐亚洲版在线观看 | 韩国福利视频一区二区 | 操美女久久| 影音先锋欧美资源 | 波多野吉衣高清无码 |