webpack4、5打包后代码对比分析

2,587 阅读7分钟

前言

前端日新月异,我们要不断学习新的知识文化才能跟上时代的步伐。

介绍

webpack现在是前端打包常用的工具。 今天分别使用 webpack.4x 和 webpack.5x 进行打包代码,对比看一下 webpack4、5 产出代码的区别。(以下简称 webpack4 、webpack5) webpack 官网

// 基本安装
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev

首先来看只有一个 index.js 文件的 demo . 在 src 下新建一个入口文件 index.js .

先来看webpack4

  // index.js
  console.log("我是 webpack 4")

经过webpack打包之后变成了一个main.js文件,简单整理一下

// 打包后生成的的 main.js
(function(modules) {
  // 模块的缓存
  var installedModules = {};

  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)
    // 构建 commonjs 模块标准
    var module = installedModules[moduleId] = {
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    return module.exports;
  }

 	// 执行的入口函数
  return __webpack_require__("./src/index.js");
})({
  "./src/index.js": function(module, exports) {
    console.log("我是 webpack 4")
  }
})

可将上面代码,复制到浏览器验证一下 是所有的模块,每个文件对应一个模块,格式是-文件名:方法 最外层是一个立即执行函数,入参是所有的 modules(模块) list。传入的 modules 参数是一个的对象。 格式是 -> 文件名:方法。 key 是 index.js 文件的相对路径,value 是一个匿名函数,函数体里面就是咱们写在 index.js 里的代码。(这就是 webpack 加载模块的方式)

接下来,看一下这个函数怎么执行的: 首先定义了一个 installedModules 用来缓存模块,在函数 __webpack_require__ 执行时,会通过 moduleId 先判断是有此模块的缓存。

  • moduleId:就是我们最外层立即执行函数的key 如果存在此模块直接返回缓存,return installedModules[moduleId].exports; 若不存在则声明一个 module 用来接收模块并进行缓存
var module = installedModules[moduleId] = {
  exports: {}
};

等同于

 installedModules["./src/index.js"] = module.exports = {} 

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 这里是绑定this指向,并把参数传递给module函数,主要是用来收集 module 中所有的 export xxx 。但是我们这里没有用到 import xxx ,所以这里只是执行了 "./src/index.js" 的函数。

再来看一下 webpack 5

  // index.js
  console.log("我是 webpack 5")

经过webpack打包之后变成了一个main.js文件,简单整理一下

// 打包后生成的的 main.js
(() => {
  console.log("我是 webpack 5")
})();

webpack 5的版本使用箭头函数,只用了一行代码

下面再增加一个同步文件 sync.js ,看看打包后有什么区别

先来看webpack 4

// sync.js
const data = '同步文件'

export default data

index.js 也做了修改

// index.js
import data from './sync.js'
console.log("我是", data)
console.log("我是 webpack 4")

整理一下打包后生成的 main.js

(function(modules) {
  // 模块的缓存
  var installedModules = {};

  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)
 		var module = installedModules[moduleId] = {
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    return module.exports;
  }

 	// 执行的入口函数
  return __webpack_require__("./src/index.js");
})({
  "./src/index.js": function(module, __webpack_exports__, __webpack_require__){
    "use strict";
    // import => 替换成 __webpack_require__ 
    var _sync_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js");
    console.log(_sync_js__WEBPACK_IMPORTED_MODULE_0__["default"])
    console.log("我是 webpack 4")
  },
  "./src/sync.js": function(module, __webpack_exports__, __webpack_require__) {
    // 1、__webpack_exports__ = module.exports = {}
    // 2、__webpack_require__ 加载模块 转换 import
    "use strict";
    const data = '同步文件'
    /* harmony default export */
    // module.exports.default = data
    __webpack_exports__["default"] = data;
  }
})

先来看入口文件,key为 './src/index.js' 的函数, __webpack_exports__ 这里没导出所以没用到。 加载模块:__webpack_require__ 就是我们用import xxx的转换 导出模块:exports 转换成 __webpack_exports__ __webpack_require__("./src/sync.js") 加载我们的同步文件

