栗子🌰

我们在 html 页面上加一个 load 按钮,点击时动态加载模块。

  1. <!-- ./public/index.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  7. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  8. <title>Document</title>
  9. <script defer src="main.js"></script></head>
  10. <body>
  11. <button id="load">load</button>
  12. </body>
  13. </html>
/** ./src/index.js **/ //打包后变成 main.js
const load = document.getElementById('load');
load.addEventListener('click', ()=>{
  import(/* webpackChunkName: 'title' */'./title').then(result => {
    console.log(result.default);
  })
})
/** ./src/title.js **/
module.exports = 'title';

在这个例子中,每点击一次按钮,就会打印一遍 title.js 中导出的内容。
打包后与以往不同,会生成两个打包文件,如下:
image.png

main.js 源码

模块定义

模块定义这边,我们在 require 上新增一个 m 参数用来保存所有的 modules。
模块定义刚开始是空的,因为引入的 title.js 还没有被加载进来。

var modules = {};
require.m = modules;

require 方法和 cache

require 方法和 cache 还是老样子,如下:

var modules = {};
var cache = {};
function require(moduleId){
  var cacheModule = cache[moduleId];
  if(cacheModule){
    return cacheModule.exports;
  }

  var module = cache[moduleId] = {
    exports: {}
  };

  modules[moduleId](module, module.exports, require);
  return module.exports;
}
require.m = modules;

主流程

接下来是我们的主流程(也就是我们加载模块的过程)。思路如下:

  1. 懒加载或者说动态加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。
  2. 代码的主是一块模块,每个代码块里面又有诺干个模块
  3. 通过 require.e(‘title’) 动态加载 title 代码块,通过 jsonp加载 title.main.js 文件,取得对应的代码块,然后把代码块里的模块定义合并到当前文件的 modules 里。
  4. 然后通过 require 加载 ‘./src/title.js’ 模块,得到返回值,不管原来是 commonjs 还是 es,都会转成 es。 ```javascript var modules = {}; var cache = {}; function require(moduleId){ var cacheModule = cache[moduleId]; if(cacheModule){ return cacheModule.exports; }

    var module = cache[moduleId] = { exports: {} };

    modulesmoduleId; return modules.exports; } require.m = modules;

const load = document.getElementById(‘load’); load.addEventListener(‘click’, () => { // 懒加载或者说动态加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。 // 代码的主是一块模块,每个代码块里面又有诺干个模块 // 通过 require.e(‘title’) 动态加载 title 代码块,通过 jsonp加载 title.main.js 文件,取得对应的代码块, // 然后把代码块里的模块定义合并到当前文件的 modules 里。 // 然后通过 require 加载 ‘./src/title.js’ 模块,得到返回值,不管原来是 commonjs 还是 es,都会转成 es。 require.e(“title”).then(require.bind(require, “./src/title.js”)).then(result => { console.log(result.default); }) })

<a name="maSxo"></a>
### require.e 方法
此方法的主要作用是调用加载函数,然后返回一个 promise 对象。

1. 声明一个 promise 的空数组 promises。
1. 调用 require.f.j 方法给 promises 赋值。
1. 通过 Promise.all 执行所有 promises,并且返回一个 promise 对象。
```javascript
require.e = (chunkId) => {
  var promises = [];
  require.f.j(chunkId, promises);
  return Promise.all(promises);
}

installedChunks 对象

