webpack 模块加载原理及打包文件分析 (一)

一、demo 代码和 webpack 配置

首先我是 webpack 4+。源文件如下:

// src/a.js 入口
import { add } from '@/b'
import _ from 'lodash';
import('@/c.js').then(({default: sub}) => sub(2, 1))
const a = 1
add(3, 2 + a)
console.log('lodash,', _.join(['a', 'b', 'c'], '~'))

// src/b.js
import mul from '@/d'
export function add(n1, n2) {
  return n1 + n2 + mul(10, 5)
}

// src/c.js
import mul from '@/d'
import('@/b.js').then(b => b.add(200, 100))
export default function sub(n1, n2) {
  return n1 - n2 + mul(100, 50)
}

// src/d.js
export default function mul(n1, n2) {
  const x = 10000
  return n1 * n2 + x
}

webpack 配置:

const path = require('path')

module.exports = {
  entry: {
    index: './src/a'
  },
  output: {
    path: __dirname + '/dist',
  },
  mode: 'development', // 开发环境比较便于观察打包后的代码
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src/'),
    },
  },
  module: {
    rules: [
      { test: /\.js$/, use: [{ loader: 'babel-loader' }] },
    ],
  },
}

package.json 包版本:

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "src/a.js",
  "scripts": {
    "build": "webpack",
    "debug": "node --inspect-brk=9339 ./node_modules/.bin/webpack --watch",
    "start": "webpack-dev-server --open"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.6",
    "babel-loader": "^8.0.6",
    "html-webpack-plugin": "^3.2.0",
    "webpack-dev-server": "^3.11.2"
  },
  "dependencies": {
    "lodash": "^4.17.21",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}

二、打包后的核心代码

我们运行npm run build,输出 2 个文件:0.jsindex.js

index.js是我们的入口 chunk,也就是首屏加载的同步 <script> 脚本。
让我们看看index.js的内容(简化后):

(function(modules) { // webpackBootstrap
  // The module cache
  // 缓存加载过的 module
  // key 是模块ID,值为模块路径(开发环境)或模块标识ID(生产环境),
  // value 是一个对象,包含模块ID、模块加载状态(boolean) 和 模块导出对象(exports)
  // 如 { 0: {i: 0, l: true, exports: Module} }
  var installedModules = {};

  // The require function
  // webpack 实现的 import/require() 函数,也就是同步加载模块方法
  function __webpack_require__(moduleId) {}
  // ...
  // 给 __webpack_require__ 添加一些必要的属性和方法
  // Load entry module and return exports
  // 加载入口模块,并返回模块对象
  // __webpack_require__.s 表示入口模块的 id 
  return __webpack_require__(__webpack_require__.s = "./src/a.js");
}({
  "./node_modules/lodash/lodash.js": (function() {...}),
  // ...
  "./src/a.js": (function() {...}),
  "./src/b.js": (function() {...}),
  "./src/d.js": (function() {...})
});

里面包含了 webpackBootstrap 代码,即所有项目运行时模块交互所需的加载和解析的逻辑
就是一个立即执行函数,传入的参数 modules是一个以模块ID为 key,包装后的模块代码(模块函数)为 value 的对象,用于缓存应用的 modules,此时它包含了入口文件所有同步模块。
这个moduleswebpackBootstrap 的闭包变量,在它内部都可以修改并获取到。
⚠️:模块ID的值是模块路径(开发环境)或模块标识ID(生产环境)

webpack 打包会把所有文件都转化成模块函数,项目运行时配合它的加载机制,加载对应的模块就会运行这些模块函数

再看一下这个函数做了什么:

  • 定义了一个模块缓存对象 installedModules (Object),用于缓存已经加载过的模块。其中 key 是模块ID,value 是一个包含:模块 ID、加载状态(boolean) 和 模块导出对象(exports) 三项的对象。

    • ⚠️:模块ID的值是模块路径(开发环境)或模块标识ID(生产环境)
    • 举例:installedModules: { 0: {i: 0, l: true, exports: Module} }
  • 定义了一个同步模块加载函数 __webpack_require__(核心)

  • __webpack_require__上添加一些必要的(要用到的)属性和方法

  • 通过__webpack_require__()加载入口模块。

如果通过配置将 runtime 抽出了,那么传入的参数将是一个空数组。

三、webpack 模块的核心加载方法__webpack_require__

function __webpack_require__(moduleId) {
  // Check if module is in cache
  // 如果 module 已经加载过,直接返回缓存中该模块的导出对象
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // Create a new module (and put it into the cache)
  // 如未加载则创建一个新模块,并将其存入缓存对象 installedModules  
  var module = installedModules[moduleId] = {
    i: moduleId, // 模块 ID
    l: false, // 是否已加载
    exports: {} // 模块导出值
  };

  // Execute the module function
  // 执行模块函数,this 指向模块的 exports
  // 传入的三个参数,分别为 module、module.exports、__webpack_require__
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // Flag the module as loaded
  // 将模块标识为已加载
  module.l = true;

  // Return the exports of the module
  // 返回该模块的 exports 对象
  return module.exports;
}

__webpack_require__的作用就是加载并执行指定的模块,然后返回模块的 module.exports。具体执行过程:

  • 判断模块是否已加载过,是则直接返回 installedModules 中该模块的 exports 对象,到此结束(不会重复执行模块函数)。
  • 没有则新建一个当前模块 ID为 key,{模块ID, 模块加载状态, 模块导出对象}为值的键值对,放入模块缓存对象。如{ 0: {i: 0, l: false, exports: {}} },新建的情况这个exports自然是空的。
  • 执行模块函数,同时传入三个参数:modulemodule.exports__webpack_require__自身。this指向模块的导出对象exports。(此时传入的module.exports 同样是空对象)
  • 将这个新建模块标识为已加载。
  • 最后返回该模块的 exports 对象。

应用到我们的实例上,执行的模块函数modules["./src/a.js"]的内容如下:

(function(module, __webpack_exports__, __webpack_require__) {
  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @/b */ \"./src/b.js\");\n// src/a.js\n\n__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! @/c.js */ \"./src/c.js\")).then(function (sub) {\n  return sub(2, 1);\n});\nvar a = 1;\nObject(_b__WEBPACK_IMPORTED_MODULE_0__[\"add\"])(3, 2 + a);\n\n//# sourceURL=webpack:///./src/a.js?");
})

