最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从Webpack打包后的文件分析导入的原理

    正文概述 掘金(大转转FE)   2021-02-02   543

    主流框架中,不论是React还是Vue,都是使用Webpack进行资源的打包,这篇文章我们试着分析打包后的bundle文件来了解下其是如何进行静态和动态导入的。

    文章目录结构如下:

    • bundle文件分析
    • 静态导入文件
    • 动态导入文件

    bundle文件分析

    下面是代码整体的目录结构:

    从Webpack打包后的文件分析导入的原理

    入口文件 main.js

    import a from './a'
    import b from './b'
    

    a.js

    const asyncText = 'async'
    export default asyncText
    

    b.js

    import c from './c'
    const b = 'b'
    export default b
    

    c.js

    export const c = 1
    

    打包后的bundle文件过于冗长,删繁就简之后让我们看看他的主体部分: 从Webpack打包后的文件分析导入的原理

    我们看到整个文件就是一个自执行函数,所传入的参数是经过分析过后的文件路径,我们先来认识两个参数和一个函数:

    • modules​:缓存 ​module​ 代码块,每个 ​module​ 有一个 ​id​,开发环境默认以 ​module​ 所在文件的文件名标识,生产环境默认以一个数字标识。​modules​ 是一个 ​object​, ​key​ 为 ​module id​,​value​ 为对应 ​module​ 的源代码块。

    • ​installedModules​:缓存已经加载过的 ​module​,简单理解就是已经运行了源码中 ​import a from 'xxx'​ 这样的语句。​installedModules​ 是一个 ​object​, ​key​ 为 ​module id​,​value​ 为对应 ​module​ 导出的变量。

    • __webpack_require__: 根据传入的moduleId,判断是否加载过该模块,加载过则直接返回,未加载过则执行该模块代码。

    静态导入文件

    根据上面的内容分析,整个自执行函数执行的时候,传入的参数是根据我们文件内容生成的路径,参数内容如下。

    {
      "./src/a.js":
        function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        const asyncText = "async";
        __webpack_exports__["default"] = asyncText;
      },
      "./src/b.js":
        function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
          var _c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
          /*! ./c */ "./src/c.js"
        );
        const b = "b";
        __webpack_exports__["default"] = b;
      },
    
      "./src/c.js":
        function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony export (binding) */ __webpack_require__.d(
          __webpack_exports__,
          "c",
          function () {
            return c;
          }
        );
        const c = 1;
      },
      "./src/main.js":
        function (module, __webpack_exports__, __webpack_require__) {
          "use strict";
          __webpack_require__.r(__webpack_exports__);
            var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
              "./src/a.js"
          );
        var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
            "./src/b.js"
        );
      }
    }
    

    对象以当前文件路径为key,文件内容为value,发现文件内容里有import,则转化成 __webpack_require__ 函数执行。

    回看我们的自执行函数,当函数执行的时候,会根据 webpack.config 配置首先去执行 moduleId 为 ./src/main.js 的入口文件的内容。

    return __webpack_require__(
      (__webpack_require__.s = "./src/main.js")
    );
    

    执行key为 ./src/main.js 的模块内容时发现其依赖a.js、b.js,依次递归去执行对应的模块内容,由于installedModules的存在,不会再次去执行加载过的模块,依次类推,这就是Webpack导入静态依赖的过程。

    动态导入文件

    前面分析了静态文件的导入,我们现在来改下main.js的内容,如下:

    main.js

    import('./a').then((({ default: text}) => {
      console.log(text)
    }))
    

    在main.js通过动态导入语法导入a.js,生成的dist文件如下:

    从Webpack打包后的文件分析导入的原理

    相比静态导入,动态导入多了个 0.bundle.js,我们再来看看bundle.js文件的主体:

    从Webpack打包后的文件分析导入的原理

    对比静态导入,动态导入多了几个重要的函数和对象installedChunks和__webpack_require__.e,我们还是先来说明下这两个新增成员的具体用途。

    • installedChunks​:缓存已经加载过的 ​chunk​,简单理解就是把其他 ​js​ 文件中的 ​chunk​ 包含的 ​modules​ 同步到了当前文件中。每个 ​chunk​ 有一个 ​id​,默认以一个数字标识。​installedChunks​ 也是一个对象,​key​ 为 ​chunk id​,​value​ 有四种情况:

      • undefined:chunk not loaded
      • null:chunk preloaded/prefetched
      • Promise:chunk loading
      • 0:chunk loaded
    • __webpack_require__.e:根据 installedChunks 检查是否加载过该 chunk,假如没加载过,则发起一个 JSONP 请求去加载 chunk,

    设置一些请求的错误处理,然后返回一个 Promise。

    我们还是来看看传入自执行函数中的参数

    {
      /***/ "./src/main.js": /***/ function (
        module,
        exports,
        __webpack_require__
      ) {
        __webpack_require__.e(/*! import() */ 0)
          .then(__webpack_require__.bind(null, "./src/a.js"))
          .then(({ default: text }) => {
            console.log(text);
          });
        /***/
      }
      /******/
    }
    

    有个新面孔,__webpack_require__.e,函数逻辑如下:

    __webpack_require__.e = function requireEnsure(chunkId) {
      var promises = []; // JSONP chunk loading for javascript
      var installedChunkData = installedChunks[chunkId];
      if (installedChunkData !== 0) {
        if (installedChunkData) {
          promises.push(installedChunkData[2]);
      } else {
          // setup Promise in chunk cache
          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) {
          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;
        }
      };
        var timeout = setTimeout(function () {
          onScriptComplete({ type: "timeout", target: script });
        }, 120000);
        script.onerror = script.onload = onScriptComplete;
        document.head.appendChild(script);
      }
      return Promise.all(promises);
    };
    

    代码有点长,具体做了些什么前面已经说过,不再赘述,我们来看下重点返回的Promise,下面这段代码里,如果你读完了上述代码会发现,我们将promise的resolve和reject保存了起来

    installedChunkData = installedChunks[chunkId] = [
      resolve,
      reject
    ];
    

    但是仅仅在脚本文件的onerror事件中调用了reject,整个函数里并没有调用resolve的地方,那这边是在哪里调用了resolve让函数正常走下去呢?

    我们先来看看我们请求的 0.bundle.js 的内容

    (window["webpackJsonp"] = window["webpackJsonp"] || []).push([
      [0],
      {
        "./src/a.js": function (module, __webpack_exports__, __webpack_require__) {
          "use strict";
          __webpack_require__.r(__webpack_exports__);
          const asyncText = "async";
          __webpack_exports__["default"] = asyncText;
        }
      }
    ]);
    

    Webpack会往 (window["webpackJsonp"] = window["webpackJsonp"] || []) 对象里push对应chunk的chunkId和对应的模块代码,而在bundle文件里,有一段很巧妙的代码

    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]);
    }
    

    这里将 (window["webpackJsonp"] = window["webpackJsonp"] || []) 对象赋值给了jsonpArray,当Webpack在 0.bundle.js 里执行了 (window["webpackJsonp"] = window["webpackJsonp"] || []).push 就相当于执行webpackJsonpCallback这个函数,现在我们再来看看webpackJsonpCallback这个函数里做了些什么

    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()();
      }
    }
    

    传入的data就是我们 0.bundle.js 里push的参数,所以逻辑就很简单了,主要是做了两件事,第一个是往modules里push异步模块,第二件事就是我们要找的执行resolve。 最后还有一步就是要利用 __webpack_require__ 将异步模块注册到我们的installedModules里,防止重复加载

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

    到这里我们就基本分析完了Webpack动态导入的过程,基本的逻辑就是将要加载的异步模块作为一个脚本文件发出新的请求,这样就避免了所有要导入的文件都要通过一个js文件去请求。

    本文参考:
    聊聊 webpack 异步加载
    webpack是如何实现动态导入的

    本月文章预告

    预告下,接下来我们会陆续发布转转在 性能、多端SDK、移动端等基础架构和中台技术相关的实践与思考,欢迎大家关注公众号 “大转转FE”,期望与大家多多交流


    起源地下载网 » 从Webpack打包后的文件分析导入的原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元