关于webpack:webpack输出文件分析

5次阅读

共计 7064 个字符,预计需要花费 18 分钟才能阅读完成。

打包原理

  • 简略讲就是生成 ast 语法树,依据语法树生成对应的 js 代码
  • 这里仅剖析打包输入的后果,从后果剖析 webpack 对咱们代码做了啥

剖析

  • 入口文件
// main.js
// 通过 CommonJS 标准导入
const show = require('./show.js');
// 执行 show 函数
show('Webpack');
  • 依赖文件
// show.js
// 操作 DOM 元素,把 content 显示到网页上
function show(content) {window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 通过 CommonJS 标准导出 show 函数
module.exports = show;
  • 打包后果
// bundle.js
(
    // webpackBootstrap 启动函数
    // modules 即为寄存所有模块的数组,数组中的每一个元素都是一个函数
    function (modules) {
        // 装置过的模块都寄存在这外面
        // 作用是把曾经加载过的模块缓存在内存中,晋升性能
        var installedModules = {};

        // 去数组中加载一个模块,moduleId 为要加载模块在数组中的 index
        // 作用和 Node.js 中 require 语句类似
        function __webpack_require__(moduleId) {
            // 如果须要加载的模块曾经被加载过,就间接从内存缓存中返回
            if (installedModules[moduleId]) {return installedModules[moduleId].exports;
            }

            // 如果缓存中不存在须要加载的模块,就新建一个模块,并把它存在缓存中
            var module = installedModules[moduleId] = {
                // 模块在数组中的 index
                i: moduleId,
                // 该模块是否曾经加载结束
                l: false,
                // 该模块的导出值
                exports: {}};

            // 从 modules 中获取 index 为 moduleId 的模块对应的函数
            // 再调用这个函数,同时把函数须要的参数传入
            modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
            // 把这个模块标记为已加载
            module.l = true;
            // 返回这个模块的导出值
            return module.exports;
        }

        // Webpack 配置中的 publicPath,用于加载被宰割进来的异步代码
        __webpack_require__.p = "";

        // 应用 __webpack_require__ 去加载 index 为 0 的模块,并且返回该模块导出的内容
        // index 为 0 的模块就是 main.js 对应的文件,也就是执行入口模块
        // __webpack_require__.s 的含意是启动模块对应的 index
        return __webpack_require__(__webpack_require__.s = 0);

    })(

    // 所有的模块都寄存在了一个数组里,依据每个模块在数组的 index 来辨别和定位模块
    [
        /* 0 */
        (function (module, exports, __webpack_require__) {
            // 通过 __webpack_require__ 标准导入 show 函数,show.js 对应的模块 index 为 1
            const show = __webpack_require__(1);
            // 执行 show 函数
            show('Webpack');
        }),
        /* 1 */
        (function (module, exports) {function show(content) {window.document.getElementById('app').innerText = 'Hello,' + content;
            }
            // 通过 CommonJS 标准导出 show 函数
            module.exports = show;
        })
    ]
);

// 以上看上去简单的代码其实是一个立刻执行函数,能够简写为如下:(function(modules) {

  // 模仿 require 语句
  function __webpack_require__() {}

  // 执行寄存所有模块数组中的第 0 个模块
  __webpack_require__(0);

})([/* 寄存所有模块的数组 */])
  • 能够看到 bundle.js 是一个自执行函数,入参就是 main.jsshow.js革新后的代码块所形成的数组
  • 自执行函数里运行了 __webpack_require__ 这个函数,入参是 0,0 其实就是代码块数组中对应的入参,示意第一个代码块
  • 再来看 __webpack_require__ 函数,首先执行的是缓存判断,通过 moduleId 判断之前是否曾经加载过,如果加载过,间接返回间接的加载后果 exportsmouduleId 就是不同代码模块在入参数组中的 index
  • 而如果没有加载过,则新建一个对象,重要的是这个对象中的 exports 属性,外面寄存的就是加载模块后,对应模块 export 进去的货色
  • 而后用这个 exports 作为上下文去执行对应的代码块,传递参数为方才新建的 module,module 里的 exports,以及 __webpack_require__ 这个办法自身
  • 而后看到 main.js 中的 require 被革新成了 __webpack_require____webpack_require__(1) 代表加载第二个代码块
  • 第二个代码块中,定义了 show 这个办法,而后 show 会作为 module.exports 的导出,也就是赋值给了installedModules[0].module.exports,也就是这个导出曾经被缓存起来了,下次再有别的中央用到,会间接被导出
  • 这就是 webpack 大抵的打包思路,将各个独自的模块革新成数组作为入参,传给自执行函数,同时保护一个 installedModules 记录加载过的模块,利用模块数组中的 index 作为 key 值,exports 记录导出对象

按需加载

  • 因为单页利用也会有路由这个概念,在没有切换到对应路由之前,可能并不心愿浏览器对这部分页面的 js 进行下载,从而晋升首页关上的速度,就波及到一个懒加载,即按需加载的问题
  • webpack 的按需加载是通过 import(XXX) 实现的,import()是一个提案,而 webpack 反对了它
// 异步加载 show.js
import(/* webpackChunkName: 'show' */ './show').then((module) => {
  // 执行 show 函数
  const show = module.default;
  show('Webpack');
});
  • 通过这种形式打包,咱们能够发现最终打包进去的文件分成了两个,bundle.jsshow.xxx.js
  • 其中 /* webpackChunkName: 'show' */ 是专门正文给 webpack 看的,为的是指定按需加载的包的名字,同时记得在 webpack 的配置文件的 entry 中,配置chunkFilename: '[name].[hash].js',不然这个指定不会失效
  • 先来看入口文件,将临时没有用到的函数都暗藏后如下:
(function (modules) {
  // webpackJsonp 用于从异步加载的文件中装置模块
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {// ... 先省略};

  // 缓存曾经装置的模块
  var installedModules = {};

  // 存储每个 Chunk 的加载状态;// 键为 Chunk 的 ID,值为 0 代表曾经加载胜利
  var installedChunks = {1: 0};

  // 模仿 require 语句,和下面介绍的统一
  function __webpack_require__(moduleId) {// ... 省略和下面一样的内容}

  // 用于加载被宰割进来的,须要异步加载的 Chunk 对应的文件
  __webpack_require__.e = function requireEnsure(chunkId) {// ... 先省略};

  // 加载并执行入口模块,和下面介绍的统一
  return __webpack_require__(__webpack_require__.s = 0);
})
(
  // 寄存所有没有通过异步加载的,随着执行入口文件加载的模块
  [
    // main.js 对应的模块
    (function (module, exports, __webpack_require__) {
      // 通过 __webpack_require__.e 去异步加载 show.js 对应的 Chunk
      __webpack_require__.e('show').then(__webpack_require__.bind(null, 'show')).then((show) => {
        // 执行 show 函数
        show('Webpack');
      });
    })
  ]
);
  • 能够看到 import(xxx).then 被替换成了 __webpack_require__.e(0).then__webpack_require__.e(0) 返回了一个 promise
  • 第一个 then 里相当于执行了 __webpack_require__(1),但很显著能够看到自执行函数的入参数组只有一个元素,不存在[1],这个[1] 是什么时候被插入的呢
  • 看一下 __webpack_require__.e 的实现
__webpack_require__.e = function requireEnsure(chunkId) {
    // 从下面定义的 installedChunks 中获取 chunkId 对应的 Chunk 的加载状态
    var installedChunkData = installedChunks[chunkId];
    // 如果加载状态为 0 示意该 Chunk 曾经加载胜利了,间接返回 resolve Promise
    if (installedChunkData === 0) {return new Promise(function (resolve) {resolve();
      });
    }

    // installedChunkData 不为空且不为 0 示意该 Chunk 正在网络加载中
    if (installedChunkData) {
      // 返回寄存在 installedChunkData 数组中的 Promise 对象
      return installedChunkData[2];
    }

    // installedChunkData 为空,示意该 Chunk 还没有加载过,去加载该 Chunk 对应的文件
    var promise = new Promise(function (resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    installedChunkData[2] = promise;

    // 通过 DOM 操作,往 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.charset = 'utf-8';
    script.async = true;
    script.timeout = 120000;

    // 文件的门路为配置的 publicPath、chunkId 拼接而成
    script.src = __webpack_require__.p + ""+ chunkId +".bundle.js";

    // 设置异步加载的最长超时工夫
    var timeout = setTimeout(onScriptComplete, 120000);
    script.onerror = script.onload = onScriptComplete;

    // 在 script 加载和执行实现时回调
    function onScriptComplete() {
      // 避免内存泄露
      script.onerror = script.onload = null;
      clearTimeout(timeout);

      // 去查看 chunkId 对应的 Chunk 是否装置胜利,装置胜利时才会存在于 installedChunks 中
      var chunk = installedChunks[chunkId];
      if (chunk !== 0) {if (chunk) {chunk[1](new Error('Loading chunk' + chunkId + 'failed.'));
        }
        installedChunks[chunkId] = undefined;
      }
    };
    head.appendChild(script);

    return promise;
  };
  • 首先判断这个 chunkId 是否曾经加载过,如果是的话,间接返回一个 resolve 的 promise
  • 如果不为空又不为 0,阐明正在加载中,这里的 installedChunks[chunkId] 是一个数组,外面保留着[resovle, reject],是在发动网络申请的时候赋值的
  • 如果下面两个判断都没击中,阐明是没有加载过,上面开始结构加载办法,次要是通过 jsonp 的模式
  • 首先新建一个 promise,并对 installedChunks[chunkId] 赋值,把这个 promise 以及他的 resolve 和 reject 保留在外面,这也是下面为什么能够通过判断 installedChunks[chunkId] 不为空又不为 0 即正处于申请当中,间接返回数组第三个值,即新建的 promise,让后续操作能够在这个 promise 上进行回调的注册
  • 而后前面的办法就是通过结构一个 script 标签,插入到 head 中,保障代码能马上被下载,同时定义代码执行结束时的回调,判断是曾经加载了代码,如果加载胜利革除监听等,如果加载失败,抛出异样
  • 最初返回这个 promise,供内部注册回调
  • 而这里通过 jsonp 加载的代码就是打包分离出来的另一个文件 show.xx.js,也就是异步加载的show.js 相干的代码
webpackJsonp(
  // 在其它文件中寄存着的模块的 ID
  ['show'],
  // 本文件所蕴含的模块
  {// show.js 所对应的模块
    show: (function (module, exports) {function show(content) {window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  }
);
  • 接着看 webpackJsonp 这个办法是怎么定义的
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    // 把 moreModules 增加到 modules 对象中
    // 把所有 chunkIds 对应的模块都标记成曾经加载胜利 
    var moduleId, chunkId, i = 0, resolves = [], result;
    
    for (; i < chunkIds.length; i++) {chunkId = chunkIds[i];
      if (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];
        }
    }
    
    while (resolves.length) {resolves.shift()();}
  };
  • chunkIds代表本人这个文件的 id 名,因为动静加载的时候,是利用动静加载的文件名形成 script 标签进行下载的,这里传入这个 id 是为了触发后续 promise 的 resolve 以及标记模块以及被加载
  • moreModules就是对应的代码模块汇合
  • executeModules 就是加载实现后须要被执行模块的 index
  • 首先遍历 installedChunks,后面提到过installedChunks[chunkId] 通过网络下载的时候,回赋予三个值,代表其对应的 promise,这里取出第一个 resolve,保存起来,同时将加载标记置为 0,示意已加载
  • 而后遍历动静加载的模块,把代码块塞到 modules 数组里
  • 最初执行之前保留下来的 resolve 函数,触发 __webpack_require__.e(0).then 的执行
  • 这样动静加载的代码通过结构 jsonp 进行下载,并且将对应代码传到 bundle.js 的 modules 中进行保留,而后在 then 函数中通过 __webpack_require__ 执行模块,缓存输入
  • 这里为了便于了解,有对代码做肯定调整,实在的输入状况,能够通过具体打包输入查看,这里仅形容具体打包思路
正文完
 0