面试官:Webpack 究竟打包出来的是什么?

Posted 小生方勤

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:Webpack 究竟打包出来的是什么?相关的知识,希望对你有一定的参考价值。

前言

Webpack 作为普遍使用的打包工具,在开发和发布的过程中产出的代码结构,你是否关心过?本文为你揭开它的神秘面纱。

1、开发模式

一般情况,开发的过程都会使用 devServer 并开启 hot 热更新。假如我们有一个页面入口文件 index.js 和依赖模块 dateUtils.js,代码如下:

src\\pages\\index\\index.js

import dateUtils from '@/utils/dateUtils'
dateUtils.print()


src\\utils\\dateUtils.js

export default {
  print() {
    console.log('DateUtils.js==>>print', new Date())
  }
}

Ok,我们来看打包后的代码:

(function(modules) { // webpackBootstrap
    function hotCreateRequire(moduleId) {
      var me = installedModules[moduleId];
      if (!me) return __webpack_require__;
      var fn = function(request) {
        //省略
      }
      return fn
    }
    function hotCreateModule(moduleId) {
    }

    // 模块缓存,被执行过的模块都会放到这里面
       var installedModules = {};

       // require 函数
       function __webpack_require__(moduleId) {

           // 检查模块是否在缓存中,有就取出来返回模块的 exports 属性
           if(installedModules[moduleId]) {
               return installedModules[moduleId].exports;
           }
           // 创建模块并放入缓存
           var module = installedModules[moduleId] = {
               i: moduleId,
               l: false,
               exports: {},
               hot: hotCreateModule(moduleId),
               parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
               children: []
           };

           // 执行模块
           modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

           // 修改标识
           module.l = true;

           // 返回模块的 exports 属性
           return module.exports;
       }

    // 省略代码

    // 加载入口模块
       return hotCreateRequire(0)(__webpack_require__.s = 0); 
  }
  ({
    // 省略掉其他模块代码
    "./src/pages/index/index.js":
    (function(module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _utils_dateUtils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @/utils/dateUtils */ "./src/utils/dateUtils.js");

      _utils_dateUtils__WEBPACK_IMPORTED_MODULE_0__["default"].print();
    }),

    "./src/utils/dateUtils.js":
    (function(module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      __webpack_exports__["default"] = ({
        print: function print() {
          console.log('DateUtils.js==>>print', new Date());
        }
      });
    }),

    0:
    (function(module, exports, __webpack_require__) {

      __webpack_require__(/*! G:\\WebDev\\webpack-study\\node_modules\\webpack-dev-server\\client\\index.js?http://0.0.0.0:85 */"./node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:85");
      __webpack_require__(/*! G:\\WebDev\\webpack-study\\node_modules\\webpack\\hot\\dev-server.js */"./node_modules/webpack/hot/dev-server.js");
      module.exports = __webpack_require__(/*! G:\\WebDev\\webpack-study\\src\\pages\\index\\index.js */"./src/pages/index/index.js");
    })
  });

打包后的代码在浏览器中格式化之后非常多,对于我们来说,我们的关注放在代码块和打包后的代码执行流程上,精简一下:

(function(modules) { // webpackBootstrap
    函数体代码
  }
  // 省略调其他模块代码
  ({
    "./src/pages/index/index.js":
    (function(module, __webpack_exports__, __webpack_require__) {
    }),

    "./src/utils/dateUtils.js":
    (function(module, __webpack_exports__, __webpack_require__) {
    }),

    0:
    (function(module, exports, __webpack_require__) {
    })
  });

我们看到打包后的代码其实就是一个 IIFE。这个函数接受一个对象类型的参数,其中这个参数的 key 就是模块路径,value 则是对模块代码包裹后的一个函数,该函数有几个固定参数(这个其实可以解释 Node 中模块文件中 require 和 module 是如何来的?其实就是 Node 在模块之外包装了一层,把 require 和 module 给传了进来)。暂且先不管参数具体是什么,我们接着看函数体里是什么:

(function(modules) { // webpackBootstrap
    function hotCreateRequire(moduleId) {
      var me = installedModules[moduleId];
      if (!me) return __webpack_require__;
      var fn = function(request) {
        //省略
      }
      return fn
    }
    function hotCreateModule(moduleId) {
    }

    // 模块缓存,被执行过的模块都会放到这里面
       var installedModules = {};

       // require 函数
       function __webpack_require__(moduleId) {

           // 检查模块是否在缓存中,有就取出来返回模块的 exports 属性
           if(installedModules[moduleId]) {
               return installedModules[moduleId].exports;
           }
           // 创建模块并放入缓存
           var module = installedModules[moduleId] = {
               i: moduleId,
               l: false,
               exports: {},
               hot: hotCreateModule(moduleId),
               parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
               children: []
           };

           // 执行模块
           modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

           // 修改标识
           module.l = true;

           // 返回模块的 exports 属性
           return module.exports;
       }

    // 省略代码

    // 加载入口模块
       return hotCreateRequire(0)(__webpack_require__.s = 0); 
  }
  ({
    // 省略调其他模块代码
  });

我们看函数体里定义了很多对象和方法,含有 hot 的部分基本上和热更新模块相关。这些是被添加进来的代码,最终上线是没有的。我们直接看函数体的最后一行,它执行了 hotCreateRequire(0)(__webpack_require__.s = 0)。这一行两个括号,很明显 hotCreateRequire 是返回了一个函数出来。我们接下来看看看 hotCreateRequire :

function hotCreateRequire(moduleId) {
         var me = installedModules[moduleId];
         if (!me) return __webpack_require__;
         var fn = function(request) {
             if (me.hot.active) {
                 if (installedModules[request]) {
                     if (installedModules[request].parents.indexOf(moduleId) === -1) {
                         installedModules[request].parents.push(moduleId);
                     }
                 } else {
                     hotCurrentParents = [moduleId];
                     hotCurrentChildModule = request;
                 }
                 if (me.children.indexOf(request) === -1) {
                     me.children.push(request);
                 }
             } else {
                 console.warn(
                     "[HMR] unexpected require(" +
                         request +
                         ") from disposed module " +
                         moduleId
                 );
                 hotCurrentParents = [];
             }
             return __webpack_require__(request);
         };
  }
  //
  return fn
}

启动的时候,传入了 moduleId 是 0,在 installedModules 中找不到模块,直接返回了 webpack_require__。然后继续执行这个返回的函数,并传入参数 __webpack_require.s = 0,那么其实是执行了下面这个函数代码:

(function(module, exports, __webpack_require__) {

  __webpack_require__(/*! G:\\WebDev\\webpack-study\\node_modules\\webpack-dev-server\\client\\index.js?http://0.0.0.0:85 */"./node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:85");
  __webpack_require__(/*! G:\\WebDev\\webpack-study\\node_modules\\webpack\\hot\\dev-server.js */"./node_modules/webpack/hot/dev-server.js");
  module.exports = __webpack_require__(/*! G:\\WebDev\\webpack-study\\src\\pages\\index\\index.js */"./src/pages/index/index.js");
})

这个函数,通过 __webpack_require__ 执行了三个模块,前两个是在 devServer 添加的入口文件,为了实现开发和热更新的一些功能。最后一行 moduel.exports 属性是我们的 index.js 模块执行的返回,这样就执行到了我们的程序入口。

在 index.js 中我们看到通过 import 导入的 dateutils 也是通过 __webpack_require__ 进行模块引用的,并对里面的方法进行了调用。那么,以此类推,所有的模块引用都是这么实现的。

2、生产模式

我们把这个代码使用生产模式进行打包,可以得到如下代码:

!function(e) {
    // 模块缓存
    var t = {};
    // 模块require函数
    function n(r) {
        if (t[r])
            return t[r].exports;
        var o = t[r] = {
            i: r,
            l: !1,
            exports: {}
        };
        return e[r].call(o.exports, o, o.exports, n),
        o.l = !0,
        o.exports
    }
    // 执行入口
    n(n.s = 0)
}([function(e, t, n) {
    e.exports = n(1)
}
, function(e, t, n) {
    "use strict";
    n.r(t),
    n(2).a.print()
}
, function(e, t, n) {
    "use strict";
    t.a = {
        print: function() {
            console.log("DateUtils.js==>>print", new Date)
        }
    }
}
]);
//# sourceMappingURL=entry_index~._m_nosources-source-map.min.js.map

因为,我们的代码足够简单,没有其他的依赖模块被打进来。和开发模式类似,整体上它也是一个 IIFE,只不过做了一些混淆和压缩的处理。另外,参数变成了数组类型。同时,去掉了开发模式下的一些辅助代码。我们很容易和上面的东西做一些对应(见注释)。

3、再进一步

OK,开发模式和生产模式的代码结构几乎一样。只是参数类型略有差别,开发模式下,key 是作为模块的标识来使用的。热更新开启后,修改模块可以很轻易的修改该模块的代码。

另外,我们的项目代码里无论是使用 require/module.exports 这种 ComomonJS 的模块化方案,还是采用 import/export 的 ESM 模块化方案。通过 webpack 打包最终其实是 ComomonJS 的模块化方案,也就是说,它可以像 CommonJs 那样进行动态模块引用。

还有就是如果使用了 ESM 的 export (仅有 export.default 这种方式除外,它和 CommonJS 没什么差别,都是只导出了一个对象出来),在打包后的代码有一些区别。比如:

esmtest.js

export let flag = false
setTimeout(()=>{
  flag = true
}, 1000)



index.js

const j = require('./js/esmtest')
console.log('0', j, j.flag) //  f.falg 为 false

setTimeout(()=>{
  const j = require('./js/esmtest')
  console.log('2000', j, j.flag) // f.falg 为 true
}, 2000)


打包后代码

"./src/pages/index/js/esmtest.js":
(function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "flag", function() { return flag; });
    var flag = false;

    var obj = {};
    setTimeout(function () {
      flag = true;
      obj.objFlag = true;
    }, 1000);
})