我们给它美化一下,把eval的字符串改成 js:

(function(module, __webpack_exports__, __webpack_require__) {
  __webpack_require__.r(__webpack_exports__);
  var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/b.js");
  __webpack_require__.e(0)
    .then(__webpack_require__.bind(null,"./src/c.js"))
    .then(function (sub) { return sub(2, 1);});
  var a = 1;
  Object(_b__WEBPACK_IMPORTED_MODULE_0__["add"])(3, 2 + a);
}

对比 demo 的a.js,可以看出就是对源内容的转化和包装。可以直观看出的是同步去加载了b.js,并把 b 的模块导出赋给一个变量(这里简化为_b_MODULE_),然后用_b_MODULE_["add"]()去调用方法;
除此之外还有__webpack_require__.r__webpack_require__.e两个函数需要进一步去分析。我们接着往下看。

四、__webpack_require__上挂载的属性/方法

(1) __webpack_require__.r

作用是给模块导出(__webpack_exports__) 添加一个__esModuletrue属性,表示这是一个 ES6 module。结合上面就是标记我们的a.js为一个 ES6 模块。

// define __esModule on exports
// 在模块 exports 中 定义 __esModule 属性
__webpack_require__.r = function(exports) {
  if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  }
  Object.defineProperty(exports, '__esModule', { value: true });
};

(2) __webpack_require__.d

作用是给模块导出(__webpack_exports__)定义导出的变量

// define getter function for harmony exports
// 为模块导出定义 get 方法
__webpack_require__.d = function(exports, name, getter) {
  if(!__webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};

我们的b.js的内容被包装后有对此方法的调用:
__webpack_require__.d(__webpack_exports__, "add", function() { return add; });
以上为模块导出对象 exports 添加一个add属性,这个属性的 get 方法被重写为一个函数,这样就可以通过 __webpack_exports__["add"]() 这种方式来获取模块导出的add。如果用export default导出,则会添加一个名为default的属性。

(3) __webpack_require__.n

结合__webpack_require__.r__webpack_require__.d,这三者负责解决不同模块类型的兼容性,比如混合使用 ES6 module 和 CommonJS 的情况。

// getDefaultExport function for compatibility with non-harmony modules
// 通过判断模块有无 __esModule 属性,决定获取模块导出变量的方式
__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;
};

(4) __webpack_require__.t

import()动态引入一个CommonJS模块的时候,会创建一个对象,将这个对象标记为esModule,将CommonJS模块转换得到等价的esModule的内容输出

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
  if(mode & 1) value = __webpack_require__(value);
  if(mode & 8) return value;
  if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
  var ns = Object.create(null);
  __webpack_require__.r(ns);
  Object.defineProperty(ns, 'default', { enumerable: true, value: value });
  if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
  return ns;
};

(5) 其他

用来加载异步模块的__webpack_require__.e函数也很核心,放到后面结合异步加载单独说。

// the module id of the entry point
// 入口模块的ID
__webpack_require__.s = "./src/index.js" 

// expose the modules object (__webpack_modules__)
// 将应用所有的模块挂载到 require() 函数的 m 属性上
__webpack_require__.m = modules;

// expose the module cache
// 将模块缓存对象挂载到 require() 函数的 c 属性上
__webpack_require__.c = installedModules;

// __webpack_public_path__
// 定义打包输出文件的 publicPath
__webpack_require__.p = "";

// on error function for async loading
// 捕获异步加载模块时的错误
__webpack_require__.oe = function(err) { console.error(err); throw err; };

五、异步加载

为了提高首屏性能,一个项目都会用到按需加载组件,比如Vue Router的路由懒加载
我们都知道动态导入的模块会被单独封装成一个 js 文件 (chunk),比如我们的 demo 打包后就把c.js单独输出了一个0.js,美化如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
  "./src/c.js": 
  (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    __webpack_require__.d(__webpack_exports__, "default", function() { return sub; });
    var _d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__"./src/d.js");
    Promise.resolve).then(__webpack_require__.bind(null, "./src/b.js"))
      .then(function (b) { return b.add(200, 100); });
    function sub(n1, n2) { 
      return n1 - n2 + Object(_d__WEBPACK_IMPORTED_MODULE_0__["default"])(100, 50);
    } 
  );
 })
}]);

存在异步块的情况,webpackBootstrap 函数内会多出不少代码:

  • 定义了 installedChunks变量(Object),用来缓存加载过的 chunk。key 为 chunk ID(也就是名称),value 是 chunk 对应的状态,有四种情况:
    • undefined:chunk 未加载
    • null:chunk preloaded/prefetched
    • Promise:chunk 正在加载中
    • 0:表示 chunk 已经加载完成 ✅

在用__webpack_require__.e加载异步 chunk 时会把 value 改为加载中 promise 的相关变量,而webpackJsonCallback会把这个 value 标记成已完成。

  • 两个核心函数webpackJsonpCallback__webpack_require__.e
  • 一个辅助函数jsonpScriptSrc,作用是根据 chunk ID 生成 chunk 的请求 URL。
  • 定义全局变量window["webpackJsonp"] = [](Array),用来存储会用到webpackJsonpCallback的 chunks 信息(不仅是异步 chunk、入口 chunk 中分出的同步 chunk 也会用到,下文会讲)。
  • 重写window["webpackJsonp"].push方法为webpackJsonpCallback函数。

简化代码如下:

// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
  // 加载异步 chunk 或其他被拆分的同步 chunk 后的回调函数
  // 主要负责 chunk 的加载状态管理,并将 moreModules 装载到 modules 中
  // 后面会展开详解
}
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
// 缓存已加载和加载中的 chunk
// key 为 chunk ID,值为 chunk 对应的状态
// undefined 未加载;null chunk preloaded/prefetched;Promise 正在加载;0 已加载完成
var installedChunks = {
  "index": 0
};
// script path function
// 根据 chunk ID 生成模块文件的URL
function jsonpScriptSrc(chunkId) {
  return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
  // 打包文件名有配置 hash 的情况 
  // return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"0":"b1ab1cbc"}[chunkId] + ".js"
}

// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
  // 异步加载 chunk 的方法,返回封装加载过程的 Promise
  // 后面会展开详解
}
// 全局变量 window["webpackJsonp"],存储动态导入/入口拆分出的同步 chunks 数据
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 用 oldJsonpFunction 变量保存 jsonpArray 的原生 push 方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重写 window["webpackJsonp"].push 方法为 webpackJsonpCallback 函数
jsonpArray.push = webpackJsonpCallback;
// 把 jsonpArray 还原成普通数组
jsonpArray = jsonpArray.slice();
// jsonpArray 不为空时为每项循环执行 webpackJsonpCallback
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// jsonpArray 原生 push 方法赋给 parentJsonpFunction
var parentJsonpFunction = oldJsonpFunction;