在使用 require.f.j 方法前,必须定义一个 installedChunks 对象来存储已经安装或者说已经加载好的模块。

  1. key 是代码块的名字,入口默认是 main。
  2. 值 0 表示已经就绪,加载完成
  3. 如果没有加载完成(值不是 0)的话,是一个数组,第一个是 promise 的 resolve 方法,第二个是 reject 方法,第三个是 promise 本身。 ```javascript // chunkId title promises=[] // 已经安装好的或者说加载好的代码块 // key代码块的名字,入口默认是main // 值0表示已经就绪 加载完成

var installedChunks = { main: 0, // title: [resolve, reject, promise] }

<a name="UD4jt"></a>
### require.f.j 方法
f 是一个 对象,包含了一些处理数据的方法,j 方法就是实现 jsonp 的方法。

1. 创建一个 installedChunkData 数组,后面用来保存 installedChunks 中的 value 对象。
1. 创建一个 promise 对象,将resolve, reject 赋值给 installedChunkData 的 第 1,2 个元素
1. 然后再把 promise 对象放入到 promises 数组中,并且把 installedChunkData 的第 3 个元素赋值为这个promise对象。
1. 通过 require.p 和 require.u 生成资源的路径。
1. 通过 require.l 加载资源。
```javascript
require.f = {};

require.f.j = (chunkId, promises) => {
  var installedChunkData;
  var promise = new Promise((resolve, reject)=>{
    installedChunkData = installedChunks[chunkId] = [resolve, reject]
  })
  promises.push(installedChunkData[2] = promise);
  var url = require.p + require.u(chunkId);
  require.l(url);
}

require.p 方法

获取文件的路径,默认是 /

require.p = '/';

require.u 方法

获取文件名

require.u = chunkId => chunkId + '.main.js';

require.l 方法

主要是用来生成 script 标签,并且引入动态导入的资源路径,如下:

require.l = (url) => {
  var script = document.createElement('script');
  script.src = url;
  document.head.append(script);
}

挂载在window 上的方法

在执行 require.l 方法后,会立刻执行导入的代码,这块导入模块会调用 window 上的一个方法,实现加载完成后的回调,这个方法需要我们自己定义,如下:

  1. 创建一个 chunkLoadingGlobal 全局方法,把它挂在window 的 webpackChunkt4 属性上,这个属性名是自动生成的。
  2. 重写 chunkLoadingGlobal 的 push 方法。
  3. push 方法接收一个数组为参数,这个数组固定只接收两个值

    1. chunkIds 第一个元素,表示 chunkId 的数组。
    2. moreModules 第二个元素,代表模块的定义,与主模块的定义是一样的。最终我们需要把moreModules 中的模块定义,与主模块的 modules 合并。 ```javascript var webpackJsonpCallback = (data) => { var [chunkIds, moreModules] = data; var moduleId, chunkId, i = 0, resolves = []; // 把返回的模块定义合并到当前的模块定义对象 modules 里 for (moduleId in moreModules) { modules[moduleId] = moreModules[moduleId]; }

    for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; resolves.push(installedChunks[chunkId][0]); installedChunks[chunkId] = 0; }

    while (resolves.length) { resolves.shift()(); } }

var chunkLoadingGlobal = window[“webpackChunkt4”] = []; chunkLoadingGlobal.push = webpackJsonpCallback;

<a name="lAo0k"></a>
## title.main.js 源码
```javascript
window["webpackChunkt4"].push([["title"], {
  "./src/title.js":
    ((module) => {
      module.exports = 'title';
    })
}]);

这块代码就是动态导入的模块,调用 main.js 中的 webpackChunkt4 属性,实现了回调。

整理最终代码

上面的代码比较零碎,我们整理下,

/** main.js **/
var modules = {};
var cache = {};
function require(moduleId){
  var cacheModule = cache[moduleId];
  if(cacheModule){
    return cacheModule.exports;
  }

  var module = cache[moduleId] = {
    exports: {}
  };

  modules[moduleId](module, module.exports, require);
  return module.exports;
}
require.m = modules;



require.f = {};
require.p = '/';
require.u = chunkId => chunkId + '.main.js';
require.l = (url) => {
  var script = document.createElement('script');
  script.src = url;
  document.head.append(script);
}

var installedChunks = {
  main: 0,
  // title: [resolve, reject, promise]
}
require.f.j = (chunkId, promises) => {
  var installedChunkData;
  var promise = new Promise((resolve, reject)=>{
    installedChunkData = installedChunks[chunkId] = [resolve, reject]
  })
  promises.push(installedChunkData[2] = promise);
  var url = require.p + require.u(chunkId);
  require.l(url);
}
require.e = (chunkId) => {
  var promises = [];
  require.f.j(chunkId, promises);
  return Promise.all(promises);
}


var webpackJsonpCallback = (data) => {
  var [chunkIds, moreModules] = data;
  var moduleId, chunkId, i = 0, resolves = [];
  // 把返回的模块定义合并到当前的模块定义对象 modules 里
  for (moduleId in moreModules) {
    modules[moduleId] = moreModules[moduleId];
  }

  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    resolves.push(installedChunks[chunkId][0]);
    installedChunks[chunkId] = 0;
  }

  while (resolves.length) {
    resolves.shift()();
  }
}

var chunkLoadingGlobal = window["webpackChunkt4"] = [];
chunkLoadingGlobal.push = webpackJsonpCallback;

var load = document.getElementById('load');
load.addEventListener('click', () => {
  // 懒加载或者说动态加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。
  // 代码的主是一块模块,每个代码块里面又有诺干个模块
  // 通过 require.e('title') 动态加载 title 代码块,通过 jsonp加载 title.main.js 文件,取得对应的代码块,
  // 然后把代码块里的模块定义合并到当前文件的 modules 里。
  // 然后通过 require 加载 ‘./src/title.js’ 模块,得到返回值,不管原来是 commonjs 还是 es,都会转成 es。
  require.e("title").then(require.bind(require, "./src/title.js")).then(result => {
    console.log(result);
  })
})
/** title.main.js **/
window["webpackChunkt4"].push([["title"], {
  "./src/title.js":
    ((module) => {
      module.exports = 'title';
    })
}]);

简化代码

生成的源码中 方法定义的比较多,我们简化一下

var modules = ({});
var cache = {};
function require(moduleId) {
  var cachedModule = cache[moduleId];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  var module = cache[moduleId] = {
    exports: {}
  };
  modules[moduleId](module, module.exports, require);
  return module.exports;
}
require.m = modules;
require.f = {};
// chunkId title promises=[]
// 已经安装好的或者说加载好的代码块
// key代码块的名字,入口默认是main
// 值0表示已经就绪 加载完成
var installedChunks = {
  main: 0,
  // title: [resolve, reject, promise]
}
require.l = (url) => {
  var script = document.createElement('script');
  script.src = url;
  document.head.append(script);
}
require.e = (chunkId) => {
  let installedChunkData;
  let promise = new Promise((resolve, reject) => {
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });
  installedChunkData[2] = promise;
  var url = chunkId + '.main.js'; // /title.main.js
  require.l(url);
  return promise;
}

var webpackJsonpCallback = (data) => {
  let [chunkIds, moreModules] = data;
  // 把返回的模块定义合并到当前的模块定义对象 modules 里
  for (let moduleId in moreModules) {
    modules[moduleId] = moreModules[moduleId];
  }

  for (let i = 0; i < chunkIds.length; i++) {
    let chunkId = chunkIds[i];
    let resolve = installedChunks[chunkId][0];
    installedChunks[chunkId] = 0;
    resolve(); // 调用 promise 的 resolve 方法让 promise成功
  }
}

var chunkLoadingGlobal = window["webpackChunkt4"] = [];
chunkLoadingGlobal.push = webpackJsonpCallback

const load = document.getElementById('load');
load.addEventListener('click', () => {
  // 懒加载或者说董涛加载 title 这个代码块,返回一个 promise,然后再加载此模块,得到模块exports,然后打印就可以了。
  require.e("title").then(require.bind(require, "./src/title.js")).then(result => {
    console.log(result);
  })
})

实现效果

image.png
image.png
点击 load ,在 network 中就会加载 title.main.js 资源
image.png
同时会打印title
image.png