webpack 源码解析1

源码解析1

打包文件解析

安装 webpack 插件

yarn add webpack webpack-cli -D

新建 src/index.js data.js,写点写点内容

const data = "webpack 4"

export default data

// index.js
import data from './data'

console.log(data);

我们执行开发环境打包

"dev": "webpack --mode development",

在 dist 目录下会生成一个 main.js 里面有一拖代码,我们把 注释,evel 去掉有用的代码差不多就是下面这一些:

(function (modules) { 
    //  module 缓存的对象
    var installedModules = {};

    function __webpack_require__(moduleId) {
        console.log(moduleId);
        // 检查 module 是否在缓存中
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // 创建一个 module
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };

        // 执行 module 里面的方法
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // 设置加载状态
        module.l = true;

        // 返回 module
        return module.exports;
    }

    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;

    // __webpack_public_path__
    __webpack_require__.p = "";

    return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
    ({

        "./src/data.js": (function (module, __webpack_exports__, __webpack_require__) {

            __webpack_require__.r(__webpack_exports__);
            const data = "webpack 4";
            /* harmony default export */
            __webpack_exports__["default"] = (data);
        }),

        "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
            __webpack_require__.r(__webpack_exports__);
            /* harmony import */
            var _data__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./data */ "./src/data.js");
            console.log(_data__WEBPACK_IMPORTED_MODULE_0__["default"]);
        })
    });

一看到这些 第一反应肯定是 这都是些啥啊,不急,让我们慢慢来分析。

  1. 首先这是一个 立即执行函数,我们传入了一个对象:分别有两个 key 为 ./src/data.js,./src/index.js,每个对应了一个函数.
  2. 来到 函数里面,首先定义了一个 installedModules 的全局对象,它就是来记录传入的 key 是否可以复用
  3. 重点来了 webpack_require, 首先判断传入的 moduleId (也就是./src/data.js,./src/index.js) 是否在 installedModules 里面,如果有直接返回对应缓存里面的 exports,如果没有的就创建一个 module 对象,并 执行 call,最后返回 module.exports
  4. 函数最后返回 webpack_require("./src/index.js") 的结果

webpack_require

ok,我们首先打印下 modules 对象,发现他是如下这样的

{
"./src/data.js": ƒ (module, __webpack_exports__, __webpack_require__)
"./src/index.js": ƒ (module, __webpack_exports__, __webpack_require__)
}

那么我们现在执行 webpack_require函数传入入口文件 ./src/index.js ,第一次肯定不在缓存里面,因此会走创建的过程并执行 modules[moduleId].call,看到这里就清晰了. 哦, modules[moduleId] 就是 我们入口 ./src/data.js 对应的函数,现在 call了一下 执行

我们现在来看看 ./src/index.js 对应的函数

"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _data__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./data */ "./src/data.js");
        console.log(_data__WEBPACK_IMPORTED_MODULE_0__["default"]);
    })

在这个函数 它又去 调用了 webpack_require,并且传入了 moduleId./src/data.js. 哦,看到这个就豁然开朗了, 我 index.js 需要用到 data.js 里面的数据,那么好 我就在 index.js 去 webpack_require 对应的模块,然后 又会走到 data.js 自己的 call,那我们现在来看看 data.js 对应的函数执行

    "./src/data.js": (function (module, __webpack_exports__, __webpack_require__) {
        __webpack_require__.r(__webpack_exports__);
        const data = "webpack 4";
        /* harmony default export */
        __webpack_exports__["default"] = (data);
    }),
  1. 定义一个 const data 的变量
  2. 把 data 变量挂载在 webpack_exports["default"] 属性上, webpack_exports 就是执行 call 的时候 传入的 module.exports 对象
  3. ./src/index.js 看到 data__WEBPACK_IMPORTED_MODULE_0_ 就等于 webpack_require(./src/data.js) 的返回结果, webpack_require 函数返回什么呢,我们在上面看到了 它返回 module.exports
  4. module.exports 上面挂载了 一个 default 属性 也就是 data,我们输入它

我们把上面的 代码 粘贴到浏览器发现 正常输出了 打印

我们在 data.js 里面再引入 output.js

const output = "我是 output"

export default output

我们执行打包,看下入口的传入

{

    "./src/data.js":
      (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _output__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/output.js");
        const data = "webpack 4";
        console.log(_output__WEBPACK_IMPORTED_MODULE_0__["default"]);
        __webpack_exports__["default"] = (data);
      }),

    "./src/index.js":
      (function (module, __webpack_exports__, __webpack_require__) {
        __webpack_require__.r(__webpack_exports__);
        var _data__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/data.js");
        console.log(_data__WEBPACK_IMPORTED_MODULE_0__["default"]);
      }),

    "./src/output.js":
      (function (module, __webpack_exports__, __webpack_require__) {
        __webpack_require__.r(__webpack_exports__);
        const output = "我是 output"
        __webpack_exports__["default"] = (output);
      })
  }

看到了吗? 我们 index.js 引入了 data.js 模块里面的变量, data.js 又引入了 output.js 里面的变量,他们加载顺序会如下

  1. 执行方法 webpack_require("./src/index.js");
  2. 在对应的 "./src/index.js" 里面又会执行 webpack_require("./src/data.js")
  3. 同样在 webpack_require("./src/output.js") 里面 又会执行 webpack_require("./src/output.js"), 返回 webpack_exports["default"] 并执行打印输出, 再把自身 的 data 变量 挂载到自己 module 的 webpack_exports["default"] 上
  4. ./src/index.js 接受 webpack_require 的返回在 default 属性上取到 data 并打印输出

因此是先打印 "我是 output" 再是 webpack 4, 在浏览器执行发现和我们推理的打印顺序一样。

异步加载文件

对于一些异步文件加载我们可以这样引用

const data = "webpack 4"

import('./output').then(_ => {
    console.log(_.default);
})
export default data

我们现在再来看看打包后的文件,现在有了一个 main.js 和一个 0.js,先看看 0.js

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], {
  "./src/output.js": (function (module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    const output = "我是 output"
    __webpack_exports__["default"] = (output);
  })
}]);

main.js 整理后 主要的 js 如下:

(function (modules) { 
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];

    var moduleId, chunkId, i = 0, resolves = [];
    // 收集模块 将所有 chunkIds 标记为已加载 chunkId=0
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        // installedChunks[chunkId] = [resolve, reject]
        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);
    // 执行每个 resolve
    while (resolves.length) {
      resolves.shift()();
    }
  };
  // The module cache
  var installedModules = {};

  var installedChunks = {
    "main": 0
  };

  // script path function
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".js"
  }
  function __webpack_require__(moduleId) {
      ...
  }

  // 根据  chunkId 加载
  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    var installedChunkData = installedChunks[chunkId];
    // // 0 标志已缓存.
    if (installedChunkData !== 0) {

      //  Promise 当前正在加载
      if (installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
        // 设置缓存
        var promise = new Promise(function (resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        promises.push(installedChunkData[2] = promise);
        // start chunk loading
        var script = document.createElement('script');
        var onScriptComplete;

        script.charset = 'utf-8';
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.src = jsonpScriptSrc(chunkId);

        // create error before stack unwound to get useful stacktrace later
        var error = new Error();
        onScriptComplete = function (event) {
          // avoid mem leaks in IE.
          script.onerror = script.onload = null;
          clearTimeout(timeout);
          var chunk = installedChunks[chunkId];
          console.log(chunk);
          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;
          }
        };
        var timeout = setTimeout(function () {
          onScriptComplete({ type: 'timeout', target: script });
        }, 120000);
        script.onerror = script.onload = onScriptComplete;
        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
  };

  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;
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  ({
    "./src/data.js": (function (module, __webpack_exports__, __webpack_require__) {
      const data = "webpack 4"
      __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./output */ "./src/output.js")).then(_ => { console.log(_.default); })
      __webpack_exports__["default"] = (data);
    }),

    "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
     ...
    })
  });

首先入口文件 还是 ./src/index.js,它会再去加载 ./src/data.js 这和我们上面的同步 步骤完全一致. 但是在 data.js 中使用了异步加载的方法,对于的打包文件中 可以看到使用了 webpack_require.e 这样的一个方法,我们来仔细分析下这个方法

webpack_require.e

webpack_require.e 方法会接受一个 chunkId,也就是我们打包的 "0.js"的 0,然后依次做了以下操作

  1. 申明一个总 promises,判断 chunkId 是否已经被缓存过, 0 标志着缓存
  2. 设置一个 promise,并设置 installedChunks[chunkId] 分别添加 resolve,reject 以及 promise 本身,并把 promise 添加到总的 promises
  3. 创建一个 script 标签,并设置超时时间为 120 秒
  4. 处理 script 加载异常的情况
  5. 执行 document.head.appendChild(script) 把script 追加到页面上;
  6. 然后 return Promise.all(promises);

我们有多少个异步就创建了多少个 promise 并维护在了 installedChunks 对象里面, 我们在 ./src/data.js 加载异步的时候 执行了

__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./output */ "./src/output.js")).then(_ => { console.log(_.default); })

webpack_require.e 返回了 Promise.all(promises),只有 promises 里面的每一个promise 执行 resolve 外面大的才能 resolve,关键这里的代码 我们并没有看到 每个小的 promise 执行 resolve啊???

webpackJsonpCallback

我们顺着主代码往下看, 有这么一段 代码

 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;

啥意思呢这段代码, window["webpackJsonp"] 我们看着很熟悉, 它和 0.js 好像是一样的东西

window[“webpackJsonp”].push 指向 webpackJsonpCallback 函数。我们在 执行上面操作的时候会去创建一个标签,里面的内容就是 window["webpackJsonp"] || []).push,异步文件通过这个指针,把内容传进 webpackJsonpCallback 函数内。

webpackJsonpCallback 做了那些事情呢

  1. 收集模块 将所有 chunkIds 标记为已加载 chunkId=0
  2. 创建一个 resolves 数组并添加 对应 chunkId 的 第 0 项,也就是我们上面的 installedChunks[chunkId] 添加的第一项 resolve
  3. 把异步组件的入口挂载到 modules 对象上
  4. while 把 每个 promise 执行 resolve()

于是乎 也跟 下面这段代码对应上了

__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./output */ "./src/output.js")).then(_ => { console.log(_.default); })

总结

webpack 打包文件加载流程如下:

  1. 执行入口文件 webpack_require("./src/index.js"),执行 modules[moduleId].call
  2. 如果文件里面有依赖别人 则继续调用 webpack_require 并传入对应的入口,把返回结果挂载到 default
  3. 如果是异步加载 则 先执行 webpack_require.e 创建 script 标签,设置超时时间为120,并返回Promisea.all()
  4. 通过 webpackJsonpCallback 函数 去依次执行 promise 的 resolve,然后就可以执行 then 方法了,然后里面再传入一个 webpack_require.bind() 的函数 再次 then 后 就可以拿到 对应模块的变量
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342