Skip to content

Webpack 模块加载器解析 #18

@luoway

Description

@luoway

Webpack不同于Rollup,其总是会在打包产物添加额外的代码,即Webpack模块加载器。

本文旨在分析Webpack模块加载器的功能逻辑。

打包源码

//index.js
import chunk, {chunkFn} from './chunk'
export const test = chunk
export const testFn = chunkFn

//chunk.js
export default 'chunk'
export function chunkFn() {
    return 'chunk'
}

打包产物

以下是libraryTarget = 'window'的构建产物,其他模块化产物的模块加载器代码相同。

window["$library"] = 
  (function(modules){
    var installedModules = {}
    
    function __webpack_require__(moduleId){
      //判断模块是否已加载
      if(installedModules[moduleId]) 
        //已加载则直接返回模块结果
        return installedModules[moduleId].exports
      
      //创建新模块
      var module = installedModules[moduleId] = {
        i: moduleId,//id
        l: false,//loaded
        exports: {}//存储模块的运行结果
      }
      
      //执行模块逻辑
      modules[moduleId].call(
        module.exports, 
        module, 
        module.exports, 
        __webpack_require__
      )
      //记录模块已执行
      module.l = true
      //返回模块的运行结果
      return module.exports
    }
    
    //定义只读属性
    __webpack_require__.d = function(exports, name, getter) {
      if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter })
      }
    }
    //定义为ES模块
    __webpack_require__.r = function(exports) {
      //设置[Symbol.toStringTags]属性
      //以被Object.prototype.toString.call读取为'[object Module]'
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      Object.defineProperty(exports, '__esModule', { value: true });
   }
    //工具方法 hasOwnProperty
    __webpack_require__.o = function(object, property) {
      return Object.prototype.hasOwnProperty.call(object, property)
    }
    //加载入口模块
    return __webpack_require__(__webpack_require__.s = 0)//modules数组第一项
  })([
    (function(module, exports, require){
      'use strict'
      require.r(exports)//定义导出模块为ES模块
      
      // CONCATENATED MODULE: ./chunk.js
      var chunk = ('chunk');
      const test = 'test-chunk'
      function chunkFn() {
          return 'chunk'
      }
      // CONCATENATED MODULE: ./index.js
      require.d(exports, 'test', ()=>test)
      require.d(exports, 'testFn', ()=>testFn)
      
      const es6_test = chunk
      const testFn = chunkFn
    })
  ])

Webpack模块加载逻辑为:

  1. 创建已安装的模块存储对象
  2. 判断模块是否已安装,是则直接返回模块结果,避免重复执行模块逻辑
  3. 创建新模块对象,用于记录模块加载状态及执行结果
  4. 执行模块逻辑

打包源码 - 动态导入模块

对于动态导入模块,打包产物的额外代码会复杂些

//index.js
export function testFn() {
    import('./chunk').then(console.log)
}
//chunk.js
export default 'chunk'
export function chunkFn() {
    return 'chunk'
}

打包产物 - 动态导入模块

构建产物入口文件