这里出来了 .d 和 .r 方法,这个在之前的函数体里有定义:

// 为 exports 定义 getter 函数
     __webpack_require__.d = function(exports, name, getter) {
         if(!__webpack_require__.o(exports, name)) {
             Object.defineProperty(exports, name, { enumerable: true, get: getter });
         }
     };

     // 在 exports 上定义 __esModule 属性
     __webpack_require__.r = function(exports) {
         if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
             Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
         }
         Object.defineProperty(exports, '__esModule', { value: true });
     };

Ok,通过定义 getter 函数的方式,我们在 esmtest.js 中对于 flag 的改动代码起了作用,随后在 index.js 模块 2s 后的定时器中的打印也说明了这一点。

最后一个是如果采用代码分割或动态引入的情况下,会怎么样?我们直接上打包前的代码:

index.js

import('./js/dynamicImport').then(dm=>{
  dm.default.hello(123)
})

dynamicImport.js

export default {
  hello(msg){
    console.log('dynamicImport', msg)
  }
}

我们再来看在开发模式下打包的代码在浏览器里多了一个 chunk 文件请求,里面打包后的代码如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
  "./src/pages/index/js/dynamicImport.js":
  (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony default export */ __webpack_exports__["default"] = ({
      hello: function hello(msg) {
        console.log('dynamicImport', msg);
      }
    });
  })
}]);

主体代码没什么差别,就是我们的 dynamicImport.js 模块的代码,然后外面调用了 window["webpackJsonp"].push 这个方法。那么这个方法是哪里来的,我们看一下 index.js 打包后的代码部分:

(function(modules) { // webpackBootstrap
      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()();
        }
      };


      __webpack_require__.e = function requireEnsure(chunkId) {
             var promises = [];
             var installedChunkData = installedChunks[chunkId];
             if(installedChunkData !== 0) { // 0 means "already installed".
                 if(installedChunkData) {
                     promises.push(installedChunkData[2]);
                 } else {

                     // 开始请求 chunk 文件
                     var script = document.createElement('script');
                     var onScriptComplete;

                     onScriptComplete = function (event) {
                         // avoid mem leaks in IE.
                         script.onerror = script.onload = null;
                         clearTimeout(timeout);
                         var chunk = installedChunks[chunkId];
                         if(chunk !== 0) {
                             if(chunk) {
                                 var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                                 var realSrc = event && event.target && event.target.src;
                                 error.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';
                                 error.name = 'ChunkLoadError';
                                 error.type = errorType;
                                 error.request = realSrc;
                                 chunk[1](error);
                             }
                             installedChunks[chunkId] = undefined;
                         }
                     };

                     document.head.appendChild(script);
                 }
             }
             return Promise.all(promises);
         };

      var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
      jsonpArray.push = webpackJsonpCallback;

      return hotCreateRequire(0)(__webpack_require__.s = 0);
    }
    ({
    "./src/pages/index/index.js":
    (function(module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);

      __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./js/dynamicImport */ "./src/pages/index/js/dynamicImport.js")).then(function (dm) {
        console.log('dm', dm);
        dm.default.hello(123);
      });
    }),
    // 省略代码
  });

我们简单看一下上面的这部分代码。首先,这部分代码先执行,在 window 上挂载了一个 window["webpackJsonp"],并为它定义了一个 push 方法就是 webpackJsonpCallback,用于从异步请求的文件中加载模块。此外,定义一个 __webpack_require__.e 方法去异步请求 chunk 文件,并返回一个 Promise 对象。

至此,关于 Webpack 打包后的内容部分的介绍全部结束,你学废了吗?

以上是关于面试官:Webpack 究竟打包出来的是什么?的主要内容,如果未能解决你的问题,请参考以下文章

webpack一步步实现实时打包打包vue打包csslessscss文件babel用法

又到金三银四,还敢不重视 Webpack 打包原理吗?

03 校招面试究竟在面什么?

面试官提问webpack篇

面试官提问webpack篇

函数式编程的一些心得与体会