输出文件分析

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26

最简化的bundle.js

// main.js
const show = require('./show.js');
show('Webpack');

// =====================================

// show.js
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

module.exports = show;

// =====================================

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist'),
  }
};

生成的bundle.js如下:

// webpackBootstrap 启动函数
// modules 即存放所有模块的数组,数组中的每个元素都是一个函数
(function(modules) { 
  // 作用是将己经加载过的模块缓存在内存中,提升性能
  var installedModules = {};

  // 加载模块的函数,与Node中的require作用一致
  // 因为浏览器环境不支持require语句
  // moduleId 为要加载模块在数组中的index

  function __webpack_require__(moduleId) {

    // 如果需要加载的模块己经被加载过,就直接从缓存中返回
    if(installedModules[moduleId]) {
      // exports 模块的导出的值
      // 当第一次加载的时候会定义
      return installedModules[moduleId].exports;
    }

    // 如果缓存中不存在需要加载的模块,就新建一个模块,并将它存在缓存中
    var module = installedModules[moduleId] = {
      // 模块在数组中的index
      i: moduleId,
      // 代表 该模块是否己经加载完毕 标识。第一次加载的模块,默认未完成为false。
      l: false,
      // 该模块的导出值
      exports: {}
    };

    // 从modules 中获取index 为moduleId 的模块对应的函数
    // 再调用这个函数,同时将函数需要的参数传入
    /**
     * @params module 本次加载的模块
     * @params module.exports 本次模块的导出值
     * @params __webpack_require__ 加载函数即现在定义的这个函数本身(可能该模块中还需要加载别的模块)
     * */ 
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    // 将这个模块标记为己加载 返回这个模块的导出值
    module.l = true;
    return module.exports;
  }

  // 暴露所有的模块,即传进来的模块数组
  __webpack_require__.m = modules;

  // 暴露模块缓存输出
  __webpack_require__.c = installedModules;

  // 为模块定义 getter,决定在不同的环境如何正确导出
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter
      });
    }
  };

  // 如果模块存在且是es6模式的导出形式 则获取的时候 返回 module['default']
  // 否则,即对应的导出形式是CommonJS的 则获取的时候 返回 module
  // 与非协调模块兼容的getDefaultExport函数

  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // 定义函数用于检查属性是否是本身的 而不是继承的
  __webpack_require__.o = function(object, property) {
    return Object.prototype.hasOwnProperty.call(object, property); 
  };

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

  // index 为0 的模块就是main.js对应的文件,也就是执行入口模块
  // __webpack_require__.s 的含义是启动模块对应的index
  return __webpack_require__(__webpack_require__.s = 0);
 })
//  modules 参数列表 实参
([
  /* 模块0 */
  (function(module, exports, __webpack_require__) {
    // 使用定义的__webpack_require__ 导入模块1,
    const show = __webpack_require__(1);
    // 执行 show 函数
    show('Webpack');
  /***/ }),

  /* 模块1 */
  (function(module, exports) {
    // 操作 DOM 元素,把 content 显示到网页上
    function show(content) {
      window.document.getElementById('app').innerText = 'Hello,' + content;
    }

    // 导出 show 函数
    module.exports = show;

  /***/ })
]);

简单的来说,整个代码为:

// 立即执行函数
(function(modules){
  // 模拟require语句
  function __webpack_require__() {}

  // 执行存放所有模块数组中的第0个模块 即入口模块
  __webpack_require__(0);
}([/*存放所有模块的数组*/]))

bundle.js 能直接运行在浏览器中的原因是,在输出的文件中通过__webpack_require__函数,定义了一个可以在浏览器中执行的加载函数,来模拟 Node.js 中的require语句。

当使用代码分割时的输出分析

// 把第一例中的main.js 改为异步加载 其中的show.js webpack.config.js不变
// 使用import异步加载返回一个promise 其中resolve的参数就是导出值

// main.js 
import('./show.js').then((show) => {
  show('webpack');
})

打包之后,会输出两个文件,bundle.js 和 异步加载的文件 0.bundle.js,其中0.bundle.js的内容如下:

// 加载本文件(0.bundle.js)中包含的模块

// 第一个参数:Array 在其他文件中存放的模块的ID,即别的文件通过什么方式引入0.bundle.js
// 第二个参数:Array 本文件所包含的模块

webpackJsonp([0],[
  // show.js对应的模块
  (function(module, exports){
    function show(content) {
      window.document.getElementById('app').innerText = 'Hello,' + content;
    }
    module.exports = show;
  })
])

其中bundle.js内容如下

(function(modules) {
  var parentJsonpFunction = window["webpackJsonp"];

  /**
   * webpackJsonp 用于从异步加载的文件中安装模块。
   * 将webpackJsonp 挂载到全局是为了方便在其他文件中调用。
   * 
   * @param chunkIds 异步加载的文件中存放的需要安装的模块对应的Chunk ID
   * @param moreModules 异步加载的文件中存放的需要安装的模块列表
   * @param executeModules 在异步加载的文件中存放的需要安装的模块都安装成功后,需要执行的模块对应的index
   * */ 
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    var moduleId;
    var chunkId;
    var i = 0;
    var resolves = []; // 所有已加载的模块的resolve方法
    var result;

    // 缓存已安装的模块
    var installedModules = {};

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

    // 将 moreModules 添加到 modules 对象中 -> 本文件所需的模块
    // 将所有chunkIds 对应的模块都标记成己经加载成功
    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];
      }
    }

    // 如果window.webpackJsonp存在,则直接调用
    if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);

    // 调用所有resolve回调
    while(resolves.length) {
      resolves.shift()();
    }

   };

  // 模拟require方法
  function __webpack_require__(moduleId) {

    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    module.l = true;

    return module.exports;
  }

  /**
   *  用于加载被分割出去的需要异步加载的Chunk 对应的文件
   * @param chunkId 需要异步加载的Chunk 对应的ID
   * @return {Promise}
   * */
  __webpack_require__.e = function requireEnsure(chunkId) {
    // 获取该需要异步加载的模块是否存在于已加载数组中
    var installedChunkData = installedChunks[chunkId];
    // 为0 说明 已加载成果 直接返回 Promise对象 并直接执行resolve 如果外部定义有then成功回调,则直接被触发
    if(installedChunkData === 0) {
      return new Promise(function(resolve) { resolve(); });
    }

    // installedChunkData 不为空且不为0 时,表示该Chunk 正在网络加裁中
    if(installedChunkData) {
      // 返回存放在installedChunkData 数组中的Promise 对象
      // 下方有定义
      // installedChunkData[0] -> resolve
      // installedChunkData[1] -> reject
      // installedChunkData[2] -> [resolve, reject]
      // 并且定义了installedChunks
      // 上方resolves.push的是installedChunks[chunkId][0] 即resolve
      return installedChunkData[2];
    }

    // installedChunkData 为空,表示该Chunk 还没有加载过,去加载该Chunk 对应的文件

    // 先为该文件以Promise存储在状态数组中
    var promise = new Promise(function(resolve, reject) {
      installedChunkData = installedChunks[chunkId] = [resolve, reject]; 
    });

     installedChunkData[2] = promise;

     // 通过DOM操作往HEAD中插入script标签,去异步加载chunk 对应的文件
     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;

    if (__webpack_require__.nc) {
      script.setAttribute("nonce", __webpack_require__.nc);
    }

    // 文件的路径由配置的publicPath 、chunkid 拼接而成
    // 在这里即'./0.bundle.js'
    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) {
        // 为0且不为空 时,表示该Chunk 正在网络加裁中,但是加载失败了
        if(chunk) {
          chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
        }
        
        // 为空时,代表未加载,置为undefined
        installedChunks[chunkId] = undefined;
      }
    };

    head.appendChild(script);

    return promise;
  };

  // 暴露所有的模块对象 从moreModules复制的modules
  __webpack_require__.m = modules;

  // 暴露已缓存的模块数组
  __webpack_require__.c = installedModules;

  // 定义getter函数 为exports 
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter
      });
    }
  };

  // 根据不同情况 返回不同的导出值取值
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // 定义检查属性是否属于自身函数
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // 配置的publicPath 
  __webpack_require__.p = "";

  // 定义抛出错误函数
  __webpack_require__.oe = function(err) { console.error(err); throw err; };

  // 加载并执行入口模块,和上面介绍的一致
  return __webpack_require__(__webpack_require__.s = 0);
})
([
  /* 0 */  
  // main.js 对应的模块
  (function(module, exports, __webpack_require__) {
    // 异步加载 show.js对应的chunk 
    __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then((show) => {
      // 执行 show 函数
      show('Webpack');
    });
  })
]);

多了一个webpack.require.e ,用于加载被分割出去的需要异步加载的Chunk对应的文件。 多了一个webpackJsonp 函数,用于从异步加载的文件中安装模块。

使用CommonsChunkPJugin 提取公共代码时输出的文件和使用异步加载时输出的文件是一样的,都会有webpack.require.ewebpackJsonp。原因在于提取公共代码和异步加载在本质上都是代码分割。