乐趣区

关于javascript:webpack的加载

webpack 在前端开发中很罕用,但很多人对于 webpack 的常识仅仅停留在理解阶段,以及一些配置操作,对于 webpack 更多的运行过程都不甚了解。心愿本文带你更多的常识。

个别状况下,webpack 打包只会生成被称为 bundle 的 js 文件。bundle 文件可能会很长,并且可能被混同了,所以个别开发者都不看这个文件的具体内容,只晓得加载这个文件能够运行就是了。

那么 bundle 文件中的逻辑是什么样的呢?

间接看 bundle 的话的确不太清晰,能够批改一些配置(mode=development,独自分出 manifest 文件),让代码更加清晰

这样配置的状况下,除了生成一些 bundle 文件外,还有一个 manifest 文件,文件比拟小,并且因为应用了 development 模式,这个 manifest 文件是能够浏览的。

manifest 文件是 bundle 运行的入口文件,以下是对于 manifest 中逻辑的解读,读者们也能够参考代码自行浏览和了解,举荐本人梳理一遍代码。

对于文中呈现的代码片段,webpack 版本为 4,在配置为 mode: development 状况下的编译后果,//是代码本来的正文,////是笔者写的正文,正文内容包含对于代码逻辑的注解、浏览中纳闷和思考的问题。

主体逻辑

整个 manifest 文件是一个自执行函数 IIFE。大部分的内容是对于变量和函数的定义。

在 IIFE 中的间接运行的代码如下

var installedModules = {}
var installedChunks = {
  // 默认 manifest 是加载的
  "webpackManifest": 0
};

var deferredModules = [];

//// 定义 / 获取 webpackJsonp,绑定 push 函数
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
//// 重定义 push
jsonpArray.push = webpackJsonpCallback;
//// 复制数组
jsonpArray = jsonpArray.slice();
//// 预计是模块的加载逻辑
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

// run deferred modules from other chunks
checkDeferredModules();

代码逻辑如下 (后文称为 逻辑 A ):

  1. 定义一些变量,获取名为 window.webpackJsonp 的数组
  2. 在保留 webpackJsonp 的旧 push 办法之后,将 webpackJsonpCallback 函数作为新的 push 函数
  3. 对 webpackJsonp 中的内容进行循环,调用webpackJsonpCallback()
  4. 定义parentJsonpFunction
  5. 调用 checkDeferredModules() 函数

其中第 1、2、3 步次要逻辑是定义一些变量获取 webpackJsonp,并且循环内容进行解决,第 4、5 步在这里临时看不出作用,或者不晓得是做什么的。

看到这里会有一个疑难:webpackJsonp 数组外面到底装的是什么?

webpackJsonp 变量

为了理解 webpackJsonp 中的内容,须要找到它在哪里内应用了。
让咱们来看看 bundle 代码,以下是名为 main 的 bundle 中的代码片段,隐去了大量的代码细节。

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["main"],
  {'/7QA': function(module, __webpack_exports__, __webpack_require__) {eval('模块具体代码')
    },
    'edgnb': function(module, __webpack_exports__, __webpack_require__) {eval('模块具体代码')
    },
    // ... 其余相似代码
  },
  [["/7QA","webpackManifest","vendors"]]
]);

能够看到,为了零碎健壮性思考,首先确保 webapckJsonp 变量的存在。其次是调用 push 办法并且传入一个数组,该数组中次要蕴含三个元素(之后称为 三元组 )。这三个元素具体是做什么用的暂且不论。
如果细看的话,三元组 中第二个元素内存储着大量模块代码,即业务代码,或者是援用的包代码,或者是依赖图构造中的包代码。

再看看其余的 bundle 文件,其中内容也是类似的。

webpackJsonpCallback 函数

在上一大节中调用了 push 办法,但在逻辑 A 的第 2 步中,应用 webpackJsonpCallback 函数替换了本来的 push 办法,所以来看下其中的实现