key 为 "./src/sync.js" 的函数的第2个参数其实就是 __webpack_exports__ = module.exports = {}

而咱们导出使用的是 export default xxx , __webpack_exports__["default"] = data 所以这里有个default,相当于 module.exports.default = data

再来看一下webpack 5 增加一个同步文件

// sync.js
const data = '同步文件'
export default data

index.js 也做了修改

// index.js
import data from './sync.js'
console.log("我是", data)
console.log("我是 webpack 5")
// 整理一下打包后生成的 main.js
(() => {
  "use strict";
  var __webpack_modules__ = {
    "./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      var _sync_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js")
      console.log(_sync_js__WEBPACK_IMPORTED_MODULE_0__.default)
      console.log("我是 webpack 5")
    }),
    "./src/sync.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        __webpack_require__.d(__webpack_exports__, {
        //  default 变成函数,方便执行一些特殊的属性(方法)
          "default": () => __WEBPACK_DEFAULT_EXPORT__});
          const data = '同步文件'
          const __WEBPACK_DEFAULT_EXPORT__ = (data);
    })
  };
  // The module cache
  var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    if(__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };
    // 箭头函数  不需要再绑定this 
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    return module.exports;
  }
  // polyfill
  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) => {
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  })();

  /* webpack/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
  })();

  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();

  // startup
  // Load entry module
  __webpack_require__("./src/index.js");
})()

首先很明显的可以看到 webpack5 不再使用传参的形式来引入文件,而是用__webpack_modules__对象来进行存储。

"default": () => __WEBPACK_DEFAULT_EXPORT__} 变成了一个函数

把每一个 polyfill 都变成闭包。__webpack_require__.d,就是去做定义。 同步文件 webpack没有太多的变化,接下来咱们看一下异步文件。

webpac 4 异步引入

// index.js 先用异步的方式引入 sync.js
import("./sync.js").then( (_) => {
  console.log(_)
})
console.log("我是 webpack 4")

此时进行打包,打包后的 dist目录:

  dist
  |-- 0.js // 也就是咱们的 sync.js
  |-- main.js
// 0.js 
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js": 
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
    __webpack_require__.r(__webpack_exports__);
    const data = '同步文件'
    __webpack_exports__["default"] = (data);
    //# sourceURL=webpack:///./src/async.js?");
  })
}]);

先来说一下 webpack4 的一个问题,那就是会串id,造成缓存失效。

  webpack 中每个模块有一个唯一的 id,是从 0 开始递增的。

这时咱们修改一下index.js,增加一个async.js

// async.js
const data = '我是异步数据'
export default data
// index.js 把async.js也用引入进来
import("./sync.js").then( (_) => {
  console.log(_)
})
import("./async.js").then( (_) => {
  console.log(_)
})
console.log("我是 webpack 4")

再次打包之后会发现

// 0.js 
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js": 
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
    __webpack_require__.r(__webpack_exports__);
    const data = '我是异步数据'
    __webpack_exports__["default"] = (data);
  })
}]);
// 1.js 
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js": 
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
    __webpack_require__.r(__webpack_exports__);
    const data = '同步文件'
    __webpack_exports__["default"] = (data);
  })
}]);

这个时候 0.js 不再是之前的 sync.js 了

// 只有一个文件的时候
sync.js => 0.js

// 引入sync.js
async.js => 0.js 
sync.js => 1.js

这样就会导致缓存在客户端的文件失效。解决方法可以加入一行注释来固定chunkId 修改一下 index.js

// index.js
import(/* webpackChunkName: "sync" */ "./sync.js").then( (_) => {
  console.log(_)
})
import(/* webpackChunkName: "async" */ "./async.js").then( (_) => {
  console.log(_)
})
console.log("我是 webpack 4")

此时dist目录:

  dist
  |-- async.js
  |-- main.js
  |-- sync.js

这样异步文件都需要加上这行注释(相信你在vue项目里一定见过),无形之中增加了维护成本。(也可以使用插件,但是总会出现更新不及时、不更新问题),这个问题在 webpack5 中得到了很好的解决。

再来看一下 webpack5 将 async.js 同样放入到 src 下面, 然后修改index.js

