关于webpack:webpack打包js文件的分析

3次阅读

共计 10087 个字符,预计需要花费 26 分钟才能阅读完成。

在应用 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、文件构造

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 = "今天天气很冷!"

筹备工作做完后,执行打包命令删除无用的正文,相干代码都折叠,运行调试。

能够看到,其实打包后的文件就是个 IIFE. 传入的对象就是咱们之前两个文件门路作为键名,以及各自一个函数作为对象的的对象汇合(实际上就是咱们的依赖汇合,键名是能够通过 webpack.config.js 进行配置的,默认是以后门路加文件名的形式命名的)。

进入到自执行函数中能够看到,__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 导出的值。

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中的值,就是咱们须要的后果了

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 是会报错的

esModule 导入 commonjs 模块

这就是 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 模块按钮,失去如下后果


能够看到,

  1. import 办法是返回的一个 promise,返回的是被 webpack 解决过的一个对象,这个对象就是上述 __webpack_require__解决过并返回的 exports 对象
  2. network中有 login.js 的网络申请,head标签中多了一个 script 的脚本文件

看看 webpack 打包后的后果

能够看到,应用了懒加载,会呈现两个新的办法调用

  • __webpack_require.e 加载 chunk 的办法
  • __webpack_require.t 为以后 module 创立一个 fake namespace
    同时在 IIFE 外面的函数体中也多了 一些其余的代码

    这里的性能次要是将 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

触发再次加载 Login 模块后

动静加载 login.js 后,执行__wepack_require__办法时,就会找到之前的缓存,无需再次发动资源申请

总结 / 引申:

  1. webpack对于 js 文件的编译操作,会应用他本身的一个 __webpack_require 的办法,并依据不同的援用模块的形式,调用不同的办法,最终目标是达到将被导出的模块内容,通过 exports 对象去导出,以便咱们能获取失去正确的值
  2. 懒加载模块其实就是动静的创立了一个 script 标签,通过 promise 的包装,让咱们能够很优雅的获取到加载胜利后的module
  3. webpack怎么晓得咱们的引入模块的规定 以及导出模块的规定的呢?通过 ast
  4. 比照 Rollup, 二者有啥区别?Rollup 只对 esmodule 进行打包,而 webpack 则是新旧通吃

欢送留言探讨!

正文完
 0