//// data 即三元组
function webpackJsonpCallback(data) {
  //// 以 webpackJsonp 大节中的代码为例
  //// chunkIds = ["main"]
  var chunkIds = data[0];
  //// moreModules = {"/7QA": function() {}}
  var moreModules = data[1];
  //// executeModules = [["/7QA","webpackManifest","vendors"]]
  var executeModules = data[2];
  
  // 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];
      //// 了解为,如果 installChunks 存在 chunkId,并且对应的内容存在
      //// 然而默认状况下,应该都是没有的,所以这里会跳过
      
      //// 也可能 installChunks 起初会是 chunks 内容
      if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);
      }
      //// webpackManifest 这个模块也会被标记为 0,然而这和下面的 installedChunks[chunkId][0] 是否抵触,因为这里是一个数字,而下面是一个数组? 可能是为了兼容以前的逻辑?installedChunks[chunkId] = 0;
  }
  //// 写入到 modules 中了
  for(moduleId in moreModules) {if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        //// modules 应该是一个数组,然而 moduleId 感觉是一个奇怪的字符,例如 /7QA 这样,这个是否会有问题
          modules[moduleId] = moreModules[moduleId];
      }
  }
  //// 从新再调用 push 函数
  if(parentJsonpFunction) parentJsonpFunction(data);
  
  //// 弹出元素,并且运行
  while(resolves.length) {resolves.shift()();}
  
  // add entry modules from loaded chunk to deferred list
  //// 三元组最初一个元素的内容会被放在 deferredModules
  deferredModules.push.apply(deferredModules, executeModules || []);
  
  // run deferred modules when all chunks ready
  return checkDeferredModules();}

代码逻辑如下 (后文称为 逻辑 B ):

  1. 对于 chunkIds,在 installedChunks 对象中将其标记为 0
  2. 对于 moreModules,将其中的模块存储在 modules 变量中
  3. 调用 parentJsonpFunction 函数
  4. 对于 deferredModules,将其放入 executeModules 中
  5. 调用 checkDeferredModules 函数

对于 1、2、4 来说,要么是进行一些标记,要么是存储数据,具体为什么要存,存下来怎么用,暂且不论。

对于第 3 步,和 逻辑 A 第 4 步 响应,即从新调用本来的 push 函数,将 三元组 退出

对于第 5 步,和 逻辑 A 第 5 步 雷同。

仔细的读者可能会发现,在第 2 步中,第一次呈现了名为 modules 的变量,那么这个变量是哪里来的?或者是在哪里定义的?

modules 变量

回到结尾细看 IIFE 代码,就会发现 modules 其实是 IIFE 的参数。IIFE 代码如下:

//// 初始时是一个空数组
(function(modules) {//// 主体逻辑})([]);

有时候也能够看到这样的代码,间接将模块放入 modules

(function(modules) {//// 主体逻辑})({
  //// 省略细节
  "/7QA": function() {},
  "edgnb": function() {},
});

checkDeferredModules 函数

逻辑 A 逻辑 B 中剩下没弄懂的逻辑都是第 5 步,函数中的具体代码如下:

function checkDeferredModules() {
  var result;
  //// 依据之前的代码,这里 deferredModules = [["/7QA","webpackManifest","vendors"]]
  for(var i = 0; i < deferredModules.length; i++) {//// deferredModule = ["/7QA","webpackManifest","vendors"]
      var deferredModule = deferredModules[i];
      var fulfilled = true;
      for(var j = 1; j < deferredModule.length; j++) {var depId = deferredModule[j];
          //// 如果有存在不是 0 状况下
          if(installedChunks[depId] !== 0) fulfilled = false;
      }
      if(fulfilled) {
        //// 不再查看
          deferredModules.splice(i--, 1);
          //// __webpack_require__(__webpack_require__.s = '/7QA')
          result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
      }
  }
  
  return result;
}

代码逻辑如下:

  1. 查看 三元组 中第三个元素的数组中,从下标为 1 开始,是否在 installChunks 中都标记为 0。
  2. 如果第一个的后果为真,那么就调用__webpack_require__函数,参数是下标为 0 所对应的模块,最初返回后果。

在之前的代码中,即 webpackManifestvendors这两个 chunk 都标记的状况下,将调用__webpack_require__(__webpack_require__.s = '/7QA')

__webpack_require__函数

这个函数也是在 IIFE 中定义,具体代码如下

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] = {
      i: moduleId,
      l: false,
      exports: {}};
  // Execute the module function
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  // Flag the module as loaded
  module.l = true;
  // Return the exports of the module
  return module.exports;
}

代码逻辑如下:

  1. 如果 installModules 曾经有 moduleId 对应内容,那么间接返回。
  2. 反之,没有内容时,先向 installModules 中增加内容,并且通过 call 函数的形式调用对应的模块,最初将后果返回。

到这一步,业务模块就曾经运行了。

总结

通过对于 manifest 代码的浏览,能够理解到 webpack bundle 代码的局部运行逻辑,除此之外,IIFE 中还有很多其余代码没有列出,读者能够自行浏览。

退出移动版