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

news/2024/7/19 13:33:07 标签: vue, js, java, javascript, python
js_content">

前言

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 打包后的内容部分的介绍全部结束,你学废了吗?


http://www.niftyadmin.cn/n/1251125.html

相关文章

golang 重要知识:mutex

摘要 Go 号称是为了高并发而生的&#xff0c;在高并发场景下&#xff0c;势必会涉及到对公共资源的竞争。当对应场景发生时&#xff0c;我们经常会使用 mutex 的 Lock() 和 Unlock() 方法来占有或释放资源。虽然调用简单&#xff0c;但 mutex 的内部却涉及挺多的。今天&#x…

又一家巨头决定降级 React,全面拥抱 Vue3.0!

在 Vue2.0 时代&#xff0c;国内大厂的前端开发框架&#xff0c;几乎清一色偏向 React。因为对于业务成熟的公司而言&#xff0c;一个项目&#xff0c;可能会包含很多非侵入式的代码和服务&#xff0c;并不是功能实现了就万事大吉。但随着 Vue3.0 横空出世&#xff0c;字节跳动…

golang 重要知识:RWMutex 读写锁分析

摘要 在上一篇文章 golang 重要知识&#xff1a;mutex 里我们介绍了互斥锁 mutex 的相关原理实现。而且在 Go 里除了互斥锁外&#xff0c;还有读写锁 RWMutex&#xff0c;它主要用来实现读共享&#xff0c;写独占的功能。今天我们也顺便分析下读写锁&#xff0c;加深对 Go 锁的…

学完这篇 Nest.js 实战,还没入门的来锤我!(长文预警)

为什么选择Nest.js前面也说了&#xff0c; 大家都说香啊~其次&#xff0c;我之前也使用过Egg.js&#xff0c;19年使用的时候&#xff0c;感觉egg约束性比较强&#xff0c;但是对于内部统一规范还是有好处的&#xff0c;但现在2021了&#xff0c; 已经习惯了TS&#xff0c;但Egg…

nginx安装目录

安装目录详解 路径类型作用/etc/logrotate/nginx配置文件Nginx日志轮转&#xff0c;用于logrotate服务的日志切割 /etc/nginx /etc/nginx/nginx.conf /etc/nginx/conf.d /etc/nginx/conf.d/default.conf 目录&#xff0c;配置文件Nginx主要配置文件 /etc/nginx/fastcgi_params …

golang 重要知识:定时器 timer

摘要 在 Go 里有很多种定时器的使用方法&#xff0c;像常规的 Timer、Ticker 对象&#xff0c;以及经常会看到的 time.After(d Duration) 和 time.Sleep(d Duration) 方法&#xff0c;今天将会介绍它们的使用方法以及会对它们的底层源码进行分析&#xff0c;以便于在更好的场景…

弄懂 SourceMap,前端开发提效 100%

一、什么是 Source Map通俗的来说&#xff0c; Source Map 就是一个信息文件&#xff0c;里面存储了代码打包转换后的位置信息&#xff0c;实质是一个 json 描述文件&#xff0c;维护了打包前后的代码映射关系。关于 Source Map 的解释可以看下 Introduction to JavaScript Sou…

nginx的stub_status模块详解

nginx开启stub_status模块配置方法 location /mystatus {allow 106.87.6.224;deny all;stub_status on;access_log off;}返回各数据项说明&#xff1a; Active connections: 当前nginx正在处理的活动连接数. Server accepts handled requests request_time: nginx总共处理了1…