// index.js
import("./sync.js").then( (_) => {
  console.log(_)
})
import("./async.js").then( (_) => {
  console.log(_)
})
console.log("我是 webpack 5")

webpack5下的dist目录

  dist
  |-- main.js
  |-- src_async_js.js
  |-- src_sync_js.js

默认就做了区分,用目录做前缀,用文件类型做后缀。 当然你打成开发环境也是一样的

  dist // 目录: 数字是md5
  |-- 67.js // sync.js
  |-- 853.js // async.js
  |-- main.js

'deterministic'这个是webpack5新增的,详情可参考官网 webpack5可配置chunkId webpack5可配置moduleId

接下里咱们再来了解一下异步的加载方式,回到 webpack4 打包后的async.js(如果不加配置的话,默认打出来的是0.js

// async.js === 0.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/async.js": 
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
    __webpack_require__.r(__webpack_exports__);
    const data = '我是异步数据'
    __webpack_exports__["default"] = (data);
  })
}]);

同等于

window["webpackJsonp"].push([
  ['async'],
  { /* 上面分析过的函数体 */ }
])
  // main.js
  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()();
    }

  };
  // The module cache
  var installedModules = {};
  // object to store loaded and loading chunks
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // Promise = chunk loading, 0 = chunk loaded
  var installedChunks = {
    "main": 0
  };

在 webpackJsonpCallback 中会将 async.js 中的 chunks 和 modules 保存到全局的 modules 变量中,并用哨兵变量 installedChunks 来记录异步文件加载的个数。

  __webpack_require__.e = function requireEnsure(chunkId) { ... }

  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  var parentJsonpFunction = oldJsonpFunction;

__webpack_require__.e 是通过加载 script 标签来引入异步文件的。

流程

  入口文件 -> 加载模块 -> 处理模块(处理浏览器兼容) 取第二项(第一项是名字)-> 加缓存 -> 放到 main.js 最后

webpack5打包后的src_async_js.js

(self["webpackChunkwebpack5"] = self["webpackChunkwebpack5"] || []).push([["src_async_js"],{

  "./src/async.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  __webpack_require__.d(__webpack_exports__, {
      "default": () => __WEBPACK_DEFAULT_EXPORT__
    });
    const data = '我是异步数据'
    const __WEBPACK_DEFAULT_EXPORT__ = (data);
  })
}]);

其实没有太多变化,用了新的 api

self.self === self

下面来看webpack 5 的 topLevelAwait

在 src 下新建 data.js 、demo.js 如下

  // data.js
  const data = '我是狮子  '
  export default data
  // demo.js
  let output = ''
  // top-level-await 的写法  
  const dynamic = import('./data') // await import('./data') 这样写也可以
  output = (await dynamic).default + ' 🦁 ' + Math.random() * 100
  // 之前的写法
  /* async function main () {
    const dynamic = await import('./data')
    output = dynamic.default + '🦁'
  }
  main() */
  export { output }

top-level-await 的写法,需要配置 webpack.config.js

// webpack.config.js
module.exports = {
  experiments: {
    // importAsync: true,
    // importAwait: true,
    topLevelAwait: true
  },
}

总结,对于异步文件引用 webpack5 和 webpack4 不同点在于:

window上挂载的用于存放 webpack 打包的 json 变量名
webpack4: webpackJsonp
webpack5: webpackJsonpwebpack5,为了防止和我们自定义的冲突,加了 webpack5 后缀

在 webpack 4 、5 进行打包的时候,能明显感觉到 5.x 的版本比 4.x 打包速度有了很大的提升。 主要原因是 Webpack4 的缓存是在运行时的,所以缓存只存在于内存中,在热更新的时候代码更新很快,但是在进行打包的时候 Webpack 的运行程序关闭了,缓存就丢失了。这就导致我们打包时无缓存可用。 而 webpack5 使用的是持久化缓存,在本地开发时使用 MemoryCachePlugin ,而在打包时使用 IdleFileCachePlugin。 参考:Webpack5 内置缓存方案探索


IdleFileCachePlugin:持久化到本地磁盘
MemoryCachePlugin:持久化到内存