window["$library"] =
  (function(modules) { // webpackBootstrap
    //为正在加载的chunk安装JSONP回调
    function webpackJsonpCallback(data) {
      var chunkIds = data[0];
      var moreModules = data[1];

      var moduleId, chunkId, i = 0, resolves = [];
      for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) 
           && installedChunks[chunkId]) {
          //未执行的chunk,加入待执行的chunk数组
          resolves.push(installedChunks[chunkId][0]);
        }
        //将chunk记录为已加载
        installedChunks[chunkId] = 0;
      }
      //将moreModules添加到modules
      for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
          modules[moduleId] = moreModules[moduleId];
        }
      }
      if(parentJsonpFunction) parentJsonpFunction(data);
      //执行chunk
      while(resolves.length) {
        resolves.shift()();
      }

    };


    var installedModules = {};

    // 存储已加载和加载中的chunks
    // undefined = chunk not loaded, null = chunk preloaded/prefetched
    // Promise = chunk loading, 0 = chunk loaded
    var installedChunks = {
      0: 0
    };

    // 获取jsonp脚本的加载路径
    function jsonpScriptSrc(chunkId) {
      //由webpack.config中的output.chunkFilename配置
      return __webpack_require__.p + "chunk.js"
    }

    // require函数,与上文相同
    function __webpack_require__(moduleId) {/*...*/}

    // 当前文件只包含入口chunk
    // 使用加载chunk的函数来加载额外的chunk
    __webpack_require__.e = function requireEnsure(chunkId) {
      var promises = [];
      // 使用JSONP加载chunk
      
      var installedChunkData = installedChunks[chunkId];
      if(installedChunkData !== 0) { // 0 意思是已安装
        // 有值表示正在加载
        if(installedChunkData) {
          promises.push(installedChunkData[2]);
        } else {
          // 往chunk存储中添加Promise
          var promise = new Promise(function(resolve, reject) {
            installedChunkData = installedChunks[chunkId] = [resolve, reject];
          });
          promises.push(installedChunkData[2] = promise);

          // 开始加载chunk
          var script = document.createElement('script');
          var onScriptComplete;

          script.charset = 'utf-8';
          script.timeout = 120;
          // nonce是script标签上与安全相关的一个属性
          if (__webpack_require__.nc) {
            script.setAttribute("nonce", __webpack_require__.nc);
          }
          script.src = jsonpScriptSrc(chunkId);

          // 在内存回收前创建错误对象,以便之后回溯
          var error = new Error();
          onScriptComplete = function (event) {
            // 代码执行后清除标签上绑定的事件,以在IE中避免内存泄漏
            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);//120秒后超时
          script.onerror = script.onload = onScriptComplete;
          document.head.appendChild(script);
        }
      }
      return Promise.all(promises);
    };
  
    __webpack_require__.d = function(exports, name, getter) {/*...*/}
    __webpack_require__.r = function(exports) {/*...*/}
    __webpack_require__.o = function(object, property) {/*...*/}

    // 由webpack.config中的output.publicPath设置内容
    __webpack_require__.p = "";

    // on error function for async loading
    __webpack_require__.oe = function(err) { console.error(err); throw err; };

    var jsonpArray = window["webpackJsonp$library"] = window["webpackJsonp$library"] || [];
    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;

    //加载入口模块
    return __webpack_require__(__webpack_require__.s = 0);
   })([
   (function(module, exports, require) {
     "use strict";
     require.r(exports);
     require.d(exports, "testFn", ()=>testFn);
     function testFn() {
       require.e(1).then(require.bind(null, 1)).then(console.log)
     }
   })
 ]);

被异步加载的chunk.js产物:

(window["webpackJsonp$library"] = window["webpackJsonp$library"] || []).push([[1],[
/* 0 */,
/* 1 */
(function(module, exports, require) {

  "use strict";
  require.r(exports);
  require.d(exports, "test", ()=>test);
  require.d(exports, "chunkFn", ()=>chunkFn);

  exports["default"] = ('chunk');
  const test = 'test-chunk'
  function chunkFn() {
      return 'chunk'
  }

})
]]);

支持动态导入的Webpack加载器逻辑应当分为两部分来理解:

一、当前模块文件的加载逻辑,在模块加载时立即执行

二、模块运行时的动态导入逻辑

其中第一部分的逻辑为:

  1. 创建已安装的模块存储对象、已加载和加载中的chunks存储对象
  2. 查询构建时已指定的存储异步chunk加载结果的全局变量,设置全局变量上的push()方法为webpackJsonpCallback()。若全局变量已有值(即chunk已安装),则立即执行回调函数webpackJsonpCallback()
  3. 同上节加载当前模块

第二部分的逻辑为:

使用Promise、script标签实现JSONP,以此加载异步chunk。

小结

支持动态导入的Webpack加载器在原有的加载模块逻辑基础上,增加了以JSONP方式加载异步chunk的方法,并借助全局变量来避免chunk被重复加载和执行,支持在chunk加载出错时返回错误信息。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions