【webpack 进阶】Webpack 打包后的代码是怎么的?

webpack 是咱们现阶段要把握的重要的打包工具之一,咱们晓得 webpack 会递归的构建依赖关系图,其中蕴含应用程序的每个模块,而后将这些模块打包成一个或者多个 bundle

那么webpack 打包后的代码是怎么的呢?是怎么将各个 bundle连贯在一起的?模块与模块之间的关系是怎么解决的?动静 import() 的时候又是怎么的呢?

本文让咱们一步步来揭开 webpack 打包后代码的神秘面纱

筹备工作

创立一个文件,并初始化

mkdir learn-webpack-outputcd learn-webpack-outputnpm init -y yarn add webpack webpack-cli -D

根目录中新建一个文件 webpack.config.js,这个是 webpack 默认的配置文件

const path = require('path');module.exports = {  mode: 'development', // 能够设置为 production  // 执行的入口文件  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.jsimport sayHello from './sayHello';console.log(sayHello, sayHello('Gopal'));
// src/sayHello.jsfunction sayHello(name) {  return `Hello ${name}`;}export default sayHello;

所有筹备结束,执行 yarn build

剖析主流程

看输入文件,这里不放具体的代码,有点占篇幅,能够点击这里查看

其实就是一个 IIFE

莫慌,咱们一点点拆离开看,其实总体的文件就是一个 IIFE——立刻执行函数。

(function(modules) { // webpackBootstrap    // The module cache    var installedModules = {};    function __webpack_require__(moduleId) {    // ...省略细节    }    // 入口文件    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__) {})});

函数的入参 modules 是一个对象,对象的 key 就是每个 js 模块的相对路径,value 就是一个函数(咱们上面称之为模块函数)。IIFE 会先 require 入口模块。即下面就是 ./src/index.js

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

而后入口模块会在执行时 require 其余模块例如 ./src/sayHello.js" 以下为简化后的代码,从而一直的加载所依赖的模块,造成依赖树,比方如下的模块函数中就援用了其余的文件 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'));  })}

重要的实现机制——__webpack_require__

这里去 require 其余模块的函数次要是 __webpack_require__ 。接下来次要介绍一下 __webpack_require__ 这个函数

  // 缓存模块应用  var installedModules = {};  // The require function  // 模仿模块的加载,webpack 实现的 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)    // 没有则创立并放入缓存中,其中 key 值就是模块 Id,也就是下面所说的文件门路    var module = installedModules[moduleId] = {      i: moduleId, // Module ID      l: false, // 是否曾经执行      exports: {}    };    // Execute the module function    // 执行模块函数,挂载到 module.exports 上。this 指向 module.exports    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);    // Flag the module as loaded    // 标记这个 module 曾经被加载    module.l = true;    // Return the exports of the module    // module.exports通过在执行module的时候,作为参数存进去,而后会保留module中裸露给外界的接口,如函数、变量等    return module.exports;  }

第一步,webpack 这里做了一层优化,通过对象 installedModules 进行缓存,查看模块是否在缓存中,有则间接从缓存中获取,没有则创立并放入缓存中,其中 key 值就是模块 Id,也就是下面所说的文件门路

第二步,而后执行模块函数,将 module, module.exports, __webpack_require__ 作为参数传递,并把模块的函数调用对象指向 module.exports,保障模块中的 this 指向永远指向以后的模块。

第三步,最初返回加载的模块,调用方间接调用即可。

所以这个__webpack_require__就是来加载一个模块,并在最初返回模块 module.exports 变量

webpack 是如何反对 ESM 的

可能大家曾经发现,我下面的写法是 ESM 的写法,对于模块化的一些计划的理解,能够看看我的另外一篇文章【面试说】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?

咱们从新看回模块函数

{"./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 函数

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

就是为 __webpack_exports__ 增加一个属性 __esModule,值为 true

再看一个 __webpack_require__.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 函数绑定到 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, {            configurable: false,            enumerable: true,            get: getter        });    }};

咱们最初再看会 sayHello.js 打包后的模块函数,能够看到这里的导出是 __webpack_exports__["default"] ,实际上就是 __webpack_require__.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 打包进去的文件是怎么作用的了,接下来咱们剖析下代码拆散的一种非凡场景——动静导入

动静导入

代码拆散是 webpack 中最引人注目的个性之一。此个性可能把代码拆散到不同的 bundle 中,而后能够按需加载或并行加载这些文件。代码拆散能够用于获取更小的 bundle,以及管制资源加载优先级,如果应用正当,会极大影响加载工夫。

常见的代码宰割有以下几种办法:

  • 入口终点:应用 entry 配置手动地拆散代码。
  • 避免反复:应用 Entry dependencies 或者 SplitChunksPlugin 去重和拆散 chunk。
  • 动静导入:通过模块的内联函数调用来拆散代码。

本文咱们次要看看动静导入,咱们在 src 上面新建一个文件 another.js

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

批改 index.js

import sayHello from './sayHello';console.log(sayHello, sayHello('Gopal'));// 单纯为了演示,就是有条件的时候才去动静加载if (true) {  import('./Another.js').then(res => console.log(res))}

咱们来看下打包进去的内容,疏忽 .map 文件,能够看到多出一个 0.bundle.js 文件,这个咱们称它为动静加载的 chunkbundle.js 咱们称为主 chunk

输入的代码的话,主 chunk 看这里,动静加载的 chunk 看这里 ,上面是针对这两份代码的剖析

主 chunk 剖析

咱们先来看看主 chunk

内容多了很多,咱们来细看一下:

首先咱们留神到,咱们动静导入的中央编译后变成了以下,这是看起来就像是一个异步加载的函数

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 这个函数的实现

__webpack_require__.e ——应用 JSONP 动静加载

// 已加载的chunk缓存var installedChunks = {  "main": 0};// ...__webpack_require__.e = function requireEnsure(chunkId) {  // promises 队列,期待多个异步 chunk 都加载实现才执行回调  var promises = [];  // JSONP chunk loading for javascript  var installedChunkData = installedChunks[chunkId];  // 0 代表曾经 installed  if(installedChunkData !== 0) { // 0 means "already installed".    // a Promise means "currently loading".    // 指标chunk正在加载,则将 promise push到 promises 数组    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加载的三种状态并缓存在 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";      // 申请超时的时候间接调用办法完结,工夫为 120 s      var timeout = setTimeout(function(){        onScriptComplete({ type: 'timeout', target: script });      }, 120000);      script.onerror = script.onload = onScriptComplete;      // 设置加载实现或者谬误的回调      function onScriptComplete(event) {        // avoid mem leaks in IE.        // 避免 IE 内存泄露        script.onerror = script.onload = null;        clearTimeout(timeout);        var chunk = installedChunks[chunkId];        // 如果为 0 则示意已加载,次要逻辑看 webpackJsonpCallback 函数        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);          }          installedChunks[chunkId] = undefined;        }      };      head.appendChild(script);    }  }  return Promise.all(promises);};
  • 能够看出将 import() 转换成模仿 JSONP 去加载动静加载的 chunk 文件
  • 设置 chunk 加载的三种状态并缓存在installedChunks中,避免chunk反复加载。这些状态的扭转会在 webpackJsonpCallback 中提到

    // 设置 installedChunks[chunkId]installedChunkData = installedChunks[chunkId] = [resolve, reject];
    • installedChunks[chunkId]0,代表该 chunk 曾经加载结束
    • installedChunks[chunkId]undefined,代表该 chunk 加载失败、加载超时、从未加载过
    • installedChunks[chunkId]Promise对象,代表该 chunk 正在加载

看完__webpack_require__.e,咱们晓得的是,咱们通过 JSONP 去动静引入 chunk 文件,并依据引入的后果状态进行解决,那么咱们怎么晓得引入之后的状态呢?咱们来看异步加载的 chunk 是怎么的

异步 Chunk

// window["webpackJsonp"] 实际上是一个数组,向中增加一个元素。这个元素也是一个数组,其中数组的第一个元素是chunkId,第二个对象,跟传入到 IIFE 中的参数一样(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

次要做的事件就是往一个数组 window['webpackJsonp'] 中塞入一个元素,这个元素也是一个数组,其中数组的第一个元素是 chunkId,第二个对象,跟主 chunk 中 IIFE 传入的参数相似。要害是这个 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 办法的实现批改为 webpackJsonpCallback// 这样咱们在异步 chunk 中执行的 window['webpackJsonp'].push 其实是 webpackJsonpCallback 函数。jsonpArray.push = webpackJsonpCallback;jsonpArray = jsonpArray.slice();// 对已在数组中的元素顺次执行webpackJsonpCallback办法for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);var parentJsonpFunction = oldJsonpFunction;

jsonpArray 就是 window["webpackJsonp"] ,重点看上面这一句代码,当执行 push 办法的时候,就会执行 webpackJsonpCallback,相当于做了一层劫持,也就是执行完 push 操作的时候就会调用这个函数

jsonpArray.push = webpackJsonpCallback;

webpackJsonpCallback ——加载完动静 chunk 之后的回调

咱们再来看看 webpackJsonpCallback 函数,这里的入参就是动静加载的 chunkwindow['webpackJsonp'] push 进去的参数。

var installedChunks = {  "main": 0};    function webpackJsonpCallback(data) {  // window["webpackJsonp"] 中的第一个参数——即[0]  var chunkIds = data[0];  // 对应的模块详细信息,详见打包进去的 chunk 模块中的 push 进 window["webpackJsonp"] 中的第二个参数  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 中设置的状态    // 示意正在执行的chunk,退出到 resolves 数组中    if(installedChunks[chunkId]) {      resolves.push(installedChunks[chunkId][0]);    }    // 标记成曾经执行完    installedChunks[chunkId] = 0;  }  // 挨个将异步 chunk 中的 module 退出主 chunk 的 modules 数组中  for(moduleId in moreModules) {    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {      modules[moduleId] = moreModules[moduleId];    }  }  // parentJsonpFunction: 原始的数组 push 办法,将 data 退出 window["webpackJsonp"] 数组。  if(parentJsonpFunction) parentJsonpFunction(data);  // 等到 while 循环完结后,__webpack_require__.e 的返回值 Promise 失去 resolve  // 执行 resolove  while(resolves.length) {    resolves.shift()();  }};

当咱们 JSONP 去加载异步 chunk 实现之后,就会去执行 window["webpackJsonp"] || []).push,也就是 webpackJsonpCallback。次要有以下几步

  • 遍历要加载的 chunkIds,找到未执行完的 chunk,并退出到 resolves 中
for(;i < chunkIds.length; i++) {  chunkId = chunkIds[i];  // 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]  // 这个能够看 __webpack_require__.e 中设置的状态  // 示意正在执行的chunk,退出到 resolves 数组中  if(installedChunks[chunkId]) {    resolves.push(installedChunks[chunkId][0]);  }  // 标记成曾经执行完  installedChunks[chunkId] = 0;}
    • 这里未执行的是非 0 状态,执行完就设置为0
    • installedChunks[chunkId][0] 实际上就是 Promise 构造函数中的 resolve

      // __webpack_require__.e var promise = new Promise(function(resolve, reject) {    installedChunkData = installedChunks[chunkId] = [resolve, reject];});
    • 挨个将异步 chunk 中的 module 退出主 chunkmodules 数组中
    • 原始的数组 push 办法,将 data 退出 window["webpackJsonp"] 数组
    • 执行各个 resolves 办法,通知 __webpack_require__.e 中回调函数的状态

    只有当这个办法执行实现的时候,咱们才晓得 JSONP 胜利与否,也就是script.onload/onerror 会在 webpackJsonpCallback 之后执行。所以 onload/onerror 其实是用来查看 webpackJsonpCallback 的完成度:有没有将 installedChunks 中对应的 chunk 值设为 0

    动静导入小结

    大抵的流程如下图所示

    总结

    本篇文章剖析了 webpack 打包主流程以及和动静加载状况下输入代码,总结如下

    • 总体的文件就是一个 IIFE——立刻执行函数
    • webpack 会对加载过的文件进行缓存,从而优化性能
    • 次要是通过 __webpack_require__ 来模仿 import 一个模块,并在最初返回模块 export 的变量
    • webpack 是如何反对 ES Module
    • 动静加载 import() 的实现次要是应用 JSONP 动静加载模块,并通过 webpackJsonpCallback 判断加载的后果

    参考

    • 剖析 webpack 打包后的文件
    • webpack 打包产物代码剖析
    • 『Webpack系列』—— 路由懒加载的原理