代码量有点大,我们一点一点捋。通过 0.js 文件可以发现,它正是使用 window["webpackJsonp"].push()来放入动态模块的。push 传入的参数有两个:

  • 第一个是[0],为 chunk ID 数组;
  • 第二个是 chunk 包含模块键值对组成的对象:key 为模块 ID(开发环境为模块原路径),value 为包裹后的模块代码 (模块函数) ,和 webpackBootstrap 自执行函数的参数格式一致。

再看回上面入口模块a.js编译后的模块内容,有这样一句:

__webpack_require__.e(0)
  .then(__webpack_require__.bind(null,"./src/c.js"))
  .then(function (sub) { return sub(2, 1);});

也就是import('@/c.js').then(sub => sub(2, 1))被转化的结果。显而易见__webpack_require__.e就是用来加载异步 chunk 的。我们来看下它做了什么:

// 加载异步 chunk 的方法,返回封装加载过程的 Promise
// 传入的参数为 chunkID,比如 0
__webpack_require__.e = function requireEnsure(chunkId) {
  var promises = [];

  // JSONP chunk loading for javascript
  // 创建一个 installedChunkData 对象,用来管理 chunk 在 installedChunks 缓存对象中的状态
  var installedChunkData = installedChunks[chunkId];
  // 如果这个 chunk 在缓存中的值不是 0,即表示未加载完成
  if(installedChunkData !== 0) { // 0 means "already installed".
    // a Promise means "currently loading".
    // 不为 0 且 不为 undefined,即正在加载中         
    if(installedChunkData) {
      // 将加载中的 promise 添加到 promises 数组
      promises.push(installedChunkData[2]);
    } else {
      // setup Promise in chunk cache
      // 创建一个新的 promise 对象,用于加载动态导入的 chunk
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      }); 
      // 把新建的 promise 添加到 promise 数组,并且作为第三个参数放入 installedChunkData
      // 由此可见加载中的异步 chunk 的状态, 即 installedChunkData 的结构为 [resolve,reject,promise]
      promises.push(installedChunkData[2] = promise);

      // start chunk loading
      // 创建 script 标签
      var script = document.createElement('script');
      var onScriptComplete;
      script.charset = 'utf-8';
      script.timeout = 120;
      if (__webpack_require__.nc) { 
        // 若脚本需要安全加载,CSP策略,则给 <script></script> 加上 nonce 属性
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      // 用 jsonpScriptSrc 生成异步 chunk 请求地址
      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;
        // 清除 timeout 定时器 (无论加载成功或失败)
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if(chunk !== 0) { // 如果 chunk 还未加载完成
          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); // reject(error)
          }
          installedChunks[chunkId] = undefined; // 把 chunk 标记为 未加载
        }
      };
      // 开启一个延后 2 分钟的定时器进行超时处理
      var timeout = setTimeout(function(){
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      // 定义脚本 onerror 和 onload 为 onScriptComplete 函数
      script.onerror = script.onload = onScriptComplete;
      // 把 script 标签插入 <head></head> 中的最末
      document.head.appendChild(script);
    }
  }
  // 最终返回一个 promise,它的 then 触发时机是 promises 数组里所有 promise 对象都确定了完成状态。
  // 也就是里面所有 promise 都 resolve 或 reject 了,此时 __webpack_require__.e(chunkId) 的 then() 里的回调就可以执行了
  return Promise.all(promises);
};

__webpack_require__.e(chunkId)的处理逻辑如下:

  • 先根据传入的 chunk ID 判断 chunk 是否已加载完成,如没有则进一步判断 chunk 是否正在加载中。如果是则将这个加载中的 Promise push 到 promises 数组。
  • 没加载过则创建一个新的 promise 对象,用于加载动态该 chunk。并且把installedChunks中该 chunk 的值改为[resolve, reject, promise],然后把这个 promise push 到 promise 数组。
  • 动态创建一个 script 标签,将它的 onerror 和 onload 事件定义为onScriptComplete函数,将<script>插入页面<head>的最末,并进行加载超时错误处理。
  • 最后返回一个 promise,它的 then 触发时机是当 promises 数组里所有 promise 都有了确定状态,也就是这些 promise 的 resolve 或 reject 被调用了。彼时__webpack_require__.e(0).then()里的回调就能接着执行了。

那么问题来了,加载 chunk 的 promise 只是把 resolve 和 reject 存入了 installedChunks,并没有在成功获取异步脚本的 onload 回调中执行 resolve,这些 promise 到底是什么时候 resolve() 的呢?
这就是webpackJsonpCallback干的其中一件要事了:

// 该方法会记录管理 chunk 的加载状态,并将 moreModules 装载到 modules 中
// 如果是异步 chunk 会把它的 promise resolve 出去,也就是让`_webpack_require__.e(chunkId).then` 被触发 
function webpackJsonpCallback(data) {
  var chunkIds = data[0]; // window["webpackJsonp"].push 的第一个参数,也就是 chunkID 数组
  var moreModules = data[1]; // chunk 里所有的模块对象,{ '模块ID': '模块函数', ... }

  // 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];
    // installedChunks 已存在这个 chunk 且这个 chunk 在加载中
    if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
      // 那么把这个异步 chunk 的 resolve 方法添加到 resolves 数组 (此时 installedChunks[chunkId] 内容是 [resolve,reject,promise])
      resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0; // 把这个 chunk 标记为已加载完成
  }
  // 遍历 moreModules,把内容深拷贝给 modules,也就是 webpackBootstrap 的参数指向的地址
  // modules 为 webpackBootstrap 的闭包变量,作用域内的函数自然可以获取
  for(moduleId in moreModules) {
    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  // 执行 window["webpackJsonp"] 原生的.push,那么 "webpackJsonp" 就有了这个 chunk 的所有信息
  if(parentJsonpFunction) parentJsonpFunction(data);

  // 将异步加载的 chunks 依次 resolve(),将会执行_webpack_require__.e.then() 里的回调
  while(resolves.length) {
    // resolves.shift() 会删除并返回它的第一个元素,也就是执行 _webpack_require__.e 里产生的 promise 对象的 resolve
    resolves.shift()();
  }
};

结合我们的实例,__webpack_require__执行入口模块a.js的模块函数时,处理到__webpack_require__.e(0)会去异步下载动态资源,返回表示加载过程的 promises 数组。下载完成后执行0.js里面的代码:window["webpackJsonp"].push(),即 webpackJsonpCallback()。执行到这个函数时,0.js chunk 已经被正确加载,因此可以拿到它 promise 的 resolve,同时将installedChunks中该 chunk 的值置为 0,标记为已加载完成。还有把当前 chunk 包含的 modules 保存到入口文件的 modules 变量。最后这个resolve()执行,此时__webpack_require__.e().then()里的回调便可以执行了。

再看一下入口模块异步加载 c 模块的代码: __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/c.js")),意味着加载执行完0这个 chunk后,直接调用__webpack_require__()去真正加载 c 模块的内容。

放张别人总结的流程图

如果异步模块里再异步引入模块呢?
我们将 a.js 中的 lodash 部分注释掉,然后把 c.js 修改成:

// src/c.js
import mul from '@/d'
import _ from 'lodash';
import('@/b.js').then(b => b.add(200, 100))
import('@/e.js').then(e => console.log(e.default))
console.log("c 中引入 lodash,",_.join(['a', 'b', 'c'], '~'))

export default function sub(n1, n2) {
  return n1 - n2 + mul(100, 50)
}

分两种情况,如果是同步块 (入口 chunk) 里已经存在的模块,则:
注:代码位于异步模块 c 打包的 chunk js 文件,./src/c.js模块对应的模块函数中
Promise.resolve().then(__webpack_require__.bind(null, "./src/b.js")).then();
直接返回一个 resolved 状态的 Promise 对象。

若是之前没加载过的模块,那么显然又是另一个独立 chunk,其实就和任何同步模块加载异步 chunk 一样:
注:代码位于异步模块 c 打包的 chunk js 文件,./src/c.js模块对应的模块函数中
__webpack_require__.e(2).then(__webpack_require__.bind(null, "./src/e.js")).then();

还有一种异步块同步依赖于其他 chunk(比如默认会被抽出的第三方库)的情况,加载这个异步模块(c)的语句就会变这样:
注:代码位于入口 chunk,./src/a.js模块对应的模块函数中;0 是 第三方库 chunk、1 是 c 打包的 chunk

Promise.all([__webpack_require__.e(0), __webpack_require__.e(1)])
.then(__webpack_require__.bind(null,"./src/c.js"))
.then();

下一篇【webpack 模块加载原理及打包文件分析 (二)】 我们接着说入口文件拆包的情况

⚠️:标注 ①、②、③、④ 的四个变量需要重点理解,对理解 webpack 加载逻辑很有帮助。

参考文章:
Webpack 是怎样运行的?(一)
Webpack 是怎样运行的?(二)
聊聊 webpack 异步加载(二):webpack 如何处理 import()
webpack是如何实现动态导入的

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

推荐阅读更多精彩内容