Stories

Detail Return Return

webpack打包js文件的分析 - Stories Detail

在使用webpack中的項目的時候,我們可以使用esModule,也可以使用commonJS,還可以使用import(moduleName)進行模塊的懶加載,那麼這一切webpack是怎麼做到的呢?

1、準備工作

1.1、 使用webapck@4 webpack-cli@3

"html-webpack-plugin": "4",
"webpack": "4",
"webpack-cli": "3"

1.2、 文件結構

image.png

1.3、 webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  devtool: false,
  mode: 'development',
  entry: './src/index.js',
  output: {
        filename: 'js/[name].js',
        path: path.resolve(__dirname, 'dist')
  },

  plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        template: './src/index.html',
      })
  ],

  optimization: {
      chunkIds: 'named',
      splitChunks: {
          cacheGroups: {
            commons: {
              test: /[\\/]node_modules[\\/]/,
              name: 'vendors',
              chunks: 'all',
            },
        },
      }
  }
}

1.4、打包命令

npx webpack 或者 在package.json中添加

"scripts": {
      "build": "webpack"
}

然後執行 npm run build

2、CommonJS模塊打包

2.1、使用commonJS導出模塊, commonJS導入模塊

// index.js
const title = require('./login')
console.log('commonjs 文件打包分析')

// login.js
// commonJS導出
module.exports = "今天天氣很冷!"

準備工作做完後,執行打包命令刪除無用的註釋,相關代碼都摺疊,運行調試。
image.png
可以看到,其實打包後的文件就是個IIFE. 傳入的對象就是我們之前兩個文件路徑作為鍵名,以及各自一個函數作為對象的的對象集合(實際上就是我們的依賴集合,鍵名是可以通過webpack.config.js進行配置的,默認是當前路徑加文件名的方式命名的)。
image.png
進入到自執行函數中可以看到,__webpack_require__這個上面掛載了很多的方法和屬性

  • __webpack_require__.m 導出的modules對象
  • __webpack_require__.c 導出的module緩存
  • __webpack_require__.d 為exports定義 getter方法
  • __webpack_require__.r 為即將導出的exports定義 __esModuleSymbol.toStringTag(esModule)
  • __webpack_require__.t
  • __webpack_require__.n n方法 為module定義獲取模塊值的getter方法, 非esmodule則返回當前module
  • __webpack_require__.o o方法 判斷對象是否擁有某個屬性,工具函數
  • __webpack_require__.p public_path 配置文件中配置,默認為空字符串 在使用jsonp動態引入時會被使用到

我們繼續往下運行,當代碼走到最後的return時,這時候會調用__webpack_require__(這個方法就是核心方法。其實做的事情很簡單。),這時候就是程序在加載入口文件(此是加載的是index.js

function __webpack_require__(moduleId) {
  // 檢查是否有緩存
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // 創建一個module對象,並將其保存在緩存中,一遍下次使用
  var module = (installedModules[moduleId] = {
    i: moduleId, // 模塊ID
    l: false, // 是否已經加載
    exports: {}, // 對外返回的exports對象, 模塊導出的東西都掛載在該對象上面
  });
  // 調用call方法,執行當前的module
  modules[moduleId].call(
    module.exports,
    module,
    module.exports,
    __webpack_require__
  );
  // 標記模塊已加載
  module.l = true;
  // 返回模塊的exports對象
  return module.exports;
}

__webpack_require__內部,會執行當前module中的代碼,第一次執行的時候,我們看到index.js中有對login.js的引用,這裏被webpack包裝成了__webpack_require__這個方法,無論是require還是import語法都將會執行這個方法。

// dits/main.js
{
    "./src/index.js": function (module, exports, __webpack_require__) {
      const title = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log("commonjs 文件打包分析");
      console.log(title, "title");
    },
    "./src/login.js": function (module, exports) {
      module.exports = "今天天氣很冷!";
      console.log("login.js 執行了");
    },
  }

運行到__webpack_require__(/*! ./login */ "./src/login.js")這裏時,會去加載login.js的內容。login.js內容很少,就是module.exports = '今天天氣很冷',由上面的分析可知,被__webpack_require__加載的代碼都會返回其exports.所以加載了login.js後,title的值就應該是我們module.exports導出的值。
image.png
commonJS規範的導入導出分析完成。
結論:在使用commonJS導入,commonJS導出模塊的時候,webpack會使用自己的__webpack_require__方法進行模塊的加載。

2.2、使用commonJS規範導入模塊,esModule導出模塊打包

更改index.js和.login.js, 執行打包

// index.js
const object = require('./login')
console.log('commonjs 文件打包分析')
console.log(object.default, 'default')
console.log(object.user, 'user')

// login.js
export const user = {
  name: '法外狂徒-張三',
  age: 33
}
export default '今天天氣很冷!'
console.log('login.js 執行了')

導出結果, 這裏我們只看IIFE中的modules,因為上面的內容都是一樣的。

{
  "./src/index.js":
    /*! no static exports found */
    function (module, exports, __webpack_require__) {
      const object = __webpack_require__(/*! ./login */ "./src/login.js");
      console.log("commonjs 文件打包分析");
      console.log(object.default, "default");
      console.log(object.user, "user");
    },
  "./src/login.js":
    /*! exports provided: user, default */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(
        __webpack_exports__,
        "user",
        function () {
          return user;
        }
      );
      const user = {
        name: "法外狂徒-張三",
        age: 33,
      };
      /* harmony default export */ __webpack_exports__["default"] =
        "今天天氣很冷!";
      console.log("login.js 執行了");
    },
}

可以看見在使用esModule進行導出的時候,多了這些內容。

  • __webpack_require__.r 為exports對象 定義用於標識esModule標識

    __webpack_require__.r = function (exports) {
      // 判斷當前環境是否是es6的環境,如果是,則對exports設置一個 `Module`屬性
      if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
      }
      // 為exports 設置一個默認的`__esModule`值, 用於標識esModule
      Object.defineProperty(exports, "__esModule", { value: true });
    };
  • __webpack_require__.d 為exports對象上的屬性,定義getter

    __webpack_require__.d = function (exports, name, getter) {
      // 判斷當前exports對象是否有某個屬性值,如果沒有, 則重新定義這個屬性,並設置getter方法
      if (!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter });
      }
    };
    // 工具函數,判斷對象是否擁有某個屬性值
    __webpack_require__.o = function (object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
    };

    可以看到,在login.js中,對user對象,設置了一個getter方法,返回的是定義在login.js代碼塊中的user對象,這就是我們定義在該文件中的user對象。同時export defaults返回的數據,也通過__webpack_exports__["default"]進行了賦值,因此,index.js在執行const object = __webpack_require__(/*! ./login */ "./src/login.js");這個代碼時,object中的值,就是我們需要的結果了
    image.png

3、esModule 導入模塊打包

3.1、使用esModule 導入模塊,esmodule方式導出

更改index.js和login.js的代碼,然後執行打包操作

// login.js
export const user = {
  name: '法外狂徒-張三',
  age: 33
}
export default '今天天氣很冷!'
console.log('login.js 執行了')

// index.js
import title, { user } from './login'
console.log('commonjs 文件打包分析')
console.log(title, 'default')
console.log(user, 'user')

打包結果

{
    "./src/index.js":
      /*! no exports provided */
      function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/login.js");
        console.log("commonjs 文件打包分析");
        console.log(_login__WEBPACK_IMPORTED_MODULE_0__["default"], "default");
        console.log(_login__WEBPACK_IMPORTED_MODULE_0__["user"], "user");
      },
    "./src/login.js":
      /*! exports provided: user, default */
      function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, "user", function () {
          return user;
        });
        const user = {
          name: "法外狂徒-張三",
          age: 33,
        };
        __webpack_exports__["default"] = "今天天氣很冷!";
        console.log("login.js 執行了");
      },
  }

同上面一樣,這個文件是個IIFE,只是裏面的內容發生了變化。同4.2進行比較,只是獲取值的方式不同而已。

3.2、使用esModule導入 commonJS導出

更改文件,然後執行打包

// login.js
module.exports = "今天天氣很冷!"

// index.js
import title from './login.js'
console.log('esmodule 文件打包分析')

打包結果:

{
  "./src/index.js":
    /*! no exports provided */
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _login__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/login.js");
      var _login__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_login__WEBPACK_IMPORTED_MODULE_0__);
      console.log(_login__WEBPACK_IMPORTED_MODULE_0___default.a, "default");
    },
  "./src/login.js":
    /*! no static exports found */
    function (module, exports) {
      module.exports = "今天天氣很冷!";
      console.log("login.js 執行了");
    },
}

可以看到,使用這種方式的導出,在導入該文件的地方,多了一個n方法的調用
,下面來看看這個n方法幹了什麼事情

__webpack_require__.n = function (module) {
  var getter = module && module.__esModule ? // 當前module是否是先前標記的__esModule
    function getDefault() {return module['default'];} : // 是esmodule就返回默認
    function getModuleExports() {return module;}; // 否則返回改module
  __webpack_require__.d(getter, 'a', getter); // 為該module調用d方法,重新定義getter, 重新定義屬性 a, 用於後面獲取該值
  return getter;
};

繼續往下看,執行到獲取title的代碼時,回去調用之前定義d方法定義的getter方法,獲取到默認導出的值,這樣在使用commonjs導出,使用 esModule的方式導入的時候,就能正確的獲取到值了,否則按正常來講,esModule導入commonjs是會報錯的
image.png
esModule導入commonjs模塊
image.png

這就是webpack的加持下, 我們可以在項目中混合使用commonjs esModule規範的代碼進行npm 包的引入。


4、模塊懶加載

webpack中,允許我們使用import('module')來進行模塊的懶加載。

4.1、準備工作

還是使用上面的配置,只是更改我們的被打包的文件內容

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>測試打包文件分析</title>
</head>
<body>
  <button id="btn">加載</button>
</body>
</html>

在index.js文件中,我們創建一個按鈕用於觸發加載login.js內容,然後在import導入模塊成功後,再次新建一個新的按鈕,這個按鈕在進行一次動態導入。

// index.js 入口文件,通過註冊一個點擊事件完成異步加載login.js的內容
const btn = document.getElementById('btn')

btn.addEventListener('click', () => {
  import(/*webpackChunkName: "login"*/'./login.js').then(login => {
    console.log(login, 'login -------->')
    // 創建一個新的按鈕,再次加載一次login模塊
    const button = document.createElement('button')
    button.innerHTML = '再次加載Login模塊'
    button.onclick = () => {
      import(/*webpackChunkName: "login"*/'./login.js').then(module => {
        console.log('<<<<<<loaded login again ------')
        console.log(module, 'module')
        console.log('loaded login again ------->>>>>>')
      })
    }
    document.body.appendChild(button)
  })
})

console.log('index js 文件執行')
// login.js
module.exports = 'login.js value'

console.log('loginjs 執行了')

點擊加載按鈕,然後點擊 再次加載Login模塊按鈕,得到如下結果
image.png
image.png
可以看到,

  1. import方法是返回的一個promise,返回的是被webpack處理過的一個對象,這個對象就是上述 __webpack_require__處理過並返回的exports對象
  2. network中有login.js的網絡請求,head標籤中多了一個script的腳本文件

看看webpack打包後的結果
image.png
可以看到,使用了懶加載,會出現兩個新的方法調用

  • __webpack_require.e 加載chunk的方法
  • __webpack_require.t 為當前module創建一個fake namespace
    同時在IIFE裏面的函數體中也多了 一些其他的代碼
    image.png
    這裏的功能主要是將window['webpackJsonp'] 調用push方法的時候,直接調用webpackJsonpCallback方法,
    聲明一個jsonpArraywindow['webpackJsonp']共享一個數組空間
    然後將webpackJsonpCallback方法賦值給jsonpArray.push, 這樣就將window['webpackJsonp']webpackJsonpCallback建立起了鏈接,即調用window['webpackJsonp'].push方法就會執行webpackJsonpCallback方法
    因為是第一次加載,jsonpArray數組為空數組, 所以不會執行下面的for循環
    然後我們再看看需要異步加載的模塊被webpack打包成什麼內容了

    (window['webpackJsonp'] = window['webpackJsonp'] || []).push([
    ['login'],
    {
      './src/login.js': function (module, exports) {
          module.exports = 'login.js value';
          console.log('loginjs 執行了');
      },
    },
    ]);
    

    這個文件中,當執行login.js模塊時,會調用push方法,實際上就會調用webpackJsonpCallback方法

    function webpackJsonpCallback(data) {
      var chunkIds = data[0];
      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];
        if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
          resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
      }
      for (moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
          modules[moduleId] = moreModules[moduleId];
        }
      }
      if (parentJsonpFunction) parentJsonpFunction(data);
    
      while (resolves.length) {
        resolves.shift()();
      }
    }

第二次加載login.js
image.png
觸發再次加載Login模塊後
image.png
動態加載login.js後,執行__wepack_require__方法時,就會找到之前的緩存,無需再次發起資源請求
image.png

總結/引申:

  1. webpack對於js文件的編譯操作,會使用他自身的一個__webpack_require的方法,並根據不同的引用模塊的方式,調用不同的方法,最終目的是達到將被導出的模塊內容,通過exports對象去導出,以便我們能獲取得到正確的值
  2. 懶加載模塊其實就是動態的創建了一個script標籤,通過promise的包裝,讓我們可以很優雅的獲取到加載成功後的module
  3. webpack怎麼知道我們的引入模塊的規則 以及導出模塊的規則的呢? 通過 ast
  4. 對比Rollup,二者有啥區別?Rollup只對esmodule進行打包,而webpack則是新舊通吃

歡迎留言討論!

Add a new Comments

Some HTML is okay.